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;
+ }
+ }
+
+}