Add support for post-processing via annotated method

dev
Exlll 1 year ago
parent ef6fb19651
commit 89a2e9057b

@ -33,7 +33,7 @@ final class ConfigurationSerializer<T> extends TypeSerializer<T, FieldElement> {
}
}
return result;
return postProcessor.apply(result);
}
@Override

@ -39,7 +39,8 @@ final class RecordSerializer<R> extends TypeSerializer<R, RecordComponentElement
}
}
return Reflect.callCanonicalConstructor(type, constructorArguments);
final R result = Reflect.callCanonicalConstructor(type, constructorArguments);
return postProcessor.apply(result);
}
@Override

@ -242,4 +242,13 @@ final class Reflect {
throw new RuntimeException(e);
}
}
static Object invoke(Method method, Object object, Object... arguments) {
try {
method.setAccessible(true);
return method.invoke(object, arguments);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}

@ -1,8 +1,13 @@
package de.exlll.configlib;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import static de.exlll.configlib.Validator.requireNonNull;
@ -14,12 +19,14 @@ sealed abstract class TypeSerializer<T, E extends ConfigurationElement<?>>
protected final ConfigurationProperties properties;
protected final NameFormatter formatter;
protected final Map<String, Serializer<?, ?>> serializers;
protected final UnaryOperator<T> postProcessor;
protected TypeSerializer(Class<T> 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<T, E extends ConfigurationElement<?>>
return deserialized;
}
final UnaryOperator<T> createPostProcessorFromAnnotatedMethod() {
final List<Method> 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);

@ -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));
}
}

@ -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));
}
}

@ -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"));
}
}

@ -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));
}
}

Loading…
Cancel
Save