From bb58025239e9b4ca0e6d8f40b5c785e94c5689df Mon Sep 17 00:00:00 2001 From: Exlll Date: Wed, 10 Aug 2022 11:26:26 +0200 Subject: [PATCH] Add SerializerContext interface Instances of this interface contain information about the context in which a serializer was selected. They are passed to the constructors of custom serializers, if the serializer classes define such a constructor. --- .../main/java/de/exlll/configlib/Reflect.java | 42 +++++++++-- .../de/exlll/configlib/SerializerContext.java | 39 +++++++++++ .../configlib/SerializerContextImpl.java | 18 +++++ .../exlll/configlib/SerializerSelector.java | 15 ++-- .../java/de/exlll/configlib/Serializers.java | 9 +++ .../java/de/exlll/configlib/ReflectTest.java | 70 +++++++++++++++++++ .../configlib/SerializerSelectorTest.java | 57 ++++++++++++++- .../de/exlll/configlib/SerializersTest.java | 37 ++++++++++ .../de/exlll/configlib/TypeComponentTest.java | 4 +- 9 files changed, 277 insertions(+), 14 deletions(-) create mode 100644 configlib-core/src/main/java/de/exlll/configlib/SerializerContext.java create mode 100644 configlib-core/src/main/java/de/exlll/configlib/SerializerContextImpl.java diff --git a/configlib-core/src/main/java/de/exlll/configlib/Reflect.java b/configlib-core/src/main/java/de/exlll/configlib/Reflect.java index 8550661..d984fc1 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/Reflect.java +++ b/configlib-core/src/main/java/de/exlll/configlib/Reflect.java @@ -5,6 +5,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -38,6 +39,40 @@ final class Reflect { return defaultValue; } + static T callConstructor(Class cls, Class[] argumentTypes, Object... arguments) { + try { + Constructor constructor = cls.getDeclaredConstructor(argumentTypes); + constructor.setAccessible(true); + return constructor.newInstance(arguments); + } catch (NoSuchMethodException e) { + String msg = "Type '%s' doesn't have a constructor with parameters: %s." + .formatted(cls.getSimpleName(), argumentTypeNamesJoined(argumentTypes)); + throw new RuntimeException(msg, e); + } catch (IllegalAccessException e) { + // cannot happen because we set the constructor to be accessible. + throw new RuntimeException(e); + } catch (InstantiationException e) { + String msg = "Type '%s' is not instantiable.".formatted(cls.getSimpleName()); + throw new RuntimeException(msg, e); + } catch (InvocationTargetException e) { + String msg = "Constructor of type '%s' with parameters '%s' threw an exception." + .formatted(cls.getSimpleName(), argumentTypeNamesJoined(argumentTypes)); + throw new RuntimeException(msg, e); + } + } + + private static String argumentTypeNamesJoined(Class[] argumentTypes) { + return Arrays.stream(argumentTypes) + .map(Class::getName) + .collect(Collectors.joining(", ")); + } + + static boolean hasConstructor(Class type, Class... argumentTypes) { + final Predicate> predicate = + ctor -> Arrays.equals(ctor.getParameterTypes(), argumentTypes); + return Arrays.stream(type.getDeclaredConstructors()).anyMatch(predicate); + } + static T callNoParamConstructor(Class cls) { try { Constructor constructor = cls.getDeclaredConstructor(); @@ -113,11 +148,8 @@ final class Reflect { } static boolean hasDefaultConstructor(Class type) { - for (Constructor constructor : type.getDeclaredConstructors()) { - if (constructor.getParameterCount() == 0) - return true; - } - return false; + return Arrays.stream(type.getDeclaredConstructors()) + .anyMatch(ctor -> ctor.getParameterCount() == 0); } static Object getValue(Field field, Object instance) { diff --git a/configlib-core/src/main/java/de/exlll/configlib/SerializerContext.java b/configlib-core/src/main/java/de/exlll/configlib/SerializerContext.java new file mode 100644 index 0000000..504f32b --- /dev/null +++ b/configlib-core/src/main/java/de/exlll/configlib/SerializerContext.java @@ -0,0 +1,39 @@ +package de.exlll.configlib; + +import java.lang.reflect.AnnotatedType; + +/** + * Instances of this class provide contextual information for custom serializers. + *

+ * Custom serializers classes are allowed to declare a constructor with one parameter of + * type {@code SerializerContext}. If such a constructor exists, an instance of this class is + * passed to it when the serializer is instantiated by this library. + */ +public interface SerializerContext { + /** + * Returns the {@code ConfigurationProperties} object in use when the serializer was selected. + * + * @return properties object in use when the serializer was selected + */ + ConfigurationProperties properties(); + + /** + * Returns the {@code TypeComponent} (i.e. the field or record component) which led to the + * selection of the serializer. + * + * @return component which led to the selection of the serializer + */ + TypeComponent component(); + + /** + * Returns the {@code AnnotatedType} which led to the selection of the serializer. The annotated + * type returned by this method might be different from the one returned by + * {@link TypeComponent#annotatedType()}. Specifically, the type is different when the + * serializer is applied to a nested type via {@link SerializeWith} in which case the annotated + * type represents the type at that nesting level. + * + * @return annotated type which led to the selection of the serializer + */ + AnnotatedType annotatedType(); +} + diff --git a/configlib-core/src/main/java/de/exlll/configlib/SerializerContextImpl.java b/configlib-core/src/main/java/de/exlll/configlib/SerializerContextImpl.java new file mode 100644 index 0000000..115fe12 --- /dev/null +++ b/configlib-core/src/main/java/de/exlll/configlib/SerializerContextImpl.java @@ -0,0 +1,18 @@ +package de.exlll.configlib; + +import java.lang.reflect.AnnotatedType; + +import static de.exlll.configlib.Validator.requireNonNull; + +record SerializerContextImpl( + ConfigurationProperties properties, + TypeComponent component, + AnnotatedType annotatedType +) implements SerializerContext { + SerializerContextImpl { + properties = requireNonNull(properties, "configuration properties"); + component = requireNonNull(component, "type component"); + annotatedType = requireNonNull(annotatedType, "annotated type"); + } +} + 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 753a8fb..856f07b 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/SerializerSelector.java +++ b/configlib-core/src/main/java/de/exlll/configlib/SerializerSelector.java @@ -51,7 +51,11 @@ final class SerializerSelector { ); private final ConfigurationProperties properties; /** - * Holds the {@code SerializeWith} value of the last {@literal select}ed component. If the + * Holds the last {@link #select}ed component. + */ + private TypeComponent component; + /** + * Holds the {@code SerializeWith} value of the last {@link #select}ed component. If the * component is not annotated with {@code SerializeWith}, the value of this field is null. */ private SerializeWith serializeWith; @@ -69,8 +73,9 @@ final class SerializerSelector { } public Serializer select(TypeComponent component) { - this.currentNesting = -1; + this.component = component; this.serializeWith = component.annotation(SerializeWith.class); + this.currentNesting = -1; return selectForType(component.annotatedType()); } @@ -102,8 +107,10 @@ final class SerializerSelector { private Serializer selectCustomSerializer(AnnotatedType annotatedType) { // SerializeWith annotation - if ((serializeWith != null) && (currentNesting == serializeWith.nesting())) - return Reflect.callNoParamConstructor(serializeWith.serializer()); + if ((serializeWith != null) && (currentNesting == serializeWith.nesting())) { + final var context = new SerializerContextImpl(properties, component, annotatedType); + return Serializers.newCustomSerializer(serializeWith.serializer(), context); + } // Serializer registered for Type via configurations properties final Type type = annotatedType.getType(); diff --git a/configlib-core/src/main/java/de/exlll/configlib/Serializers.java b/configlib-core/src/main/java/de/exlll/configlib/Serializers.java index 214caf6..69f49f1 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/Serializers.java +++ b/configlib-core/src/main/java/de/exlll/configlib/Serializers.java @@ -22,6 +22,15 @@ import java.util.stream.Stream; final class Serializers { private Serializers() {} + static > S newCustomSerializer( + Class serializerType, + SerializerContext context + ) { + return Reflect.hasConstructor(serializerType, SerializerContext.class) + ? Reflect.callConstructor(serializerType, new Class[]{SerializerContext.class}, context) + : Reflect.callNoParamConstructor(serializerType); + } + static final class BooleanSerializer implements Serializer { @Override public Boolean serialize(Boolean element) { diff --git a/configlib-core/src/test/java/de/exlll/configlib/ReflectTest.java b/configlib-core/src/test/java/de/exlll/configlib/ReflectTest.java index 3b110b0..cff2f62 100644 --- a/configlib-core/src/test/java/de/exlll/configlib/ReflectTest.java +++ b/configlib-core/src/test/java/de/exlll/configlib/ReflectTest.java @@ -347,4 +347,74 @@ class ReflectTest { assertFalse(Reflect.hasDefaultConstructor(R1.class)); assertTrue(Reflect.hasDefaultConstructor(R2.class)); } + + + @Test + void hasConstructor1() { + record R1() {} + + assertTrue(Reflect.hasConstructor(R1.class)); + assertFalse(Reflect.hasConstructor(R1.class, int.class)); + } + + @Test + void hasConstructor2() { + record R1(int i) {} + + assertFalse(Reflect.hasConstructor(R1.class)); + assertTrue(Reflect.hasConstructor(R1.class, int.class)); + assertFalse(Reflect.hasConstructor(R1.class, int.class, float.class)); + } + + @Test + void hasConstructor3() { + record R1(int i, float f) {} + + assertFalse(Reflect.hasConstructor(R1.class)); + assertFalse(Reflect.hasConstructor(R1.class, int.class)); + assertTrue(Reflect.hasConstructor(R1.class, int.class, float.class)); + } + + @Test + void callConstructor1() { + record R1(int i) {} + R1 r1 = Reflect.callConstructor(R1.class, new Class[]{int.class}, 10); + assertEquals(10, r1.i); + } + + @Test + void callConstructor2() { + record R1(int i, float f) {} + R1 r1 = Reflect.callConstructor(R1.class, new Class[]{int.class, float.class}, 10, 20f); + assertEquals(10, r1.i); + assertEquals(20, r1.f); + } + + @Test + void callMissingConstructor() { + record R1(int i, float f) {} + assertThrowsRuntimeException( + () -> Reflect.callConstructor( + R1.class, + new Class[]{int.class, float.class, String.class}, + 10, 20f, "" + ), + "Type 'R1' doesn't have a constructor with parameters: int, float, java.lang.String." + ); + } + + @Test + void callThrowingConstructor() { + record R1(int i, float f) { + R1 {throw new RuntimeException("");} + } + assertThrowsRuntimeException( + () -> Reflect.callConstructor( + R1.class, + new Class[]{int.class, float.class}, + 10, 20f + ), + "Constructor of type 'R1' with parameters 'int, float' threw an exception." + ); + } } \ No newline at end of file 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 c9e2833..fd1df41 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.reflect.AnnotatedParameterizedType; import java.lang.reflect.Field; import java.math.BigDecimal; import java.math.BigInteger; @@ -30,9 +31,9 @@ import static org.hamcrest.Matchers.*; @SuppressWarnings("unused") class SerializerSelectorTest { - private static final SerializerSelector SELECTOR = new SerializerSelector( - ConfigurationProperties.newBuilder().build() - ); + private static final ConfigurationProperties DEFAULT_PROPS = + ConfigurationProperties.newBuilder().build(); + private static final SerializerSelector SELECTOR = new SerializerSelector(DEFAULT_PROPS); private static final SerializerSelector SELECTOR_POINT = new SerializerSelector( ConfigurationProperties.newBuilder().addSerializer(Point.class, POINT_SERIALIZER).build() ); @@ -587,5 +588,55 @@ class SerializerSelectorTest { var serializer = (ListSerializer) SELECTOR.select(forField(A.class, "list")); assertThat(serializer.getElementSerializer(), instanceOf(StringSerializer.class)); } + + @Test + void selectCustomSerializerWithContext() { + class A { + @SerializeWith(serializer = SerializerWithContext.class) + String s; + } + + var component = forField(A.class, "s"); + var field = getField(A.class, "s"); + var serializer = (SerializerWithContext) SELECTOR.select(component); + var context = serializer.ctx; + + assertThat(context.properties(), sameInstance(DEFAULT_PROPS)); + assertThat(context.component(), is(component)); + assertThat(context.annotatedType(), is(field.getAnnotatedType())); + } + + @Test + void selectCustomSerializerWithContextAndNesting() { + class A { + @SerializeWith(serializer = SerializerWithContext.class, nesting = 1) + List l; + } + + var component = forField(A.class, "l"); + var field = getField(A.class, "l"); + var outerSerializer = (ListSerializer) SELECTOR.select(component); + var innerSerializer = (SerializerWithContext) outerSerializer.getElementSerializer(); + var context = innerSerializer.ctx; + + assertThat(context.properties(), sameInstance(DEFAULT_PROPS)); + assertThat(context.component(), is(component)); + + var annotatedType = (AnnotatedParameterizedType) field.getAnnotatedType(); + var argument = annotatedType.getAnnotatedActualTypeArguments()[0]; + + 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;} } } diff --git a/configlib-core/src/test/java/de/exlll/configlib/SerializersTest.java b/configlib-core/src/test/java/de/exlll/configlib/SerializersTest.java index f320cb6..85eed7c 100644 --- a/configlib-core/src/test/java/de/exlll/configlib/SerializersTest.java +++ b/configlib-core/src/test/java/de/exlll/configlib/SerializersTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; import java.io.File; import java.math.BigDecimal; @@ -1017,4 +1018,40 @@ class SerializersTest { return element.toString(); } } + + private static final class StringToIntSerializerWithCtx implements Serializer { + private static final StringToIntSerializer INSTANCE = new StringToIntSerializer(); + private final SerializerContext context; + + public StringToIntSerializerWithCtx(SerializerContext context) { + this.context = context; + } + + @Override + public Integer serialize(String element) { + return Integer.valueOf(element); + } + + @Override + public String deserialize(Integer element) { + return element.toString(); + } + } + + @Test + void newCustomSerializerWithoutContext() { + Serializer serializer = + Serializers.newCustomSerializer(StringToIntSerializer.class, null); + assertThat(serializer, instanceOf(StringToIntSerializer.class)); + } + + @Test + void newCustomSerializerWithContext() { + SerializerContext ctx = Mockito.mock(SerializerContext.class); + StringToIntSerializerWithCtx serializer = + Serializers.newCustomSerializer(StringToIntSerializerWithCtx.class, ctx); + assertThat(serializer, instanceOf(StringToIntSerializerWithCtx.class)); + assertThat(serializer.context, sameInstance(ctx)); + } + } \ No newline at end of file diff --git a/configlib-core/src/test/java/de/exlll/configlib/TypeComponentTest.java b/configlib-core/src/test/java/de/exlll/configlib/TypeComponentTest.java index 8680c2c..626bbf3 100644 --- a/configlib-core/src/test/java/de/exlll/configlib/TypeComponentTest.java +++ b/configlib-core/src/test/java/de/exlll/configlib/TypeComponentTest.java @@ -35,7 +35,7 @@ class TypeComponentTest { @Test void componentAnnotatedType() { - assertThat(COMPONENT.annotatedType(), equalTo(FIELD.getAnnotatedType())); + assertThat(COMPONENT.annotatedType(), is(FIELD.getAnnotatedType())); } @Test @@ -73,7 +73,7 @@ class TypeComponentTest { @Test void componentAnnotatedType() { - assertThat(COMPONENT.annotatedType(), equalTo(RECORD_COMPONENT.getAnnotatedType())); + assertThat(COMPONENT.annotatedType(), is(RECORD_COMPONENT.getAnnotatedType())); } @Test