Add 'addSerializerFactory' method to properties

The factory method gives access to a SerializerContext object which is
not accessible if 'addSerializer' is used.
dev
Exlll 2 years ago
parent f0c76d5c5a
commit 37ad956d8e

@ -550,6 +550,8 @@ chosen according to the following precedence rules:
referenced by the annotation is selected.
2. Otherwise, if the `ConfigurationProperties` contain a serializer for the type in question, that
serializer is returned.
* Serializers created by factories that were added through `addSerializerFactory` for some type
take precedence over serializers added by `addSerializer` for the same type.
3. Otherwise, if this library defines a serializer for that type, that serializer is selected.
4. Ultimately, if no serializer can be found, an exception is thrown.
@ -637,16 +639,14 @@ with `@Configuration`, or if you don't like how one of the supported types is se
you can write your own custom serializer.
Serializers are instances of the `de.exlll.configlib.Serializer` interface. When implementing that
interface you have to make sure that...
* your class either has a constructor with no parameters or one with exactly one parameter of
type [`SerializerContext`](#the-serializercontext-interface), and
* you convert your source type into one of the valid target types listed
in [type conversion](#type-conversion-and-serializer-selection) section.
interface you have to make sure that you convert your source type into one of the valid target types
listed in [type conversion](#type-conversion-and-serializer-selection) section.
The serializer then has to be registered through a `ConfigurationProperties` object or alternatively
be applied to a configuration element
with [the `@SerializeWith` annotation](#the-serializewith-annotation).
be applied to a configuration element with [`@SerializeWith`](#the-serializewith-annotation). If you
want to use the `@SerializeWith` annotation, your serializer class must either have a constructor
with no parameters or one with exactly one parameter of
type [`SerializerContext`](#the-serializercontext-interface).
The following `Serializer` serializes instances of `java.awt.Point` into strings and vice versa.
@ -675,11 +675,33 @@ YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder(
##### The `SerializerContext` interface
Instead of a no-args constructor custom serializers are allowed to declare a constructor with one
parameter of type `SerializerContext`. If such a constructor exists, an instance of that class is
passed to it when the serializer is instantiated by this library. The context object gives access
to the configuration properties, configuration element, and the annotated type for which the
serializer was selected.
Instances of the `SerializerContext` interface contain contextual information for custom
serializers. A context object gives access to the configuration properties, configuration element,
and the annotated type for which the serializer was selected.
The context object can be accessed when adding a serializer factory through
the `addSerializerFactory` method:
```java
public final class PointSerializer implements Serializer<Point, String> {
private final SerializerContext context;
public PointSerializer(SerializerContext context) {
this.context = context;
}
// implementation ...
}
```
```java
YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder()
.addSerializerFactory(Point.class, PointSerializer::new)
.build();
```
Custom serializers used with `@SerializeWith` are allowed to declare a constructor with one
parameter of type `SerializerContext`. If such a constructor exists, a context object is passed to
it when the serializer is instantiated by this library.
### Changing the type of configuration elements

@ -5,6 +5,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
import static de.exlll.configlib.Validator.requireNonNull;
@ -14,6 +15,8 @@ import static de.exlll.configlib.Validator.requireNonNull;
*/
class ConfigurationProperties {
private final Map<Class<?>, Serializer<?, ?>> serializersByType;
private final Map<Class<?>, Function<? super SerializerContext, ? extends Serializer<?, ?>>>
serializerFactoriesByType;
private final Map<Predicate<? super Type>, Serializer<?, ?>> serializersByCondition;
private final NameFormatter formatter;
private final FieldFilter filter;
@ -29,6 +32,7 @@ class ConfigurationProperties {
*/
protected ConfigurationProperties(Builder<?> builder) {
this.serializersByType = Map.copyOf(builder.serializersByType);
this.serializerFactoriesByType = Map.copyOf(builder.serializerFactoriesByType);
this.serializersByCondition = Collections.unmodifiableMap(new LinkedHashMap<>(
builder.serializersByCondition
));
@ -76,6 +80,8 @@ class ConfigurationProperties {
*/
public static abstract class Builder<B extends Builder<B>> {
private final Map<Class<?>, Serializer<?, ?>> serializersByType = new HashMap<>();
private final Map<Class<?>, Function<? super SerializerContext, ? extends Serializer<?, ?>>>
serializerFactoriesByType = new HashMap<>();
private final Map<Predicate<? super Type>, Serializer<?, ?>> serializersByCondition =
new LinkedHashMap<>();
private NameFormatter formatter = NameFormatters.IDENTITY;
@ -88,6 +94,7 @@ class ConfigurationProperties {
protected Builder(ConfigurationProperties properties) {
this.serializersByType.putAll(properties.serializersByType);
this.serializerFactoriesByType.putAll(properties.serializerFactoriesByType);
this.serializersByCondition.putAll(properties.serializersByCondition);
this.formatter = properties.formatter;
this.filter = properties.filter;
@ -124,15 +131,20 @@ class ConfigurationProperties {
}
/**
* Adds a serializer for the given type. If this library already provides a serializer
* for the given type (e.g. {@code BigInteger}, {@code LocalDate}, etc.) the serializer
* added by this method takes precedence.
* Adds a serializer for the given type.
* <p>
* If this library already provides a serializer for the given type (e.g. {@code BigInteger},
* {@code LocalDate}, etc.) the serializer added by this method takes precedence.
* <p>
* If a factory is added via the {@link #addSerializerFactory(Class, Function)} method for
* the same type, the serializer created by that factory takes precedence.
*
* @param serializedType the class of the type that is serialized
* @param serializer the serializer
* @param <T> the type that is serialized
* @return this builder
* @throws NullPointerException if any argument is null
* @see #addSerializerFactory(Class, Function)
*/
public final <T> B addSerializer(Class<T> serializedType, Serializer<T, ?> serializer) {
requireNonNull(serializedType, "serialized type");
@ -141,6 +153,32 @@ class ConfigurationProperties {
return getThis();
}
/**
* Adds a serializer factory for the given type.
* <p>
* If this library already provides a serializer for the given type (e.g. {@code BigInteger},
* {@code LocalDate}, etc.) the serializer created by the factory takes precedence.
* <p>
* If a serializer is added via {@link #addSerializer(Class, Serializer)} method for the
* same type, the serializer created by the factory added by this method takes precedence.
*
* @param serializedType the class of the type that is serialized
* @param serializerFactory the factory that creates a new serializer
* @param <T> the type that is serialized
* @return this builder
* @throws NullPointerException if any argument is null
* @see #addSerializer(Class, Serializer)
*/
public final <T> B addSerializerFactory(
Class<T> serializedType,
Function<? super SerializerContext, ? extends Serializer<T, ?>> serializerFactory
) {
requireNonNull(serializedType, "serialized type");
requireNonNull(serializerFactory, "serializer factory");
serializerFactoriesByType.put(serializedType, serializerFactory);
return getThis();
}
/**
* Adds a serializer for the condition. The serializer is selected when the condition
* evaluates to true. The {@code test} method of the condition object is invoked with
@ -249,6 +287,17 @@ class ConfigurationProperties {
return serializersByType;
}
/**
* Returns an unmodifiable map of serializer factories by type. The serializers created by the
* factories take precedence over any default serializers provided by this library.
*
* @return serializer factories by type
*/
public final Map<Class<?>, Function<? super SerializerContext, ? extends Serializer<?, ?>>>
getSerializerFactories() {
return serializerFactoriesByType;
}
/**
* Returns an unmodifiable map of serializers by condition.
*
@ -258,7 +307,6 @@ class ConfigurationProperties {
return serializersByCondition;
}
/**
* Returns whether null values should be output.
*
@ -277,7 +325,6 @@ class ConfigurationProperties {
return inputNulls;
}
/**
* Returns whether sets should be serialized as lists.
*

@ -115,6 +115,8 @@ final class SerializerSelector {
// Serializer registered for Type via configurations properties
final Type type = annotatedType.getType();
if (type instanceof Class<?> cls) {
if (properties.getSerializerFactories().containsKey(cls))
return newSerializerFromFactory(annotatedType, cls);
if (properties.getSerializers().containsKey(cls))
return properties.getSerializers().get(cls);
}
@ -127,6 +129,17 @@ final class SerializerSelector {
return null;
}
private Serializer<?, ?> newSerializerFromFactory(AnnotatedType annotatedType, Class<?> cls) {
final var context = new SerializerContextImpl(properties, element, annotatedType);
final var factory = properties.getSerializerFactories().get(cls);
final var serializer = factory.apply(context);
if (serializer == null) {
String msg = "Serializer factories must not return null.";
throw new ConfigurationException(msg);
}
return serializer;
}
private Serializer<?, ?> selectForClass(AnnotatedType annotatedType) {
final Class<?> cls = (Class<?>) annotatedType.getType();
if (DEFAULT_SERIALIZERS.containsKey(cls))

@ -1,6 +1,7 @@
package de.exlll.configlib;
import de.exlll.configlib.Serializers.StringSerializer;
import de.exlll.configlib.TestUtils.PointSerializer;
import org.junit.jupiter.api.Test;
import java.awt.Point;
@ -14,6 +15,19 @@ import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertThrows;
class ConfigurationPropertiesTest {
private static final NameFormatter FORMATTER = String::toLowerCase;
private static final FieldFilter FILTER = field -> field.getName().startsWith("f");
private static final PointSerializer SERIALIZER = new PointSerializer();
private static final Predicate<? super Type> PREDICATE = type -> true;
private static final ConfigurationProperties.Builder<?> BUILDER = ConfigurationProperties.newBuilder()
.addSerializer(Point.class, SERIALIZER)
.addSerializerFactory(Point.class, ignored -> SERIALIZER)
.addSerializerByCondition(PREDICATE, SERIALIZER)
.setNameFormatter(FORMATTER)
.setFieldFilter(FILTER)
.outputNulls(true)
.inputNulls(true)
.serializeSetsAsLists(false);
@Test
void builderDefaultValues() {
@ -23,6 +37,7 @@ class ConfigurationPropertiesTest {
assertThat(properties.outputNulls(), is(false));
assertThat(properties.inputNulls(), is(false));
assertThat(properties.getSerializers().entrySet(), empty());
assertThat(properties.getSerializerFactories().entrySet(), empty());
assertThat(properties.getSerializersByCondition().entrySet(), empty());
assertThat(properties.getNameFormatter(), is(NameFormatters.IDENTITY));
assertThat(properties.getFieldFilter(), is(FieldFilters.DEFAULT));
@ -30,71 +45,48 @@ class ConfigurationPropertiesTest {
@Test
void builderCopiesValues() {
NameFormatter formatter = String::toLowerCase;
FieldFilter filter = field -> field.getName().startsWith("f");
TestUtils.PointSerializer serializer = new TestUtils.PointSerializer();
Predicate<? super Type> predicate = type -> true;
ConfigurationProperties properties = ConfigurationProperties.newBuilder()
.addSerializer(Point.class, serializer)
.addSerializerByCondition(predicate, serializer)
.setNameFormatter(formatter)
.setFieldFilter(filter)
.outputNulls(true)
.inputNulls(true)
.serializeSetsAsLists(false)
.build();
assertThat(properties.getSerializers(), is(Map.of(Point.class, serializer)));
assertThat(properties.getSerializersByCondition(), is(Map.of(predicate, serializer)));
assertThat(properties.outputNulls(), is(true));
assertThat(properties.inputNulls(), is(true));
assertThat(properties.serializeSetsAsLists(), is(false));
assertThat(properties.getNameFormatter(), sameInstance(formatter));
assertThat(properties.getFieldFilter(), sameInstance(filter));
ConfigurationProperties properties = BUILDER.build();
assertConfigurationProperties(properties);
}
@Test
void builderCtorCopiesValues() {
NameFormatter formatter = String::toLowerCase;
FieldFilter filter = field -> field.getName().startsWith("f");
TestUtils.PointSerializer serializer = new TestUtils.PointSerializer();
Predicate<? super Type> predicate = type -> true;
ConfigurationProperties properties = ConfigurationProperties.newBuilder()
.addSerializer(Point.class, serializer)
.addSerializerByCondition(predicate, serializer)
.setNameFormatter(formatter)
.setFieldFilter(filter)
.outputNulls(true)
.inputNulls(true)
.serializeSetsAsLists(false)
.build()
.toBuilder()
.build();
assertThat(properties.getSerializers(), is(Map.of(Point.class, serializer)));
assertThat(properties.getSerializersByCondition(), is(Map.of(predicate, serializer)));
ConfigurationProperties properties = BUILDER.build().toBuilder().build();
assertConfigurationProperties(properties);
}
private static void assertConfigurationProperties(ConfigurationProperties properties) {
assertThat(properties.getSerializers(), is(Map.of(Point.class, SERIALIZER)));
assertThat(properties.getSerializersByCondition(), is(Map.of(PREDICATE, SERIALIZER)));
assertThat(properties.outputNulls(), is(true));
assertThat(properties.inputNulls(), is(true));
assertThat(properties.serializeSetsAsLists(), is(false));
assertThat(properties.getNameFormatter(), sameInstance(formatter));
assertThat(properties.getFieldFilter(), sameInstance(filter));
assertThat(properties.getNameFormatter(), sameInstance(FORMATTER));
assertThat(properties.getFieldFilter(), sameInstance(FILTER));
var factories = properties.getSerializerFactories();
assertThat(factories.size(), is(1));
assertThat(factories.get(Point.class).apply(null), is(SERIALIZER));
}
@Test
void builderSerializersUnmodifiable() {
ConfigurationProperties properties = ConfigurationProperties.newBuilder().build();
var serializersByType = properties.getSerializers();
var serializersFactoriesByType = properties.getSerializerFactories();
var serializersByCondition = properties.getSerializersByCondition();
assertThrows(
UnsupportedOperationException.class,
() -> serializersByType.put(Point.class, new TestUtils.PointSerializer())
() -> serializersByType.put(Point.class, new PointSerializer())
);
assertThrows(
UnsupportedOperationException.class,
() -> serializersFactoriesByType.put(Point.class, ignored -> new PointSerializer())
);
assertThrows(
UnsupportedOperationException.class,
() -> serializersByCondition.put(t -> true, new TestUtils.PointSerializer())
() -> serializersByCondition.put(t -> true, new PointSerializer())
);
}
@ -130,6 +122,19 @@ class ConfigurationPropertiesTest {
);
}
@Test
void addSerializerFactoryByTypeRequiresNonNull() {
assertThrowsNullPointerException(
() -> builder.addSerializerFactory(null, ignored -> new StringSerializer()),
"serialized type"
);
assertThrowsNullPointerException(
() -> builder.addSerializerFactory(String.class, null),
"serializer factory"
);
}
@Test
void addSerializerByConditionRequiresNonNull() {
assertThrowsNullPointerException(

@ -23,6 +23,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import java.util.function.Predicate;
import static de.exlll.configlib.TestUtils.*;
@ -269,6 +270,61 @@ class SerializerSelectorTest {
assertThat(bigIntegerSerializer, sameInstance(CUSTOM_BIG_INTEGER_SERIALIZER));
}
@Test
void selectSerializerFactoryByCustomType() {
final var configurationElement = findByType(Point.class);
Function<SerializerContext, Serializer<Point, ?>> factory = ctx -> {
assertThat(ctx.element(), is(configurationElement));
assertThat(ctx.annotatedType(), is(configurationElement.annotatedType()));
return POINT_SERIALIZER;
};
var properties = ConfigurationProperties.newBuilder()
.addSerializerFactory(Point.class, factory)
.build();
SerializerSelector selector = new SerializerSelector(properties);
var pointSerializer = selector.select(configurationElement);
assertThat(pointSerializer, sameInstance(POINT_SERIALIZER));
}
@Test
void selectSerializerFactoryByCustomTypeTakesPrecedence() {
var properties = ConfigurationProperties.newBuilder()
.addSerializerFactory(BigInteger.class, ignored -> CUSTOM_BIG_INTEGER_SERIALIZER)
.build();
SerializerSelector selector = new SerializerSelector(properties);
var bigIntegerSerializer = selector.select(findByType(BigInteger.class));
assertThat(bigIntegerSerializer, instanceOf(TestUtils.CustomBigIntegerSerializer.class));
assertThat(bigIntegerSerializer, sameInstance(CUSTOM_BIG_INTEGER_SERIALIZER));
}
@Test
void selectSerializerFactoryTakesPrecedence() {
var serializer1 = IdentifiableSerializer.of(1);
var serializer2 = IdentifiableSerializer.of(2);
var properties = ConfigurationProperties.newBuilder()
.addSerializer(int.class, serializer1)
.addSerializerFactory(int.class, ignored -> serializer2)
.build();
SerializerSelector selector = new SerializerSelector(properties);
var serializer = selector.select(findByType(int.class));
assertThat(serializer, instanceOf(IdentifiableSerializer.class));
assertThat(serializer, sameInstance(serializer2));
}
@Test
void selectSerializerFactoryRequiresNonNull() {
var properties = ConfigurationProperties.newBuilder()
.addSerializerFactory(Point.class, ignored -> null)
.build();
SerializerSelector selector = new SerializerSelector(properties);
assertThrowsConfigurationException(
() -> selector.select(findByType(Point.class)),
"Serializer factories must not return null."
);
}
@Test
void selectSerializerByCondition() {
var properties = ConfigurationProperties.newBuilder()
@ -291,7 +347,7 @@ class SerializerSelectorTest {
}
@Test
void selectSerializerByCustomTypeTakesPrecedenceOverCustomType() {
void selectSerializerByCustomTypeTakesPrecedenceOverCondition() {
var serializer1 = IdentifiableSerializer.of(1);
var serializer2 = IdentifiableSerializer.of(2);
var properties = ConfigurationProperties.newBuilder()

Loading…
Cancel
Save