Sérialisation d’objets

La librairie Configurate fournit aussi les moyens d’alérer la sérialisation et la désérialisation automatique des objets. Par défaut, un ensemble de types de données peut être (dé)sérialisé : entre autres les Strings, ints, doubles, UUIDs, Lists (de valeurs sérialisables) et les Maps (où les clés et les valeurs sont sérialisables). Mais si vous voulez écrire vos structures de données personnalisés dans un fichier de configuration, ça ne sera pas suffisant.

Imaginez une structure de données suivant le nombre de diamants que le joueur a miné. Ça pourrait ressembler à ceci :

public class DiamondCounter {
    private UUID playerUUID;
    private int diamonds;

    ...
}

Mettez également quelques méthodes pour accéder à ces variables, un joli constructeur les définissant, etc.

Créer un TypeSerializer customisé

Un moyen très simple d’écrire et de charger une telle structure de données est de fournir un TypeSerializer cusomisé. L’interface TypeSerializer fournit deux méthodes, une pour écrire les données depuis un objet dans une node de configuration, et une pour créer un objet depuis une node de configuration donnée.

import com.google.common.reflect.TypeToken;
import ninja.leaping.configurate.objectmapping.ObjectMappingException;
import ninja.leaping.configurate.objectmapping.serialize.TypeSerializer;

public class DiamondCounterSerializer implements TypeSerializer<DiamondCounter> {

    @Override
    public DiamondCounter deserialize(TypeToken<?> type, ConfigurationNode value)
      throws ObjectMappingException {
        UUID player = value.getNode("player").getValue(TypeToken.of(UUID.class));
        int diamonds = value.getNode("diamonds").getInt();
        return new DiamondCounter(player, diamonds);
    }

    @Override
    public void serialize(TypeToken<?> type, DiamondCounter obj, ConfigurationNode value)
      throws ObjectMappingException {
        value.getNode("player").setValue(obj.getPlayerUUID());
        value.getNode("diamonds").setValue(obj.getDiamonds());
    }
}

Ce TypeSerializer doit ensuite être enregistré avec Configurate. C’est possible de le faire soit globalement, en l’enregistrant au TypeSerializerCollection par défaut, soit localement, en le spécifiant dans le ConfigurationOptions au chargement de la configuration.

Exemple de Code : Enregistrer un TypeSerializer globalement

import ninja.leaping.configurate.objectmapping.serialize.TypeSerializers;

TypeSerializers.getDefaultSerializers().registerType(TypeToken.of(DiamondCounter.class), new DiamondCounterSerializer());

Exemple de Code : Enregistrer un TypeSerializer localement

import ninja.leaping.configurate.ConfigurationNode;
import ninja.leaping.configurate.ConfigurationOptions;
import ninja.leaping.configurate.objectmapping.serialize.TypeSerializerCollection;
import ninja.leaping.configurate.objectmapping.serialize.TypeSerializers;

TypeSerializerCollection serializers = TypeSerializers.getDefaultSerializers().newChild();
serializers.registerType(TypeToken.of(DiamondCounter.class), new DiamondCounterSerializer());
ConfigurationOptions options = ConfigurationOptions.defaults().setSerializers(serializers);
ConfigurationNode rootNode = someConfigurationLoader.load(options);

Avertissement

Si vous fournissez un TypeSerializer customisé pour les types qui ne sont pas introduits par votre propre plugin, vous devriez jamais les enregistrer localement afin d’éviter les conflits avec les autres plugins ou Sponge, causé par un TypeSerializer se faisant écraser.

Utiliser les ObjectMappers

Étant donné que dans de nombreux cas, la (dé)sérialisation se résume au mappage de variables aux nodes de configuration, écrire un tel TypeSerializer est une affaire assez ennuyeuse et c’est quelque chose que nous aimerions que Configurate fasse lui-même. Donc annotons notre classe avec les annotations ConfigSerializable et Setting.

import ninja.leaping.configurate.objectmapping.Setting;
import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable;

@ConfigSerializable
public class DiamondCounter {

    @Setting(value="player", comment="Player UUID")
    private UUID playerUUID;
    @Setting(comment="Number of diamonds mined")
    private int diamonds;

    ...
}

L’exemple ci-dessus peut maintenant être sérialisé et désérialisé depuis les nodes de configuration sans plus d’enregistrement. L’annotation @Setting map une node de configuration à la variable qui a été annotée. Il accepte deux paramètres optionnels, value et comment. Si le paramètre value existe, il définit le nom de la node dans laquelle la variable sera sauvegardée. Si il n’est pas présent, le nom de la variable sera utilisé à la place. Donc dans notre exemple ci-dessus, l’annotation garantit que le contenu de la variable playerUUID est sauvegardé à la node « player », commentée avec « Player UUID ». La variable diamonds sera sauvegardé sous ce nom exact puisque l’annotation spécifie seulement un commentaire. Ce commentaire sera écrit dans la config si l’implémentation supporte les nodes de configuration commentées, sinon il sera rejeté.

Astuce

Vous pouvez aussi utiliser le raccourci @Setting("someNode") au lieu de @Setting(value="someNode")

L’annotation @ConfigSerializable élimine le besoin de faire des enregistrements puisqu’il permet à Configurate de seulement génrer un ObjectMapper pour la classe. La seule restriction est que Configurate a besoin d’un constructeur vide pour instancier un nouvel objet avant de le remplir par les variables annotées.

Fournir un ObjectMapperFactory customisé

Cette restriction, toutefois, peut être levée si nous utilisons un autre ObjectMapperFactory, par exemple un GuiceObjectMapperFactory. Au lieu d’avoir besoin d’un constructeur vide, il va fonctionner sur n’aimporte quel classe que guice peut créer via l’injection de dépendances. Cela permet également un mélange des variables annotées @Inject et @Setting.

Votre plugin peut acquérir un GuiceObjectMapperFactor simplement par l’injection de dépendances (voir Injection de Dépendances) et le passer ensuite au ConfigurationOptions.

import org.spongepowered.api.event.Listener;
import org.spongepowered.api.event.game.state.GamePreInitializationEvent;
import org.spongepowered.api.plugin.Plugin;
import com.google.inject.Inject;
import ninja.leaping.configurate.commented.CommentedConfigurationNode;
import ninja.leaping.configurate.loader.ConfigurationLoader;
import ninja.leaping.configurate.objectmapping.GuiceObjectMapperFactory;

@Plugin(name="IStoleThisFromZml", id="shamelesslystolen", version="0.8.15")
public class StolenCodeExample {

    @Inject private GuiceObjectMapperFactory factory;
    @Inject private ConfigurationLoader<CommentedConfigurationNode> loader;

    @Listener
    public void enable(GamePreInitializationEvent event) {
        CommentedConfigurationNode node =
          loader.load(ConfigurationOptions.defaults().setObjectMapperFactory(factory));
        DiamondCounter myDiamonds = node.getValue(TypeToken.of(DiamondCounter.class));
    }
}

Note

Le code ci-dessus est un exemple et, par souci de brièveté, ne gère pas correctement les exceptions.