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> * <p>
* For correct deserialization, if an instance of polymorphic type (or one of its implementations / * 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 * 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> * <pre>
* {@code * {@code
@ -39,6 +40,8 @@ import java.lang.annotation.Target;
* List<B> bs = List.of(new Impl1(...), new Impl2(...), ...); * List<B> bs = List.of(new Impl1(...), new Impl2(...), ...);
* } * }
* </pre> * </pre>
*
* @see PolymorphicTypes
*/ */
@Target(ElementType.TYPE) @Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)

@ -1,36 +1,77 @@
package de.exlll.configlib; package de.exlll.configlib;
import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
final class PolymorphicSerializer implements Serializer<Object, Map<?, ?>> { final class PolymorphicSerializer implements Serializer<Object, Map<?, ?>> {
private final SerializerContext context; private final SerializerContext context;
private final Class<?> polymorphicType; 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) { public PolymorphicSerializer(SerializerContext context) {
this.context = context; this.context = context;
// we know it's a class because of SerializerSelector#findMetaSerializerOnType // we know it's a class because of SerializerSelector#findMetaSerializerOnType
this.polymorphicType = (Class<?>) context.annotatedType().getType(); this.polymorphicType = (Class<?>) context.annotatedType().getType();
this.annotation = polymorphicType.getAnnotation(Polymorphic.class); this.polymorphic = polymorphicType.getAnnotation(Polymorphic.class);
requireNonBlankProperty(); requireNonBlankProperty();
initAliases();
} }
private void requireNonBlankProperty() { private void requireNonBlankProperty() {
if (annotation.property().isBlank()) { if (polymorphic.property().isBlank()) {
String msg = "The @Polymorphic annotation does not allow a blank property name but " + String msg = "The @Polymorphic annotation does not allow a blank property name but " +
"type '%s' uses one.".formatted(polymorphicType.getName()); "type '%s' uses one.".formatted(polymorphicType.getName());
throw new ConfigurationException(msg); 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 @Override
public Map<?, ?> serialize(Object element) { 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 // this cast won't cause any exceptions as we only pass objects of types the
// serializer expects // serializer expects
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
final var serializer = (TypeSerializer<Object, ?>) TypeSerializer.newSerializerFor( final var serializer = (TypeSerializer<Object, ?>) TypeSerializer.newSerializerFor(
element.getClass(), elementType,
context.properties() context.properties()
); );
final var serialization = serializer.serialize(element); final var serialization = serializer.serialize(element);
@ -38,34 +79,43 @@ final class PolymorphicSerializer implements Serializer<Object, Map<?, ?>> {
requireSerializationNotContainsProperty(serialization); requireSerializationNotContainsProperty(serialization);
final var result = new LinkedHashMap<>(); final var result = new LinkedHashMap<>();
result.put(annotation.property(), element.getClass().getName()); result.put(polymorphic.property(), getTypeIdentifierByType(elementType));
result.putAll(serialization); result.putAll(serialization);
return result; return result;
} }
private String getTypeIdentifierByType(Class<?> type) {
final String alias = aliasByType.get(type);
return (alias == null) ? type.getName() : alias;
}
private void requireSerializationNotContainsProperty(Map<?, ?> serialization) { 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 " + String msg = ("Polymorphic serialization for type '%s' failed. The type contains a " +
"configuration element with name '%s' but that name is " + "configuration element with name '%s' but that name is " +
"used by the @Polymorphic property.") "used by the @Polymorphic property.")
.formatted(polymorphicType.getName(), annotation.property()); .formatted(polymorphicType.getName(), polymorphic.property());
throw new ConfigurationException(msg); throw new ConfigurationException(msg);
} }
} }
@Override @Override
public Object deserialize(Map<?, ?> element) { public Object deserialize(Map<?, ?> element) {
requirePropertyPresent(element); requirePropertyPresent(element);
final var typeIdentifier = element.get(annotation.property()); final var typeIdentifier = element.get(polymorphic.property());
requireTypeIdentifierString(typeIdentifier); requireTypeIdentifierString(typeIdentifier);
Class<?> type = tryFindClass((String) typeIdentifier); final var type = getTypeByTypeIdentifier((String) typeIdentifier);
TypeSerializer<?, ?> serializer = TypeSerializer.newSerializerFor(type, context.properties()); final var serializer = TypeSerializer.newSerializerFor(type, context.properties());
return serializer.deserialize(element); 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) { private Class<?> tryFindClass(String className) {
try { try {
return Reflect.getClassByName(className); return Reflect.getClassByName(className);
@ -78,7 +128,7 @@ final class PolymorphicSerializer implements Serializer<Object, Map<?, ?>> {
} }
private void requirePropertyPresent(Map<?, ?> element) { private void requirePropertyPresent(Map<?, ?> element) {
if (element.get(annotation.property()) != null) if (element.get(polymorphic.property()) != null)
return; return;
String msg = """ String msg = """
Polymorphic deserialization for type '%s' failed. \ Polymorphic deserialization for type '%s' failed. \
@ -88,7 +138,7 @@ final class PolymorphicSerializer implements Serializer<Object, Map<?, ?>> {
""" """
.formatted( .formatted(
polymorphicType.getName(), polymorphicType.getName(),
annotation.property(), polymorphic.property(),
element element
); );
throw new ConfigurationException(msg); 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; 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 * 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 { static final class PolymorphicSerializerPropertyTest {
private static final String CUSTOM_PROPERTY = DEFAULT_PROPERTY + DEFAULT_PROPERTY; private static final String CUSTOM_PROPERTY = DEFAULT_PROPERTY + DEFAULT_PROPERTY;
@ -91,8 +196,6 @@ class PolymorphicSerializerTest {
record R(int i) implements A {} record R(int i) implements A {}
record S(int i) implements B {}
record Config(A a, B b, List<A> as, List<B> bs) {} record Config(A a, B b, List<A> as, List<B> bs) {}
@Test @Test

Loading…
Cancel
Save