All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.deephacks.tools4j.config.spi.Conversion Maven / Gradle / Ivy

There is a newer version: 0.15.0
Show newest version
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 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 the most specific type that the value should be converted to. * @return A converted object. */ public T convert(V source, Class 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 { @Override public String convert(Object source, Class specificType) { return (source != null ? source.toString() : null); } } static final class StringToBooleanConverter implements Converter { private static final Set trueValues = new HashSet(); private static final Set falseValues = new HashSet(); 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 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 { @Override public Enum convert(String source, Class 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 values = new ArrayList(); 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 { @Override public Number convert(String source, Class 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 { private static final HashMap, Optional> valueofMethodCache = new HashMap<>(); private static final HashMap, Constructor> constructorCache = new HashMap<>(); @Override public Object convert(String source, Class specificType) { Optional 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 Constructor getConstructor(Class 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, 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; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy