mirror of
https://github.com/lightbend/config.git
synced 2025-03-14 11:20:25 +08:00
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:
parent
1b7460b6c9
commit
c96977bb14
52
config/src/main/java/com/typesafe/config/ConfigSubTypes.java
Normal file
52
config/src/main/java/com/typesafe/config/ConfigSubTypes.java
Normal 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 "";
|
||||
}
|
||||
}
|
70
config/src/main/java/com/typesafe/config/ConfigTypeInfo.java
Normal file
70
config/src/main/java/com/typesafe/config/ConfigTypeInfo.java
Normal 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;
|
||||
}
|
22
config/src/main/java/com/typesafe/config/ConfigTypeName.java
Normal file
22
config/src/main/java/com/typesafe/config/ConfigTypeName.java
Normal 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 "";
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user