Skip to content
Rossi Lorenzo edited this page Jul 31, 2019 · 5 revisions

The Config-based view

The Config API is used to load data in a simple way, abstracting the details so that it can have more control over the failures.It's main advantages over the Spigot API are:

  • Easier to pass around
  • Better failure printing (file and line/column)
  • Easier to use

There are two ways that we can look at this API coming from different versions of the plugin, we decided to keep both of them because they can be useful in different situations. The first one is an object-based view where the Config object gets passed around every method. This object represent a yaml node, you can parse its entries, list them or nagivate them getting sub-configs or lists. An example speaks 1000 words:

Config cfg = Config.fromYaml(new StringReader(
  "str: Text\n" +
  "count: 129\n" +
  "enum: [hide enchants, hide attributes]\n" +
  "type: redstone wire\n" +
  "center: [15.0, 30.0, 60.0]\n" +
  "center2:\n" +
  "  x: 1.0\n" +
  "  y: 2.0\n" +
  "  z: 3.0\n"
));

Integer optCount = cfg.getInt("opt_count");
// You can add "required" to throw an exception if the key is not present
// Every method that does not contain "required" can return null
int count = cfg.getIntRequired("count");
String str = cfg.getStringRequired("str");
// Can also parse material
Material mat = cfg.getMaterialRequired("type");

updateCenter(cfg.getConfigRequired("center2"));// returns the subconfig "center2"

for all of the supported types you have three methods:

Getter type Description
getT(String key) Gets the element with key "key" and parse it as a type "T", if not found returns null or default value if primitive.
getT(String key, T default) Gets the element with key "key" and parse it as a type "T", if not found returns the value passed.
getTRequired(String key) Gets the element with key "key" and parse it as a type "T", if not found throws a RequiredPropertyNotFoundException.

In addition to this there are some extra methods including some generic ones (without T) that return a generic Object and some list types. For more complex types (imagine a list of enums) there is another method that is something that uses both config APIs and will be explained later

Quick note: never pass from config to map and back to config, even if this is possible and was widely used in the old config version it is now deprecated as it loses all of the context data, if a parsing exception occurs the user would not be provided with the origin in the config file.

The definition-based view

The second way of parsing the config is using data objects, maybe you might be more familiar with this terminology from other languages (ex. kotlin), in a nutshell they are custom classes with little to no logic and the only purpose of holding the data to be parsed. This offers a clearer way to parse the config as it lets the Config API have more control. With this the API can analyze the type to parse and build the right parser for complex types automatically. Let's make an example, if you want to parse a complex object as Map<String, Material> you don't have to do it manually, the Config API would decompose the type, get the parser for every class and compose a custom parser that can manage the type that was requested. So, how can we do this for custom classes? Let's take a real world example right from the core:

@ConfigConstructor
public UItem(
        @ConfigProperty("type") Material type,
        @ConfigProperty(value = "data", optional = true) PlaceholderValue<Short> data,
        @ConfigProperty(value = "amount", optional = true) PlaceholderValue<Integer> amount,
        @ConfigProperty(value = "name", optional = true) String rawName,
        @ConfigProperty(value = "lore", optional = true) List<PlaceholderValue<String>> lore,
        @ConfigProperty(value = "flags", optional = true) List<ItemFlag> flags,
        @ConfigProperty(value = "enchantments", optional = true) Map<Enchantment, PlaceholderValue<Integer>> enchantments
) {
    this.type = type;
    this.data = data != null ? data : PlaceholderValue.fake((short)0);
    this.amount = amount != null ? amount : PlaceholderValue.fake(1);
    this.displayName = rawName == null ? null : PlaceholderValue.stringValue(ChatColor.RESET.toString() + rawName);
    this.lore = lore != null ? lore : Collections.emptyList();
    this.flags = flags != null ? flags : Collections.emptyList();
    if (enchantments != null) {
        this.enchantments.putAll(enchantments);
    }
}

The only custom logic implemented is to manage optional types. The only parsing is done in the argument declaration, you just need to write the name of the parameter, the type and wheter it's optional or not, and that's it. The Config API will manage complex types such as Map<Enchantment, PlaceholderValue<Integer>> without any trouble. This of course comes with some restrictions: The types used in the parameters need to be either:

  • Primitives (int, long, byte...)
  • Java Collections
  • Bukkit primitives (they are already implemented)
  • Other classes with a @ConfigConstructor

If you want to add other classes that don't fall into those categories you can either implement the @ConfigConstructor or do a custom parser (when you don't have control on the implementation of the class). The latter definition is an advanced topic so it will have it's own chapter: External Declarators

As a bonus: you can use the Optional java class instead of optional = true, it all comes down to preference.

Let's check the JUnit test to see a full example of the usage:

public static class ConfigLoaderExample {
    @ConfigConstructor
    public ConfigLoaderExample(
            @ConfigProperty("str") String str,
            @ConfigProperty("count") int count,
            @ConfigProperty("enum") List<ItemFlag> flags,
            @ConfigProperty("type") Material type,
            @ConfigProperty("center") Position center,
            @ConfigProperty("center2") Position center2
    ) {
        assertEquals("Stringa", str);
        assertEquals(129, count);
        assertEquals(ImmutableList.of(ItemFlag.HIDE_ENCHANTS, ItemFlag.HIDE_ATTRIBUTES), flags);
        assertEquals(Material.REDSTONE_WIRE, type);
        assertEquals(new Position(15, 30, 60), center);
        assertEquals(new Position(1, 2, 3), center2);
    }
}

@Test
public void basicTest() {
    Config.fromYaml(new StringReader(
            "str: Stringa\n" +
            "count: 129\n" +
            "enum: [hide enchants, hide attributes]\n" +
            "type: redstone wire\n" +
            "center: [15.0, 30.0, 60.0]\n" +
            "center2:\n" +
            "  x: 1.0\n" +
            "  y: 2.0\n" +
            "  z: 3.0\n"
    )).get(ConfigLoaderExample.class, plugin);
}

The plugin is needed for some complex types (as GUI interfaces) that might need it, you can pass null if you're sure that it doesn't get used.

Merging the views

In practice both of the former definitions are used, you can use a Config to parse a @ConfigConstructor and you can have a Config as a parameter type of a @ConfigCostructor, this enables the user to have the complete control on how the data should be parsed while he can let the Config work out the parsing method for the majority of the cases.

You also don't need to create a custom class for all of the types, let's get the former example:

str: Text
count: 129
enum: [hide enchants, hide attributes]
type: redstone wire
center: [15.0, 30.0, 60.0]
center2:
  x: 1.0
  y: 2.0
  z: 3.0

We left the enum entry unparsed, that would require a parser for List<ItemFlag> and the Config object doesn't provide an enum list parser, should we create a custom class just to parse this single field? We can also use another method in the Config object, this is config.get(String key, Type type, Plugin plugin). If we want to parse the example we can write:

List<ItemFlag> enumTest = cfg.get("enum", typeOf(List.class, ItemFlag.class), plugin);

This prevents the construction of a customized class, replacing it with the specification of the type that we want to parse.

Advanced topics

Polymorhpism

This seems all too good until a little detail comes to mind, what about polymorhpism? A library written in Java should certaintly handle polymorhpism in a simple way as most of the complex configs need this (the bukkit config is just an example of this).

To implement a polymorhpic class you just need to create a static method in the father with the annotation @PolymorphicSelector, this method will get the same parameters as a @ConfigConstructor but it must not parse the class, it's purpose is to select the right class that will parse the request. In most cases it will need to multiplex the class based on a type field or something similar to it.

Let's see the JUnit tests for a full example:

public static class PolymorphicFather {

    @PolymorphicSelector
    private static Class<? extends PolymorphicFather> selectChild(@ConfigProperty("type") String type) {
        switch (type) {
            case "dog": return Dog.class;
            case "cat": return Cat.class;
            default:    return null;
        }
    }

    public static class Dog extends PolymorphicFather {
        @Getter
        private Color furColor;

        @ConfigConstructor
        public Dog(@ConfigProperty("furColor") Color furColor) {
            super("dog");
            this.furColor = furColor;
        }
    }

    public static class Cat extends PolymorphicFather {
        @Getter
        private String meowMessage;

        @ConfigConstructor
        public Cat(@ConfigProperty("meowMessage") String meowMessage) {
            super("cat");
            this.meowMessage = meowMessage;
        }
    }
}

@Test
public void testPolymorphic() {
    // This is the more explicit way to parse the config
    // First you query the parser
    ConfigParser parser = ConfigParserRegistry.getStandard().getFor(PolymorphicFather.class);

    // And then you parse
    PolymorphicFather dog = (PolymorphicFather) parser.parse(
            plugin,
            new StringReader(
                    "type: dog\n" +
                            "furColor: blue\n"
            )
    );
    assertEquals(PolymorphicFather.Dog.class, dog.getClass());
    assertEquals(Color.BLUE, ((PolymorphicFather.Dog) dog).getFurColor());

    PolymorphicFather cat = (PolymorphicFather) parser.parse(
            plugin,
            new StringReader(
                    "type: cat\n" +
                            "meowMessage: meoow\n"
            )
    );
    assertEquals(PolymorphicFather.Cat.class, cat.getClass());
    assertEquals("meoow", ((PolymorphicFather.Cat) cat).getMeowMessage());
}

The classes don't need to be inner classes, it was just written like this to reduce the line number.

Unfolding keys

Sometimes you have types little enough that an entire class is redundant, in those cases you could just explore the inner object and map it's key to real keys, this operation is called "unfolding" and it's not easy to descrive, so as always let's write an example to make it clearer:

Given the yaml:

document: "this"
writer:
  name: "Snowy"
  power: 90001

We can parse it with this single constructor:

@ConfigConstructor
public ExampleTest(
        @ConfigProperty("document") String document,
        @ConfigProperty("writer.name") String writerName,
        @ConfigProperty("writer.power") int writerPower
) {...}

This also works in the Config objects.

ConfigParser registries

All of the parsers used in the @ConfigConstructor are registered in a ConfigParserRegistry, this is also used as a cache for custom class parser and complex type parsers. You can both create a new ConfigParserRegistry (if you don't want to pollute the standard one with custom parsers) or use the stanrdard parser with ConfigParserRegistry.getStandard(). Once you obtain the registry you can get the parser for any object with registry.getFor(type), this will also build the parser for the object if none is available. The Config class uses the standard registry as the default one.

ExternalDeclarators

In some cases you can't change your own class to implement a @ConfigConstructor maybe you need to implement a parser for a class of another library. For this you can use external declarators! Most of the built-in parsers are not hard-coded inside of the parsing logic, they are rather defined in a class called StandardExternalDeclarator in which all of the methods with the annotation ConfigConstructor will be registered as constructors of the type they return. For example instead of writing an additional constructor for the Vector type we could write

@ConfigConstructor(inlineable = true)
private Vector parsePosition(@ConfigProperty("x") double x,
                             @ConfigProperty("y") double y,
                             @ConfigProperty("z") double z) {
    return new Vector(x, y, z);
}

The inlineable parameter indicates that the object could also be constructed from an array, so writing:

vector:
  x: 1.0
  y: 2.0
  z: 3.0

and

vector: [1.0, 2.0, 3.0]

will be interpreted as the same. You also need to register the external declarators, using the appropriate method:

ConfigParserRegistry.getDefault().registerFromDeclarator(new YourExternalDeclarator());

Raw constructors

Sometimes you want full control on the parsing method of your custom constructor (or external declarator), you can do this by having a single Config as argument, this will give to the constructor the config without any automated parsing. You can also take a single Node as argument, in that case the raw yaml node will be provided, this is used in some of the standard object declarations.

Clone this wiki locally