From 30430527a1459a6b139943089cee74da0649bc1f Mon Sep 17 00:00:00 2001 From: Exlll Date: Thu, 18 Aug 2022 04:14:18 +0200 Subject: [PATCH] Allow SerializeWith (meta-)annotation on types --- .../configlib/ConfigurationProperties.java | 5 +- .../de/exlll/configlib/SerializeWith.java | 12 +- .../exlll/configlib/SerializerSelector.java | 96 ++++++++---- .../configlib/SerializerSelectorTest.java | 137 +++++++++++++++++- .../java/de/exlll/configlib/TestUtils.java | 18 ++- 5 files changed, 221 insertions(+), 47 deletions(-) diff --git a/configlib-core/src/main/java/de/exlll/configlib/ConfigurationProperties.java b/configlib-core/src/main/java/de/exlll/configlib/ConfigurationProperties.java index a3ebc47..4b0f5e3 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/ConfigurationProperties.java +++ b/configlib-core/src/main/java/de/exlll/configlib/ConfigurationProperties.java @@ -146,7 +146,10 @@ class ConfigurationProperties { * @throws NullPointerException if any argument is null * @see #addSerializerFactory(Class, Function) */ - public final B addSerializer(Class serializedType, Serializer serializer) { + public final B addSerializer( + Class serializedType, + Serializer serializer + ) { requireNonNull(serializedType, "serialized type"); requireNonNull(serializer, "serializer"); serializersByType.put(serializedType, serializer); diff --git a/configlib-core/src/main/java/de/exlll/configlib/SerializeWith.java b/configlib-core/src/main/java/de/exlll/configlib/SerializeWith.java index 6b77108..7b15f2f 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/SerializeWith.java +++ b/configlib-core/src/main/java/de/exlll/configlib/SerializeWith.java @@ -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; * } * */ -@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 { /** diff --git a/configlib-core/src/main/java/de/exlll/configlib/SerializerSelector.java b/configlib-core/src/main/java/de/exlll/configlib/SerializerSelector.java index 84c3ce0..8d86c21 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/SerializerSelector.java +++ b/configlib-core/src/main/java/de/exlll/configlib/SerializerSelector.java @@ -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> 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> 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> 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> 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> 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> 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) { diff --git a/configlib-core/src/test/java/de/exlll/configlib/SerializerSelectorTest.java b/configlib-core/src/test/java/de/exlll/configlib/SerializerSelectorTest.java index d6d8a0b..bd0868c 100644 --- a/configlib-core/src/test/java/de/exlll/configlib/SerializerSelectorTest.java +++ b/configlib-core/src/test/java/de/exlll/configlib/SerializerSelectorTest.java @@ -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 { + + @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 { + 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)); + } } } diff --git a/configlib-core/src/testFixtures/java/de/exlll/configlib/TestUtils.java b/configlib-core/src/testFixtures/java/de/exlll/configlib/TestUtils.java index 2d825f1..4b25a38 100644 --- a/configlib-core/src/testFixtures/java/de/exlll/configlib/TestUtils.java +++ b/configlib-core/src/testFixtures/java/de/exlll/configlib/TestUtils.java @@ -127,17 +127,19 @@ public final class TestUtils { } } - public static final class IdentitySerializer implements Serializer { - @Override - public Object serialize(Object element) { - return element; - } + public record IdentitySerializer(SerializerContext context) + implements Serializer { @Override - public Object deserialize(Object element) { - return element; + public Object serialize(Object element) { + return element; + } + + @Override + public Object deserialize(Object element) { + return element; + } } - } @SafeVarargs