Skip to content

Creating custom SavableObjects

Fulminazzo edited this page Oct 2, 2023 · 2 revisions

When working on larger projects and on various variables, it becomes a necessity to save some data in YAML files. Think about a clan plugin, that has to create a YAML file for each clan created, holding the name, the members, the leader and more.
This is nothing new or complicated, once you get the hang of it, but it gets pretty annoying and repetitive, especially if required for multiple projects. Not only that, but it also allows to create more bugs in your code that could have been avoided in the first place.
Thanks to BearCommands, you will not have to worry about saving manually anything ever again: welcome to SavableObjects!

Contents
Savable
YamlField
PreventSaving
YamlObjects
YamlPairs
A Practical Example

What is a "Savable Object"? The Savable Class

With Savable Objects we refer to all custom classes that extend the Savable class. This class introduces a bunch of new methods to load and dump fields to a file and from an object. The plugin will handle everything for you, all you have to do is pass the destination file in the constructor and call the save() method.

How does it work? YamlFields

To create this kind of behaviour, Savable converts every field into a YamlField object. This class is basically a wrapper for the real field, waiting to be retrieved from or saved to a determined configuration section of the file.
When calling the reload() method, every field is converted into a YamlField and setObject() is called. On the contrary, when saving a Savable object, every field is again converted and the YamlField method save() is called.

What if I do not want every field?

Fear not because the plugin also provides a custom annotation: PreventSaving. Simply use it on any field you do not want to be saved. More examples later.

How does a YamlField load/dump an object? YamlObjects

YamlField are not only wrappers for the real fields, but they also have an important role: to find the correct YamlObject to associate to the field type they are holding.
A YamlObject is an abstract class capable of loading and saving an object.

public abstract class YamlObject<O> {

    /**
     * Constructor used for loading.
     * @param yamlPairs: the YAML pairs.
     */
    public YamlObject(YamlPair<?>[] yamlPairs);

    /**
     * Constructor used for saving.
     * @param object: the object to save.
     * @param yamlPairs: the YAML pairs.
     */
    public YamlObject(O object, YamlPair<?>[] yamlPairs);

    /**
     * @return the stored object.
     */
    public O getObject();

    /**
     * Loads the object from the configuration section from the given path.
     * @param configurationSection: the configuration section (can be a Configuration file);
     * @param path: the path where to find the object.
     * @return the loaded object, null if some errors occur.
     */
    public abstract O load(Configuration configurationSection, String path) throws Exception;

    /**
     * Dumps the object to the configuration section at the given path.
     * @param configurationSection: the configuration section (can be a Configuration file);
     * @param path: the path where to save the object.
     */
    public abstract void dump(Configuration configurationSection, String path) throws Exception;
}

BearCommands offers several predefined YamlObjects, but more can be created:

  • UUIDYamlObject;
  • DateYamlObject;
  • BearPlayerYamlObject (works for every BearPlayer implementation);
  • MapYamlObject;
  • CollectionYamlObject;
  • EnumYamlObject.

What are YamlPairs?

As you might have noticed from the constructors of YamlObject, YamlPairs are very important for this system to work. The predefined YamlObjects we discussed earlier do not have a direct correlation to the classes they represent. It is thanks to YamlPairs that this bond is formed:

YamlPair<?>[] defaultPairs = new YamlPair[]{
        new YamlPair<>(UUID.class, UUIDYamlObject.class),
        new YamlPair<>(Date.class, DateYamlObject.class),
        new YamlPair<>(ABearPlayer.class, BearPlayerYamlObject.class),
        new YamlPair<>(Map.class, MapYamlObject.class),
        new YamlPair<>(Collection.class, CollectionYamlObject.class),
        new YamlPair<>(Enum.class, EnumYamlObject.class)
};

YamlPair accepts two classes: the first class is the destination class, the second is the corresponding YamlObject class.

That is great and all, but why should I be interested?

Well, glad you asked! As I anticipated, you can use this system to automate your own custom classes loading and dumping. All you have to do is create your own YamlObject and pair it with your class, then simply call the save() method on your object to save it and reload() to load it from file.

A Practical Example

All of this might seem daunting and complicated, but once you fully understand, you truly see the power of Savable object. Let's use an example to demonstrate it:
First thing first, we will need an object to save. Let's use the Clan example I used earlier:

package it.fulminazzo.testplugin.Objects;

import it.angrybear.Annotations.PreventSaving;
import it.angrybear.Objects.Savable;
import it.fulminazzo.testplugin.TestPlugin;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class Clan extends Savable {
    private String name;
    @PreventSaving
    private final List<String> invites = new ArrayList<>();
    private final List<TestPlayer> members = new ArrayList<>();

    public Clan(TestPlugin plugin, String name, TestPlayer leader) {
        super(plugin, new File(plugin.getDataFolder(), name + ".yml"));
        this.name = name;
        members.add(leader);
    }

    public Clan(TestPlugin plugin, File clanFile) {
        super(plugin, clanFile);
    }

    public String getName() {
        return name;
    }

    public List<String> getInvites() {
        return invites;
    }

    public List<TestPlayer> getMembers() {
        return members;
    }
}

As you can see, nothing too complicated. Just some things to keep in mind:

  • Remember to extend the Savable class and call super() for its constructor;
  • Every field should NOT be final, since it will be later loaded by the plugin; However, if the field is of type List or Map (in general, Iterable), it should be initiated as an empty one;
  • Note how in this case we do not want to save the invites, therefore we use the PreventSaving annotation to prevent it;
  • The first constructor will only be called if the Clan object is newly created, while the second one only when retrieving.

You might have also noticed that we used a TestPlayer class to identify the members. We already used this class when talking about CustomPlayers but let's check it out again:

public class TestPlayer extends BearPlayer {
    private String data;

    public TestPlayer(TestPlugin plugin, File playersFolder, OfflinePlayer player, String data) throws Exception {
        super(plugin, playersFolder, player);
        this.data = data;
    }

    @Override
    protected void createNew(UtilPlayer utilPlayer) {
        data = ((Player) utilPlayer.getPlayer()).getDisplayName();
    }

    public String getData() {
        return data;
    }
}

Since TestPlayer is a custom class, we will need to create a custom YamlObject for it. Let's call it TestPlayerYamlObject, for the sake of uniformity:

package it.fulminazzo.testplugin.Objects.YamlElements;

import it.angrybear.Objects.Configurations.Configuration;
import it.angrybear.Objects.YamlElements.UUIDYamlObject;
import it.angrybear.Objects.YamlElements.YamlObject;
import it.angrybear.Objects.YamlPair;
import it.fulminazzo.testplugin.Objects.TestPlayer;
import it.fulminazzo.testplugin.TestPlugin;
import org.bukkit.Bukkit;

import java.util.UUID;

public class TestPlayerYamlObject extends YamlObject<TestPlayer> {
    public TestPlayerYamlObject(YamlPair<?>... yamlPairs) {
        super(yamlPairs);
    }

    public TestPlayerYamlObject(TestPlayer object, YamlPair<?>... yamlPairs) {
        super(object, yamlPairs);
    }

    @Override
    public TestPlayer load(Configuration configurationSection, String path) throws Exception {
        // Get the section at the given path.
        Configuration playerSection = configurationSection.getConfiguration(path);
        // If the section is not found, return a null object.
        if (playerSection == null) return null;
        // Prepare to load a UUID type object from file using YamlObject.newObject(Class<?>, YamlPairs[]).
        // Since we know we want to load a UUID, we can pass the UUID.class and
        // assume it will return a UUIDYamlObject.
        UUIDYamlObject uuidYamlObject = YamlObject.newObject(UUID.class, yamlPairs);
        UUID uuid = uuidYamlObject.load(playerSection, "uuid");
        // Load primitive-type object "data".
        String data = playerSection.getString("data");
        // Create the new object.
        TestPlugin plugin = TestPlugin.getPlugin();
        object = new TestPlayer(plugin, plugin.getDataFolder(), Bukkit.getOfflinePlayer(uuid), data);
        // Return the newly created object.
        return object;
    }

    @Override
    public void dump(Configuration configurationSection, String path) throws Exception {
        // Delete every previous information.
        configurationSection.set(path, null);
        // If the object is null, stop.
        if (object == null) return;
        // Create a section to the given path to host the new object.
        Configuration playerSection = configurationSection.createSection(path);
        // Calling YamlObject.newObject(Object, YamlPairs[]) to dump non-primitive type object object.getUuid().
        // Since we know this is a UUID, we can assume it will return a UUIDYamlObject.
        UUIDYamlObject uuidYamlObject = YamlObject.newObject(object.getUuid(), yamlPairs);
        uuidYamlObject.dump(playerSection, "uuid");
        // Dump primitive-type object object.getData().
        playerSection.set("data", object.getData());
    }
}

A couple of notes before we continue:

  • the two given constructors are required and should not be altered (do not add any more parameter);
  • when loading and dumping a UUID we used the YamlObject.newObject() static methods. This functions will return the correct YamlObject according to the given yamlPairs and object/class given (custom ones included).

Now that we have created our YamlObject, we will need to specify it to the plugin in the form of a YamlPair. In the plugin main class, we can call the addAdditionalYamlPairs() to achieve that:

import it.angrybear.Bukkit.SimpleBearPlugin;
import it.angrybear.Objects.YamlPair;
import it.fulminazzo.testplugin.Objects.YamlElements.TestPlayerYamlObject;
import it.rrberto.testplugin.TestPlayer;

public class TestPlugin extends SimpleBearPlugin {

    @Override
    public void onEnable() {
        addAdditionalYamlPairs(new YamlPair<>(TestPlayer.class, TestPlayerYamlObject.class));
        super.onEnable();
    }
}

That's it! You now have a fully functional Savable object of type Clan that will automatically load or save whenever required!
You can maybe create a manager that loads every clan on start and saves it on shutdown:

public class ClansManager {
    private final List<Clan> clans;

    public ClansManager(TestPlugin plugin) {
        this.clans = new ArrayList<>();
        File[] clanFiles = plugin.getDataFolder().listFiles();
        if (clanFiles == null) return;
        for (File clanFile : clanFiles) {
            Clan clan = new Clan(plugin, clanFile);
            clan.reload();
            this.clans.add(clan);
        }
    }

    public List<Clan> getClans() {
        return clans;
    }
    
    public void saveAll() {
        clans.forEach(c -> {
            try {
                c.save();
            } catch (IOException e) {
                TestPlugin.logError(BearLoggingMessage.GENERAL_ERROR_OCCURRED,
                        "%task%", "saving clan object " + c.getName(),
                        "%error%", e.getMessage());
            }
        });
    }
}
Clone this wiki locally