Skip to content
zml edited this page Nov 25, 2020 · 15 revisions

Object Mapper

The object mapper is a specialized TypeSerializer that can extract information from Java objects to be able to serialize and deserialize them from ConfigurationNodes.

Basic Usage

The object mapper can only create objects that have no-arg constructor, are record classes, or with the Kotlin extras module data classes, but is otherwise restricted to populating already existing objects (with a ObjectMapper.Mutable instance). It's generally easier to work with immutable data types, which is why the method assuming mutability is only exposed in a subtype.

Supported Types

The object mapper supports any of the standard type serializers registered with a node's type serializer collection.

Extending

Extending the object mapper

As of version 4.0, the object mapper has defined extension points, set up on an ObjectMapper.Factory.Builder. See their class documentation for details on what each one does.

To use customized object mapper factories, they must be registered with a TypeSerializerCollection. For example:

public ConfigurationLoader<?> createLoader(final Path source) {
    final ObjectMapper.Factory customFactory = ObjectMapper.factoryBuilder()
        .addResolver(NodeResolver.onlyWithSetting())
        .build();
        
    return YamlConfigurationLoader.builder()
        .path(source)
        .defaultOptions(opts -> opts.serializers(build -> build.registerAnnotatedObjects(customFactory)))
        .build;
}

Bringing it all together

Here's a example of a standalone ObjectMapper setup. This uses a few value types, some nodes, and shows that type parameters are interpreted where specified.

public final class ObjectMapperExample {

    private ObjectMapperExample() {}

    public static void main(final String[] args) throws ConfigurateException {
        final Path file = Paths.get(args[0]);
        final HoconConfigurationLoader loader = HoconConfigurationLoader.builder()
                .defaultOptions(opts -> opts.shouldCopyDefaults(true))
                .path(file) // or setUrl(), or setFile(), or setSource/Sink
                .build();

        final CommentedConfigurationNode node = loader.load(); // Load from file
        final MyConfiguration config = MyConfiguration.loadFrom(node); // Populate object

        // Do whatever actions with the configuration, then...
        config.itemName("Steve");

        config.saveTo(node); // Update the backing node
        loader.save(node); // Write to the original file
    }

    @ConfigSerializable
    static class MyConfiguration {

        private static final ObjectMapper<MyConfiguration> MAPPER;

        static {
            try {
                MAPPER = ObjectMapper.factory().get(MyConfiguration.class); // We hold on to the instance of our ObjectMapper
            } catch (final SerializationException e) {
                throw new ExceptionInInitializerError(e);
            }
        }

        public static MyConfiguration loadFrom(final ConfigurationNode node) throws SerializationException {
            return MAPPER.load(node);
        }

        private @Nullable String itemName;

        @Comment("Here is a comment to describe the purpose of this field")
        private Pattern filter = Pattern.compile("cars?"); // Set defaults by initializing the field

        // As long as custom classes are annotated with @ConfigSerializable, they can be nested as ordinary fields.
        private List<Section> sections = new ArrayList<>();

        // This won't be written to the file because it's marked as `transient`
        private transient @MonotonicNonNull String decoratedName;

        public @Nullable String itemName() {
            return this.itemName;
        }

        public void itemName(final String itemName) {
            this.itemName = requireNonNull(itemName, "itemName");
        }

        public Pattern filter() {
            return this.filter;
        }

        public List<Section> sections() {
            return this.sections;
        }

        public String decoratedItemName() {
            if (this.decoratedName == null) {
                this.decoratedName = "[" + this.itemName + "]";
            }
            return this.decoratedName;
        }

        public <N extends ScopedConfigurationNode<N>> void saveTo(final N node) throws SerializationException {
            MAPPER.save(this, node);
        }

    }

    @ConfigSerializable
    static class Section {

        private String name;
        private UUID id;

        // the ObjectMapper resolves settings based on fields -- these methods are provided as a convenience
        public String name() {
            return this.name;
        }

        public UUID id() {
            return this.id;
        }

    }

}
Clone this wiki locally