diff --git a/configlib-core/src/main/java/de/exlll/configlib/Polymorphic.java b/configlib-core/src/main/java/de/exlll/configlib/Polymorphic.java
index 71c0bb3..3efbdc9 100644
--- a/configlib-core/src/main/java/de/exlll/configlib/Polymorphic.java
+++ b/configlib-core/src/main/java/de/exlll/configlib/Polymorphic.java
@@ -13,7 +13,8 @@ import java.lang.annotation.Target;
*
* 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.
*
*
* {@code
@@ -39,6 +40,8 @@ import java.lang.annotation.Target;
* List bs = List.of(new Impl1(...), new Impl2(...), ...);
* }
*
+ *
+ * @see PolymorphicTypes
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
diff --git a/configlib-core/src/main/java/de/exlll/configlib/PolymorphicSerializer.java b/configlib-core/src/main/java/de/exlll/configlib/PolymorphicSerializer.java
index 6624f67..bd75308 100644
--- a/configlib-core/src/main/java/de/exlll/configlib/PolymorphicSerializer.java
+++ b/configlib-core/src/main/java/de/exlll/configlib/PolymorphicSerializer.java
@@ -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> {
private final SerializerContext context;
private final Class> polymorphicType;
- private final Polymorphic annotation;
+ private final Polymorphic polymorphic;
+ private final Map> typeByAlias = new HashMap<>();
+ private final Map, 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) TypeSerializer.newSerializerFor(
- element.getClass(),
+ elementType,
context.properties()
);
final var serialization = serializer.serialize(element);
@@ -38,34 +79,43 @@ final class PolymorphicSerializer implements Serializer> {
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> {
}
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> {
"""
.formatted(
polymorphicType.getName(),
- annotation.property(),
+ polymorphic.property(),
element
);
throw new ConfigurationException(msg);
diff --git a/configlib-core/src/main/java/de/exlll/configlib/PolymorphicTypes.java b/configlib-core/src/main/java/de/exlll/configlib/PolymorphicTypes.java
new file mode 100644
index 0000000..1a59186
--- /dev/null
+++ b/configlib-core/src/main/java/de/exlll/configlib/PolymorphicTypes.java
@@ -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.
+ *
+ *
+ * {@code
+ * @Polymorphic
+ * @PolymorphicTypes({
+ * @PolymorphicTypes.Type(type = Impl1.class, alias = "IMPL_1"),
+ * @PolymorphicTypes.Type(type = Impl2.class, alias = "IMPL_2")
+ * }
+ * })
+ * {@code
+ * interface A { ... }
+ *
+ * record Impl1(...) implements A { ... }
+ * record Impl2(...) implements A { ... }
+ *
+ * List as = List.of(new Impl1(...), new Impl2(...), ...);
+ * }
+ *
+ *
+ * @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 "";
+ }
+}
diff --git a/configlib-core/src/main/java/de/exlll/configlib/SerializeWith.java b/configlib-core/src/main/java/de/exlll/configlib/SerializeWith.java
index a689ec2..5da2294 100644
--- a/configlib-core/src/main/java/de/exlll/configlib/SerializeWith.java
+++ b/configlib-core/src/main/java/de/exlll/configlib/SerializeWith.java
@@ -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
diff --git a/configlib-core/src/test/java/de/exlll/configlib/PolymorphicSerializerTest.java b/configlib-core/src/test/java/de/exlll/configlib/PolymorphicSerializerTest.java
index 8815cb3..0221f15 100644
--- a/configlib-core/src/test/java/de/exlll/configlib/PolymorphicSerializerTest.java
+++ b/configlib-core/src/test/java/de/exlll/configlib/PolymorphicSerializerTest.java
@@ -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 as) {}
+
+ @Test
+ void serializeUsesTypeAliasIfPresent() {
+ @SuppressWarnings("unchecked")
+ var serializer = (ListSerializer )
+ 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 as, List bs) {}
@Test