QuarterBukkit

ServerMods Query API

The ServerMods Query API (package com.quartercode.quarterbukkit.api.query) is a powerful tool for accessing the Bukkit ServerMods API without having to worry about JSON or parsing.

Let's start with the most abstract classes.

Object Queries

Object queries are queries which return an entire dto (data object) without much effort. Data objects are objects which only store other fields and don't provide any interactivity. You will see, using the object queries is extremely simple.

SearchQuery

The search query can be used to search for BukkitDev projects whose slug contains a given string (just normal search). The slug is the part in the BukkitDev url of a project which specifies the project (if your BukkitDev url is http://dev.bukkit.org/server-mods/quarterbukkit, quarterbukkit would be the slug).

A basic example of listing all projects with the slug quarterbukkit would be:

List<Project> projects = new SearchQuery("quarterbukkit").execute();

If you don't like oneliners, you can also write the same thing on two lines:

SearchQuery query = new SearchQuery("quarterbukkit");
List<Project> projects = query.execute();

That's it! projects now contains all projects whose slug contains quarterbukkit. You could iterate over the list and access the properties of the projects by using the following methods:

public int getId()
public String getName()
public String getSlug()
public ProjectStage getStage()

Using the search query is equivalent to using the following server mods api call:

https://api.curseforge.com/servermods/projects?search={slug}

FilesQuery

The files query can be used to list all files of a BukkitDev project which are currently avaiable and not deleted. In order to create a files query, you need to provide the id of the project whose files you want to retrieve.

A basic example of listing all files of a project whose project id is 47006 (QuarterBukkit):

List<ProjectFile> files = new FilesQuery(47006).execute();

Or the longer version:

FilesQuery query = new FilesQuery(47006);
List<ProjectFile> files = query.execute();

The resulting ProjectFile objects have the following properties:

public int getName()
public ReleaseType getReleaseType()
public URL getLocation()
public String getFileName()

public String getVersion()

Using the search query is equivalent to using the following server mods api call:

https://api.curseforge.com/servermods/files?projectids={projectId}

Parsing the Version

As you may have noticed, we emphasized the version property somehow. The reason is that the files query doesn't automatically retrieve the version because it simply isn't possible. There is no version field or something like that when uploading a file to BukkitDev. That means that the version field is null if you don't parse the version yourself.

You can do that by writing a simple VersionParser which takes an uncompleted ProjectFile objects without the version property and returns the version that file would have. You can then pass the version parser to the files query which uses its information in order to set the version field.

Here's an example of a simple version parser which uses the file title for parsing the version. As you probably now, the files of QuarterBukkit have a title like QuarterBukkit <version> (e.g. QuarterBukkit 1.1.2). The parser takes that title and just removes the QuarterBukkit in front of it (notice the space after QuarterBukkit):

List<ProjectFile> files = new FilesQuery(47006, new VersionParser() {

    @Override
    public String parseVersion(ProjectFile file) {

        return file.getName().replace("QuarterBukkit ", "");
    }
}).execute();

Or the longer version:

VersionParser versionParser = new VersionParser() { ... };
FilesQuery query = new FilesQuery(47006, versionParser);
List<ProjectFile> files = query.execute();

QueryException

Every one of these queries (also the ServerModsAPIQuery which is described below) can throw a QueryException. That exception contains the queried url (e.g. https://api.curseforge.com/servermods/projects?search=quarterbukkit), the ServerModsAPIQuery which was used to get the data (actually, the ServerModsAPIQuery is the only one which throws the exception, but that's another topic) and the type of error which occurred. In some cases, the exception also contains a catched exception which was thrown during the querying process.

There are different types of query exceptions. As mentioned above, the type is set by an enumeration object.

Type Description Exception
MALFORMED_URL The request url isn't in the correct format. MalformedURLException
CANNOT_OPEN_CONNECTION Can't open a connection to the request url. IOException
CANNOT_READ_RESPONSE Can't read the response the server mods api may return. IOException
INVALID_API_KEY The api key provided in QuarterBukkit's configuration was rejected by the server mods api. IOException
INVALID_RESPONSE The response of the server mods api isn't a valid JSON array. -

JSON Query

The object queries internally use something called the ServerModsAPIQuery. That is a query object that doesn't take any clear parameters and doesn't return a nice data object. Instead, it takes a query string and returns a JSON array.

The ServerModsAPIQuery is just there for providing an implementation the object queries can use. If it's possible, you should really try to use the object queries. However, one use case for the query would be a newly added api endpoint. It takes a while until a new object query is added, and you could use the "raw" ServerModsAPIQuery until then.

Query String

The first thing is the query string. That's just the string you would append to https://api.curseforge.com/servermods/ in order to send a GET request to the service. For example, if you would want to search for projects without using the SearchQuery (don't know why you would do that), you would call the url:

https://api.curseforge.com/servermods/projects?search={search}

In that case, the query string would be:

projects?search={search}

JSON Array Response

For the query projects?search=minecartrevolution, the service would respond with something like:

[
   {
      "id":50146,
      "name":"MinecartRevolutionTags",
      "slug":"minecartrevolutiontags",
      "stage":"alpha"
   },
   {
      "id":36965,
      "name":"MinecartRevolution",
      "slug":"minecartrevolution",
      "stage":"release"
   }
]

That's actually a JSON array. The square brackets define the start and the end of the array, the , in the middle starts a new entry, and the curly brackets define an object. As you can see, every "project" object contains an id, a name a slug and a (project) stage. For example, the search query just takes these objects, creates Project objects out of them and returns all of these newly created objects in a list.

Using the ServerModsAPIQuery

Using the ServerModsAPIQuery is fairly straightforward. You just create a query object and invoke the execute() method in order to get a JSONArray. You could then iterate over that array and print out every entry, for example:

JSONArray result = new ServerModsAPIQuery("projects?search=minecartrevolution").execute();

for (Object entry : result) {
    if (entry instanceof JSONObject) { 
        System.out.println(((JSONObject) entry).get("name"));
    }
}

Examples

Updater

You can implement a very flexible updater using the SeverMods Query API. It is so flexible because you don't need to extend any superclass or something like that, you just call some methods which do some magical things in the background :). In my opinion, it's the best updater you can get because you have full control over what happens while using an elegant and simple api.

Here's a simple updater which just downloads a jar file and reloads the plugin. Note that we use the FileUtils class which contains utility methods for download, copy, delete and unzip:

public boolean checkAndUpdate() {

    try {
        // Get latest version
        List<ProjectFile> avaiableFiles = new FilesQuery(PROJECT_ID, new VersionParser() { ... }).execute();
        if (avaiableFiles.size() == 0) {
            // No file avaiable
            return false;
        }

        ProjectFile latestFile = avaiableFiles.get(avaiableFiles.size() - 1);
        if (latestFile.getVersion().equals(plugin.getDescription().getVersion())) {
            // No update required (latest version already installed)
            return false;
        }

        // Set download variables
        File pluginJar = new File(plugin.getDataFolder().getParentFile(), File.separator + plugin.getName() + ".jar");

        // Disable plugin
        Bukkit.getPluginManager().disablePlugin(plugin);

        // Download jar
        FileUtils.download(latestFile.getLocation().toURL(), pluginJar);

        // Load plugin from file
        try {
            Bukkit.getPluginManager().enablePlugin(Bukkit.getPluginManager().loadPlugin(pluginJar));
        }
        catch (Exception e) {
            ExceptionHandler.exception(new InstallException(plugin, e, "Error while reloading the plugin with the new jar"));
            return false;
        }

        return true;
    }
    catch (QueryException e) {
        ExceptionHandler.exception(new InstallException(plugin, e, "Error while querying the server mods api: " + e.getType()));
    }
    catch (IOException e) {
        ExceptionHandler.exception(new InstallException(plugin, e, "Error while doing some file operations"));
    }

    return false;
}