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