package aQute.configurable; import static java.lang.invoke.MethodHandles.publicLookup; import static java.lang.invoke.MethodType.methodType; import static java.nio.charset.StandardCharsets.UTF_8; import java.io.File; import java.lang.invoke.MethodType; import java.lang.reflect.Array; import java.lang.reflect.GenericArrayType; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Proxy; import java.lang.reflect.Type; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Dictionary; import java.util.Enumeration; import java.util.HashMap; 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; import java.util.regex.Pattern; public class Configurable { public final static Pattern SPLITTER_P = Pattern.compile("(? T createConfigurable(Class c, Map properties) { Object o = Proxy.newProxyInstance(c.getClassLoader(), new Class[] { c }, new ConfigurableHandler(properties, c.getClassLoader())); return c.cast(o); } public static T createConfigurable(Class c, Dictionary properties) { Map alt = new HashMap<>(); for (Enumeration e = properties.keys(); e.hasMoreElements();) { Object key = e.nextElement(); alt.put(key, properties.get(key)); } return createConfigurable(c, alt); } static class ConfigurableHandler implements InvocationHandler { final Map properties; final ClassLoader loader; ConfigurableHandler(Map properties, ClassLoader loader) { this.properties = properties; this.loader = loader; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Config ad = method.getAnnotation(Config.class); String id = Configurable.mangleMethodName(method.getName()); if (ad != null && !ad.id() .equals(Config.NULL)) id = ad.id(); Object o = properties.get(id); if (o == null && args != null && args.length == 1) o = args[0]; if (o == null) { if (ad != null) { if (ad.required()) throw new IllegalStateException("Attribute is required but not set " + method.getName()); o = ad.deflt(); if (o.equals(Config.NULL)) o = null; } } if (o == null) { Class rt = method.getReturnType(); if (rt == boolean.class || rt == Boolean.class) return false; if (method.getReturnType() .isPrimitive() || Number.class.isAssignableFrom(method.getReturnType())) { o = "0"; } else return null; } if (args != null && args.length == 1) { String s = (String) convert(String.class, o); // Allow a base to be specified for File and URL if (method.getReturnType() == File.class && args[0].getClass() == File.class) { return new File((File) args[0], s); } else if (method.getReturnType() == URL.class && args[0].getClass() == File.class) { return new URL(((File) args[0]).toURI() .toURL(), s); } else if (method.getReturnType() == URL.class && args[0].getClass() == URL.class) { return new URL((URL) args[0], s); } } return convert(method.getGenericReturnType(), o); } @SuppressWarnings({ "unchecked", "rawtypes" }) public Object convert(Type type, Object o) throws Exception { // TODO type variables // TODO wildcards if (type instanceof ParameterizedType) { ParameterizedType pType = (ParameterizedType) type; return convert(pType, o); } if (type instanceof GenericArrayType) { GenericArrayType gType = (GenericArrayType) type; return convertArray(gType.getGenericComponentType(), o); } Class resultType = (Class) type; if (resultType.isArray()) { return convertArray(resultType.getComponentType(), o); } Class actualType = o.getClass(); if (actualType.isAssignableFrom(resultType)) return o; if (resultType == boolean.class || resultType == Boolean.class) { if (actualType == boolean.class || actualType == Boolean.class) return o; if (Number.class.isAssignableFrom(actualType)) { double b = ((Number) o).doubleValue(); if (b == 0) return false; return true; } return true; } else if (resultType == byte.class || resultType == Byte.class) { if (Number.class.isAssignableFrom(actualType)) return ((Number) o).byteValue(); resultType = Byte.class; } else if (resultType == char.class) { resultType = Character.class; } else if (resultType == short.class) { if (Number.class.isAssignableFrom(actualType)) return ((Number) o).shortValue(); resultType = Short.class; } else if (resultType == int.class) { if (Number.class.isAssignableFrom(actualType)) return ((Number) o).intValue(); resultType = Integer.class; } else if (resultType == long.class) { if (Number.class.isAssignableFrom(actualType)) return ((Number) o).longValue(); resultType = Long.class; } else if (resultType == float.class) { if (Number.class.isAssignableFrom(actualType)) return ((Number) o).floatValue(); resultType = Float.class; } else if (resultType == double.class) { if (Number.class.isAssignableFrom(actualType)) return ((Number) o).doubleValue(); resultType = Double.class; } if (resultType.isPrimitive()) throw new IllegalArgumentException("Unknown primitive: " + resultType); if (Number.class.isAssignableFrom(resultType) && actualType == Boolean.class) { Boolean b = (Boolean) o; o = b ? "1" : "0"; } else if (actualType == String.class) { String input = (String) o; if (Enum.class.isAssignableFrom(resultType)) { return Enum.valueOf((Class) resultType, input); } if (resultType == Class.class && loader != null) { return loader.loadClass(input); } if (resultType == Pattern.class) { return Pattern.compile(input); } } try { return newInstance(resultType, o.toString()); } catch (Throwable t) { // handled on next line } throw new IllegalArgumentException( "No conversion to " + resultType + " from " + actualType + " value " + o); } private Object convert(ParameterizedType pType, Object o) throws InstantiationException, IllegalAccessException, Exception { Class resultType = (Class) pType.getRawType(); if (Collection.class.isAssignableFrom(resultType)) { Collection input = toCollection(o); if (resultType.isInterface()) { 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 == * Deque.class */) resultType = LinkedList.class; else if (resultType == Queue.class /* * || resultType == * Deque.class */) resultType = LinkedList.class; else throw new IllegalArgumentException( "Unknown interface for a collection, no concrete class found: " + resultType); } @SuppressWarnings("unchecked") Collection result = (Collection) newInstance(resultType); Type componentType = pType.getActualTypeArguments()[0]; for (Object i : input) { result.add(convert(componentType, i)); } return result; } else if (pType.getRawType() == Class.class) { return loader.loadClass(o.toString()); } if (Map.class.isAssignableFrom(resultType)) { Map input = toMap(o); if (resultType.isInterface()) { if (resultType == SortedMap.class) resultType = TreeMap.class; else if (resultType == Map.class) resultType = LinkedHashMap.class; else throw new IllegalArgumentException( "Unknown interface for a collection, no concrete class found: " + resultType); } @SuppressWarnings("unchecked") Map result = (Map) resultType.getConstructor() .newInstance(); Type keyType = pType.getActualTypeArguments()[0]; Type valueType = pType.getActualTypeArguments()[1]; for (Map.Entry entry : input.entrySet()) { result.put(convert(keyType, entry.getKey()), convert(valueType, entry.getValue())); } return result; } throw new IllegalArgumentException( "cannot convert to " + pType + " because it uses generics and is not a Collection or a map"); } Object convertArray(Type componentType, Object o) throws Exception { if (o instanceof String) { String s = (String) o; if (componentType == Byte.class || componentType == byte.class) return s.getBytes(UTF_8); if (componentType == Character.class || componentType == char.class) return s.toCharArray(); } Collection input = toCollection(o); Class componentClass = getRawClass(componentType); Object array = Array.newInstance(componentClass, input.size()); int i = 0; for (Object next : input) { Array.set(array, i++, convert(componentType, next)); } return array; } private Class getRawClass(Type type) { if (type instanceof Class) return (Class) type; if (type instanceof ParameterizedType) return (Class) ((ParameterizedType) type).getRawType(); throw new IllegalArgumentException( "For the raw type, type must be ParamaterizedType or Class but is " + type); } private Collection toCollection(Object o) { if (o instanceof Collection) return (Collection) o; if (o.getClass() .isArray()) { if (o.getClass() .getComponentType() .isPrimitive()) { int length = Array.getLength(o); List result = new ArrayList<>(length); for (int i = 0; i < length; i++) { result.add(Array.get(o, i)); } return result; } return Arrays.asList((Object[]) o); } if (o instanceof String) { String s = (String) o; if (SPLITTER_P.matcher(s) .find()) return Arrays.asList(s.split("\\|")); else return unescape(s); } return Arrays.asList(o); } private Map toMap(Object o) { if (o instanceof Map) return (Map) o; throw new IllegalArgumentException("Cannot convert " + o + " to a map as requested"); } } public static String mangleMethodName(String id) { StringBuilder sb = new StringBuilder(id); for (int i = 0; i < sb.length(); i++) { char c = sb.charAt(i); boolean twice = i < sb.length() - 1 && sb.charAt(i + 1) == c; if (c == '$' || c == '_') { if (twice) sb.deleteCharAt(i + 1); else if (c == '$') sb.deleteCharAt(i--); // Remove dollars else sb.setCharAt(i, '.'); // Make _ into . } } return sb.toString(); } public static List unescape(String s) { // do it the OSGi way List tokens = new ArrayList<>(); String[] parts = s.split("(? T newInstance(Class rawClass) throws Exception { try { return (T) publicLookup().findConstructor(rawClass, defaultConstructor) .invoke(); } catch (Error | Exception e) { throw e; } catch (Throwable e) { throw new RuntimeException(e); } } private static final MethodType stringConstructor = methodType(void.class, String.class); private static T newInstance(Class rawClass, String arg) throws Exception { try { return (T) publicLookup().findConstructor(rawClass, stringConstructor) .invoke(arg); } catch (Error | Exception e) { throw e; } catch (Throwable e) { throw new RuntimeException(e); } } }