Allow SerializeWith (meta-)annotation on types

dev
Exlll 3 years ago
parent 37ad956d8e
commit 30430527a1

@ -146,7 +146,10 @@ class ConfigurationProperties {
* @throws NullPointerException if any argument is null
* @see #addSerializerFactory(Class, Function)
*/
public final <T> B addSerializer(Class<T> serializedType, Serializer<T, ?> serializer) {
public final <T> B addSerializer(
Class<T> serializedType,
Serializer<? super T, ?> serializer
) {
requireNonNull(serializedType, "serialized type");
requireNonNull(serializer, "serializer");
serializersByType.put(serializedType, serializer);

@ -1,9 +1,6 @@
package de.exlll.configlib;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.*;
/**
* Indicates that the annotated element should be serialized with the given serializer. Serializers
@ -44,7 +41,12 @@ import java.lang.annotation.Target;
* }
* </pre>
*/
@Target({ElementType.FIELD, ElementType.RECORD_COMPONENT})
@Target({
ElementType.ANNOTATION_TYPE, // usage as meta-annotation
ElementType.TYPE, // usage on types
ElementType.FIELD, // usage on configuration elements
ElementType.RECORD_COMPONENT
})
@Retention(RetentionPolicy.RUNTIME)
public @interface SerializeWith {
/**

@ -14,6 +14,7 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import static de.exlll.configlib.Validator.requireNonNull;
@ -54,11 +55,6 @@ final class SerializerSelector {
* Holds the last {@link #select}ed configuration element.
*/
private ConfigurationElement<?> element;
/**
* Holds the {@code SerializeWith} value of the last {@link #select}ed configuration element.
* If the element is not annotated with {@code SerializeWith}, the value of this field is null.
*/
private SerializeWith serializeWith;
/**
* The {@code currentNesting} is used to determine the nesting of a type and is incremented each
* time the {@code selectForType} method is called. It is reset when {@code select} is called.
@ -74,7 +70,6 @@ final class SerializerSelector {
public Serializer<?, ?> select(ConfigurationElement<?> element) {
this.element = element;
this.serializeWith = element.annotation(SerializeWith.class);
this.currentNesting = -1;
return selectForType(element.annotatedType());
}
@ -106,38 +101,87 @@ final class SerializerSelector {
}
private Serializer<?, ?> selectCustomSerializer(AnnotatedType annotatedType) {
// SerializeWith annotation
if ((serializeWith != null) && (currentNesting == serializeWith.nesting())) {
return findConfigurationElementSerializer(annotatedType)
.or(() -> findSerializerFactoryForType(annotatedType))
.or(() -> findSerializerForType(annotatedType))
.or(() -> findSerializerOnType(annotatedType))
.or(() -> findMetaSerializerOnType(annotatedType))
.or(() -> findSerializerByCondition(annotatedType))
.orElse(null);
}
private Optional<Serializer<?, ?>> findConfigurationElementSerializer(AnnotatedType annotatedType) {
// SerializeWith annotation on configuration elements
final var annotation = element.annotation(SerializeWith.class);
if ((annotation != null) && (currentNesting == annotation.nesting())) {
return Optional.of(newSerializerFromAnnotation(annotatedType, annotation));
}
return Optional.empty();
}
private Optional<Serializer<?, ?>> findSerializerFactoryForType(AnnotatedType annotatedType) {
// Serializer factory registered for Type via configurations properties
if ((annotatedType.getType() instanceof Class<?> cls) &&
properties.getSerializerFactories().containsKey(cls)) {
final var context = new SerializerContextImpl(properties, element, annotatedType);
return Serializers.newCustomSerializer(serializeWith.serializer(), context);
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 Optional.of(serializer);
}
return Optional.empty();
}
private Optional<Serializer<?, ?>> findSerializerForType(AnnotatedType annotatedType) {
// 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);
if ((annotatedType.getType() instanceof Class<?> cls) &&
properties.getSerializers().containsKey(cls)) {
return Optional.of(properties.getSerializers().get(cls));
}
return Optional.empty();
}
private Optional<Serializer<?, ?>> findSerializerOnType(AnnotatedType annotatedType) {
// SerializeWith annotation on type
if ((annotatedType.getType() instanceof Class<?> cls) &&
(cls.getDeclaredAnnotation(SerializeWith.class) != null)) {
final var annotation = cls.getDeclaredAnnotation(SerializeWith.class);
return Optional.of(newSerializerFromAnnotation(annotatedType, annotation));
}
return Optional.empty();
}
private Optional<Serializer<?, ?>> findMetaSerializerOnType(AnnotatedType annotatedType) {
// SerializeWith meta annotation on type
if ((annotatedType.getType() instanceof Class<?> cls)) {
for (final var meta : cls.getDeclaredAnnotations()) {
final var metaType = meta.annotationType();
final var annotation = metaType.getDeclaredAnnotation(SerializeWith.class);
if (annotation != null)
return Optional.of(newSerializerFromAnnotation(annotatedType, annotation));
}
}
return Optional.empty();
}
private Optional<Serializer<?, ?>> findSerializerByCondition(AnnotatedType annotatedType) {
// Serializer registered for condition via configurations properties
for (var entry : properties.getSerializersByCondition().entrySet()) {
if (entry.getKey().test(type))
return entry.getValue();
if (entry.getKey().test(annotatedType.getType()))
return Optional.of(entry.getValue());
}
return null;
return Optional.empty();
}
private Serializer<?, ?> newSerializerFromFactory(AnnotatedType annotatedType, Class<?> cls) {
private Serializer<?, ?> newSerializerFromAnnotation(
AnnotatedType annotatedType,
SerializeWith annotation
) {
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;
return Serializers.newCustomSerializer(annotation.serializer(), context);
}
private Serializer<?, ?> selectForClass(AnnotatedType annotatedType) {

@ -8,6 +8,7 @@ import org.junit.jupiter.params.provider.ValueSource;
import java.awt.Point;
import java.io.File;
import java.lang.annotation.*;
import java.lang.reflect.AnnotatedParameterizedType;
import java.lang.reflect.Field;
import java.math.BigDecimal;
@ -512,7 +513,7 @@ class SerializerSelectorTest {
);
}
static final class SerializeWithTests {
static final class SerializeWithOnConfigurationElementsTests {
static class Z {
@SerializeWith(serializer = IdentitySerializer.class)
String string;
@ -684,15 +685,137 @@ class SerializerSelectorTest {
assertThat(context.annotatedType(), is(not(annotatedType)));
assertThat(context.annotatedType(), is(argument));
}
private record SerializerWithContext(SerializerContext ctx)
implements Serializer<String, String> {
@Override
public String serialize(String element) {return null;}
@Override
public String deserialize(String element) {return null;}
}
}
static final class SerializeWithOnTypesTest {
@SerializeWith(serializer = IdentitySerializer.class)
static final class MyType1 {}
@SerializeWith(serializer = IdentitySerializer.class)
static abstract class MyType2 {}
@SerializeWith(serializer = IdentitySerializer.class)
interface MyType3 {}
@SerializeWith(serializer = IdentitySerializer.class)
record MyType4() {}
@SerializeWith(serializer = IdentitySerializer.class)
static class MyType5 {}
static class MyType6 extends MyType5 {}
record Config(
MyType1 myType1,
MyType2 myType2,
MyType3 myType3,
MyType4 myType4,
MyType5 myType5,
MyType6 myType6
) {}
@ParameterizedTest
@ValueSource(strings = {"myType1", "myType2", "myType3", "myType4", "myType5"})
void selectCustomSerializerForTypes(String fieldName) {
var element = forField(Config.class, fieldName);
var serializer = (IdentitySerializer) SELECTOR.select(element);
assertThat(serializer.context().element(), is(element));
}
@Test
void serializeWithNotInherited() {
assertThrowsConfigurationException(
() -> SELECTOR.select(forField(Config.class, "myType6")),
("Missing serializer for type %s.\nEither annotate the type with " +
"@Configuration or provide a custom serializer by adding it to the properties.")
.formatted(MyType6.class)
);
}
@Test
void serializeWithHasLowerPrecedenceThanSerializersAddedViaConfigurationProperties() {
var serializer = new IdentifiableSerializer<>(1);
var properties = ConfigurationProperties.newBuilder()
.addSerializer(MyType1.class, serializer)
.build();
var selector = new SerializerSelector(properties);
var actual = (IdentifiableSerializer<?, ?>) selector.select(forField(Config.class, "myType1"));
assertThat(actual, sameInstance(serializer));
}
}
private record SerializerWithContext(SerializerContext ctx)
implements Serializer<String, String> {
static final class SerializeWithMetaAnnotationTest {
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@SerializeWith(serializer = IdentitySerializer.class)
@interface MetaSerializeWith {}
@MetaSerializeWith
static final class MyType1 {}
@MetaSerializeWith
static abstract class MyType2 {}
@MetaSerializeWith
interface MyType3 {}
@MetaSerializeWith
record MyType4() {}
@MetaSerializeWith
static class MyType5 {}
static class MyType6 extends MyType5 {}
@Override
public String serialize(String element) {return null;}
@MetaSerializeWith
@SerializeWith(serializer = PointSerializer.class)
static final class MyType7 {}
@Override
public String deserialize(String element) {return null;}
record Config(
MyType1 myType1,
MyType2 myType2,
MyType3 myType3,
MyType4 myType4,
MyType5 myType5,
MyType6 myType6,
MyType7 myType7
) {}
@ParameterizedTest
@ValueSource(strings = {"myType1", "myType2", "myType3", "myType4", "myType5"})
void selectCustomSerializerForTypes(String fieldName) {
var element = forField(Config.class, fieldName);
var serializer = (IdentitySerializer) SELECTOR.select(element);
assertThat(serializer.context().element(), is(element));
}
@Test
void metaSerializeWithNotInherited() {
assertThrowsConfigurationException(
() -> SELECTOR.select(forField(Config.class, "myType6")),
("Missing serializer for type %s.\nEither annotate the type with " +
"@Configuration or provide a custom serializer by adding it to the properties.")
.formatted(MyType6.class)
);
}
@Test
void metaSerializeWithHasLowerPrecedenceThanSerializeWith() {
var serializer = SELECTOR.select(forField(Config.class, "myType7"));
assertThat(serializer, instanceOf(PointSerializer.class));
}
}
}

@ -127,17 +127,19 @@ public final class TestUtils {
}
}
public static final class IdentitySerializer implements Serializer<Object, Object> {
@Override
public Object serialize(Object element) {
return element;
}
public record IdentitySerializer(SerializerContext context)
implements Serializer<Object, Object> {
@Override
public Object deserialize(Object element) {
return element;
public Object serialize(Object element) {
return element;
}
@Override
public Object deserialize(Object element) {
return element;
}
}
}
@SafeVarargs

Loading…
Cancel
Save