/**
* 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 org.deephacks.tools4j.cli;
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 org.deephacks.tools4j.cli.Conversion.Converter.ObjectToStringConverter;
import org.deephacks.tools4j.cli.Conversion.Converter.StringToBooleanConverter;
import org.deephacks.tools4j.cli.Conversion.Converter.StringToEnumConverter;
import org.deephacks.tools4j.cli.Conversion.Converter.StringToNumberConverter;
import org.deephacks.tools4j.cli.Conversion.Converter.StringToObjectConverter;
/**
* Conversion is responsible for converting values using registered converters.
*
* Inspiration from http://www.springsource.org
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
final class Conversion {
/** Keeper for converters available. */
private final HashMap<Class<?>, SourceTargetPair> converters = new HashMap<Class<?>, SourceTargetPair>();
/** Lookup cache for finding converters. */
private final ConcurrentHashMap<SourceTargetPairKey, Converter> cache = new ConcurrentHashMap<SourceTargetPairKey, Converter>();
private static Conversion INSTANCE;
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> T convert(final Object source, final Class<T> targetclass) {
if (source == null) {
return null;
}
final Class<?> sourceclass = source.getClass();
final SourceTargetPairKey key = new SourceTargetPairKey(sourceclass, targetclass);
Converter converter = cache.get(key);
if (converter != null) {
return (T) converter.convert(source, targetclass);
}
final LinkedList<SourceTargetPairMatch> matches = new LinkedList<SourceTargetPairMatch>();
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 <T, V> Collection<T> convert(Collection<V> values, final Class<T> clazz) {
final ArrayList<T> objects = new ArrayList<T>();
if (values == null) {
return new ArrayList<T>();
}
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 <T, V> 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<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<Conversion.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 Class<?> source;
final Class<?> target;
public SourceTargetPairKey(Class<?> source, Class<?> target) {
this.source = source;
this.target = target;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((source == null) ? 0 : source.hashCode());
result = prime * result + ((target == null) ? 0 : target.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
SourceTargetPairKey other = (SourceTargetPairKey) obj;
if (source == null) {
if (other.source != null)
return false;
} else if (!source.equals(other.source))
return false;
if (target == null) {
if (other.target != null)
return false;
} else if (!target.equals(other.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 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);
}
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.
*/
interface Converter<V, T> {
/**
* @param source The source value to convert.
* @param 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> {
@Override
public Object convert(String source, Class<? extends Object> specificType) {
final Method valueof = getStaticMethod(specificType, "valueof", String.class);
try {
if (valueof != null) {
valueof.setAccessible(true);
return valueof.invoke(null, source);
}
final Constructor<?> cons = getConstructor(specificType, String.class);
if (cons != null) {
cons.setAccessible(true);
return cons.newInstance(source);
}
} catch (InvocationTargetException e) {
throw new ConversionException(e.getTargetException());
} catch (Throwable e) {
throw new ConversionException(e);
}
throw new UnsupportedOperationException(
"No static valueOf(String.class) method or Constructor(String.class) exists on "
+ specificType.getName());
}
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;
}
}
}
}
}