package org.deephacks.confit.serialization; import com.google.common.base.Optional; import com.google.common.base.Strings; import org.deephacks.confit.serialization.Conversion.Converter.ObjectToStringConverter; import org.deephacks.confit.serialization.Conversion.Converter.StringToBooleanConverter; import org.deephacks.confit.serialization.Conversion.Converter.StringToEnumConverter; import org.deephacks.confit.serialization.Conversion.Converter.StringToNumberConverter; import org.deephacks.confit.serialization.Conversion.Converter.StringToObjectConverter; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.GenericArrayType; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; /** * Conversion is responsible for converting values using registered converters. * * Inspiration from http://www.springsource.org */ @SuppressWarnings({ "unchecked", "rawtypes" }) public final class Conversion { /** Keeper for converters available. */ private static final HashMap<Class<?>, SourceTargetPair> converters = new HashMap<>(); /** Lookup cache for finding converters. */ private static final ConcurrentHashMap<SourceTargetPairKey, Converter> cache = new ConcurrentHashMap<>(); private static Conversion INSTANCE; private static final UniqueId ids = new UniqueId(); static { register(new ClassToSchemaConverter()); register(new FieldToSchemaPropertyConverter()); register(new BeanToObjectConverter()); register(new ObjectToBeanConverter()); } private Conversion() { registerDefault(); } public static synchronized Conversion get() { if (INSTANCE == null) { INSTANCE = new Conversion(); } return INSTANCE; } private void registerDefault() { register(new StringToEnumConverter()); register(new StringToObjectConverter()); register(new ObjectToStringConverter()); register(new StringToNumberConverter()); register(new StringToBooleanConverter()); } /** * Convert a value to a specific class. * * The algorithm for finding a suitable converter is as follows: * * Find converters that is able to convert both source and target; a exact or * superclass match. Pick the converter that have the best target match, if both * are equal, pick the one with best source match. * * That is, the converter that is most specialized in converting a value to * a specific target class will be prioritized, as long as it recognizes the source * value. * * @param source value to convert. * @param targetclass class to convert to. * @return converted value */ public <T> T convert(final Object source, final Class<T> targetclass) { if (source == null) { return null; } final Class<?> sourceclass = source.getClass(); if (targetclass.isPrimitive() && String.class.isAssignableFrom(sourceclass)) { return (T) parsePrimitive(source.toString(), targetclass); } final int sourceId = ids.getId(sourceclass); final int targetId = ids.getId(targetclass); final SourceTargetPairKey key = new SourceTargetPairKey(sourceId, targetId); Converter converter = cache.get(key); if (converter != null) { return (T) converter.convert(source, targetclass); } final LinkedList<SourceTargetPairMatch> matches = new LinkedList<>(); for (SourceTargetPair pair : converters.values()) { SourceTargetPairMatch match = pair.match(sourceclass, targetclass); if (match.matchesSource() && match.matchesTarget()) { matches.add(match); } } if (matches.size() == 0) { throw new ConversionException("No suitable converter found for target class [" + targetclass.getName() + "] and source value [" + sourceclass.getName() + "]. The following converters are available [" + converters.keySet() + "]"); } Collections.sort(matches, SourceTargetPairMatch.bestTargetMatch()); converter = matches.get(0).pair.converter; cache.put(key, converter); return (T) converter.convert(source, targetclass); } private Object parsePrimitive(String value, Class<?> targetclass) { if (Strings.isNullOrEmpty(value)) { throw new IllegalArgumentException("Cannot parse a primitive from empty string."); } if (int.class.isAssignableFrom(targetclass)) { return Integer.parseInt(value); } else if (boolean.class.isAssignableFrom(targetclass)) { return Boolean.parseBoolean(value); } else if (long.class.isAssignableFrom(targetclass)) { return Long.parseLong(value); } else if (float.class.isAssignableFrom(targetclass)) { return Float.parseFloat(value); } else if (double.class.isAssignableFrom(targetclass)) { return Double.parseDouble(value); } else if (byte.class.isAssignableFrom(targetclass)) { return Byte.parseByte(value); } else if (short.class.isAssignableFrom(targetclass)) { return Short.parseShort(value); } else if (char.class.isAssignableFrom(targetclass)) { return value.charAt(0); } throw new IllegalArgumentException("Did not recognize primitive type [" + targetclass + "]."); } public <T, V> Set<T> convert(Set<V> values, final Class<T> clazz) { final HashSet<T> objects = new HashSet<>(); if (values == null) { return new HashSet<>(); } for (V object : values) { objects.add(convert(object, clazz)); } return objects; } public <T, V> Collection<T> convert(Collection<V> values, final Class<T> clazz) { final ArrayList<T> objects = new ArrayList<>(); if (values == null) { return new ArrayList<>(); } for (V object : values) { objects.add(convert(object, clazz)); } return objects; } public <T, V> Map<V, T> convert(Map<V, Object> values, final Class<T> clazz) { if (values == null) { return null; } throw new UnsupportedOperationException(); } public static <T, V> void register(Converter converter) { if (converters.get(converter.getClass()) != null) { return; } converters.put(converter.getClass(), new SourceTargetPair(converter)); cache.clear(); } private static class SourceTargetPair { private final Class<?> source; private final Class<?> target; private final Converter converter; public SourceTargetPair(Converter converter) { List<Class<?>> types = getParameterizedType(converter.getClass(), Converter.class); if (types.size() < 2) { throw new IllegalArgumentException( "Unable to the determine generic source and target type " + "for converter. Please declare these generic types."); } this.source = types.get(0); this.target = types.get(1); this.converter = converter; } public SourceTargetPairMatch match(Class<?> sourceValueClass, Class<?> targetClass) { return new SourceTargetPairMatch(this, getSourceMatchDistance(sourceValueClass), getTargetMatchDistance(targetClass)); } /** * Returns a list of classes that matches the candidate in terms * of converter source. The list is sorted with the most specific match first. */ private int getSourceMatchDistance(Class<?> candidate) { return distance(candidate, source); } /** * Returns a list of classes that matches the candidate in terms * of converter target. The list is sorted with the most specific match first. */ private int getTargetMatchDistance(Class<?> candidate) { return distance(candidate, target); } /** * Climb the class hierarchy of the candidate class and calculate the distance * between to the capability class. * * @return The distance in the class hierarchy between the candidate and capability. */ private int distance(Class<?> candidate, Class<?> capability) { int distance = 0; if (candidate == capability) { return distance; } final LinkedList<Class<?>> superclasses = new LinkedList<Class<?>>(); superclasses.add(candidate.getSuperclass()); while (!superclasses.isEmpty()) { Class<?> candidateSuperclazz = superclasses.removeLast(); if (candidateSuperclazz == null) { // Object converters are absolute last resort return Integer.MAX_VALUE; } if (candidateSuperclazz.equals(capability)) { if (capability == Object.class) { // Object converters are absolute last resort return Integer.MAX_VALUE; } return ++distance; } addInterfaces(candidateSuperclazz, superclasses); if (candidateSuperclazz.getSuperclass() != null) { superclasses.add(candidateSuperclazz.getSuperclass()); } } // no match return -1; } private void addInterfaces(Class<?> clazz, LinkedList<Class<?>> superclasses) { for (Class<?> inheritedIfc : clazz.getInterfaces()) { addInterfaces(inheritedIfc, superclasses); } } } private static class SourceTargetPairMatch { private int bestTargetMatch = -1; private int bestSourceMatch = -1; private final SourceTargetPair pair; public SourceTargetPairMatch(SourceTargetPair pair, int bestSourceMatch, int bestTargetMatch) { this.pair = pair; this.bestSourceMatch = bestSourceMatch; this.bestTargetMatch = bestTargetMatch; } public boolean matchesTarget() { return (bestTargetMatch > -1 ? true : false); } public boolean matchesSource() { return (bestSourceMatch > -1 ? true : false); } public static Comparator<SourceTargetPairMatch> bestTargetMatch() { return new Comparator<SourceTargetPairMatch>() { @Override public int compare(SourceTargetPairMatch o1, SourceTargetPairMatch o2) { if (o1.bestTargetMatch < o2.bestTargetMatch) { return -1; } else if (o1.bestTargetMatch > o2.bestTargetMatch) { return 1; } // equal target, pick best source. if (o1.bestSourceMatch < o2.bestSourceMatch) { return -1; } else if (o1.bestSourceMatch > o2.bestSourceMatch) { return 1; } return 0; } }; } } private static class SourceTargetPairKey { final int source; final int target; public SourceTargetPairKey(int source, int target) { this.source = source; this.target = target; } @Override public int hashCode() { int result = source; result = 31 * result + target; return result; } @Override public boolean equals(Object o) { if (this == o) return true; SourceTargetPairKey that = (SourceTargetPairKey) o; if (source != that.source) return false; if (target != that.target) return false; return true; } } /** * Returns the parameterized type of a class, if exists. Wild cards, type * variables and raw types will be returned as an empty list. * <p> * If a field is of type Set<String> then java.lang.String is returned. * </p> * <p> * If a field is of type Map<String, Integer> then [java.lang.String, * java.lang.Integer] is returned. * </p> * * @param ownerClass the implementing target class to check against * @param genericSuperClass the generic interface to resolve the type argument from * @return A list of classes of the parameterized type. */ public static List<Class<?>> getParameterizedType(final Class<?> ownerClass, Class<?> genericSuperClass) { Type[] types = null; if (genericSuperClass.isInterface()) { types = ownerClass.getGenericInterfaces(); } else { types = new Type[] { ownerClass.getGenericSuperclass() }; } final List<Class<?>> classes = new ArrayList<Class<?>>(); for (Type type : types) { if (!ParameterizedType.class.isAssignableFrom(type.getClass())) { // the field is it a raw type and does not have generic type // argument. Return empty list. return new ArrayList<Class<?>>(); } final ParameterizedType ptype = (ParameterizedType) type; final Type[] targs = ptype.getActualTypeArguments(); for (Type aType : targs) { classes.add(extractClass(ownerClass, aType)); } } return classes; } private static Class<?> extractClass(Class<?> ownerClass, Type arg) { if (arg instanceof ParameterizedType) { return extractClass(ownerClass, ((ParameterizedType) arg).getRawType()); } else if (arg instanceof GenericArrayType) { throw new UnsupportedOperationException("GenericArray types are not supported."); } else if (arg instanceof TypeVariable) { throw new UnsupportedOperationException("GenericArray types are not supported."); } return (arg instanceof Class ? (Class<?>) arg : Object.class); } public static class ConversionException extends RuntimeException { private static final long serialVersionUID = 3116958531528669531L; public ConversionException(String msg) { super(msg); } public ConversionException(Throwable e) { super(e); } public ConversionException(String msg, Exception e) { super(msg, e); } } /** * Converts a object of type V to a object of type T. * * Both V and T can be a super class or interface that handles a range of * subclasses. * * The algorithm for finding a suitable converter begins by first finding * converters that are able to convert both source and target; a exact or * superclass match. The final decision falls on the converter that have * the best target match. * * That is, the converter that is most specialized in converting a value T to * a specific target class will be prioritized, as long as it recognizes * the source value V. * * Converter providers are regsitered using the standard java service provider * mechanism. */ public interface Converter<V, T> { /** * @param source The source value to convert. * @param specificType the most specific type that the value should be converted to. * @return A converted object. */ public T convert(V source, Class<? extends T> specificType); /** * This is the fallback string converter that simply does a toString on the * provided object. * * Works fine for Number, Boolean, Enums and all other values that have a * toString that represent their real values in a serialized form. */ static final class ObjectToStringConverter implements Converter<Object, String> { @Override public String convert(Object source, Class<? extends String> specificType) { return (source != null ? source.toString() : null); } } static final class StringToBooleanConverter implements Converter<String, Boolean> { private static final Set<String> trueValues = new HashSet<String>(); private static final Set<String> falseValues = new HashSet<String>(); static { trueValues.addAll(Arrays.asList("true", "on", "yes", "y", "1")); falseValues.addAll(Arrays.asList("false", "off", "no", "n", "0")); } @Override public Boolean convert(String source, Class<? extends Boolean> specificType) { final String value = source.trim(); if (trueValues.contains(value)) { return Boolean.TRUE; } else if (falseValues.contains(value)) { return Boolean.FALSE; } else { throw new ConversionException("Invalid boolean value '" + source + "'"); } } } /** * This class can convert any enum to a string. */ static final class StringToEnumConverter implements Converter<String, Enum> { @Override public Enum convert(String source, Class<? extends Enum> specificType) { try { return Enum.valueOf(specificType, source); } catch (IllegalArgumentException e) { throw new ConversionException("Could not convert value [" + source + "] to any of the possible values: " + getPossibleValueString(specificType) + "."); } } public String getPossibleValueString(Class<?> clazz) { StringBuffer sb = new StringBuffer(); Field[] fields = clazz.getDeclaredFields(); List<String> values = new ArrayList<String>(); for (int i = 0; i < fields.length; i++) { if (fields[i].isEnumConstant()) { try { Object aEnum = fields[i].get(null); values.add(aEnum.toString()); } catch (Exception e) { throw new RuntimeException(e); } } } for (int i = 0; i < values.size(); i++) { sb.append(values.get(i)); if ((i + 1) != values.size()) { sb.append(", "); } } return sb.toString(); } } /** * This class can convert all number types such as BigDecimal, BigInteger, Byte, Double, * Float, Integer, Long, and Short. */ static final class StringToNumberConverter implements Converter<String, Number> { @Override public Number convert(String source, Class<? extends Number> specificType) { final String value = source.trim(); try { if (specificType.equals(Byte.class)) { return Byte.valueOf(value); } else if (specificType.equals(Short.class)) { return Short.valueOf(value); } else if (specificType.equals(Integer.class)) { return Integer.valueOf(value); } else if (specificType.equals(Long.class)) { return Long.valueOf(value); } else if (specificType.equals(BigInteger.class)) { return new BigInteger(value); } else if (specificType.equals(Float.class)) { return Float.valueOf(value); } else if (specificType.equals(Double.class)) { return Double.valueOf(value); } else if (specificType.equals(BigDecimal.class) || specificType.equals(Number.class)) { return new BigDecimal(value); } throw new ConversionException("Cannot convert [" + source + "] to [" + specificType.getName() + "]"); } catch (NumberFormatException e) { throw new ConversionException("Cannot convert [" + source + "] to [" + specificType.getName() + "]", e); } } } /** * General purpose converter that is able to convert a String to an object if the * object have a suitable static valueof method or a single argument String constructor. * * This should work fine for File, URL, DateTime, DurationTime */ static final class StringToObjectConverter implements Converter<String, Object> { private static final HashMap<Class<?>, Optional<Method>> valueofMethodCache = new HashMap<>(); private static final HashMap<Class<?>, Constructor> constructorCache = new HashMap<>(); @Override public Object convert(String source, Class<? extends Object> specificType) { Optional<Method> valueof = valueofMethodCache.get(specificType); if(valueof == null) { Method method = getStaticMethod(specificType, "valueof", String.class); if(method != null) { method.setAccessible(true); valueof = Optional.of(method); } else { valueof = Optional.absent(); } valueofMethodCache.put(specificType, valueof); } try { if (valueof.isPresent()) { return valueof.get().invoke(null, source); } Constructor<?> cons = constructorCache.get(specificType); if(cons == null) { cons = getConstructor(specificType, String.class); if (cons != null) { cons.setAccessible(true); constructorCache.put(specificType, cons); return cons.newInstance(source); } } if(cons == null) { throw new UnsupportedOperationException( "No static valueOf(String.class) method or Constructor(String.class) exists on " + specificType.getName()); } return cons.newInstance(source); } catch (InvocationTargetException e) { throw new ConversionException(e.getTargetException()); } catch (Throwable e) { throw new ConversionException(e); } } public static <T> Constructor<T> getConstructor(Class<T> clazz, Class<?>... paramTypes) { try { return clazz.getConstructor(paramTypes); } catch (NoSuchMethodException ex) { return null; } } public static Method getStaticMethod(Class<?> clazz, String methodName, Class<?>... args) { try { final Method method = clazz.getMethod(methodName, args); return Modifier.isStatic(method.getModifiers()) ? method : null; } catch (NoSuchMethodException ex) { return null; } } } } public static class UniqueId { private final HashMap<Class<?>, Integer> idCache = new HashMap<>(); private final AtomicInteger counter = new AtomicInteger(); public Integer getId(final Class<?> cls) { Integer id = idCache.get(cls); if (id != null) { return id; } id = counter.incrementAndGet(); idCache.put(cls, id); return id; } } }