diff --git a/README.md b/README.md index 849fbc6..ae32b1e 100644 --- a/README.md +++ b/README.md @@ -223,9 +223,9 @@ public final class UnsupportedTypes { ``` **NOTE:** Even though this library does not support these types, it is still possible to serialize -them by providing a custom serializer via -[the `@SerializeWith` annotation](#the-serializewith-annotation). That serializer then has to -be applied to top-level type (i.e. `nesting` must be set to `0`, which is the default). +them by providing a custom serializer via the [`@SerializeWith`](#the-serializewith-annotation) +annotation. That serializer then has to be applied to top-level type (i.e. `nesting` must be set +to `0`, which is the default). @@ -644,6 +644,43 @@ Map> map; +#### The `@Polymorphic` annotation + +The `@Polymorphic` annotation indicates that the annotated type is polymorphic. Serializers for +polymorphic types are not selected based on the compile-time types of configuration elements, but +instead are chosen at runtime based on the actual types of their values. + +This enables adding instances of subclasses / implementations of a polymorphic type to collections. +The subtypes must be valid configurations. + +```java +@Polymorphic +@Configuration +static abstract class A { ... } + +static final class Impl1 extends A { ... } +static final class Impl2 extends A { ... } + +List as = List.of(new Impl1(...), new Impl2(...), ...); +``` + +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. By default, that type information is the Java class name of the actual type. It is +possible to provide type aliases by using the `PolymorphicTypes` annotation. + +```java +@Polymorphic +@PolymorphicTypes({ + @PolymorphicTypes.Type(type = Impl1.class, alias = "IMPL_1"), + @PolymorphicTypes.Type(type = Impl2.class, alias = "IMPL_2") +}) +interface B { ... } + +record Impl1(...) implements B { ... } +record Impl2(...) implements B { ... } +``` + ### Custom serializers If you want to add support for a type that is not a Java record or whose class is not annotated @@ -691,8 +728,8 @@ Instances of the `SerializerContext` interface contain contextual information fo serializers. A context object gives access to the configuration properties, configuration element, and the annotated type for which the serializer was selected. -The context object can be accessed when adding a serializer factory through -the `addSerializerFactory` method: +Context objects can be obtained when adding serializer factories through the `addSerializerFactory` +method: ```java public final class PointSerializer implements Serializer { diff --git a/configlib-core/src/main/java/de/exlll/configlib/ConfigurationProperties.java b/configlib-core/src/main/java/de/exlll/configlib/ConfigurationProperties.java index 4b0f5e3..9aedeb9 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/ConfigurationProperties.java +++ b/configlib-core/src/main/java/de/exlll/configlib/ConfigurationProperties.java @@ -162,8 +162,9 @@ class ConfigurationProperties { * If this library already provides a serializer for the given type (e.g. {@code BigInteger}, * {@code LocalDate}, etc.) the serializer created by the factory takes precedence. *

- * If a serializer is added via {@link #addSerializer(Class, Serializer)} method for the - * same type, the serializer created by the factory added by this method takes precedence. + * If a serializer is added via the {@link #addSerializer(Class, Serializer)} method + * for the same type, the serializer created by the factory that was added by this + * method takes precedence. * * @param serializedType the class of the type that is serialized * @param serializerFactory the factory that creates a new serializer 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 5da2294..0e64c5e 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/SerializeWith.java +++ b/configlib-core/src/main/java/de/exlll/configlib/SerializeWith.java @@ -6,15 +6,18 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * Indicates that the annotated configuration element or type should be serialized using the - * referenced serializer. + * Indicates that the annotated configuration element or type should be serialized using + * an instance of the referenced serializer. The serializer referenced by this annotation + * is selected regardless of whether the type of configuration element or annotated type + * matches the type the serializer expects. *

- * If this annotation is applied to a configuration element, and that element is an array, list, - * set, or map a nesting level can be set to apply the serializer not to the top-level type but to - * its elements. For maps, the serializer is applied to the values and not the keys. + * If this annotation is applied to a configuration element, and that element is an array, + * list, set, or map a nesting level can be set to apply the serializer not to the + * top-level type but to its elements. For maps, the serializer is applied to the values + * and not the keys. *

- * The following example shows how {@code nesting} can be used to apply the serializer at - * different levels. + * The following examples show how {@code nesting} can be used to apply the serializer to + * configuration elements at different levels. * *

  * {@code
@@ -43,6 +46,21 @@ import java.lang.annotation.Target;
  * Map> map;
  * }
  * 
+ *

+ * If instead the annotation is applied to a non-generic type, then, whenever that exact + * type (i.e. not a subtype or implementation) is encountered, the referenced serializer + * is selected. + *

+ * {@code
+ * @SerializeWith(serializer = MyClassSerializer.class)
+ * public final class MyClass { ... }
+ * }
+ * 
+ *

+ * Similarly, this annotation can be used as a meta-annotation on other {@code ElementType.TYPE} + * annotations. In these cases, the serializer is selected whenever a type annotated with + * the meta-annotated annotation is found. An example for this is the {@link Polymorphic} + * annotation. */ @Target({ ElementType.ANNOTATION_TYPE, // usage as meta-annotation @@ -62,8 +80,11 @@ public @interface SerializeWith { /** * Returns the nesting level at which to apply the serializer. *

- * If this annotation is applied to a type or another annotation, the value - * returned by this method has no effect. + * Setting {@code nesting} to an invalid value, i.e. a negative one or one that is greater than + * the number of levels the element actually has, results in the serializer not being selected. + *

+ * If this annotation is applied to a type or another annotation, the value returned by this + * method has no effect. * * @return the nesting level */ diff --git a/configlib-core/src/main/java/de/exlll/configlib/SerializerContext.java b/configlib-core/src/main/java/de/exlll/configlib/SerializerContext.java index 231f759..892775a 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/SerializerContext.java +++ b/configlib-core/src/main/java/de/exlll/configlib/SerializerContext.java @@ -1,17 +1,39 @@ package de.exlll.configlib; import java.lang.reflect.AnnotatedType; +import java.util.function.Function; /** * Instances of this class provide contextual information for custom serializers. + * References to such instances can be obtained when adding serializer factories through + * the {@link ConfigurationProperties.Builder#addSerializerFactory(Class, Function)} + * method. *

- * 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. + * Custom serializers used with {@code @SerializeWith} are allowed to declare a constructor + * with one parameter of type {@code SerializerContext}. If such a constructor exists, a + * context object is injected into it when the serializer is instantiated. + * + *

+ * {@code
+ * public final class PointSerializer implements Serializer {
+ *     private final SerializerContext context;
+ *
+ *     public PointSerializer(SerializerContext context) {
+ *         this.context = context;
+ *     }
+ *     // implementation ...
+ * }
+ *
+ * YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder()
+ *         .addSerializerFactory(Point.class, PointSerializer::new)
+ *         .build();
+ * }
+ * 
*/ public interface SerializerContext { /** - * Returns the {@code ConfigurationProperties} object in use when the serializer was selected. + * Returns the {@code ConfigurationProperties} object in use when the serializer was + * selected. * * @return properties object in use when the serializer was selected */ @@ -25,11 +47,11 @@ public interface SerializerContext { ConfigurationElement element(); /** - * 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 ConfigurationElement#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. + * 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 ConfigurationElement#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 */