Serializando Objetos

Advertencia

These docs were written for SpongeAPI 7 and are likely out of date. If you feel like you can help update them, please submit a PR!

The Configurate library also provides the means to tweak automatic serialization and deserialization of objects. Per default, a set of data types can be (de)serialized:

  • Strings, the most commonly used primitive types and their wrappers

  • Lists and Sets of serializable values (not including specific implementations)

  • Maps where both keys and values are serializable (not including specific implementations)

  • The types UUID, URL, URI and (regex) Pattern

  • Any enum or CatalogType (FIXME)

  • The text type Component (See also here)

Nota

If you need special constraints or rules for your serialization (such as sorting the elements in a Set), then you should consider using your own TypeSerializer implementations.

But if you want to write your custom data structures to a config file, this will not be enough.

Imagina una estructura de datos rastreando cuantos diamantes ha minado un jugador. Se puede ver un poco como esto:

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

    [...]
}

También adopta algunos métodos para acceder a esos archivos, un buen constructor configurándolos a ambos, etc.

Creating a Custom TypeSerializer

Una manera muy sencilla de escribir y cargar una estructura de datos es proporcionando un :javadoc: “TypeSerializer” personalizado. La interfaz de ``TypeSerializer` proporciona dos métodos, uno para escribir los datos de un objeto en un nodo de configuración y otro para crear un objeto desde un nodo de configuración dado.

import com.google.common.reflect.TypeToken;
import org.spongepowered.configurate.objectmapping.ObjectMappingException;
import org.spongepowered.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());
    }
}

Este ``TypeSerializer` debe ser registrado con Configurate. Esto puede hacerse ya sea a nivel global, registrando el TypeSerializerCollection por defecto o localmente, especificandolo en el ConfigurationOptions al cargar tu configuración.

Nota

ConfigurationOptions are immutable. Every time you try to modify the original instance a new instance is created; so you either have to use the (chained) result directly or update your variable accordingly.

Ejemplo de Código: Registrando un TypeSerializer globalmente

import org.spongepowered.configurate.objectmapping.serialize.TypeSerializers;

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

Ejemplo de Codigo: Registrando un TypeSerializer localmente

import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.ConfigurationOptions;
import org.spongepowered.configurate.objectmapping.serialize.TypeSerializerCollection;
import org.spongepowered.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);

Advertencia

Si proporcionas un TypeSerializer personalizado para tipos que no son introducidos por la propia extension, solo deberías registrarlos localmente para evitar conflictos con otros extensiones o Sponge, causados por un TypeSerializer siendo sobrescrito.

Truco

If you need the TypeToken.of(DiamondCounter.class) in multiple places, then you should consider creating a constant for it. You can do it in a similar fashion as Sponge does in the TypeTokens class, or just define the constant inside of your data or serializer class.

Utilizando ObjectMappers

Ya que en muchos casos la (de)serialización se limita a asignar campos a los nodos de configuración, escribiendo algo como un TypeSerializer es un asunto más bien aburrido que nos gustaría que Configurate lo haga por su propia cuenta. Así que anotemos nuestra clase con las anotaciones :javadoc: “ConfigSerializable” y :javadoc: “Setting”.

import org.spongepowered.configurate.objectmapping.Setting;
import org.spongepowered.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;

    [...]
}

El ejemplo anterior ahora puede ser serializado y deserializado desde los nodos de configuración sin mayor registro. Las anotaciones``@Setting`` mapean un nodo de configuración en el campo que fue anotado. Acepta dos parámetros opcionales, value y``comment``. Si existe el parámetro value, define el nombre del nodo que se guardará en el campo. Si no está presente, se utilizará en su lugar el nombre del campo. Así en nuestro ejemplo anterior, la anotación asegura que el contenido del campo playerUUID” se guarda en el nodo «player», comentado con «Player UUID». El campo diamonds sin embargo se guardará bajo ese nombre exacto ya que su anotación sólo especifica un comentario. Ese comentario se escribirá en la configuración si la aplicación es compatible con los nodos de configuración comentados, de lo contrario se descartará.

Truco

También puedes usar la abreviación @Setting("someNode") en lugar de @Setting(value="someNode")

La anotación @ConfigSerializable elimina la necesidad de cualquier registro, ya que le permite a Configurate generar solo un ObjectMapper para la clase. La única limitación es que Configurate necesita un constructor vacío para instanciar un nuevo objeto antes de llenar los campos anotados.

Nota

You can also have fields that are not are not annotated with @Setting in your @ConfigSerializable classes. These fields won’t be persisted to config files and can be used to store temporary references for your plugin.

Using Default Values in ConfigSerializable Types

It is also possible to use default values inside of @ConfigSerializable types. You just have to use Java’s field initializers (or getters) to set some default values. As long as the entry is not present in the config file the value won’t be overwritten.

@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();

    [...]
}

Example: Loading a ConfigSerializable Config with Default Values

Instead of loading a default config from the plugin jar itself, it is also possible to just ask Configurate to create it if it is missing.

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();
}

In this case you load the entire configuration into a Configuration object that contains all of your plugins configuration. Using such a class has the following benefits:

  • Type safety is guaranteed

  • No need to update the configuration file shipped in your plugin

  • You don’t need to store lots of references for each of your configuration options

  • You can pass this config (or its parts) into methods or reference it from other classes

  • It is easy to write comments for each attribute in a place that also helps you during development

Nota

In this case Configuration.generateDefault() is called when the config file is missing or empty. If you still want to load the shipped default config asset you can load it inside of that method. Configuration.generateErrorDefault() is called when there is an error reading or parsing the config. It is not necessary to use separate methods for those cases; you can also use the no-arg constructor, or use an entirely custom solution.

Example: Saving a ConfigSerializable Config

Saving a @ConfigSerializable config is also very simple, as shown by the following example:

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);
}

Proporcionando un ObjectMapperFactory personalizado

Esa restricción, sin embargo, puede ser superada si usamos un ObjectMapperFactory diferente, por ejemplo un GuiceObjectMapperFactory. En vez de requerir un constructor vació, trabajará en cualquier clase que Guice pueda crear mediante una inyección de dependencia. Esto también permite una mezcla de los campos anotados @Inject y @Setting.

Su plug-in puede adquirir sólo un GuiceObjectMapperFactory simplemente por inyección de dependencia (consulte Inyección de Dependencia) y luego pasarlo a 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 org.spongepowered.configurate.commented.CommentedConfigurationNode;
import org.spongepowered.configurate.loader.ConfigurationLoader;
import org.spongepowered.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));
    }
}

Nota

El código que me muestra más arriba es un ejemplo, y por su brevedad, no contiene un manejo apropiado de las excepciones.