org.deephacks.tools4j.config.spi.Conversion Maven / Gradle / Ivy
package org.deephacks.tools4j.config.spi;
import com.google.common.base.Optional;
import org.deephacks.tools4j.config.spi.Conversion.Converter.ObjectToStringConverter;
import org.deephacks.tools4j.config.spi.Conversion.Converter.StringToBooleanConverter;
import org.deephacks.tools4j.config.spi.Conversion.Converter.StringToEnumConverter;
import org.deephacks.tools4j.config.spi.Conversion.Converter.StringToNumberConverter;
import org.deephacks.tools4j.config.spi.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 final HashMap, SourceTargetPair> converters = new HashMap<>();
/** Lookup cache for finding converters. */
private final ConcurrentHashMap cache = new ConcurrentHashMap();
private static Conversion INSTANCE;
private static final UniqueId ids = new UniqueId();
private Conversion() {
registerDefault();
}
public static synchronized Conversion get() {
if (INSTANCE == null) {
INSTANCE = new Conversion();
}
return INSTANCE;
}
/**
* 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 convert(final Object source, final Class targetclass) {
if (source == null) {
return null;
}
final Class> sourceclass = source.getClass();
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 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);
}
public Set convert(Set values, final Class clazz) {
final HashSet objects = new HashSet<>();
if (values == null) {
return new HashSet<>();
}
for (V object : values) {
objects.add(convert(object, clazz));
}
return objects;
}
public Collection convert(Collection values, final Class clazz) {
final ArrayList objects = new ArrayList<>();
if (values == null) {
return new ArrayList<>();
}
for (V object : values) {
objects.add(convert(object, clazz));
}
return objects;
}
public Map convert(Map values, final Class clazz) {
if (values == null) {
return null;
}
throw new UnsupportedOperationException();
}
public void register(Converter converter) {
if (converters.get(converter.getClass()) != null) {
return;
}
converters.put(converter.getClass(), new SourceTargetPair(converter));
cache.clear();
}
private void registerDefault() {
register(new StringToEnumConverter());
register(new StringToObjectConverter());
register(new ObjectToStringConverter());
register(new StringToNumberConverter());
register(new StringToBooleanConverter());
}
private static class SourceTargetPair {
private final Class> source;
private final Class> target;
private final Converter converter;
public SourceTargetPair(Converter converter) {
List> 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> superclasses = new LinkedList>();
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> 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 bestTargetMatch() {
return new Comparator() {
@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.
*
* If a field is of type Set then java.lang.String is returned.
*
*
* If a field is of type Map then [java.lang.String,
* java.lang.Integer] is returned.
*
*
* @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> getParameterizedType(final Class> ownerClass,
Class> genericSuperClass) {
Type[] types = null;
if (genericSuperClass.isInterface()) {
types = ownerClass.getGenericInterfaces();
} else {
types = new Type[] { ownerClass.getGenericSuperclass() };
}
final List> classes = new ArrayList>();
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>();
}
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 {
/**
* @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