You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2016/07/07 09:18:38 UTC

[04/10] brooklyn-server git commit: refactor TypeCoercions so that most is in utils with an interface

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/ec4da197/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/PrimitiveStringTypeCoercions.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/PrimitiveStringTypeCoercions.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/PrimitiveStringTypeCoercions.java
new file mode 100644
index 0000000..cad798d
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/PrimitiveStringTypeCoercions.java
@@ -0,0 +1,208 @@
+/*
+ * 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.brooklyn.util.javalang.coerce;
+
+import java.lang.reflect.Method;
+
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.javalang.Boxing;
+import org.apache.brooklyn.util.javalang.JavaClassNames;
+import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes;
+
+import com.google.common.primitives.Primitives;
+
+public class PrimitiveStringTypeCoercions {
+
+    public PrimitiveStringTypeCoercions() {}
+    
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    public static <T> Maybe<T> tryCoerce(Object value, Class<? super T> targetType) {
+        //deal with primitive->primitive casting
+        if (isPrimitiveOrBoxer(targetType) && isPrimitiveOrBoxer(value.getClass())) {
+            // Don't just rely on Java to do its normal casting later; if caller writes
+            // long `l = coerce(new Integer(1), Long.class)` then letting java do its casting will fail,
+            // because an Integer will not automatically be unboxed and cast to a long
+            return Maybe.of(castPrimitive(value, (Class<T>)targetType));
+        }
+
+        //deal with string->primitive
+        if (value instanceof String && isPrimitiveOrBoxer(targetType)) {
+            return Maybe.of(stringToPrimitive((String)value, (Class<T>)targetType));
+        }
+
+        //deal with primitive->string
+        if (isPrimitiveOrBoxer(value.getClass()) && targetType.equals(String.class)) {
+            return Maybe.of((T) value.toString());
+        }
+
+        //look for value.asType where Type is castable to targetType
+        String targetTypeSimpleName = JavaClassNames.verySimpleClassName(targetType);
+        if (targetTypeSimpleName!=null && targetTypeSimpleName.length()>0) {
+            for (Method m: value.getClass().getMethods()) {
+                if (m.getName().startsWith("as") && m.getParameterTypes().length==0 &&
+                        targetType.isAssignableFrom(m.getReturnType()) ) {
+                    if (m.getName().equals("as"+JavaClassNames.verySimpleClassName(m.getReturnType()))) {
+                        try {
+                            return Maybe.of((T) m.invoke(value));
+                        } catch (Exception e) {
+                            Exceptions.propagateIfFatal(e);
+                            return Maybe.absent(new ClassCoercionException("Cannot coerce type "+value.getClass()+" to "+targetType.getCanonicalName()+" ("+value+"): "+m.getName()+" adapting failed, "+e));
+                        }
+                    }
+                }
+            }
+        }
+        
+        return null;
+    }
+    
+    /**
+     * Sometimes need to explicitly cast primitives, rather than relying on Java casting.
+     * For example, when using generics then type-erasure means it doesn't actually cast,
+     * which causes tests to fail with 0 != 0.0
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> T castPrimitive(Object value, Class<T> targetType) {
+        if (value==null) return null;
+        assert isPrimitiveOrBoxer(targetType) : "targetType="+targetType;
+        assert isPrimitiveOrBoxer(value.getClass()) : "value="+targetType+"; valueType="+value.getClass();
+
+        Class<?> sourceWrapType = Primitives.wrap(value.getClass());
+        Class<?> targetWrapType = Primitives.wrap(targetType);
+        
+        // optimization, for when already correct type
+        if (sourceWrapType == targetWrapType) {
+            return (T) value;
+        }
+        
+        if (targetWrapType == Boolean.class) {
+            // only char can be mapped to boolean
+            // (we could say 0=false, nonzero=true, but there is no compelling use case so better
+            // to encourage users to write as boolean)
+            if (sourceWrapType == Character.class)
+                return (T) stringToPrimitive(value.toString(), targetType);
+            
+            throw new ClassCoercionException("Cannot cast "+sourceWrapType+" ("+value+") to "+targetType);
+        } else if (sourceWrapType == Boolean.class) {
+            // boolean can't cast to anything else
+            
+            throw new ClassCoercionException("Cannot cast "+sourceWrapType+" ("+value+") to "+targetType);
+        }
+        
+        // for whole-numbers (where casting to long won't lose anything)...
+        long v = 0;
+        boolean islong = true;
+        if (sourceWrapType == Character.class) {
+            v = (long) ((Character)value).charValue();
+        } else if (sourceWrapType == Byte.class) {
+            v = (long) ((Byte)value).byteValue();
+        } else if (sourceWrapType == Short.class) {
+            v = (long) ((Short)value).shortValue();
+        } else if (sourceWrapType == Integer.class) {
+            v = (long) ((Integer)value).intValue();
+        } else if (sourceWrapType == Long.class) {
+            v = ((Long)value).longValue();
+        } else {
+            islong = false;
+        }
+        if (islong) {
+            if (targetWrapType == Character.class) return (T) Character.valueOf((char)v); 
+            if (targetWrapType == Byte.class) return (T) Byte.valueOf((byte)v); 
+            if (targetWrapType == Short.class) return (T) Short.valueOf((short)v); 
+            if (targetWrapType == Integer.class) return (T) Integer.valueOf((int)v); 
+            if (targetWrapType == Long.class) return (T) Long.valueOf((long)v); 
+            if (targetWrapType == Float.class) return (T) Float.valueOf((float)v); 
+            if (targetWrapType == Double.class) return (T) Double.valueOf((double)v);
+            throw new IllegalStateException("Unexpected: sourceType="+sourceWrapType+"; targetType="+targetWrapType);
+        }
+        
+        // for real-numbers (cast to double)...
+        double d = 0;
+        boolean isdouble = true;
+        if (sourceWrapType == Float.class) {
+            d = (double) ((Float)value).floatValue();
+        } else if (sourceWrapType == Double.class) {
+            d = (double) ((Double)value).doubleValue();
+        } else {
+            isdouble = false;
+        }
+        if (isdouble) {
+            if (targetWrapType == Character.class) return (T) Character.valueOf((char)d); 
+            if (targetWrapType == Byte.class) return (T) Byte.valueOf((byte)d); 
+            if (targetWrapType == Short.class) return (T) Short.valueOf((short)d); 
+            if (targetWrapType == Integer.class) return (T) Integer.valueOf((int)d); 
+            if (targetWrapType == Long.class) return (T) Long.valueOf((long)d); 
+            if (targetWrapType == Float.class) return (T) Float.valueOf((float)d); 
+            if (targetWrapType == Double.class) return (T) Double.valueOf((double)d);
+            throw new IllegalStateException("Unexpected: sourceType="+sourceWrapType+"; targetType="+targetWrapType);
+        } else {
+            throw new IllegalStateException("Unexpected: sourceType="+sourceWrapType+"; targetType="+targetWrapType);
+        }
+    }
+    
+    public static boolean isPrimitiveOrBoxer(Class<?> type) {
+        // cf Boxing.isPrimitiveOrBoxerClass
+        return Primitives.allPrimitiveTypes().contains(type) || Primitives.allWrapperTypes().contains(type);
+    }
+    
+    @SuppressWarnings("unchecked")
+    public static <T> T stringToPrimitive(String value, Class<T> targetType) {
+        assert Primitives.allPrimitiveTypes().contains(targetType) || Primitives.allWrapperTypes().contains(targetType) : "targetType="+targetType;
+        // If char, then need to do explicit conversion
+        if (targetType == Character.class || targetType == char.class) {
+            if (value.length() == 1) {
+                return (T) (Character) value.charAt(0);
+            } else if (value.length() != 1) {
+                throw new ClassCoercionException("Cannot coerce type String to "+targetType.getCanonicalName()+" ("+value+"): adapting failed");
+            }
+        }
+        value = value.trim();
+        // For boolean we could use valueOf, but that returns false whereas we'd rather throw errors on bad values
+        if (targetType == Boolean.class || targetType == boolean.class) {
+            if ("true".equalsIgnoreCase(value)) return (T) Boolean.TRUE;
+            if ("false".equalsIgnoreCase(value)) return (T) Boolean.FALSE;
+            if ("yes".equalsIgnoreCase(value)) return (T) Boolean.TRUE;
+            if ("no".equalsIgnoreCase(value)) return (T) Boolean.FALSE;
+            if ("t".equalsIgnoreCase(value)) return (T) Boolean.TRUE;
+            if ("f".equalsIgnoreCase(value)) return (T) Boolean.FALSE;
+            if ("y".equalsIgnoreCase(value)) return (T) Boolean.TRUE;
+            if ("n".equalsIgnoreCase(value)) return (T) Boolean.FALSE;
+            
+            throw new ClassCoercionException("Cannot coerce type String to "+targetType.getCanonicalName()+" ("+value+"): adapting failed"); 
+        }
+        
+        // Otherwise can use valueOf reflectively
+        Class<?> wrappedType;
+        if (Primitives.allPrimitiveTypes().contains(targetType)) {
+            wrappedType = Primitives.wrap(targetType);
+        } else {
+            wrappedType = targetType;
+        }
+        
+        try {
+            return (T) wrappedType.getMethod("valueOf", String.class).invoke(null, value);
+        } catch (Exception e) {
+            ClassCoercionException tothrow = new ClassCoercionException("Cannot coerce "+JavaStringEscapes.wrapJavaString(value)+" to "+targetType.getCanonicalName()+" ("+value+"): adapting failed");
+            tothrow.initCause(e);
+            throw tothrow;
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/ec4da197/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercer.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercer.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercer.java
new file mode 100644
index 0000000..bdac81c
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercer.java
@@ -0,0 +1,31 @@
+/*
+ * 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.brooklyn.util.javalang.coerce;
+
+import org.apache.brooklyn.util.guava.Maybe;
+
+import com.google.common.reflect.TypeToken;
+
+public interface TypeCoercer {
+
+    <T> T coerce(Object input, Class<T> type);
+    <T> Maybe<T> tryCoerce(Object input, Class<T> type);
+    <T> Maybe<T> tryCoerce(Object input, TypeToken<T> type);
+    
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/ec4da197/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercerExtensible.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercerExtensible.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercerExtensible.java
new file mode 100644
index 0000000..eb17b04
--- /dev/null
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercerExtensible.java
@@ -0,0 +1,296 @@
+/*
+ * 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.brooklyn.util.javalang.coerce;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.javalang.Boxing;
+import org.apache.brooklyn.util.javalang.JavaClassNames;
+import org.apache.brooklyn.util.text.Strings;
+import org.apache.brooklyn.util.time.Duration;
+import org.apache.brooklyn.util.time.Time;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Function;
+import com.google.common.base.Objects;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Table;
+import com.google.common.reflect.TypeToken;
+
+/**
+ * Attempts to coerce {@code value} to {@code targetType}.
+ * <p>
+ * Maintains a registry of adapter functions for type pairs in a {@link Table} which
+ * is searched after checking various strategies, including the following:
+ * <ul>
+ * <li>{@code value.asTargetType()}
+ * <li>{@code TargetType.fromType(value)} (if {@code value instanceof Type})
+ * <li>{@code value.targetTypeValue()} (handy for primitives)
+ * <li>{@code TargetType.valueOf(value)} (for enums)
+ * </ul>
+ * <p>
+ * A default set of adapters will handle most common Java-type coercions
+ * as well as <code>String</code> coercion to:
+ * <ul>
+ * <li> {@link Set}, {@link List}, {@link Map} and similar -- parses as YAML
+ * <li> {@link Date} -- parses using {@link Time#parseDate(String)}
+ * <li> {@link Duration} -- parses using {@link Duration#parse(String)}
+ * </ul>
+ */
+public class TypeCoercerExtensible implements TypeCoercer {
+
+    private static final Logger log = LoggerFactory.getLogger(TypeCoercerExtensible.class);
+    
+    protected TypeCoercerExtensible() {}
+
+    /** has all the strategies (primitives, collections, etc) 
+     * and all the adapters from {@link CommonAdaptorTypeCoercions} */
+    public static TypeCoercerExtensible newDefault() {
+        return new CommonAdaptorTypeCoercions(newEmpty()).registerAllAdapters().getCoercer();
+    }
+
+    /** has all the strategies (primitives, collections, etc) but no adapters, 
+     * so caller can pick and choose e.g. from {@link CommonAdaptorTypeCoercions} */
+    public static TypeCoercerExtensible newEmpty() {
+        return new TypeCoercerExtensible();
+    }
+
+    /** Store the coercion {@link Function functions} in a {@link Table table}. */
+    private Table<Class<?>, Class<?>, Function<?,?>> registry = HashBasedTable.create();
+
+    @Override
+    public <T> T coerce(Object value, Class<T> targetType) {
+        return coerce(value, TypeToken.of(targetType));
+    }
+    
+    public <T> T coerce(Object value, TypeToken<T> targetTypeToken) {
+        return tryCoerce(value, targetTypeToken).get();
+    }
+    
+    @Override
+    public <T> Maybe<T> tryCoerce(Object input, Class<T> type) {
+        return tryCoerce(input, TypeToken.of(type));
+    }
+
+    @Override
+    public <T> Maybe<T> tryCoerce(Object value, TypeToken<T> targetTypeToken) {
+        Maybe<T> result = tryCoerceInternal(value, targetTypeToken);
+        return Maybe.Absent.changeExceptionSupplier(result, ClassCoercionException.class);
+    }
+    
+    @SuppressWarnings("unchecked")
+    protected <T> Maybe<T> tryCoerceInternal(Object value, TypeToken<T> targetTypeToken) {
+        if (value==null) return Maybe.of((T)null);
+        Class<? super T> targetType = targetTypeToken.getRawType();
+        Maybe<T> result = null;
+        Maybe<T> firstError = null;
+
+        //recursive coercion of parameterized collections and map entries
+        if (targetTypeToken.getType() instanceof ParameterizedType) {
+            if (value instanceof Collection && Collection.class.isAssignableFrom(targetType)) {
+                result = tryCoerceCollection(value, targetTypeToken, targetType);
+            } else if (value instanceof Map && Map.class.isAssignableFrom(targetType)) {
+                result = tryCoerceMap(value, targetTypeToken);
+            }
+        }
+        if (result!=null && result.isPresent()) return result;
+        if (result!=null && firstError==null) firstError = result;
+        
+        if (targetType.isInstance(value)) return Maybe.of( (T) value );
+
+        result = PrimitiveStringTypeCoercions.tryCoerce(value, targetType);
+        if (result!=null && result.isPresent()) return result;
+        if (result!=null && firstError==null) firstError = result;
+        
+        result = tryCoerceWithFromMethod(value, targetType);
+        if (result!=null && result.isPresent()) return result;
+        if (result!=null && firstError==null) firstError = result;
+        
+        //ENHANCEMENT could look in type hierarchy of both types for a conversion method...
+        
+        //at this point, if either is primitive then run instead over boxed types
+        Class<?> boxedT = Boxing.PRIMITIVE_TO_BOXED.get(targetType);
+        Class<?> boxedVT = Boxing.PRIMITIVE_TO_BOXED.get(value.getClass());
+        if (boxedT!=null || boxedVT!=null) {
+            try {
+                if (boxedT==null) boxedT=targetType;
+                Object boxedV = boxedVT==null ? value : boxedVT.getConstructor(value.getClass()).newInstance(value);
+                return tryCoerce(boxedV, (Class<T>)boxedT);
+            } catch (Exception e) {
+                return Maybe.absent(new ClassCoercionException("Cannot coerce type "+value.getClass()+" to "+targetType.getCanonicalName()+" ("+value+"): unboxing failed", e));
+            }
+        }
+        
+        //for enums call valueOf with the string representation of the value
+        if (targetType.isEnum()) {
+            result = EnumTypeCoercions.tryCoerceUntyped(Strings.toString(value), (Class<T>)targetType);
+            if (result!=null && result.isPresent()) return result;
+            if (result!=null && firstError==null) firstError = result;
+        }
+
+        //now look in registry
+        synchronized (registry) {
+            Map<Class<?>, Function<?,?>> adapters = registry.row(targetType);
+            for (Map.Entry<Class<?>, Function<?,?>> entry : adapters.entrySet()) {
+                if (entry.getKey().isInstance(value)) {
+                    try {
+                        T resultT = ((Function<Object,T>)entry.getValue()).apply(value);
+                        
+                        // Check if need to unwrap again (e.g. if want List<Integer> and are given a String "1,2,3"
+                        // then we'll have so far converted to List.of("1", "2", "3"). Call recursively.
+                        // First check that value has changed, to avoid stack overflow!
+                        if (!Objects.equal(value, resultT) && targetTypeToken.getType() instanceof ParameterizedType) {
+                            // Could duplicate check for `result instanceof Collection` etc; but recursive call
+                            // will be fine as if that doesn't match we'll safely reach `targetType.isInstance(value)`
+                            // and just return the result.
+                            return tryCoerce(resultT, targetTypeToken);
+                        }
+                        return Maybe.of(resultT);
+                    } catch (Exception e) {
+                        Exceptions.propagateIfFatal(e);
+                        if (log.isDebugEnabled()) {
+                            log.debug("When coercing, registry adapter "+entry+" gave error on "+value+" -> "+targetType+" "
+                                + (firstError==null ? "(rethrowing)" : "(suppressing as there is already an error)")
+                                + ": "+e, e);
+                        }
+                        if (firstError==null) {
+                            if (e instanceof ClassCoercionException) firstError = Maybe.absent(e);
+                            else firstError = Maybe.absent(new ClassCoercionException("Cannot coerce type "+value.getClass().getCanonicalName()+" to "+targetType.getCanonicalName()+" ("+value+")", e));
+                        }
+                        continue;
+                    }
+                }
+            }
+        }
+
+        //not found
+        if (firstError!=null) return firstError;
+        return Maybe.absent(new ClassCoercionException("Cannot coerce type "+value.getClass().getCanonicalName()+" to "+targetType.getCanonicalName()+" ("+value+"): no adapter known"));
+    }
+
+    @SuppressWarnings("unchecked")
+    protected <T> Maybe<T> tryCoerceWithFromMethod(Object value, Class<? super T> targetType) {
+        //now look for static TargetType.fromType(Type t) where value instanceof Type  
+        for (Method m: targetType.getMethods()) {
+            if (((m.getModifiers()&Modifier.STATIC)==Modifier.STATIC) && 
+                    m.getName().startsWith("from") && m.getParameterTypes().length==1 &&
+                    m.getParameterTypes()[0].isInstance(value)) {
+                if (m.getName().equals("from"+JavaClassNames.verySimpleClassName(m.getParameterTypes()[0]))) {
+                    try {
+                        return Maybe.of((T) m.invoke(null, value));
+                    } catch (Exception e) {
+                        Maybe.absent(new ClassCoercionException("Cannot coerce type "+value.getClass()+" to "+targetType.getCanonicalName()+" ("+value+"): "+m.getName()+" adapting failed, "+e));
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    @SuppressWarnings("unchecked")
+    protected <T> Maybe<T> tryCoerceMap(Object value, TypeToken<T> targetTypeToken) {
+        if (!(value instanceof Map) || !(Map.class.isAssignableFrom(targetTypeToken.getRawType()))) return null;
+        Type[] arguments = ((ParameterizedType) targetTypeToken.getType()).getActualTypeArguments();
+        if (arguments.length != 2) {
+            throw new IllegalStateException("Unexpected number of parameters in map type: " + arguments);
+        }
+        Map<Object,Object> coerced = Maps.newLinkedHashMap();
+        TypeToken<?> mapKeyType = TypeToken.of(arguments[0]);
+        TypeToken<?> mapValueType = TypeToken.of(arguments[1]);
+        int i=0;
+        for (Map.Entry<?,?> entry : ((Map<?,?>) value).entrySet()) {
+            Maybe<?> k = tryCoerce(entry.getKey(), mapKeyType);
+            if (k.isAbsent()) return Maybe.absent(new ClassCoercionException(
+                "Could not coerce key of entry "+i+" in "+value+" to "+targetTypeToken,
+                ((Maybe.Absent<T>)k).getException()));
+
+            Maybe<?> v = tryCoerce(entry.getValue(), mapValueType);
+            if (v.isAbsent()) return Maybe.absent(new ClassCoercionException(
+                "Could not coerce value of entry "+i+" in "+value+" to "+targetTypeToken,
+                ((Maybe.Absent<T>)v).getException()));
+            
+            coerced.put(k.get(),  v.get());
+            
+            i++;
+        }
+        return Maybe.of((T) Maps.newLinkedHashMap(coerced));
+    }
+
+    /** tries to coerce a list;
+     * returns null if it just doesn't apply, a {@link Maybe.Present} if it succeeded,
+     * or {@link Maybe.Absent} with a good exception if it should have applied but couldn't */
+    @SuppressWarnings("unchecked")
+    protected <T> Maybe<T> tryCoerceCollection(Object value, TypeToken<T> targetTypeToken, Class<? super T> targetType) {
+        if (!(value instanceof Iterable) || !(Iterable.class.isAssignableFrom(targetTypeToken.getRawType()))) return null;
+        Type[] arguments = ((ParameterizedType) targetTypeToken.getType()).getActualTypeArguments();
+        if (arguments.length != 1) {
+            return Maybe.absent(new IllegalStateException("Unexpected number of parameters in collection type: " + arguments));
+        }
+        Collection<Object> coerced = Lists.newLinkedList();
+        TypeToken<?> listEntryType = TypeToken.of(arguments[0]);
+        int i = 0;
+        for (Object entry : (Iterable<?>) value) {
+            Maybe<?> entryCoerced = tryCoerce(entry, listEntryType);
+            if (entryCoerced.isPresent()) {
+                coerced.add(entryCoerced.get());
+            } else {
+                return Maybe.absent(new ClassCoercionException(
+                    "Could not coerce entry "+i+" in "+value+" to "+targetTypeToken,
+                    ((Maybe.Absent<T>)entryCoerced).getException()));
+            }
+            i++;
+        }
+        if (Set.class.isAssignableFrom(targetType)) {
+            return Maybe.of((T) Sets.newLinkedHashSet(coerced));
+        } else {
+            return Maybe.of((T) Lists.newArrayList(coerced));
+        }
+    }
+
+    /**
+     * Returns a function that does a type coercion to the given type. For example,
+     * {@code TypeCoercions.function(Double.class)} will return a function that will
+     * coerce its input value to a {@link Double} (or throw a {@link ClassCoercionException}
+     * if that is not possible).
+     */
+    public <T> Function<Object, T> function(final Class<T> type) {
+        return new CoerceFunctionals.CoerceFunction<T>(this, type);
+    }
+    
+    /** Registers an adapter for use with type coercion. Returns any old adapter registered for this pair. */
+    @SuppressWarnings("unchecked")
+    public synchronized <A,B> Function<? super A,B> registerAdapter(Class<A> sourceType, Class<B> targetType, Function<? super A,B> fn) {
+        return (Function<? super A,B>) registry.put(targetType, sourceType, fn);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/ec4da197/utils/common/src/test/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercionsTest.java
----------------------------------------------------------------------
diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercionsTest.java b/utils/common/src/test/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercionsTest.java
new file mode 100644
index 0000000..786d9e8
--- /dev/null
+++ b/utils/common/src/test/java/org/apache/brooklyn/util/javalang/coerce/TypeCoercionsTest.java
@@ -0,0 +1,379 @@
+/*
+ * 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.brooklyn.util.javalang.coerce;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.util.collections.MutableSet;
+import org.apache.brooklyn.util.text.StringPredicates;
+import org.codehaus.groovy.runtime.GStringImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.reflect.TypeToken;
+
+public class TypeCoercionsTest {
+
+    private static final Logger log = LoggerFactory.getLogger(TypeCoercionsTest.class);
+    
+    TypeCoercerExtensible coercer = TypeCoercerExtensible.newDefault();
+    
+    protected <T> T coerce(Object x, Class<T> type) {
+        return coercer.coerce(x, type);
+    }
+    protected <T> T coerce(Object x, TypeToken<T> type) {
+        return coercer.coerce(x, type);
+    }
+    
+    @Test
+    public void testCoerceCharSequenceToString() {
+        assertEquals(coerce(new StringBuilder("abc"), String.class), "abc");
+        assertEquals(coerce(new GStringImpl(new Object[0], new String[0]), String.class), "");
+    }
+    
+    @Test
+    public void testCoerceStringToPrimitive() {
+        assertEquals(coerce("1", Character.class), (Character)'1');
+        assertEquals(coerce(" ", Character.class), (Character)' ');
+        assertEquals(coerce("1", Short.class), (Short)((short)1));
+        assertEquals(coerce("1", Integer.class), (Integer)1);
+        assertEquals(coerce("1", Long.class), (Long)1l);
+        assertEquals(coerce("1", Float.class), (Float)1f);
+        assertEquals(coerce("1", Double.class), (Double)1d);
+        assertEquals(coerce("true", Boolean.class), (Boolean)true);
+        assertEquals(coerce("False", Boolean.class), (Boolean)false);
+        assertEquals(coerce("true ", Boolean.class), (Boolean)true);
+        assertNull(coerce(null, Boolean.class), null);
+
+        assertEquals(coerce("1", char.class), (Character)'1');
+        assertEquals(coerce("1", short.class), (Short)((short)1));
+        assertEquals(coerce("1", int.class), (Integer)1);
+        assertEquals(coerce("1", long.class), (Long)1l);
+        assertEquals(coerce("1", float.class), (Float)1f);
+        assertEquals(coerce("1", double.class), (Double)1d);
+        assertEquals(coerce("TRUE", boolean.class), (Boolean)true);
+        assertEquals(coerce("false", boolean.class), (Boolean)false);
+    }
+
+    @Test
+    public void testCoercePrimitivesToSameType() {
+        assertEquals(coerce('1', Character.class), (Character)'1');
+        assertEquals(coerce((short)1, Short.class), (Short)((short)1));
+        assertEquals(coerce(1, Integer.class), (Integer)1);
+        assertEquals(coerce(1l, Long.class), (Long)1l);
+        assertEquals(coerce(1f, Float.class), (Float)1f);
+        assertEquals(coerce(1d, Double.class), (Double)1d);
+        assertEquals(coerce(true, Boolean.class), (Boolean)true);
+    }
+    
+    @Test
+    public void testCastPrimitives() {
+        assertEquals(coerce(1L, Character.class), (Character)(char)1);
+        assertEquals(coerce(1L, Byte.class), (Byte)(byte)1);
+        assertEquals(coerce(1L, Short.class), (Short)(short)1);
+        assertEquals(coerce(1L, Integer.class), (Integer)1);
+        assertEquals(coerce(1L, Long.class), (Long)(long)1);
+        assertEquals(coerce(1L, Float.class), (Float)(float)1);
+        assertEquals(coerce(1L, Double.class), (Double)(double)1);
+        
+        assertEquals(coerce(1L, char.class), (Character)(char)1);
+        assertEquals(coerce(1L, byte.class), (Byte)(byte)1);
+        assertEquals(coerce(1L, short.class), (Short)(short)1);
+        assertEquals(coerce(1L, int.class), (Integer)1);
+        assertEquals(coerce(1L, long.class), (Long)(long)1);
+        assertEquals(coerce(1L, float.class), (Float)(float)1);
+        assertEquals(coerce(1L, double.class), (Double)(double)1);
+        
+        assertEquals(coerce((char)1, Integer.class), (Integer)1);
+        assertEquals(coerce((byte)1, Integer.class), (Integer)1);
+        assertEquals(coerce((short)1, Integer.class), (Integer)1);
+        assertEquals(coerce((int)1, Integer.class), (Integer)1);
+        assertEquals(coerce((long)1, Integer.class), (Integer)1);
+        assertEquals(coerce((float)1, Integer.class), (Integer)1);
+        assertEquals(coerce((double)1, Integer.class), (Integer)1);
+    }
+    
+    @Test
+    public void testCoercePrimitiveFailures() {
+        // error messages don't have to be this exactly, but they should include sufficient information...
+        assertCoercionFailsWithErrorMatching("maybe", boolean.class, StringPredicates.containsAllLiterals("String", "boolean", "maybe"));
+        assertCoercionFailsWithErrorMatching("NaN", int.class, StringPredicates.containsAllLiterals("int", "NaN"));
+        assertCoercionFailsWithErrorMatching('c', boolean.class, StringPredicates.containsAllLiterals("boolean", "(c)"));  // will say 'string' rather than 'char'
+        assertCoercionFailsWithErrorMatching(0, boolean.class, StringPredicates.containsAllLiterals("Integer", "boolean", "0"));
+    }
+    
+    protected void assertCoercionFailsWithErrorMatching(Object input, Class<?> type, Predicate<? super String> errorMessageRequirement) {
+        try {
+            Object result = coerce(input, type);
+            Assert.fail("Should have failed type coercion of "+input+" to "+type+", instead got: "+result);
+        } catch (Exception e) {
+            if (errorMessageRequirement==null || errorMessageRequirement.apply(e.toString()))
+                log.info("Primitive coercion failed as expected, with: "+e);
+            else
+                Assert.fail("Error from type coercion of "+input+" to "+type+" failed with wrong exception; expected match of "+errorMessageRequirement+" but got: "+e);
+        }
+        
+    }
+
+    @Test
+    public void testCastToNumericPrimitives() {
+        assertEquals(coerce(BigInteger.ONE, Integer.class), (Integer)1);
+        assertEquals(coerce(BigInteger.ONE, int.class), (Integer)1);
+        assertEquals(coerce(BigInteger.valueOf(Long.MAX_VALUE), Long.class), (Long)Long.MAX_VALUE);
+        assertEquals(coerce(BigInteger.valueOf(Long.MAX_VALUE), long.class), (Long)Long.MAX_VALUE);
+        
+        assertEquals(coerce(BigDecimal.valueOf(0.5), Double.class), 0.5d, 0.00001d);
+        assertEquals(coerce(BigDecimal.valueOf(0.5), double.class), 0.5d, 0.00001d);
+    }
+
+    @Test
+    public void testCoerceStringToBigNumber() {
+    	assertEquals(coerce("0.5", BigDecimal.class), BigDecimal.valueOf(0.5));
+    	assertEquals(coerce("1", BigInteger.class), BigInteger.valueOf(1));
+    }
+
+    @Test
+    public void testCoerceStringToEnum() {
+        assertEquals(coerce("LOWERCASE", PerverseEnum.class), PerverseEnum.lowercase);
+        assertEquals(coerce("CAMELCASE", PerverseEnum.class), PerverseEnum.camelCase);
+        assertEquals(coerce("upper", PerverseEnum.class), PerverseEnum.UPPER);
+        assertEquals(coerce("upper_with_underscore", PerverseEnum.class), PerverseEnum.UPPER_WITH_UNDERSCORE);
+        assertEquals(coerce("LOWER_WITH_UNDERSCORE", PerverseEnum.class), PerverseEnum.lower_with_underscore);
+    }
+    public static enum PerverseEnum {
+        lowercase,
+        camelCase,
+        UPPER,
+        UPPER_WITH_UNDERSCORE,
+        lower_with_underscore;
+    }
+    
+    @Test
+    public void testListToSetCoercion() {
+        Set<?> s = coerce(ImmutableList.of(1), Set.class);
+        Assert.assertEquals(s, ImmutableSet.of(1));
+    }
+    
+    @Test
+    public void testSetToListCoercion() {
+        List<?> s = coerce(ImmutableSet.of(1), List.class);
+        Assert.assertEquals(s, ImmutableList.of(1));
+    }
+    
+    @Test
+    public void testIterableToArrayCoercion() {
+        String[] s = coerce(ImmutableList.of("a", "b"), String[].class);
+        Assert.assertTrue(Arrays.equals(s, new String[] {"a", "b"}), "result="+Arrays.toString(s));
+        
+        Integer[] i = coerce(ImmutableList.of(1, 2), Integer[].class);
+        Assert.assertTrue(Arrays.equals(i, new Integer[] {1, 2}), "result="+Arrays.toString(i));
+        
+        int[] i2 = coerce(ImmutableList.of(1, 2), int[].class);
+        Assert.assertTrue(Arrays.equals(i2, new int[] {1, 2}), "result="+Arrays.toString(i2));
+        
+        int[] i3 = coerce(MutableSet.of("1", 2), int[].class);
+        Assert.assertTrue(Arrays.equals(i3, new int[] {1, 2}), "result="+Arrays.toString(i3));
+    }
+
+    @Test
+    public void testListEntryCoercion() {
+        @SuppressWarnings("serial")
+        List<?> s = coerce(ImmutableList.of("java.lang.Integer", "java.lang.Double"), new TypeToken<List<Class<?>>>() { });
+        Assert.assertEquals(s, ImmutableList.of(Integer.class, Double.class));
+    }
+    
+    @Test
+    public void testListEntryToSetCoercion() {
+        @SuppressWarnings("serial")
+        Set<?> s = coerce(ImmutableList.of("java.lang.Integer", "java.lang.Double"), new TypeToken<Set<Class<?>>>() { });
+        Assert.assertEquals(s, ImmutableSet.of(Integer.class, Double.class));
+    }
+    
+    @Test
+    public void testListEntryToCollectionCoercion() {
+        @SuppressWarnings("serial")
+        Collection<?> s = coerce(ImmutableList.of("java.lang.Integer", "java.lang.Double"), new TypeToken<Collection<Class<?>>>() { });
+        Assert.assertEquals(s, ImmutableList.of(Integer.class, Double.class));
+    }
+
+    @Test
+    public void testMapValueCoercion() {
+        @SuppressWarnings("serial")
+        Map<?,?> s = coerce(ImmutableMap.of("int", "java.lang.Integer", "double", "java.lang.Double"), new TypeToken<Map<String, Class<?>>>() { });
+        Assert.assertEquals(s, ImmutableMap.of("int", Integer.class, "double", Double.class));
+    }
+    
+    @Test
+    public void testMapKeyCoercion() {
+        @SuppressWarnings("serial")
+        Map<?,?> s = coerce(ImmutableMap.of("java.lang.Integer", "int", "java.lang.Double", "double"), new TypeToken<Map<Class<?>, String>>() { });
+        Assert.assertEquals(s, ImmutableMap.of(Integer.class, "int", Double.class, "double"));
+    }
+
+    @Test
+    public void testStringToListCoercion() {
+        List<?> s = coerce("a,b,c", List.class);
+        Assert.assertEquals(s, ImmutableList.of("a", "b", "c"));
+    }
+
+    @Test
+    @SuppressWarnings("serial")
+    public void testCoerceRecursivelyStringToGenericsCollection() {
+        assertEquals(coerce("1,2", new TypeToken<List<Integer>>() {}), ImmutableList.of(1, 2));
+    }
+    
+    @Test
+    public void testJsonStringToMapCoercion() {
+        Map<?,?> s = coerce("{ \"a\" : \"1\", b : 2 }", Map.class);
+        Assert.assertEquals(s, ImmutableMap.of("a", "1", "b", 2));
+    }
+
+    @Test
+    public void testJsonStringWithoutQuotesToMapCoercion() {
+        Map<?,?> s = coerce("{ a : 1 }", Map.class);
+        Assert.assertEquals(s, ImmutableMap.of("a", 1));
+    }
+
+    @Test
+    public void testJsonComplexTypesToMapCoercion() {
+        Map<?,?> s = coerce("{ a : [1, \"2\", '\"3\"'], b: { c: d, 'e': \"f\" } }", Map.class);
+        Assert.assertEquals(s, ImmutableMap.of("a", ImmutableList.<Object>of(1, "2", "\"3\""), 
+            "b", ImmutableMap.of("c", "d", "e", "f")));
+    }
+
+    @Test
+    public void testJsonStringWithoutBracesToMapCoercion() {
+        Map<?,?> s = coerce("a : 1", Map.class);
+        Assert.assertEquals(s, ImmutableMap.of("a", 1));
+    }
+
+    @Test
+    public void testJsonStringWithoutBracesWithMultipleToMapCoercion() {
+        Map<?,?> s = coerce("a : 1, b : 2", Map.class);
+        Assert.assertEquals(s, ImmutableMap.of("a", 1, "b", 2));
+    }
+
+    @Test
+    public void testKeyEqualsValueStringToMapCoercion() {
+        Map<?,?> s = coerce("a=1,b=2", Map.class);
+        Assert.assertEquals(s, ImmutableMap.of("a", "1", "b", "2"));
+    }
+
+    @Test(expectedExceptions=ClassCoercionException.class)
+    public void testJsonStringWithoutBracesOrSpaceDisallowedAsMapCoercion() {
+        // yaml requires spaces after the colon
+        coerce("a:1,b:2", Map.class);
+        Asserts.shouldHaveFailedPreviously();
+    }
+    
+    @Test
+    public void testEqualsInBracesMapCoercion() {
+        Map<?,?> s = coerce("{ a = 1, b = '2' }", Map.class);
+        Assert.assertEquals(s, ImmutableMap.of("a", 1, "b", "2"));
+    }
+
+    @Test
+    public void testKeyEqualsOrColonValueWithBracesStringToMapCoercion() {
+        Map<?,?> s = coerce("{ a=1, b: 2 }", Map.class);
+        Assert.assertEquals(s, ImmutableMap.of("a", "1", "b", 2));
+    }
+
+    @Test
+    public void testKeyEqualsOrColonValueWithoutBracesStringToMapCoercion() {
+        Map<?,?> s = coerce("a=1, b: 2", Map.class);
+        Assert.assertEquals(s, ImmutableMap.of("a", "1", "b", 2));
+    }
+
+    @Test
+    public void testURItoStringCoercion() {
+        String s = coerce(URI.create("http://localhost:1234/"), String.class);
+        Assert.assertEquals(s, "http://localhost:1234/");
+    }
+
+    @Test
+    public void testURLtoStringCoercion() throws MalformedURLException {
+        String s = coerce(new URL("http://localhost:1234/"), String.class);
+        Assert.assertEquals(s, "http://localhost:1234/");
+    }
+
+    @Test
+    public void testAs() {
+        Integer x = coerce(new WithAs("3"), Integer.class);
+        Assert.assertEquals(x, (Integer)3);
+    }
+
+    @Test
+    public void testFrom() {
+        WithFrom x = coerce("3", WithFrom.class);
+        Assert.assertEquals(x.value, 3);
+    }
+
+    @Test
+    public void testCoerceStringToNumber() {
+        assertEquals(coerce("1", Number.class), (Number) Double.valueOf(1));
+        assertEquals(coerce("1.0", Number.class), (Number) Double.valueOf(1.0));
+    }
+
+    @Test(expectedExceptions = org.apache.brooklyn.util.javalang.coerce.ClassCoercionException.class)
+    public void testInvalidCoercionThrowsClassCoercionException() {
+        coerce(new Object(), TypeToken.of(Integer.class));
+    }
+
+    @Test
+    public void testCoercionFunction() {
+        assertEquals(coercer.function(Double.class).apply("1"), Double.valueOf(1));
+    }
+
+    public static class WithAs {
+        String value;
+        public WithAs(Object x) { value = ""+x; }
+        public Integer asInteger() {
+            return Integer.parseInt(value);
+        }
+    }
+
+    public static class WithFrom {
+        int value;
+        public static WithFrom fromString(String s) {
+            WithFrom result = new WithFrom();
+            result.value = Integer.parseInt(s);
+            return result;
+        }
+    }
+
+}