blob: 9c328a2e6f1bef76c8cd05416a130df9c107f13d [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.felix.dm.impl;
import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* Provides a way for creating type-safe configurations from a {@link Map} or {@link Dictionary}.
* <p>
* This class takes a map or dictionary along with a class, the configuration-type, and returns a proxy that converts
* method calls from the configuration-type to lookups in the map or dictionary. The results of these lookups are then
* converted to the expected return type of the invoked configuration method.<br>
* As proxies are returned, no implementations of the desired configuration-type are necessary!
* </p>
* <p>
* The lookups performed are based on the name of the method called on the configuration type. The method names are
* "mangled" to the following form: <tt>[lower case letter] [any valid character]*</tt>. Method names starting with
* <tt>get</tt> or <tt>is</tt> (JavaBean convention) are stripped from these prefixes. For example: given a dictionary
* with the key <tt>"foo"</tt> can be accessed from a configuration-type using the following method names:
* <tt>foo()</tt>, <tt>getFoo()</tt> and <tt>isFoo()</tt>.
*
* If the property name contains some dots, the the following conventions are used:
* <ul>
* <li>camel casing: if a property contains multiple words separated by dots, then you can indicate words boundaries using medial capitalization.
* For example, the property "foo.bar" could be accessed with a method name like "fooBar()" or "getFooBar()".
* <li> use underscore to wrap dots: underscore ("_") found in method names are converted to ".", unless they are followed by another underscore.
* (in this case, the double "__" is then converted to single underscore ("_").
* For Example: foo_bar() will be mapped to "foo.bar" property, and foo__bar() will be mapped to "foo_bar" property.
* </ul>
* </p>
* <p>
* The return values supported are: primitive types (or their object wrappers), strings, enums, arrays of
* primitives/strings, {@link Collection} types, {@link Map} types, {@link Class}es and interfaces. When an interface is
* returned, it is treated equally to a configuration type, that is, it is returned as a proxy.
* </p>
* <p>
* Arrays can be represented either as comma-separated values, optionally enclosed in square brackets. For example:
* <tt>[ a, b, c ]</tt> and <tt>a, b,c</tt> are both considered an array of length 3 with the values "a", "b" and "c".
* Alternatively, you can append the array index to the key in the dictionary to obtain the same: a dictionary with
* "arr.0" =&gt; "a", "arr.1" =&gt; "b", "arr.2" =&gt; "c" would result in the same array as the earlier examples.
* </p>
* <p>
* Maps can be represented as single string values similarly as arrays, each value consisting of both the key and value
* separated by a dot. Optionally, the value can be enclosed in curly brackets. Similar to array, you can use the same
* dot notation using the keys. For example, a dictionary with <tt>"map" => "{key1.value1, key2.value2}"</tt> and a
* dictionary with <tt>"map.key1" => "value1", "map2.key2" => "value2"</tt> result in the same map being returned.
* Instead of a map, you could also define an interface with the methods <tt>getKey1()</tt> and <tt>getKey2</tt> and use
* that interface as return type instead of a {@link Map}.
* </p>
* <p>
* In case a lookup does not yield a value from the underlying map or dictionary, the following rules are applied:
* <ol>
* <li>primitive types yield their default value, as defined by the Java Specification;
* <li>string, {@link Class}es and enum values yield <code>null</code>;
* <li>for arrays, collections and maps, an empty array/collection/map is returned;
* <li>for other interface types that are treated as configuration type a null-object is returned.
* </ol>
* </p>
*/
public final class Configurable {
static class ConfigHandler implements InvocationHandler {
private final ClassLoader m_cl;
private final Map<?, ?> m_config;
private Class<?> m_configType;
/** Constant for the single element method */
private static final String VALUE_METHOD = "value";
/** Constant for the prefix constant. */
private static final String PREFIX_CONSTANT = "PREFIX_";
/** Capture all methods defined by the annotation interface */
private static final Set<Method> ANNOTATION_METHODS = new HashSet<Method>();
static
{
for(final Method m : Annotation.class.getMethods())
{
ANNOTATION_METHODS.add(m);
}
}
public ConfigHandler(Class<?> type, ClassLoader cl, Map<?, ?> config) {
m_configType = type;
m_cl = cl;
m_config = config;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("toString")) {
return m_config.toString();
}
String name = getPropertyName(method.getName());
Object result = convert(method.getGenericReturnType(), name, m_config.get(name), false /* useImplicitDefault */);
if (result == null) {
Object defaultValue = getDefaultValue(proxy, args, method, name);
if (defaultValue != null) {
return defaultValue;
}
}
return result;
}
@SuppressWarnings("unchecked")
private Object convertParameterizedType(ParameterizedType type, String key, Object value, boolean useImplicitDefault) throws Exception {
Class<?> resultType = (Class<?>) type.getRawType();
if (Class.class.isAssignableFrom(resultType)) {
if (value == null) {
return null;
}
return m_cl.loadClass(value.toString());
}
else if (Collection.class.isAssignableFrom(resultType)) {
Collection<?> input = toCollection(key, value);
if (input == null && ! useImplicitDefault) {
return null;
}
if (resultType == Collection.class || resultType == List.class) {
resultType = ArrayList.class;
}
else if (resultType == Set.class || resultType == SortedSet.class) {
resultType = TreeSet.class;
}
else if (resultType == Queue.class) {
resultType = LinkedList.class;
}
else if (resultType.isInterface()) {
throw new RuntimeException("Unknown collection interface: " + resultType);
}
Collection<Object> result = (Collection<Object>) resultType.newInstance();
if (input != null) {
Type componentType = type.getActualTypeArguments()[0];
for (Object i : input) {
result.add(convert(componentType, key, i, false /* useImplicitDefault */));
}
}
return result;
}
else if (Map.class.isAssignableFrom(resultType)) {
Map<?, ?> input = toMap(key, value);
if (input == null && ! useImplicitDefault) {
return null;
}
if (resultType == SortedMap.class) {
resultType = TreeMap.class;
}
else if (resultType == Map.class) {
resultType = LinkedHashMap.class;
}
else if (resultType.isInterface()) {
throw new RuntimeException("Unknown map interface: " + resultType);
}
Map<Object, Object> result = (Map<Object, Object>) resultType.newInstance();
Type keyType = type.getActualTypeArguments()[0];
Type valueType = type.getActualTypeArguments()[1];
if (input != null) {
for (Map.Entry<?, ?> entry : input.entrySet()) {
result.put(convert(keyType, key, entry.getKey(), false /* useImplicitDefault */), convert(valueType, key, entry.getValue(), false /* useImplicitDefault */));
}
}
return result;
}
throw new RuntimeException("Unhandled type: " + type);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private Object convert(Type type, String key, Object value, boolean useImplicitDefault) throws Exception {
if (type instanceof ParameterizedType) {
return convertParameterizedType((ParameterizedType) type, key, value, useImplicitDefault);
}
if (type instanceof GenericArrayType) {
return convertArray(((GenericArrayType) type).getGenericComponentType(), key, value, useImplicitDefault);
}
Class<?> resultType = (Class<?>) type;
if (resultType.isArray()) {
return convertArray(resultType.getComponentType(), key, value, useImplicitDefault);
}
if (resultType.isInstance(value)) {
return value;
}
if (Boolean.class.equals(resultType) || Boolean.TYPE.equals(resultType)) {
if (value == null) {
return useImplicitDefault && resultType.isPrimitive() ? DEFAULT_BOOLEAN : null;
}
return Boolean.valueOf(value.toString());
}
else if (Byte.class.equals(resultType) || Byte.TYPE.equals(resultType)) {
if (value == null) {
return useImplicitDefault && resultType.isPrimitive() ? DEFAULT_BYTE : null;
}
if (value instanceof Number) {
return ((Number) value).byteValue();
}
return Byte.valueOf(value.toString());
}
else if (Short.class.equals(resultType) || Short.TYPE.equals(resultType)) {
if (value == null) {
return useImplicitDefault && resultType.isPrimitive() ? DEFAULT_SHORT : null;
}
if (value instanceof Number) {
return ((Number) value).shortValue();
}
return Short.valueOf(value.toString());
}
else if (Integer.class.equals(resultType) || Integer.TYPE.equals(resultType)) {
if (value == null) {
return useImplicitDefault && resultType.isPrimitive() ? DEFAULT_INT : null;
}
if (value instanceof Number) {
return ((Number) value).intValue();
}
return Integer.valueOf(value.toString());
}
else if (Long.class.equals(resultType) || Long.TYPE.equals(resultType)) {
if (value == null) {
return useImplicitDefault && resultType.isPrimitive() ? DEFAULT_LONG : null;
}
if (value instanceof Number) {
return ((Number) value).longValue();
}
return Long.valueOf(value.toString());
}
else if (Float.class.equals(resultType) || Float.TYPE.equals(resultType)) {
if (value == null) {
return useImplicitDefault && resultType.isPrimitive() ? DEFAULT_FLOAT : null;
}
if (value instanceof Number) {
return ((Number) value).floatValue();
}
return Float.valueOf(value.toString());
}
else if (Double.class.equals(resultType) || Double.TYPE.equals(resultType)) {
if (value == null) {
return useImplicitDefault && resultType.isPrimitive() ? DEFAULT_DOUBLE : null;
}
if (value instanceof Number) {
return ((Number) value).doubleValue();
}
return Double.valueOf(value.toString());
}
else if (Number.class.equals(resultType)) {
if (value == null) {
return null;
}
String numStr = value.toString();
if (numStr.indexOf('.') > 0) {
return Double.valueOf(numStr);
}
return Long.valueOf(numStr);
}
else if (String.class.isAssignableFrom(resultType)) {
return value == null ? null : value.toString();
}
else if (Enum.class.isAssignableFrom(resultType)) {
if (value == null) {
return null;
}
Class<Enum> enumType = (Class<Enum>) resultType;
return Enum.valueOf(enumType, value.toString().toUpperCase());
}
else if (resultType.isInterface()) {
Map<?, ?> map = toMap(key, value);
if (map == null) {
return useImplicitDefault ? create(resultType, Collections.emptyMap()) : null;
}
return create(resultType, map);
}
throw new RuntimeException("Unhandled type: " + type);
}
private Object convertArray(Type type, String key, Object value, boolean useImplicitDefault) throws Exception {
if (value instanceof String) {
String str = (String) value;
if (type == Byte.class || type == byte.class) {
return str.getBytes("UTF-8");
}
if (type == Character.class || type == char.class) {
return str.toCharArray();
}
}
Collection<?> input = toCollection(key, value);
if (input == null && useImplicitDefault) {
input = Collections.emptyList();
}
if (input == null) {
return null;
}
Class<?> componentClass = getRawClass(type);
Object array = Array.newInstance(componentClass, input.size());
int i = 0;
for (Object next : input) {
Array.set(array, i++, convert(type, key, next, false /* useImplicitDefault */));
}
return array;
}
private Object getDefaultValue(Object proxy, Object[] args, Method method, String key) throws Throwable {
Object def = null;
// Handle cases where the method is part of an annotation or is a java8 default method.
Class<?> methodClass = method.getDeclaringClass();
if (methodClass.isAnnotation()) {
// the config type is an annotation: simply invoke the default value
def = method.getDefaultValue();
} else if (method.isDefault()) {
if (System.getProperty("java.version", "1.8").startsWith("1.8")) {
// The config type is a java8 interface with a default method, invoke it.
// But it's challenging to invoke a default method from a dynamic proxy ... we have to use the MethodHandles.
// see https://zeroturnaround.com/rebellabs/recognize-and-conquer-java-proxies-default-methods-and-method-handles
Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);
constructor.setAccessible(true);
def = constructor.newInstance(methodClass, MethodHandles.Lookup.PRIVATE)
.unreflectSpecial(method, methodClass)
.bindTo(proxy)
.invokeWithArguments(args);
} else {
// see https://dzone.com/articles/correct-reflective-access-to-interface-default-methods
def = MethodHandles.lookup()
.findSpecial(methodClass,
method.getName(),
MethodType.methodType(method.getReturnType(), method.getParameterTypes()),
methodClass)
.bindTo(proxy)
.invokeWithArguments();
}
}
return convert(method.getGenericReturnType(), key, def, true /* useImplicitDefault */);
}
private Class<?> getRawClass(Type type) {
if (type instanceof ParameterizedType) {
return (Class<?>) ((ParameterizedType) type).getRawType();
}
if (type instanceof Class) {
return (Class<?>) type;
}
throw new RuntimeException("Unhandled type: " + type);
}
private Collection<?> toCollection(String prefix, Object value) {
if (value instanceof Collection) {
return (Collection<?>) value;
}
if (value == null) {
List<Object> result = new ArrayList<>();
String needle = prefix.concat(".");
for (Map.Entry<?, ?> entry : m_config.entrySet()) {
String key = entry.getKey().toString();
if (!key.startsWith(needle)) {
continue;
}
int idx = 0;
try {
idx = Integer.parseInt(key.substring(needle.length()));
}
catch (NumberFormatException e) {
// Ignore
}
result.add(Math.min(result.size(), idx), entry.getValue());
}
return result.size() == 0 ? null : result;
}
if (value.getClass().isArray()) {
if (value.getClass().getComponentType().isPrimitive()) {
int length = Array.getLength(value);
List<Object> result = new ArrayList<Object>(length);
for (int i = 0; i < length; i++) {
result.add(Array.get(value, i));
}
return result;
}
return Arrays.asList((Object[]) value);
}
if (value instanceof String) {
String str = (String) value;
if (str.startsWith("[") && str.endsWith("]")) {
str = str.substring(1, str.length() - 1);
}
// don't split in case we are parsing an empty [] list, in which case we need to return an empty list.
return str.length() == 0 ? Collections.emptyList() : Arrays.asList(str.split("\\s*,\\s*"));
}
return Arrays.asList(value);
}
private Map<?, ?> toMap(String prefix, Object value) {
if (value instanceof Map) {
return (Map<?, ?>) value;
}
Map<String, Object> result = new HashMap<>();
if (value == null) {
String needle = prefix.concat(".");
for (Map.Entry<?, ?> entry : m_config.entrySet()) {
String key = entry.getKey().toString();
if (key.startsWith(needle)) {
result.put(key.substring(needle.length()), entry.getValue());
}
}
if (result.size() == 0) {
return null;
}
}
else if (value instanceof String) {
String str = (String) value;
if (str.startsWith("{") && str.endsWith("}")) {
str = str.substring(1, str.length() - 1);
}
for (String entry : str.split("\\s*,\\s*")) {
String[] pair = entry.split("\\s*\\.\\s*", 2);
if (pair.length == 2) {
result.put(pair[0], pair[1]);
}
}
}
return result;
}
private String getPropertyName(String methodName) {
// First, check if the config type defines a standard PREFIX_ string.
String prefix = getPrefix(m_configType);
// If the configuration type is a single valued annotation, derive the property name
// from the interface name, using OSGi R7 Scr convention
if (isSingleElementAnnotation(m_configType) && methodName.equals(VALUE_METHOD)) {
String propertyName = mapTypeNameToKey(m_configType.getSimpleName());
return prefix == null ? propertyName : prefix.concat(propertyName);
}
// Now, derive the property name from the method name, using simple javabean convention.
// i.e: fooBar() or getFooBar() will map to "fooBar" property.
String javaBeanMethodName = derivePropertyNameUsingJavaBeanConvention(methodName);
if (hasValueFor(javaBeanMethodName)) {
// there is a value in the actual configuration for the derived property name.
return javaBeanMethodName;
}
// Derive the property name from the method name, using javabeans and/or camel casing convention,
// where each capital letter is assumed to map a "dot".
// i.e: fooBar() or getFooBar() will map to "foo.bar" property.
String camelCasePropertyName = derivePropertyNameUsingCamelCaseConvention(javaBeanMethodName);
if (hasValueFor(camelCasePropertyName)) {
// there is a value in the actual configuration for the derived property name.
return camelCasePropertyName;
}
// Derive the property name from the method name, using OSGi metatype convention,
// where a "_" is mapped to a dot, except if the understcore is followed by another undescore
// (in this case, the double "__" is replaced by "_").
// i.e: foo_bar() will map to "foo.bar" property and foo__bar() will map to "foo_bar" property.
String metaTypePropertyName = derivePropertyNameUsingMetaTypeConvention(methodName);
if (hasValueFor(metaTypePropertyName)) {
// there is a value in the actual configuration for the derived property name.
return metaTypePropertyName;
}
// No value could be found, return by default a property name derived from javabean convention.
return javaBeanMethodName;
}
private String derivePropertyNameUsingJavaBeanConvention(String methodName) {
StringBuilder sb = new StringBuilder(methodName);
if (methodName.startsWith("get")) {
sb.delete(0, 3);
} else if (methodName.startsWith("is")) {
sb.delete(0, 2);
}
char c = sb.charAt(0);
if (Character.isUpperCase(c)) {
sb.setCharAt(0, Character.toLowerCase(c));
}
return (sb.toString());
}
private String derivePropertyNameUsingCamelCaseConvention(String methodName) {
StringBuilder sb = new StringBuilder(methodName);
for (int i = 0; i < sb.length(); i++) {
char c = sb.charAt(i);
if (Character.isUpperCase(c)) {
// camel casing: replace fooBar -> foo.bar
sb.setCharAt(i, Character.toLowerCase(c));
sb.insert(i, ".");
}
}
return sb.toString();
}
// see metatype spec, chapter 105.9.2 in osgi r6 cmpn.
private String derivePropertyNameUsingMetaTypeConvention(String methodName) {
StringBuilder sb = new StringBuilder(methodName);
// replace "__" by "_" or "_" by ".": foo_bar -> foo.bar; foo__BAR_zoo -> foo_BAR.zoo
for (int i = 0; i < sb.length(); i ++) {
if (sb.charAt(i) == '_') {
if (i < (sb.length() - 1) && sb.charAt(i+1) == '_') {
// replace foo__bar -> foo_bar
sb.replace(i, i+2, "_");
} else {
// replace foo_bar -> foo.bar
sb.replace(i, i+1, ".");
}
} else if (sb.charAt(i) == '$') {
if (i < (sb.length() - 1) && sb.charAt(i+1) == '$') {
// replace foo__bar -> foo_bar
sb.replace(i, i+2, "$");
} else {
// remove single dollar character.
sb.delete(i, i+1);
}
}
}
return sb.toString();
}
/**
* Checks if a property name has a given value. This method takes care about special array values (arr.0, arr.1,...)
* and about map values (map.key1, map.key2, ...).
*
* @param property name
* @return true if the given property has a value in the actual configuration, false if not.
*/
private boolean hasValueFor(String property)
{
if (m_config.containsKey(property)) {
return true;
}
String needle = property.concat(".");
for (Map.Entry<?, ?> entry : m_config.entrySet()) {
String key = entry.getKey().toString();
if (key.startsWith(needle)) {
return true;
}
}
return false;
}
// Code derived from Apache Felix SCR (See org.apache.felix.scr.impl.inject.Annotations.java)
private String getPrefix(Class<?> clazz)
{
try
{
final Field f = clazz.getField(PREFIX_CONSTANT);
if ( Modifier.isStatic(f.getModifiers())
&& Modifier.isPublic(f.getModifiers())
&& Modifier.isFinal(f.getModifiers())
&& String.class.isAssignableFrom(f.getType()))
{
final Object value = f.get(null);
if ( value != null )
{
return value.toString();
}
}
}
catch ( final Exception ignore)
{
// ignore
}
return null;
}
/**
* Check whether the provided type is a single element annotation.
* A single element annotation has a method named "value" and all
* other annotation methods must have a default value.
* @param clazz The provided type
* @return {@code true} if the type is a single element annotation.
*/
static public boolean isSingleElementAnnotation(final Class<?> clazz)
{
boolean result = false;
if ( clazz.isAnnotation() )
{
result = true;
boolean hasValue = false;
for ( final Method method: clazz.getMethods() )
{
// filter out methods from Annotation
boolean isFromAnnotation = false;
for(final Method objMethod : ANNOTATION_METHODS)
{
if ( objMethod.getName().equals(method.getName())
&& Arrays.equals(objMethod.getParameterTypes(), method.getParameterTypes()) )
{
isFromAnnotation = true;
break;
}
}
if ( isFromAnnotation )
{
continue;
}
if ( VALUE_METHOD.equals(method.getName()) )
{
hasValue = true;
continue;
}
if ( method.getDefaultValue() == null )
{
result = false;
break;
}
}
if ( result )
{
result = hasValue;
}
}
return result;
}
static String mapTypeNameToKey(String name)
{
final StringBuilder sb = new StringBuilder();
boolean lastLow = false;
for(final char c : name.toCharArray())
{
if ( lastLow && (Character.isLetter(c) || Character.isDigit(c)) && Character.isUpperCase(c) )
{
sb.append('.');
}
lastLow = false;
if ( (Character.isLetter(c) || Character.isDigit(c)) && Character.isLowerCase(c))
{
lastLow = true;
}
sb.append(Character.toLowerCase(c));
}
return sb.toString();
}
}
private static final Boolean DEFAULT_BOOLEAN = Boolean.FALSE;
private static final Byte DEFAULT_BYTE = new Byte((byte) 0);
private static final Short DEFAULT_SHORT = new Short((short) 0);
private static final Integer DEFAULT_INT = new Integer(0);
private static final Long DEFAULT_LONG = new Long(0);
private static final Float DEFAULT_FLOAT = new Float(0.0f);
private static final Double DEFAULT_DOUBLE = new Double(0.0);
/**
* Creates a configuration for a given type backed by a given dictionary.
*
* @param type the configuration class, cannot be <code>null</code>;
* @param config the configuration to wrap, cannot be <code>null</code>.
* @param serviceProperties the component service properties, cannot be <code>null</code>.
* @return an instance of the given type that wraps the given configuration.
*/
public static <T> T create(Class<T> type, Dictionary<?, ?> config, Dictionary<?,?> serviceProperties) {
Map<Object, Object> map = new HashMap<Object, Object>();
for (Enumeration<?> e = serviceProperties.keys(); e.hasMoreElements();) {
Object key = e.nextElement();
map.put(key, serviceProperties.get(key));
}
for (Enumeration<?> e = config.keys(); e.hasMoreElements();) {
Object key = e.nextElement();
map.put(key, config.get(key));
}
return create(type, map);
}
/**
* Creates a configuration for a given type backed by a given map.
*
* @param type the configuration class, cannot be <code>null</code>;
* @param config the configuration to wrap, cannot be <code>null</code>.
* @return an instance of the given type that wraps the given configuration.
*/
public static <T> T create(Class<T> type, Map<?, ?> config) {
ClassLoader cl = type.getClassLoader();
Object result = Proxy.newProxyInstance(cl, new Class<?>[] { type }, new ConfigHandler(type, cl, config));
return type.cast(result);
}
}