Skip to content

Config files

Rayzr522 edited this page Sep 20, 2016 · 4 revisions

Config files

One of the features of this library is a dynamic configuration file utility which allows the direct saving and loading of data classes.

ISerializable

The way you create a data class with this utility is by implementing the ISerializable interface. An example of this is below:

public class MyDataClass implements ISerializable {

    @Serialized
    public String name = "Bob";

    @Override
	public void onDeserialize() {
        // Do stuff when it's finished deserializing
        // Allows you to act on data as soon as it's loaded
	}

	@Override
	public void onPreSerialize() {
        // Do stuff right before it serializes
        // Allows you to change data before it saves
	}

}

This is a very basic example which shows how to implement the class, as well as how to mark a variable for serialization / deserialization. Let's analyze:

  • MyDataClass implements ISerializable
  • The variable we want to save and load has the @Serialized annotation present
  • We have the default methods for modifying data as soon as it's available, as well as changing it before it's saved

Variables

What kind of variable types can be serialized?

  • All data types that are available in Bukkit's YAML system
    • String
    • int
    • double
    • float
    • etc
  • Another class that implements ISerializable (more on that later)
  • Any variable type that has been made compatible via an ISerializationHandler (more on that later)

Nesting

To create a nested data class it's actually quite easy. Let's create a new data class: public class PlayerData implements ISerializable {

    @Serialized
    public int score = 0;
    @Serialized
    public double health = 20.0;

    @Override
    public void onDeserialize() {
    }

    @Override
    public void onPreSerialize() {
    }

}

Now if we modify the original data class we can nest the new data class inside: public class MyDataClass implements ISerializable {

    @Serialized
    public String name = "Bob";

    @Serialized
    public PlayerData data;

    @Override
	public void onDeserialize() {
        // Do stuff when it's finished deserializing
        // Allows you to act on data as soon as it's loaded
	}

	@Override
	public void onPreSerialize() {
        // Do stuff right before it serializes
        // Allows you to change data before it saves
	}

}

The serialize and deserialize methods will call themselves recursively to deal with the nested ISerializable types.

Failing

There is a handy annotation available to tell the ConfigManager what to do when it can't load a variable (for whatever reason): OnFail. To use this annotation, let's go back to our data classes. We want to make it so that if the PlayerData class fails to load that we're just going to stop the whole process and return null. To do that, all we have to add is one line: public class MyDataClass implements ISerializable {

    @Serialized
    public String name = "Bob";

    @Serialized
    @OnFail(FailResponse.CANCEL_LOAD)
    public PlayerData data;

    @Override
	public void onDeserialize() {
        // Do stuff when it's finished deserializing
        // Allows you to act on data as soon as it's loaded
	}

	@Override
	public void onPreSerialize() {
        // Do stuff right before it serializes
        // Allows you to change data before it saves
	}

}

Note that right above the PlayerData variable data we added an OnFail annotation, telling ConfigManager to cancel the loading of the data if this fails to be loaded. All options are shown here. By default the ConfigManager won't even give an error if a variable fails to load because there can be very normal reasons for a variable not to load, and usually you don't want an error. But if you do, you can add @OnFail(FailResponse.CONSOLE_ERR) to your variable.

Adding Support

You might be wondering how you can add support for ConfigManager to be able to handle pre-exisiting classes. Enter: ISerializationHandler

While a bit daunting, this class is rather easy to use. Let's take an example here, this is the VectorSerializer class which comes with PerceiveCore by default:

public class VectorSerializer implements ISerializationHandler<Vector> {

	@Override
	public Map<String, Object> serialize(Vector obj) {

		Map<String, Object> map = new HashMap<>();

		map.put("x", obj.getX());
		map.put("y", obj.getY());
		map.put("z", obj.getZ());

		return map;
	}

	@Override
	public Vector deserialize(Map<String, Object> map) {

		double x = (double) map.get("x");
		double y = (double) map.get("y");
		double z = (double) map.get("z");

		return new Vector(x, y, z);
	}

}

What's this doing?

  • We implement ISerializationHandler and give it a type parameter of Vector
  • We override the interface's methods
    • serialize: We create a map and put our vector's x, y, and z values into it, then return the map
    • deserialize: We get our values from the map, casting them as we go. We then return a new vector based on those values

This is the whole Vector serializer class, but there is one more step you'll have to do: you must register the handler. This can be done like this:

ConfigManager.registerSerializationHandler(Vector.class, new VectorSerializer());

Obviously replacing Vector.class with the proper class you want to add support for and VectorSerializer with your ISerializationHandler implementation.

A couple notes:

  • ConfigManager gives instances of ISerializable priority over classes that have serialization handlers, so creating a serialization handler for your ISerializable class is pointless and a waste of time.
  • ClassCastExceptions have special errors in the console mentioning that an invalid type was encountered, saying what it was and what it should be. Might be helpful.

Saving & Loading

Before a data class can be saved or loaded you're going to need a ConfigManager instance. To create this it's as simple as ConfigManager cm = new ConfigManager(pluginInstance);, of course assuming that pluginInstance is an instance of JavaPlugin.

Now, you have a few options for saving and loading data. Let's start with saving data:

// Assumptions: cm is a ConfigManager instance, config is a YamlConfiguration instance, and section is a configuration section in config
MyDataClass data = new MyDataClass();
cm.save(data, "someConfig.yml"); // Saves the data to a file
cm.save(data, config); // Saves the data to a config object
cm.save(data, config, "someConfig.yml"); // Saves the data to a config and then saves the config to a file
cm.save(data, section); // Saves the data to a ConfigurationSection

These are the 4 available methods for saving your data classes. As for loading, it's very much the same:

// Assumptions: cm is a ConfigManager instance, config is a YamlConfiguration instance, and section is a configuration section in config
MyDataClass data;
data = cm.load(MyDataClass.class, "someConfig.yml"); // Loads the data from a file
data = cm.load(MyDataClass.class, config); // Loads the data from a config object
data = cm.load(MyDataClass.class, section); // Loads the data from a ConfigurationSection

There isn't much to say about either the loading or the saving, they're both pretty self explanatory. For the loading you just provide it a class so it knows what it's "template" is. Also there is a type parameter for cm.load but it isn't necessary as I'm pretty sure every IDE can figure out the type parameter from context.

Note by Rayzr: I write this late at night when I was exhausted and my hand is broken and I'm trying to do this typing on an iPod so admittedly this last section isn't the greatest. I'll fix it later.

Clone this wiki locally