Sérialisation de données

Alors qu’un ImmutableDataManipulator est une bonne façon de stocker des données pendant que le serveur est allumé, il ne va pas persister après un redémarrage. Toutefois, chaque DataManipulator implémente l’interface DataSerializable et peut donc être sérialisé en un DataContainer et désérialiser par un DataBuilder.

Après cette conversion initiale depuis le DataManipulator spécialisé vers une structure plus générale, le DataContainer peut être traité ultérieurement.

DataContainer et DataView

Un DataView est une structure polyvalente pour tenir tout type de données. Il supporte plusieurs valeurs et même imbriquer DataViews comme valeur, ce qui permet une structure arborescente. Chaque valeur est identifée par un DataQuery. Un DataContainer est un DataView racine.

Chaque DataSerializable fournit une méthode toContainer() qui va créé et retourner un DataContainer. Par exemple, appeler toContainer() sur une instance de HealthData va produire un DataContainer contenant deux valeurs, une pour la santé actuelle et une pour la santé maximale, chacune étant identifée par le DataQuery de la Key respective.

import org.spongepowered.api.data.DataContainer;
import org.spongepowered.api.data.key.Keys;

DataContainer serializedHealth = healthData.toContainer();
double currentHealth = serializedHealth.getDouble(Keys.HEALTH.getQuery()).get();
currentHealth == healthData.health().get();  // true

Convertir ce conteneur en une instance d”HealthData se fait par le DataBuilder correspondant. Ils sont enregistrés et gérés par le DataManager. Ils peuvent être soit obtenus depuis une instance valide de Game ou en utilisant la classe utilitaire Sponge. Le DataManager fournit une méthode pour récupérer le DataBuilder approprié pour désérialiser une classe donnée et, en outre, une méthode plus courte pour récupérer le DataBuilder et lui faire faire la désérialisation en une étape. Les deux exemples de code suivants sont fonctionnellement équivalents.

Exemple de Code : Désérialisation, la façon longue

import org.spongepowered.api.data.DataView;
import org.spongepowered.api.data.manipulator.mutable.entity.HealthData;
import org.spongepowered.api.util.persistence.DataBuilder;

import java.util.Optional;

public Optional<HealthData> deserializeHealth(DataView container) {
    final Optional<DataBuilder<HealthData>> builder = Sponge.getDataManager().getBuilder(HealthData.class);
    if (builder.isPresent()) {
        return builder.get().build(container);
    }
    return Optional.empty();
}

Exemple de Code : Désérialisation, la façon courte

import org.spongepowered.api.data.manipulator.mutable.entity.HealthData;

public Optional<HealthData> deserializeHealth(DataView container) {
    return Sponge.getDataManager().deserialize(HealthData.class, container);
}

La fonction ` deserializeHealth`` va retourner Optional.empty() sil il n’y a pas de DataBuilder enregistré pour HealthData ou si le DataContainer fournit est vide. Si des données invalides sont présentes dans le DataContainer, une InvalidDataException sera levée.

DataTranslator

Dans Sponge, généralement les implémentations MemoryDataView et MemoryDataContainer sont utilisés, qui résident en mémoire uniquement et ne persistent donc pas après un redémarrage. Afin de constamment stocker un DataContainer, il doit d’abord être converti en une représentation stockable.

En utilisant l’implémentation DataTranslators#CONFIGURATION_NODE de DataTranslator, nous pouvons convertir un DataView en ConfigurationNode et vice versa. Les ConfigurationNodes peuvent ensuite être écrites et lues depuis des fichiers persistants à l’aide de la Librairie Configurate.

Exemple de Code : Sérialiser une instance d’HealthData pour Configurate

import ninja.leaping.configurate.ConfigurationNode;
import org.spongepowered.api.data.persistence.DataTranslator;
import org.spongepowered.api.data.persistence.DataTranslators;

public ConfigurationNode translateToConfig(HealthData data) {
    final DataTranslator<ConfigurationNode> translator = DataTranslators.CONFIGURATION_NODE;
    final DataView container = data.toContainer();
    return translator.translate(container);
}

Exemple de Code : Désérialiser une instance d’HealthData depuis Configurate

import java.util.Optional;

public Optional<HealthData> translateFromConfig(ConfigurationNode node) {
    final DataTranslator<ConfigurationNode> translator = DataTranslators.CONFIGURATION_NODE;
    final DataView container = translator.translate(node);
    return deserializeHealth(container);
}

DataFormat

Une alternative au DataTranslator est le DataFormat, qui vous permet de stocker un DataContainer en format HOCON, JSON ou NBT. Vous pouvez également recréer des DataContainers en utilisant les DataFormats. Les implémentations de DataFormat fournies par Sponge sont disponibles dans la classe DataFormats.

Par exemple, nous pouvons utiliser le DataFormat DataFormats#JSON qui nous permet de créer une représentation JSON d’un DataContainer. Le JSON en sortie peut ensuite être facilemeent stocké dans une base de données. Nous pouvons alors utiliser le même DataFormat pour recréer le DataContainer original à partir de ce JSON lorsque c’est nécessaire.

Imports pour les exemples de code

import org.spongepowered.api.Sponge;
import org.spongepowered.api.data.DataContainer;
import org.spongepowered.api.data.persistence.DataFormats;
import org.spongepowered.api.item.inventory.ItemStackSnapshot;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;

Exemple de Code : Sérialiser un ItemStackSnapshot au format JSON

String json = DataFormats.JSON.write(itemStack.toContainer());

Exemple de Code : Désérialiser un ItemStackSnapshot depuis le format JSON

DataContainer container = DataFormats.JSON.read(json);

Exemple de Code : Écrire un ItemStackSnapshot dans un fichier en utilisant NBT

public void writeItemStackSnapshotToFile(ItemStackSnapshot itemStackSnapshot, Path path) {
    DataContainer dataContainer = itemStackSnapshot.toContainer();
    try (OutputStream outputStream = Files.newOutputStream(path)) {
        DataFormats.NBT.writeTo(outputStream, dataContainer);
    } catch (IOException e) {
        // For the purposes of this example, we just print the error to the console. However,
        // as this exception indicates the file didn't save, you should handle this in a way
        // more suitable for your plugin.
        e.printStackTrace();
    }
}

Exemple de Code : Lire un ItemStackSnapshot depuis un fichier en utilisant NBT

public Optional<ItemStackSnapshot> readItemStackSnapshotFromFile(Path path) {
    try (InputStream inputStream = Files.newInputStream(path)) {
        DataContainer dataContainer = DataFormats.NBT.readFrom(inputStream);
        return Sponge.getDataManager().deserialize(ItemStackSnapshot.class, dataContainer);
    } catch (IOException e) {
        e.printStackTrace();
    }

    return Optional.empty();
}