Add support for polymorphic type handling in Config Beans

Polymorphic type handling here means ability to enable addition of
enough type information so that deserializer can instantiate correct
subtype of a value.
This commit is contained in:
Nemanja Zbiljić 2017-10-15 13:21:07 +02:00
parent 1b7460b6c9
commit c96977bb14
4 changed files with 248 additions and 0 deletions

View File

@ -0,0 +1,52 @@
package com.typesafe.config;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation used with {@link ConfigTypeInfo} to indicate sub-types of
* serializable polymorphic types, and to associate logical names used within
* config (which is more portable than using physical Java class names).
*
* Note that just annotating a property or base type with this annotation does
* NOT enable polymorphic type handling: in addition, {@link ConfigTypeInfo} or
* equivalent (such as enabling of so-called "default typing") annotation is
* needed, and only in such case is subtype information used.
*/
@Documented
@Target({
ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD,
ElementType.METHOD, ElementType.PARAMETER
})
@Retention(RetentionPolicy.RUNTIME)
public @interface ConfigSubTypes {
/**
* Subtypes of the annotated type (annotated class, or property value type
* associated with the annotated method). These will be checked recursively
* so that types can be defined by only including direct subtypes.
*/
Type[] value();
/**
* Definition of a subtype, along with optional name. If name is missing,
* class of the type will be checked for {@link ConfigTypeName} annotation;
* and if that is also missing or empty, a default name will be constructed.
* Default name is usually based on class name.
*/
@interface Type {
/**
* Class of the subtype.
*/
Class<?> value();
/**
* Logical type name used as the type identifier for the class.
*/
String name() default "";
}
}

View File

@ -0,0 +1,70 @@
package com.typesafe.config;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation used for configuring details of if and how type information is
* used with conversion to Java class, to preserve information about actual
* class of Object instances. This is necessarily for polymorphic types, and may
* also be needed to link abstract declared types and matching concrete
* implementation.
*/
@Documented
@Target({
ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD,
ElementType.METHOD, ElementType.PARAMETER
})
@Retention(RetentionPolicy.RUNTIME)
public @interface ConfigTypeInfo {
/**
* Property name used when for type inclusion method.
*
* If POJO itself has a property with same name, value of property will be
* set with type id metadata: if no such property exists, type id is only
* used for determining actual type.
*/
String property() default "type";
/**
* Optional property that can be used to specify default implementation
* class to use for deserialization if type identifier is either not
* present, or can not be mapped to a registered type (which can occur for
* ids, but not when specifying explicit class to use). Property is only
* used in deciding what to do for otherwise unmappable cases.
*
* Note that while this property allows specification of the default
* implementation to use, it does not help with structural issues that may
* arise if type information is missing. This means that most often this is
* used with type-name -based resolution, to cover cases where new sub-types
* are added, but base type is not changed to reference new sub-types.
*
* There are certain special values that indicate alternate behavior:
* <ul>
* <li>
* {@link java.lang.Void} means that objects with unmappable (or
* missing) type are to be mapped to null references.
* </li>
* <li>
* Placeholder value of {@link ConfigTypeInfo} (that is, this
* annotation type itself} means "there is no default implementation"
* (in which case an error results from unmappable type).
* </li>
* </ul>
*/
Class<?> defaultImpl() default ConfigTypeInfo.class;
/**
* Property that defines whether type identifier value will be passed as
* part of config to deserializer (true), or handled and removed by {@link
* ConfigBeanFactory} (false).
*
* Default value is false, meaning that Config handles and removes the type
* identifier from config that is passed to {@link ConfigBeanFactory}.
*/
boolean visible() default false;
}

View File

@ -0,0 +1,22 @@
package com.typesafe.config;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation used for binding logical name that the annotated class has.
*/
@Documented
@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ConfigTypeName {
/**
* Logical type name for annotated type. If missing (or defined as Empty
* String), defaults to using non-qualified class name as the type.
*/
String value() default "";
}

View File

@ -7,6 +7,7 @@ import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
@ -22,6 +23,9 @@ import com.typesafe.config.ConfigObject;
import com.typesafe.config.ConfigList;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigMemorySize;
import com.typesafe.config.ConfigSubTypes;
import com.typesafe.config.ConfigTypeInfo;
import com.typesafe.config.ConfigTypeName;
import com.typesafe.config.ConfigValue;
import com.typesafe.config.ConfigValueType;
import com.typesafe.config.Optional;
@ -183,6 +187,11 @@ public class ConfigBeanImpl {
@SuppressWarnings("unchecked")
Enum enumValue = config.getEnum((Class<Enum>) parameterClass, configPropName);
return enumValue;
} else if (hasSubTypes(parameterClass)) {
// resolve subtypes
ConfigTypeInfo configTypeInfo = parameterClass.getAnnotation(ConfigTypeInfo.class);
Map<String, Class<?>> subtypes = findAllSubtypes(parameterClass);
return createTypeValue(parameterClass, configPropName, config.getConfig(configPropName), configTypeInfo, subtypes);
} else if (hasAtLeastOneBeanProperty(parameterClass)) {
return createInternal(config.getConfig(configPropName), parameterClass);
} else {
@ -223,6 +232,20 @@ public class ConfigBeanImpl {
@SuppressWarnings("unchecked")
List<Enum> enumValues = config.getEnumList((Class<Enum>) elementType, configPropName);
return enumValues;
} else if (hasSubTypes((Class<?>) elementType)) {
final Class<?> elementClass = (Class<?>) elementType;
// resolve subtypes
ConfigTypeInfo configTypeInfo = elementClass.getAnnotation(ConfigTypeInfo.class);
Map<String, Class<?>> subtypes = findAllSubtypes(elementClass);
if (subtypes != null) {
List<Object> beanList = new ArrayList<Object>();
List<? extends Config> configList = config.getConfigList(configPropName);
for (Config listMember : configList) {
beanList.add(createTypeValue(elementClass, configPropName, listMember, configTypeInfo, subtypes));
}
return beanList;
}
throw new ConfigException.BadBean("Bean property '" + configPropName + "' of class " + beanClass.getName() + " has no defined subtypes");
} else if (hasAtLeastOneBeanProperty((Class<?>) elementType)) {
List<Object> beanList = new ArrayList<Object>();
List<? extends Config> configList = config.getConfigList(configPropName);
@ -266,6 +289,32 @@ public class ConfigBeanImpl {
}
}
private static Object createTypeValue(Class<?> elementClass, String configPropName, Config config, ConfigTypeInfo configTypeInfo, Map<String, Class<?>> subtypes) {
Class<?> implClass = configTypeInfo.defaultImpl();
// get subtype
if (config.hasPath(configTypeInfo.property())) {
String type = config.getString(configTypeInfo.property());
if (subtypes.containsKey(type)) {
implClass = subtypes.get(type);
}
}
if (implClass == null) {
throw new ConfigException.BadBean("Bean property '" + configPropName + "' of class " + elementClass.getName() + " has no implementation");
} else if (implClass == ConfigTypeInfo.class) {
throw new ConfigException.BadBean("Bean property '" + configPropName + "' of class " + elementClass.getName() + " has no default implementation");
} else if (implClass == Void.class) {
return null;
}
if (!configTypeInfo.visible()) {
// remove type info config
config = config.withoutPath(configTypeInfo.property());
}
return createInternal(config, implClass);
}
private static boolean hasAtLeastOneBeanProperty(Class<?> clazz) {
BeanInfo beanInfo = null;
try {
@ -288,6 +337,10 @@ public class ConfigBeanImpl {
return field != null && (field.getAnnotationsByType(Optional.class).length > 0);
}
private static boolean hasSubTypes(Class<?> clazz) {
return clazz.isAnnotationPresent(ConfigTypeInfo.class);
}
private static Field getField(Class beanClass, String fieldName) {
try {
Field field = beanClass.getDeclaredField(fieldName);
@ -302,4 +355,55 @@ public class ConfigBeanImpl {
}
return getField(beanClass, fieldName);
}
private static Map<String, Class<?>> findAllSubtypes(Class<?> clazz) {
Map<String, Class<?>> result = new HashMap<String, Class<?>>(4);
// add self type
String selfTypeName = findTypeName(clazz);
if (selfTypeName != null) {
result.put(selfTypeName, clazz);
}
Map<String, Class<?>> subtypes1 = findSubtypes(clazz);
if (subtypes1 != null) {
for (Map.Entry<String, Class<?>> entry : subtypes1.entrySet()) {
// handle abstract
if (Modifier.isAbstract(entry.getValue().getModifiers())) {
result.putAll(findAllSubtypes(entry.getValue()));
continue;
}
// add concrete type
String typeName = findTypeName(entry.getValue());
if (typeName == null) {
typeName = entry.getKey();
}
result.putIfAbsent(typeName, entry.getValue());
}
}
return result;
}
private static Map<String, Class<?>> findSubtypes(Class<?> clazz) {
ConfigSubTypes t = clazz.getAnnotation(ConfigSubTypes.class);
if (t == null) {
return null;
}
ConfigSubTypes.Type[] types = t.value();
Map<String, Class<?>> result = new HashMap<String, Class<?>>(4);
for (ConfigSubTypes.Type type : types) {
String subTypeName = type.name().isEmpty()
? type.value().getSimpleName()
: type.name();
result.put(subTypeName, type.value());
}
return result;
}
private static String findTypeName(Class<?> clazz) {
ConfigTypeName tn = clazz.getAnnotation(ConfigTypeName.class);
return (tn == null) ? null : tn.value();
}
}