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.
dev
Exlll 3 years ago
parent c78f4b2901
commit bb58025239

@ -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> T callConstructor(Class<T> cls, Class<?>[] argumentTypes, Object... arguments) {
try {
Constructor<T> 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<Constructor<?>> predicate =
ctor -> Arrays.equals(ctor.getParameterTypes(), argumentTypes);
return Arrays.stream(type.getDeclaredConstructors()).anyMatch(predicate);
}
static <T> T callNoParamConstructor(Class<T> cls) {
try {
Constructor<T> 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) {

@ -0,0 +1,39 @@
package de.exlll.configlib;
import java.lang.reflect.AnnotatedType;
/**
* Instances of this class provide contextual information for custom serializers.
* <p>
* 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();
}

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

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

@ -22,6 +22,15 @@ import java.util.stream.Stream;
final class Serializers {
private Serializers() {}
static <S extends Serializer<?, ?>> S newCustomSerializer(
Class<S> 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<Boolean, Boolean> {
@Override
public Boolean serialize(Boolean element) {

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

@ -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<String> 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<String, String> {
@Override
public String serialize(String element) {return null;}
@Override
public String deserialize(String element) {return null;}
}
}

@ -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<String, Integer> {
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<String, Integer> 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));
}
}

@ -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

Loading…
Cancel
Save