Add PolymorphicTypes annotation

This annotation can be used to provide type aliases for subtypes which
are then used instead of Java class names.
dev
Exlll 3 years ago
parent 9f4999c726
commit 482e9464a9

@ -13,7 +13,8 @@ import java.lang.annotation.Target;
* <p>
* For correct deserialization, if an instance of polymorphic type (or one of its implementations /
* subclasses) is serialized, an additional property that holds type information is added to its
* serialization.
* serialization. The type information can be customized using the {@link PolymorphicTypes}
* annotation.
*
* <pre>
* {@code
@ -39,6 +40,8 @@ import java.lang.annotation.Target;
* List<B> bs = List.of(new Impl1(...), new Impl2(...), ...);
* }
* </pre>
*
* @see PolymorphicTypes
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)

@ -1,36 +1,77 @@
package de.exlll.configlib;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
final class PolymorphicSerializer implements Serializer<Object, Map<?, ?>> {
private final SerializerContext context;
private final Class<?> polymorphicType;
private final Polymorphic annotation;
private final Polymorphic polymorphic;
private final Map<String, Class<?>> typeByAlias = new HashMap<>();
private final Map<Class<?>, String> aliasByType = new HashMap<>();
public PolymorphicSerializer(SerializerContext context) {
this.context = context;
// we know it's a class because of SerializerSelector#findMetaSerializerOnType
this.polymorphicType = (Class<?>) context.annotatedType().getType();
this.annotation = polymorphicType.getAnnotation(Polymorphic.class);
this.polymorphic = polymorphicType.getAnnotation(Polymorphic.class);
requireNonBlankProperty();
initAliases();
}
private void requireNonBlankProperty() {
if (annotation.property().isBlank()) {
if (polymorphic.property().isBlank()) {
String msg = "The @Polymorphic annotation does not allow a blank property name but " +
"type '%s' uses one.".formatted(polymorphicType.getName());
throw new ConfigurationException(msg);
}
}
private void initAliases() {
final var polymorphicTypes = polymorphicType.getAnnotation(PolymorphicTypes.class);
if (polymorphicTypes == null)
return;
for (PolymorphicTypes.Type pType : polymorphicTypes.value()) {
final var type = pType.type();
final var alias = pType.alias().isBlank() ? type.getName() : pType.alias();
requireDistinctAliases(alias);
requireDistinctTypes(type);
typeByAlias.put(alias, type);
aliasByType.put(type, alias);
}
}
private void requireDistinctAliases(String alias) {
if (typeByAlias.containsKey(alias)) {
String msg = "The @PolymorphicTypes annotation must not use the same alias for " +
"multiple types. Alias '%s' appears more than once."
.formatted(alias);
throw new ConfigurationException(msg);
}
}
private void requireDistinctTypes(Class<?> type) {
if (aliasByType.containsKey(type)) {
String msg = "The @PolymorphicTypes annotation must not contain multiple " +
"definitions for the same subtype. Type '%s' appears more than once."
.formatted(type.getName());
throw new ConfigurationException(msg);
}
}
@Override
public Map<?, ?> serialize(Object element) {
final Class<?> elementType = element.getClass();
// this cast won't cause any exceptions as we only pass objects of types the
// serializer expects
@SuppressWarnings("unchecked")
final var serializer = (TypeSerializer<Object, ?>) TypeSerializer.newSerializerFor(
element.getClass(),
elementType,
context.properties()
);
final var serialization = serializer.serialize(element);
@ -38,34 +79,43 @@ final class PolymorphicSerializer implements Serializer<Object, Map<?, ?>> {
requireSerializationNotContainsProperty(serialization);
final var result = new LinkedHashMap<>();
result.put(annotation.property(), element.getClass().getName());
result.put(polymorphic.property(), getTypeIdentifierByType(elementType));
result.putAll(serialization);
return result;
}
private String getTypeIdentifierByType(Class<?> type) {
final String alias = aliasByType.get(type);
return (alias == null) ? type.getName() : alias;
}
private void requireSerializationNotContainsProperty(Map<?, ?> serialization) {
if (serialization.containsKey(annotation.property())) {
if (serialization.containsKey(polymorphic.property())) {
String msg = ("Polymorphic serialization for type '%s' failed. The type contains a " +
"configuration element with name '%s' but that name is " +
"used by the @Polymorphic property.")
.formatted(polymorphicType.getName(), annotation.property());
.formatted(polymorphicType.getName(), polymorphic.property());
throw new ConfigurationException(msg);
}
}
@Override
public Object deserialize(Map<?, ?> element) {
requirePropertyPresent(element);
final var typeIdentifier = element.get(annotation.property());
final var typeIdentifier = element.get(polymorphic.property());
requireTypeIdentifierString(typeIdentifier);
Class<?> type = tryFindClass((String) typeIdentifier);
TypeSerializer<?, ?> serializer = TypeSerializer.newSerializerFor(type, context.properties());
final var type = getTypeByTypeIdentifier((String) typeIdentifier);
final var serializer = TypeSerializer.newSerializerFor(type, context.properties());
return serializer.deserialize(element);
}
private Class<?> getTypeByTypeIdentifier(String typeIdentifier) {
final Class<?> type = typeByAlias.get(typeIdentifier);
return (type == null) ? tryFindClass(typeIdentifier) : type;
}
private Class<?> tryFindClass(String className) {
try {
return Reflect.getClassByName(className);
@ -78,7 +128,7 @@ final class PolymorphicSerializer implements Serializer<Object, Map<?, ?>> {
}
private void requirePropertyPresent(Map<?, ?> element) {
if (element.get(annotation.property()) != null)
if (element.get(polymorphic.property()) != null)
return;
String msg = """
Polymorphic deserialization for type '%s' failed. \
@ -88,7 +138,7 @@ final class PolymorphicSerializer implements Serializer<Object, Map<?, ?>> {
"""
.formatted(
polymorphicType.getName(),
annotation.property(),
polymorphic.property(),
element
);
throw new ConfigurationException(msg);

@ -0,0 +1,64 @@
package de.exlll.configlib;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates the subtypes of {@code Polymorphic} types. This annotation can be used to provide type
* aliases for subtypes which are then used instead of Java class names.
*
* <pre>
* {@code
* @Polymorphic
* @PolymorphicTypes({
* @PolymorphicTypes.Type(type = Impl1.class, alias = "IMPL_1"),
* @PolymorphicTypes.Type(type = Impl2.class, alias = "IMPL_2")
* }
* <code>})</code>
* {@code
* interface A { ... }
*
* record Impl1(...) implements A { ... }
* record Impl2(...) implements A { ... }
*
* List<A> as = List.of(new Impl1(...), new Impl2(...), ...);
* }
* </pre>
*
* @see Polymorphic
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface PolymorphicTypes {
/**
* Returns (possibly only a subset of) the subtypes of the annotated type.
*
* @return subtypes of the annotated type
*/
Type[] value();
/**
* Indicates a subtype of a {@code Polymorphic} type.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Type {
/**
* Returns the class of the subtype.
*
* @return class of the subtype
*/
Class<?> type();
/**
* Returns the alias of the subtype. If the alias returned by this method is blank,
* the Java class name ist used.
*
* @return alias of the subtype
* @see String#isBlank()
*/
String alias() default "";
}
}

@ -1,6 +1,9 @@
package de.exlll.configlib;
import java.lang.annotation.*;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that the annotated configuration element or type should be serialized using the

@ -80,6 +80,111 @@ class PolymorphicSerializerTest {
);
}
static final class PolymorphicTypesTest {
@Polymorphic
@PolymorphicTypes({
@PolymorphicTypes.Type(type = Impl1.class, alias = "IMPL_1"),
@PolymorphicTypes.Type(type = Impl2.class, alias = "IMPL_2"),
@PolymorphicTypes.Type(type = Impl3.class, alias = " "), // blank alias
/* @PolymorphicTypes.Type(type = Impl4.class) */ // missing
})
interface A {}
record Impl1(int i) implements A {}
record Impl2(double d) implements A {}
record Impl3(String s) implements A {}
record Impl4(long l) implements A {}
record Config(List<A> as) {}
@Test
void serializeUsesTypeAliasIfPresent() {
@SuppressWarnings("unchecked")
var serializer = (ListSerializer<A, ?>)
SELECTOR.select(fieldAsElement(Config.class, "as"));
List<?> serialized = serializer.serialize(List.of(
new Impl1(1),
new Impl2(2d),
new Impl3("3"),
new Impl4(4)
));
assertThat(serialized, is(List.of(
asMap("type", "IMPL_1", "i", 1L),
asMap("type", "IMPL_2", "d", 2d),
asMap("type", Impl3.class.getName(), "s", "3"),
asMap("type", Impl4.class.getName(), "l", 4L)
)));
}
@Test
void deserializeUsesTypeAliasIfPresent() {
@SuppressWarnings("unchecked")
var serializer = (ListSerializer<?, Map<?, ?>>)
SELECTOR.select(fieldAsElement(Config.class, "as"));
List<?> serialized = serializer.deserialize(List.of(
asMap("type", "IMPL_1", "i", 1L),
asMap("type", "IMPL_2", "d", 2d),
asMap("type", Impl3.class.getName(), "s", "3"),
asMap("type", Impl4.class.getName(), "l", 4L)
));
assertThat(serialized.size(), is(4));
assertThat(serialized.get(0), is(new Impl1(1)));
assertThat(serialized.get(1), is(new Impl2(2)));
assertThat(serialized.get(2), is(new Impl3("3")));
assertThat(serialized.get(3), is(new Impl4(4)));
}
@Test
void typesMustNotAppearMoreThanOnce() {
@Polymorphic
@PolymorphicTypes({
@PolymorphicTypes.Type(type = Impl1.class, alias = "1a"),
@PolymorphicTypes.Type(type = Impl1.class, alias = "1b")
})
interface E {}
record Config(E e) {}
RuntimeException exception = Assertions.assertThrows(
RuntimeException.class,
() -> SELECTOR.select(fieldAsElement(Config.class, "e"))
);
ConfigurationException configurationException = (ConfigurationException)
exception.getCause().getCause();
Assertions.assertEquals(
"The @PolymorphicTypes annotation must not contain multiple definitions for " +
"the same subtype. Type '%s' appears more than once."
.formatted(Impl1.class.getName()),
configurationException.getMessage()
);
}
@Test
void aliasesMustNotAppearMoreThanOnce() {
@Polymorphic
@PolymorphicTypes({
@PolymorphicTypes.Type(type = Impl1.class, alias = "2"),
@PolymorphicTypes.Type(type = Impl2.class, alias = "2")
})
interface E {}
record Config(E e) {}
RuntimeException exception = Assertions.assertThrows(
RuntimeException.class,
() -> SELECTOR.select(fieldAsElement(Config.class, "e"))
);
ConfigurationException configurationException = (ConfigurationException)
exception.getCause().getCause();
Assertions.assertEquals(
"The @PolymorphicTypes annotation must not use the same alias for multiple " +
"types. Alias '2' appears more than once.",
configurationException.getMessage()
);
}
}
static final class PolymorphicSerializerPropertyTest {
private static final String CUSTOM_PROPERTY = DEFAULT_PROPERTY + DEFAULT_PROPERTY;
@ -91,8 +196,6 @@ class PolymorphicSerializerTest {
record R(int i) implements A {}
record S(int i) implements B {}
record Config(A a, B b, List<A> as, List<B> bs) {}
@Test

Loading…
Cancel
Save