Resource Using alternative configuration formats in plugins

Discussion in 'Spigot Plugin Development' started by Redrield, Apr 30, 2017.

  1. Hey everyone,

    As the title says, this is going to be a quick guide for using formats other than YAML for configuring plugins. If you follow the steps in this guide, you're going to be able to choose from HOCON, JSON, and YAML for configuring plugins, though I'm just going to focus on getting HOCON up and running.


    HOCON, or Human Oriented Configuration Object Notation is a configuration format that's based on JSON, but is made to be more user friendly. An example HOCON config might look like this
    Code (Text):
    foo {
        message = "bar"
    }

    baz {
        qux = [
            {
                 thing = "Awesome"
            },
            {
                 thing = "Plugin"
            }
        ]
    }
    As you can see, it's easy for humans to understand, and it bears striking similarities to JSON. The question now becomes how you can use this format in Spigot plugins.

    The first thing you're going to need to do is add and shade a dependency for your project. This dependency, called Configurate, is a configuration library that allows us to use configuration files in these different formats (It's actually the library that is used by the Sponge API for creating configurations). This is the Gradle buildscript that I use to do so, Maven users will just need to add the dependency and shade as normal
    Code (Text):
    plugins {
        id "com.github.johnrengelman.shadow" version "1.2.4"
    }

    apply plugin: 'java'

    group = "com.redrield"
    version = "1.0-SNAPSHOT"

    sourceCompatibility = 1.8
    targetCompatibility = 1.8

    repositories {
        mavenCentral()
        maven {
            name = 'spigotmc-repo'
            url = 'https://hub.spigotmc.org/nexus/content/groups/public/'
        }
        maven {
            name = 'sonatype'
            url = 'https://oss.sonatype.org/content/groups/public/'
        }
    }

    configurations {
        shade

        compile.extendsFrom shade
    }

    shadowJar.configurations = [project.configurations.shade]

    dependencies {
        compile 'org.spigotmc:spigot-api:1.11.2-R0.1-SNAPSHOT'

        shade "ninja.leaping.configurate:configurate-hocon:3.2"
    }
    This will add the configurate-hocon library to your project, and shade it when you compile (Gradle users will need to use `gradle build shadowJar` to compile, and use the jar with the -all suffix)

    HOCON is simply one of the Configurate libraries, all of them can be found in central, and you can look through them at http://search.maven.org/#search|ga|1|ninja.leaping.configurate.

    Now then, into real Java! You're going to need to take a few extra steps to set this up than just calling saveDefaultConfig() in onEnable(). The things that you're going to need to do is create a File that you're going to save the config to on disk, and you're going to need to create some methods to load a default from the jar, save it to disk, and load that file. All in all, it's very similar to creating custom configurations with the built in FileConfiguration and YamlConfiguration types. This is the example plugin that I've written to do so

    Code (Text):
    package com.redrield.configurateexample;

    import ninja.leaping.configurate.commented.CommentedConfigurationNode;
    import ninja.leaping.configurate.hocon.HoconConfigurationLoader;
    import ninja.leaping.configurate.loader.ConfigurationLoader;
    import org.bukkit.plugin.java.JavaPlugin;

    import java.io.File;
    import java.io.IOException;
    import java.io.InputStream;
    import java.nio.file.Files;

    public final class ConfigurateExample extends JavaPlugin {

        private File configFile;
        private CommentedConfigurationNode config;
        private ConfigurationLoader<CommentedConfigurationNode> loader;

        @Override
        public void onEnable() {
            try {
                createAndLoadConfig();
            } catch (IOException e) {
                e.printStackTrace();
            }

            getLogger().info("The value of my message is " + config.getNode("object", "message").getString("No message"));
        }

        private void createAndLoadConfig() throws IOException {
            if(!getDataFolder().exists()) {
                getDataFolder().mkdirs();
            }
            configFile = new File(getDataFolder(), "myplugin.conf");
            loader = HoconConfigurationLoader.builder().setFile(configFile).build();

            if(!configFile.exists()) { // We don't want to overwrite existing configurations
                // Checking for defaults
                InputStream in = this.getClass().getResourceAsStream("/myplugin.conf");
                if (in != null) {
                    Files.copy(in, configFile.toPath()); // If there are defaults in the jar, we're going to read them into the config file
                }
            }
            config = loader.load();
        }
     
        public void saveConfig() {
            try {
                loader.save(config);
            }catch(IOException e) {
                e.printStackTrace();
            }
        }
     
        public void reloadConfig() {
            try {
                config = loader.load();
            }catch(IOException e) {
                e.printStackTrace();
            }
        }

        public CommentedConfigurationNode getHOCONConfig() {
            return config;
        }
    }
    This should look very similar to anyone who's made custom configs before, with the only new bits being Files.copy, and the HoconConfigurationLoader. Let me break it up

    The calls to Files.copy is simply taking the bytes from the InputStream that we got from our default, and moving it to the file on disk.

    The HoconConfigurationLoader, and CommentedConfigurationLoader classes are from Configurate, they're the classes that we're going to be interacting with for our actual config. The builder that we created simply says that we're storing the config in the configFile on disk, and loader.load() takes that file, and reads configuration data from it. loader.save saves the config in memory to the disk, and reloadConfig() simply dumps whatever we have in memory at the moment we call it, and replaces it with the version on disk.

    The last things that we have to do are to add a default config, and test it out. Considering the message that we have at the end of onEnable(), this is what my config is going to look like
    Code (Text):
    object {
      message = "Hello World!"
    }
    Put a file called myplugin.conf into src/main/resources, alongside the plugin.yml, and compile and test it out
    Configurate.png


    Other than that, here are a few things to keep in mind when using Configurate
    • Instead of using path separators like '.', configurate allows you to give it vararg node names to travel down the tree.
    • Configurate nodes are never null. If you want to check if a node doesn't exist, use #isVirtual()
     
    #1 Redrield, Apr 30, 2017
    Last edited: May 10, 2017
    • Useful Useful x 3
  2. This is really cool and stuff, and I really do appreciate you've putten so much effort into this, but you've left me with 1 question!

    Why? (Why not just use YAML?)
     
  3. The main reason that I looked into all of this was because I was making a plugin where I wanted to have an array, or a list of objects that don't have tags themselves. Basically anonymous objects. I couldn't for the life of me find a way to do this in YAML, but you can do it with HOCON as I showed in the example configuration in this thread.

    Not only that, but Configurate also provides you a way to easily serialize and deserialize different types, and also allowing you to register methods to serialize custom objects. These are things that the configuration API in Spigot either can't let you do, doesn't let you do, or makes extremely difficult
     
    • Informative Informative x 1