Sérialisation d’objets

La librairie Configurate fournit également les moyens de modifier la sérialisation et la désérialisation automatique des objets. Par défaut, un ensemble de types de donnéess peut être (dé)sérialisé :

  • Les Strings, les types primitifs les plus utilisés et leurs wrappers
  • Les Lists et les Sets de valeurs sérialisables (ne comprenant pas les implémentations spécifiques)
  • Les Maps où les clés et les valeurs sont sérialisables (ne comprenant pas les implémentations spécifiques)
  • Les types UUID, URL, URI et (regex) Pattern
  • N’importe quel enum ou CatalogType
  • Les types Text, TextFormat et TextTemplate (Voir également ici)

Note

Si vous avez besoin de contraintes ou de règles spéciales pour votre sérialisation (comme trier des éléments dans un Set), alors vous devrez envisager d’utiliser vos propres implémentations de TypeSerializer.

Mais si vous voulez écrire vos propres structures de données personnalisées 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.

Note

Les ConfigurationOptions sont immuables. À chaque fois que vous essayez de modifier l’instance originale, une nouvelle instance est créée; donc vous devez soit utiliser le résultat (chaîné) ou mettre à jour votre variable en conséquence.

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.

Astuce

Si vous avez besoin de TypeToken.of(DiamondCounter.class) dans plusieurs endroits, alors vous devriez envisager la création d’une constante. Vous pouvez le faire de façon semblable à ce que Sponge fait dans la classe TypeTokens, ou simplement définir la constante à l’intérieur de votre classe de données ou de votre sérialiseur.

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.

Note

Vous pouvez également avoir des champs qui ne sont pas annotés avec @Setting dans vos classes @ConfigSerializable. Ces champs ne seront pas sauvegardés dans les fichiers de configuration et peuvent être utilisés pour stocker des références temporaires pour votre plugin.

Utiliser les Valeurs par Défaut dans les Types ConfigSerializable

Il est également possible d’utiliser des valeurs par défaut à l’intérieur des types @ConfigSerializable. Vou avez ssimplement à utiliser les initialiseurs de champs de Java (ou des getters) pour définir des valeurs par défaut. Tant que l’entrée n’est pas présente dans le fichier de configuration la valeur ne sera pas remplacée.

@ConfigSerializable
public class DiamondCounter {

    @Setting(value="player", comment="Player UUID")
    private UUID playerUUID;

    @Setting(comment="Number of diamonds mined")
    private int diamonds = 0;

    @Setting(comment="The time the player found a diamond last.")
    private LocalDateTime diamonds = LocalDateTime.now();

    [...]
}

Exemple : Charger une Configuration ConfigSerializable avec des Valeurs par Défaut

Au lieu de charger une configuration par défault à partir du jar du plugin lui-même, il est également possible de simplement demander à Configurate de le créer si elle n’existe pas.

try {
    this.config = this.configManager.load().<Configuration>getValue(Configuration.TYPE, Configuration::generateDefault);
} catch (ObjectMappingException | IOException e) {
    this.logger.error("Failed to load the config - Using a default", e);
    this.config = Configuration.generateErrorDefault();
}

Dans ce cas vous chargez l’ensemble de la configuration dans un objet Configuration qui contient toute la configuration de votre plugin. Utiliser cette classe possède les avantages suivants :

  • La sécurité de type est garantie
  • Pas besoin de mettre à jour le fichier de configuration fourni dans votre plugin
  • Vou n’avez pas besoin de stocker beaucoup de références pour chacune de vos options de configuration
  • Vous pouvez passer cette config (ou ses parties) dans des méthodes ou la référencer depuis d’autres classes
  • Il est facile d’écrire des commentaires pour chaque attribut dans un endroit qui aide également au cours du développement

Note

Dans ce cas Configuration.generateDefault() est appelé quand le fichier de configuration est manquant ou vide. Si vous voulez toujours charger la ressource de configuration par défaut fournie, vous pouvez la charger à l’intérieur de cette méthode. Configuration.generateErrorDefault() est appelé lorsqu’il y a une erreur lors de la lecture ou de l’analyse de la config. Il n’est pas nécessaire d’utiliser des méthodes séparées pour ces cas; vous pouvez également utiliser le constructeur sans argument, ou utiliser une solution entièrement personnalisée.

Exemple : Sauvegarder une Configuration ConfigSerializable

Sauvegarde une config @ConfigSerializable est également très simple, comme montré par l’exemple suivant :

try {
    this.configManager.save(this.configManager.createEmptyNode().setValue(Configuration.TYPE, this.config));
} catch (IOException | ObjectMappingException e) {
    this.logger.error("Failed to save the config", e);
}

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", description = "Stolen")
public class StolenCodeExample {

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

    @Listener
    public void enable(GamePreInitializationEvent event) throws IOException, ObjectMappingException {
        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.