/* * Copyright (C) 2015 Google Inc. * * Licensed 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 interactivespaces.util.data.dynamic; import static com.google.common.base.Preconditions.checkArgument; import com.google.common.base.Function; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.primitives.Primitives; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Collections; import java.util.List; import java.util.Map; /** * Utility class for working with implicit conversions. * * @author Oleksandr Kelepko */ final class Conversions { /** * Prevent instantiation of a utility class. */ private Conversions() { } /** * Simple (primitives + {@link Object} + {@link String}) type conversions. */ private static final Map<Class<?>, Conversion<?>> SIMPLE_TYPE_CONVERSIONS; /** * List conversion. */ private static final ListConversion LIST_CONVERSION = new ListConversion(); /** * Map conversion. */ private static final MapConversion MAP_CONVERSION = new MapConversion(); static { Conversion<Boolean> toBoolean = new IdentityConversion<Boolean>(Boolean.class); Conversion<Integer> toInt = new IdentityConversion<Integer>(Integer.class); Conversion<Long> toLong = new IntegerOrLongToLong(); Conversion<Float> toFloat = new NumberOrStringToFloat(); Conversion<Double> toDouble = new NumberOrStringToDouble(); SIMPLE_TYPE_CONVERSIONS = ImmutableMap.<Class<?>, Conversion<?>>builder() .put(Boolean.class, toBoolean) .put(Boolean.TYPE, toBoolean) .put(Integer.class, toInt) .put(Integer.TYPE, toInt) .put(Long.class, toLong) .put(Long.TYPE, toLong) .put(Float.class, toFloat) .put(Float.TYPE, toFloat) .put(Double.class, toDouble) .put(Double.TYPE, toDouble) .put(String.class, new IdentityConversion<String>(String.class)) .put(Object.class, new IdentityConversion<Object>(Object.class)) .build(); } /** * Convert a given object to a given type. If conversion is impossible, throws an exception. * * @param expectedGenericType * expected generic type of the result * @param expectedType * expected type of the result * @param object * object to convert * @param <T> * type of the result * * @return object of the given type */ public static <T> T convert(Type expectedGenericType, Class<T> expectedType, Object object) { Object result = null; if (object == null) { if (!expectedType.isPrimitive()) { return null; } if (expectedType == Boolean.TYPE) { result = false; } // No default value for other primitive types. } else if (SIMPLE_TYPE_CONVERSIONS.containsKey(expectedType)) { result = SIMPLE_TYPE_CONVERSIONS.get(expectedType).apply(object); } else if (expectedType == List.class && object instanceof List) { result = LIST_CONVERSION.convert(expectedGenericType, object); } else if (expectedType == Map.class && object instanceof Map) { result = MAP_CONVERSION.convert(expectedGenericType, object); } else if (expectedType.isInstance(object)) { // Arbitrary object set in setter. result = object; } else if (expectedType.isInterface() && object instanceof Map) { @SuppressWarnings("unchecked") Map<String, Object> asMap = (Map) object; result = InterfaceMap.createInstance(expectedType, asMap); } return result != null ? Primitives.wrap(expectedType).cast(result) : cannotConvert(object, expectedType); } /** * Throw a proper exception saying that conversion failed. * * @param input * object that cannot be converted to the target type * @param targetType * target raw type * @param <T> * target type * * @return nothing */ private static <T> T cannotConvert(Object input, Class<T> targetType) { if (input == null) { throw new NullPointerException( String.format("Cannot convert null to primitive type %s.", targetType.getName())); } else { throw new ClassCastException(String.format("Cannot convert to %s from %s: %s.", targetType.getName(), input.getClass().getName(), input)); } } /** * Represents a function that converts values to a proper generic type. * For example, if the target type is {@code Map<String, MyInterface>}, * the original map may contain maps that need to be transformed into an {@link InterfaceMap}. * If the target type is {@code Map<String, List<MyInterface>>}, * then the elements of lists may need to be transformed. * * @param <T> * target type of the conversion */ private static final class RecursiveConversion<T> implements Function<Object, T> { /** * Creates a conversion that converts values to a given type. * * @param expectedGenericType * target generic type * @param expectedType * target raw type * @param <T> * target type * * @return conversion */ public static <T> Function<Object, T> create(Type expectedGenericType, Class<T> expectedType) { if (SIMPLE_TYPE_CONVERSIONS.containsKey(expectedType) || expectedType.isInterface()) { return new RecursiveConversion<T>(expectedGenericType, expectedType); } else { throw new UnsupportedOperationException("Unsupported dynamic property type: " + expectedGenericType); } } /** * Target generic type. */ private final Type expectedGenericType; /** * Target raw type. */ private final Class<T> expectedType; /** * Construct a new recursive conversion. * * @param expectedGenericType * target generic type * @param expectedType * target raw type */ private RecursiveConversion(Type expectedGenericType, Class<T> expectedType) { this.expectedGenericType = expectedGenericType; this.expectedType = expectedType; } @Override public T apply(Object input) { return convert(expectedGenericType, expectedType, input); } } /** * Represents an implicit conversion of a value from an original type to a target type. * If the value is {@code null} or if the original type is the same as the target type, * the original value will be returned without transformations. */ private abstract static class Conversion<T> implements Function<Object, T> { /** * Target type. */ private final Class<T> targetType; /** * Create a new conversion. * * @param targetType * target type */ Conversion(Class<T> targetType) { this.targetType = targetType; } @Override public final T apply(Object input) { if (input == null || targetType.isInstance(input)) { return targetType.cast(input); } T result = tryConvert(input); if (result != null) { return result; } return cannotConvert(input, targetType); } /** * Possibly transform a given value to return a value of a target type. * * @param input * value whose type differs from the target type * * @return value of the target type, or {@code null} if the value cannot be converted */ protected abstract T tryConvert(Object input); } /** * A no-op conversion. If a given object is of a target type, it is returned as-is. * Otherwise, an exception is thrown. * * @param <T> * target type */ private static final class IdentityConversion<T> extends Conversion<T> { /** * Create new identity conversion. * * @param targetType * target raw type */ public IdentityConversion(Class<T> targetType) { super(targetType); } @Override protected T tryConvert(Object input) { return null; } } /** * Converts {@link Long Long}s and {@link Integer Integer}s into {@link Long}. */ private static class IntegerOrLongToLong extends Conversion<Long> { /** * Create a new conversion. */ public IntegerOrLongToLong() { super(Long.class); } @Override protected Long tryConvert(Object input) { return input instanceof Integer ? ((Integer) input).longValue() : null; } } /** * Converts {@link Number}s and some {@link String}s into {@link Double}s. */ private static class NumberOrStringToDouble extends Conversion<Double> { /** * Special double values as strings. */ private static final Map<String, Double> SPECIAL_DOUBLE_VALUES = ImmutableMap.<String, Double>builder() .put(Double.toString(Double.NaN), Double.NaN) .put(Double.toString(Double.NEGATIVE_INFINITY), Double.NEGATIVE_INFINITY) .put(Double.toString(Double.POSITIVE_INFINITY), Double.POSITIVE_INFINITY) .build(); /** * Create a new conversion. */ public NumberOrStringToDouble() { super(Double.class); } @Override public Double tryConvert(Object o) { if (o instanceof Number) { return ((Number) o).doubleValue(); } Double d = SPECIAL_DOUBLE_VALUES.get(o); return d != null ? d : null; } } /** * Converts {@link Number}s and some {@link String}s into {@link Float}s. */ private static class NumberOrStringToFloat extends Conversion<Float> { /** * Special float values as strings. */ private static final Map<String, Float> SPECIAL_FLOAT_VALUES = ImmutableMap.<String, Float>builder() .put(Float.toString(Float.NaN), Float.NaN) .put(Float.toString(Float.NEGATIVE_INFINITY), Float.NEGATIVE_INFINITY) .put(Float.toString(Float.POSITIVE_INFINITY), Float.POSITIVE_INFINITY) .build(); /** * Create a new conversion. */ public NumberOrStringToFloat() { super(Float.class); } @Override public Float tryConvert(Object o) { if (o instanceof Number) { return ((Number) o).floatValue(); } Float d = SPECIAL_FLOAT_VALUES.get(o); return d != null ? d : null; } } /** * List conversion that recursively considers the list's type parameters. * Raw lists and wildcards are not supported. */ private static class ListConversion { /** * Convert a given list to a given target type. * * @param genericListType * target type * @param list * list to convert * * @return list of the target type */ public Object convert(Type genericListType, Object list) { checkArgument(list instanceof List<?>, "Not a list."); if (!(genericListType instanceof ParameterizedType)) { throw new UnsupportedOperationException("Raw lists are not supported: " + genericListType); } Type genericElementType = ((ParameterizedType) genericListType).getActualTypeArguments()[0]; Class<?> elementType; if (genericElementType instanceof ParameterizedType) { // Example: List<Map<String, String>>. elementType = (Class<?>) ((ParameterizedType) genericElementType).getRawType(); } else if (genericElementType instanceof Class<?>) { // Example: List<String>. elementType = (Class<?>) genericElementType; } else { // Example: List<? extends Number>. throw new UnsupportedOperationException("Wildcards are not supported: " + genericListType); } Function<Object, ?> transformation = RecursiveConversion.create(genericElementType, elementType); return Collections.unmodifiableList(Lists.transform((List<?>) list, transformation)); } } /** * Map conversion that recursively considers the map's type parameters. * Only maps with String keys are supported. * Raw maps and wildcards are not supported. */ private static class MapConversion { /** * Convert a given map to a given target type. * * @param genericMapType * target type * @param map * map to convert * * @return map of the target type */ public Object convert(Type genericMapType, Object map) { checkArgument(map instanceof Map<?, ?>, "Not a map."); if (!(genericMapType instanceof ParameterizedType)) { throw new UnsupportedOperationException("Raw maps are not supported: " + genericMapType); } Type[] typeArguments = ((ParameterizedType) genericMapType).getActualTypeArguments(); Type genericKeyType = typeArguments[0]; if (genericKeyType != String.class) { throw new UnsupportedOperationException("Only maps with String keys are supported: " + genericMapType); } Type genericValueType = typeArguments[1]; Class<?> elementType; if (genericValueType instanceof ParameterizedType) { // Example: Map<String, List<String>>. elementType = (Class<?>) ((ParameterizedType) genericValueType).getRawType(); } else if (genericValueType instanceof Class<?>) { // Example: Map<String>. elementType = (Class<?>) genericValueType; } else { // Example: Map<String, ? extends Number>. throw new UnsupportedOperationException("Wildcards are not supported: " + genericMapType); } Function<Object, ?> transformation = RecursiveConversion.create(genericValueType, elementType); return Collections.unmodifiableMap(Maps.transformValues((Map) map, transformation)); } } }