From 89a2e9057b6bbde60468611bdf758fb1382b0f80 Mon Sep 17 00:00:00 2001 From: Exlll Date: Sat, 3 Feb 2024 10:33:16 +0100 Subject: [PATCH] Add support for post-processing via annotated method --- .../configlib/ConfigurationSerializer.java | 2 +- .../de/exlll/configlib/RecordSerializer.java | 3 +- .../main/java/de/exlll/configlib/Reflect.java | 9 + .../de/exlll/configlib/TypeSerializer.java | 63 ++++ .../ConfigurationSerializerTest.java | 87 ++++++ .../exlll/configlib/RecordSerializerTest.java | 51 ++++ .../java/de/exlll/configlib/ReflectTest.java | 12 + .../exlll/configlib/TypeSerializerTest.java | 285 ++++++++++++++++++ 8 files changed, 510 insertions(+), 2 deletions(-) diff --git a/configlib-core/src/main/java/de/exlll/configlib/ConfigurationSerializer.java b/configlib-core/src/main/java/de/exlll/configlib/ConfigurationSerializer.java index 2210879..d19ab3e 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/ConfigurationSerializer.java +++ b/configlib-core/src/main/java/de/exlll/configlib/ConfigurationSerializer.java @@ -33,7 +33,7 @@ final class ConfigurationSerializer extends TypeSerializer { } } - return result; + return postProcessor.apply(result); } @Override diff --git a/configlib-core/src/main/java/de/exlll/configlib/RecordSerializer.java b/configlib-core/src/main/java/de/exlll/configlib/RecordSerializer.java index b5bc463..fbca874 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/RecordSerializer.java +++ b/configlib-core/src/main/java/de/exlll/configlib/RecordSerializer.java @@ -39,7 +39,8 @@ final class RecordSerializer extends TypeSerializer> protected final ConfigurationProperties properties; protected final NameFormatter formatter; protected final Map> serializers; + protected final UnaryOperator postProcessor; protected TypeSerializer(Class type, ConfigurationProperties properties) { this.type = requireNonNull(type, "type"); this.properties = requireNonNull(properties, "configuration properties"); this.formatter = properties.getNameFormatter(); this.serializers = buildSerializerMap(); + this.postProcessor = createPostProcessorFromAnnotatedMethod(); requireSerializableElements(); } @@ -94,6 +101,62 @@ sealed abstract class TypeSerializer> return deserialized; } + final UnaryOperator createPostProcessorFromAnnotatedMethod() { + final List list = Arrays.stream(type.getDeclaredMethods()) + .filter(Predicate.not(Method::isSynthetic)) + .filter(method -> method.isAnnotationPresent(PostProcess.class)) + .toList(); + + if (list.isEmpty()) + return UnaryOperator.identity(); + if (list.size() > 1) { + String methodNames = String.join("\n ", list.stream().map(Method::toString).toList()); + String msg = "Configuration types must not define more than one method for " + + "post-processing but type '%s' defines %d:\n %s" + .formatted(type, list.size(), methodNames); + throw new ConfigurationException(msg); + } + + final Method method = list.get(0); + final int modifiers = method.getModifiers(); + if (Modifier.isAbstract(modifiers) || Modifier.isStatic(modifiers)) { + String msg = "Post-processing methods must be neither abstract nor static, " + + "but post-processing method '%s' of type '%s' is." + .formatted(method, type); + throw new ConfigurationException(msg); + } + + final int parameterCount = method.getParameterCount(); + if (parameterCount > 0) { + String msg = "Post-processing methods must not define any parameters but " + + "post-processing method '%s' of type '%s' defines %d." + .formatted(method, type, parameterCount); + throw new ConfigurationException(msg); + } + + final Class returnType = method.getReturnType(); + if ((returnType != void.class) && (returnType != type)) { + String msg = "The return type of post-processing methods must either be 'void' or " + + "the same type as the configuration type in which the post-processing " + + "method is defined. The return type of the post-processing method of " + + "type '%s' is neither 'void' nor '%s'." + .formatted(type, type.getSimpleName()); + throw new ConfigurationException(msg); + } + + return object -> { + if (method.getReturnType() == void.class) { + Reflect.invoke(method, object); + return object; + } + // The following cast won't fail because our last check above guarantees + // that the return type of the method equals T at this point. + @SuppressWarnings("unchecked") + T result = (T) Reflect.invoke(method, object); + return result; + }; + } + protected abstract void requireSerializableElements(); protected abstract String baseDeserializeExceptionMessage(E element, Object value); diff --git a/configlib-core/src/test/java/de/exlll/configlib/ConfigurationSerializerTest.java b/configlib-core/src/test/java/de/exlll/configlib/ConfigurationSerializerTest.java index cd157a9..2805085 100644 --- a/configlib-core/src/test/java/de/exlll/configlib/ConfigurationSerializerTest.java +++ b/configlib-core/src/test/java/de/exlll/configlib/ConfigurationSerializerTest.java @@ -224,4 +224,91 @@ class ConfigurationSerializerTest { static final class B8 extends B7 { int j = 2; } + + @Configuration + static final class B9 { + int i; + + @PostProcess + private B9 postProcess() { + B9 b = new B9(); + b.i = i + 20; + return b; + } + } + + @Configuration + static final class B10 { + int i; + + @PostProcess + private void postProcess() { + i += 20; + } + } + + @Test + void postProcessorIsAppliedInClassDeserializer() { + B9 b9 = newSerializer(B9.class).deserialize(Map.of( + "i", 50 + )); + assertThat(b9.i, is(70)); + + + B10 b10 = newSerializer(B10.class).deserialize(Map.of( + "i", 10 + )); + assertThat(b10.i, is(30)); + } + + + @Configuration + static class B11 { + int k; + + @PostProcess + void postProcess() { + k = k * 4; + } + } + + @Configuration + static class B12 { + int j; + B11 b11; + + @PostProcess + void postProcess() { + j = j * 3; + b11.k += 1; + } + } + + @Configuration + static class B13 { + int i; + B12 b12; + + @PostProcess + void postProcess() { + i = i * 2; + b12.j += 1; + b12.b11.k *= 2; + } + } + + @Test + void postProcessNestedClasses() { + B13 b13 = newSerializer(B13.class).deserialize(Map.of( + "i", 1, + "b12", Map.of( + "j", 2, + "b11", Map.of("k", 3) + ) + )); + + assertThat(b13.i, is(2)); + assertThat(b13.b12.j, is(7)); + assertThat(b13.b12.b11.k, is(26)); + } } \ No newline at end of file diff --git a/configlib-core/src/test/java/de/exlll/configlib/RecordSerializerTest.java b/configlib-core/src/test/java/de/exlll/configlib/RecordSerializerTest.java index 2394a9d..b5698ab 100644 --- a/configlib-core/src/test/java/de/exlll/configlib/RecordSerializerTest.java +++ b/configlib-core/src/test/java/de/exlll/configlib/RecordSerializerTest.java @@ -229,4 +229,55 @@ class RecordSerializerTest { assertThat(r.i, is(10)); assertThat(r.s, is("s")); } + + @Test + void postProcessorIsAppliedInRecordDeserializer() { + record R(int i, String s) { + @PostProcess + private R postProcess() { + return new R(i + 20, s.repeat(2)); + } + } + + R r = newSerializer(R.class).deserialize(Map.of( + "i", 10, + "s", "AB" + )); + assertThat(r.i, is(30)); + assertThat(r.s, is("ABAB")); + } + + @Test + void postProcessNestedRecords() { + record R3(int k) { + @PostProcess + R3 postProcess() { + return new R3(k * 4); + } + } + record R2(int j, R3 r3) { + @PostProcess + R2 postProcess() { + return new R2(j * 3, new R3(r3.k + 1)); + } + } + record R1(int i, R2 r2) { + @PostProcess + R1 postProcess() { + return new R1(i * 2, new R2(r2.j + 1, new R3(r2.r3.k * 2))); + } + } + + R1 r1 = newSerializer(R1.class).deserialize(Map.of( + "i", 1, + "r2", Map.of( + "j", 2, + "r3", Map.of("k", 3) + ) + )); + + assertThat(r1.i, is(2)); + assertThat(r1.r2.j, is(7)); + assertThat(r1.r2.r3.k, is(26)); + } } \ No newline at end of file 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 95f8897..afd9771 100644 --- a/configlib-core/src/test/java/de/exlll/configlib/ReflectTest.java +++ b/configlib-core/src/test/java/de/exlll/configlib/ReflectTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.params.provider.ValueSource; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -435,4 +436,15 @@ class ReflectTest { "Constructor of type 'R1' with parameters 'int, float' threw an exception." ); } + + private String someString() { + return "AB"; + } + + @Test + void invokeMethod() throws Exception { + Method method = ReflectTest.class.getDeclaredMethod("someString"); + Object object = Reflect.invoke(method, new ReflectTest()); + assertThat(object, is("AB")); + } } \ No newline at end of file diff --git a/configlib-core/src/test/java/de/exlll/configlib/TypeSerializerTest.java b/configlib-core/src/test/java/de/exlll/configlib/TypeSerializerTest.java index 041e6f9..eead155 100644 --- a/configlib-core/src/test/java/de/exlll/configlib/TypeSerializerTest.java +++ b/configlib-core/src/test/java/de/exlll/configlib/TypeSerializerTest.java @@ -13,6 +13,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import static de.exlll.configlib.TestUtils.assertThrowsConfigurationException; @@ -176,4 +177,288 @@ class TypeSerializerTest { "Recursive type definitions are not supported." ); } + + @Test + void atMost1PostProcessMethodAllowed() { + @Configuration + class A { + int i; + + @PostProcess + void postProcessA() {} + + @PostProcess + void postProcessB() {} + } + + record R1(int i) { + @PostProcess + void postProcessA() {} + + @PostProcess + void postProcessB() {} + } + + assertThrowsConfigurationException( + () -> newTypeSerializer(A.class), + """ + Configuration types must not define more than one method for post-processing \ + but type 'class de.exlll.configlib.TypeSerializerTest$1A' defines 2: + void de.exlll.configlib.TypeSerializerTest$1A.postProcessA() + void de.exlll.configlib.TypeSerializerTest$1A.postProcessB()\ + """ + ); + assertThrowsConfigurationException( + () -> newTypeSerializer(R1.class), + """ + Configuration types must not define more than one method for post-processing \ + but type 'class de.exlll.configlib.TypeSerializerTest$1R1' defines 2: + void de.exlll.configlib.TypeSerializerTest$1R1.postProcessA() + void de.exlll.configlib.TypeSerializerTest$1R1.postProcessB()\ + """ + ); + } + + @Test + void postProcessMustNotBeStaticOrAbstract() { + @Configuration + class B { + int i; + + @PostProcess + static void postProcess() {} + } + @Configuration + abstract class C { + int i; + + @PostProcess + abstract void postProcess(); + } + + record R2(int i) { + @PostProcess + static void postProcess() {} + } + assertThrowsConfigurationException( + () -> newTypeSerializer(B.class), + """ + Post-processing methods must be neither abstract nor static, but post-processing \ + method 'static void de.exlll.configlib.TypeSerializerTest$1B.postProcess()' of \ + type 'class de.exlll.configlib.TypeSerializerTest$1B' is.\ + """ + ); + assertThrowsConfigurationException( + () -> newTypeSerializer(C.class), + """ + Post-processing methods must be neither abstract nor static, but post-processing \ + method 'abstract void de.exlll.configlib.TypeSerializerTest$1C.postProcess()' of \ + type 'class de.exlll.configlib.TypeSerializerTest$1C' is.\ + """ + ); + assertThrowsConfigurationException( + () -> newTypeSerializer(R2.class), + """ + Post-processing methods must be neither abstract nor static, but post-processing \ + method 'static void de.exlll.configlib.TypeSerializerTest$1R2.postProcess()' of \ + type 'class de.exlll.configlib.TypeSerializerTest$1R2' is.\ + """ + ); + } + + @Test + void postProcessMustNotHaveArguments() { + @Configuration + class D { + int i; + + @PostProcess + void postProcess(int j, int k) {} + } + + record R4(int i) { + @PostProcess + void postProcess(int l) {} + } + + assertThrowsConfigurationException( + () -> newTypeSerializer(D.class), + """ + Post-processing methods must not define any parameters but post-processing method \ + 'void de.exlll.configlib.TypeSerializerTest$1D.postProcess(int,int)' of type \ + 'class de.exlll.configlib.TypeSerializerTest$1D' defines 2.\ + """ + ); + assertThrowsConfigurationException( + () -> newTypeSerializer(R4.class), + """ + Post-processing methods must not define any parameters but post-processing method \ + 'void de.exlll.configlib.TypeSerializerTest$1R4.postProcess(int)' of type \ + 'class de.exlll.configlib.TypeSerializerTest$1R4' defines 1.\ + """ + ); + } + + @Test + void postProcessMustReturnVoidOrSameType() { + @Configuration + class E { + int i; + + @PostProcess + E postProcess() {return null;} + } + class F extends E { + @Override + @PostProcess + E postProcess() {return null;} + } + class G extends E { + @Override + @PostProcess + G postProcess() { + return null; + } + } + + // both of these are okay: + newTypeSerializer(E.class); + newTypeSerializer(G.class); + + assertThrowsConfigurationException( + () -> newTypeSerializer(F.class), + """ + The return type of post-processing methods must either be 'void' or the same \ + type as the configuration type in which the post-processing method is defined. \ + The return type of the post-processing method of \ + type 'class de.exlll.configlib.TypeSerializerTest$1F' is neither 'void' nor 'F'.\ + """ + ); + } + + @Test + void postProcessorInvokesAnnotatedMethodWithVoidReturnType1() { + final AtomicInteger integer = new AtomicInteger(0); + + @Configuration + class H1 { + int i; + + @PostProcess + void postProcess() {integer.set(20);} + } + + final var serializer = newTypeSerializer(H1.class); + final var postProcessor = serializer.createPostProcessorFromAnnotatedMethod(); + + final H1 h1_1 = new H1(); + final H1 h1_2 = postProcessor.apply(h1_1); + + assertThat(h1_2, sameInstance(h1_2)); + assertThat(integer.get(), is(20)); + } + + static int postProcessorInvokesAnnotatedMethodWithVoidReturnType2_int = 0; + + @Test + void postProcessorInvokesAnnotatedMethodWithVoidReturnType2() { + record H2(int j) { + @PostProcess + void postProcess() { + postProcessorInvokesAnnotatedMethodWithVoidReturnType2_int += 10; + } + } + + final var serializer = newTypeSerializer(H2.class); + final var postProcessor = serializer.createPostProcessorFromAnnotatedMethod(); + + final H2 h2_1 = new H2(10); + final H2 h2_2 = postProcessor.apply(h2_1); + + assertThat(h2_2, sameInstance(h2_1)); + assertThat(postProcessorInvokesAnnotatedMethodWithVoidReturnType2_int, is(10)); + } + + @Test + void postProcessorInvokesAnnotatedMethodWithSameReturnType1() { + @Configuration + class H3 { + int i; + + @PostProcess + H3 postProcess() { + H3 h3 = new H3(); + h3.i = i + 20; + return h3; + } + } + + final var serializer = newTypeSerializer(H3.class); + final var postProcessor = serializer.createPostProcessorFromAnnotatedMethod(); + + final H3 h3_1 = new H3(); + h3_1.i = 10; + final H3 h3_2 = postProcessor.apply(h3_1); + + assertThat(h3_2, not(sameInstance(h3_1))); + assertThat(h3_1.i, is(10)); + assertThat(h3_2.i, is(30)); + } + + @Test + void postProcessorInvokesAnnotatedMethodWithSameReturnType2() { + record H4(int i) { + @PostProcess + H4 postProcess() { + return new H4(i + 20); + } + } + + final var serializer = newTypeSerializer(H4.class); + final var postProcessor = serializer.createPostProcessorFromAnnotatedMethod(); + + final H4 h4_1 = new H4(10); + final H4 h4_2 = postProcessor.apply(h4_1); + + assertThat(h4_2, not(sameInstance(h4_1))); + assertThat(h4_1.i, is(10)); + assertThat(h4_2.i, is(30)); + } + + @Test + void postProcessorIsIdentityFunctionIfNoPostProcessAnnotationPresent() { + @Configuration + class J { + int i; + } + final var serializer = newTypeSerializer(J.class); + final var postProcessor = serializer.createPostProcessorFromAnnotatedMethod(); + + final J j_1 = new J(); + final J j_2 = postProcessor.apply(j_1); + assertThat(j_2, sameInstance(j_1)); + } + + @Test + void postProcessOfParentClassNotCalled() { + @Configuration + class A { + int i = 10; + + @PostProcess + void postProcess() { + this.i = this.i + 10; + } + } + + class B extends A {} + + final var serializer = newTypeSerializer(B.class); + final var postProcessor = serializer.createPostProcessorFromAnnotatedMethod(); + + + B b = new B(); + postProcessor.apply(b); + assertThat(b.i, is(10)); + } }