    001package com.hammurapi.convert;
    002
    003import java.lang.reflect.InvocationHandler;
    004import java.lang.reflect.Method;
    005import java.lang.reflect.Proxy;
    006import java.util.ArrayList;
    007import java.util.Arrays;
    008import java.util.Collection;
    009import java.util.HashMap;
    010import java.util.HashSet;
    011import java.util.Map;
    012import java.util.Set;
    013
    014import com.hammurapi.common.ClassHierarchyVisitable;
    015import com.hammurapi.common.Visitor;
    016
    017
    018
    019/**
    020 * Creates converters which use "duck" typing.
    021 * @author Pavel
    022 *
    023 */
    024public class DuckConverterFactory {
    025
    026        /**
    027         * Returns source object unchanged
    028         */
    029        private static ConverterClosure ZERO_CONVERTER = new ConverterClosure() {
    030
    031                public Object convert(Object source) {
    032                        return source;
    033                }
    034
    035        };
    036
    037        /**
    038         * Contains [sourceClass, targetClass] -> ProxyConverter(targetMethod -> sourceMethod) mapping.
    039         */
    040        private static Map<Collection, Object> converterMap = new HashMap<Collection, Object>();
    041
    042        private static class ProxyConverter implements ConverterClosure {
    043
    044                /**
    045                 * Maps target methods to source methods. Unmapped methods
    046                 * are invoked directly (e.g. equals() or if method in both source and target belongs
    047                 * to the same interface (partial overlap)).
    048                 */
    049                private Map methodMap;
    050
    051                private Class[] targetInterfaces;
    052
    053                private ClassLoader classLoader;
    054
    055                public ProxyConverter(Class targetInterface, Map methodMap, ClassLoader classLoader) {
    056                        this.methodMap = methodMap;
    057                        this.targetInterfaces = new Class[] {targetInterface};
    058                        this.classLoader = classLoader;
    059                }
    060
    061                public Object convert(final Object source) {
    062
    063                        InvocationHandler ih = new FilterInvocationHandler() {
    064
    065                                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    066                                        Method sourceMethod = (Method) (methodMap==null ? null : methodMap.get(method));
    067                                        return sourceMethod==null ? method.invoke(source, args) : sourceMethod.invoke(source, args);
    068                                }
    069
    070                                public Object getMaster() {
    071                                        return source;
    072                                }
    073
    074                        };
    075
    076                        return Proxy.newProxyInstance(classLoader, targetInterfaces, ih);
    077                }
    078
    079        }
    080
    081        /**
    082         * @param sourceClass
    083         * @param targetInterface
    084         * @param lenient If true converter is created even not all interface methods could be mapped to class methods.
    085         * @return Converter which can "duck-type" instance of source class to target interface or null if conversion is not possible.
    086         * Methods are mapped as follows: return types shall be compatible, arguments shall be compatible, exception declarations are ignored.
    087         */
    088        public static ConverterClosure getConverter(Class sourceClass, Class targetInterface, boolean lenient) {
    089                if (targetInterface.isAssignableFrom(sourceClass)) {
    090                        return ZERO_CONVERTER;
    091                }
    092
    093                Collection key=new ArrayList();
    094                key.add(sourceClass);
    095                key.add(targetInterface);
    096                key.add(lenient ? Boolean.TRUE : Boolean.FALSE);
    097                synchronized (converterMap) {
    098                        Object value = converterMap.get(key);
    099                        if (Boolean.FALSE.equals(value)) {
    100                                return null;
    101                        }
    102
    103                        if (value==null) {
    104                                Method[] targetMethods = targetInterface.getMethods();
    105                                Method[] sourceMethods = sourceClass.getMethods();
    106                                Map<Method, Method> methodMap = new HashMap<Method, Method>();
    107
    108                                if (!(duckMap(targetMethods, sourceMethods, methodMap) || lenient)) {
    109                                        converterMap.put(key, Boolean.FALSE); // Strict mapping - to indicate that we tried and failed.
    110                                        return null;
    111                                }
    112
    113                                ClassLoader cl = getChildClassLoader(sourceClass.getClassLoader(), targetInterface.getClassLoader());
    114                                if (cl==null) {
    115                                        converterMap.put(key, Boolean.FALSE); // To indicate that we tried and failed
    116                                        return null;
    117                                }
    118
    119                                value = new ProxyConverter(targetInterface, methodMap.isEmpty() ? null : methodMap, cl);
    120                                converterMap.put(key, value);
    121                        }
    122                        return (ConverterClosure) value;
    123                }
    124        }
    125
    126        /**
    127         * Duck maps source (interface methods) to compatible target (class) methods.
    128         * @param interfaceMethods Interface methods.
    129         * @param classMethods Class methods
    130         * @param methodMap Method map (interface to class methods).
    131         * @return true if all source methods have been mapped.
    132         */
    133        public static boolean duckMap(Class theInterface, Class theClass, Map<Method, Method> methodMap) {
    134                return duckMap(theInterface.getMethods(), theClass.getMethods(), methodMap);
    135        }
    136
    137        /**
    138         * Duck maps source (interface methods) to compatible target (class) methods.
    139         * Mapped interface entries are set to null.
    140         * @param interfaceMethods Interface methods.
    141         * @param classMethods Class methods
    142         * @param methodMap Method map (interface to class methods).
    143         * @return true if all source methods have been mapped.
    144         */
    145        public static boolean duckMap(Method[] interfaceMethods, Method[] classMethods, Map<Method, Method> methodMap) {
    146                boolean ret = true;
    147
    148                Z:
    149                for (int i=0, l=interfaceMethods.length; i<l; ++i) {
    150                        if (interfaceMethods[i] == null) {
    151                                continue; // Method was mapped in previous invocations.
    152                        }
    153
    154                        if (Object.class.equals(interfaceMethods[i].getDeclaringClass())) {
    155                                continue;
    156                        }
    157
    158                        Method targetMethod = interfaceMethods[i];
    159                        int candidateIndex=-1;
    160                        Method candidateMethod = null;
    161
    162                        Y:
    163                        for (int j=0, m=classMethods.length; j<m; ++j) {
    164                                Method sourceMethod = classMethods[j];
    165                                if (sourceMethod!=null) {
    166                                        if (targetMethod.equals(sourceMethod)) { // No mapping necessary
    167                                                continue Z;
    168                                        }
    169
    170                                        if (targetMethod.getName().equals(sourceMethod.getName())
    171                                                        && targetMethod.getParameterTypes().length == sourceMethod.getParameterTypes().length) {
    172                                                // Check for compatibility
    173
    174                                                // Return type shall "widen"
    175                                                if (!(targetMethod.getReturnType().isAssignableFrom(sourceMethod.getReturnType())
    176                                                                || void.class.equals(targetMethod.getReturnType()))) {
    177                                                        continue;
    178                                                }
    179
    180                                                Class[] targetParameterTypes = targetMethod.getParameterTypes();
    181                                                Class[] sourceParameterTypes = sourceMethod.getParameterTypes();
    182                                                for (int k=0, n=targetParameterTypes.length; k<n; ++k) {
    183                                                        if (!sourceParameterTypes[k].isAssignableFrom(targetParameterTypes[k])) {
    184                                                                continue Y;
    185                                                        }
    186                                                }
    187
    188                                                if (candidateMethod!=null) {
    189
    190                                                        Class[] candidateParameterTypes = candidateMethod.getParameterTypes();
    191                                                        for (int k=0, n=sourceParameterTypes.length; k<n; ++k) {
    192                                                                Integer oldAffinity = classAffinity(targetParameterTypes[k], candidateParameterTypes[k]);
    193                                                                Integer newAffinity = classAffinity(targetParameterTypes[k], sourceParameterTypes[k]);
    194                                                                if (oldAffinity!=null && (newAffinity==null || oldAffinity.intValue()<newAffinity.intValue())) {
    195                                                                        continue Y;
    196                                                                }
    197                                                        }
    198
    199                                                        Integer oldAffinity = classAffinity(candidateMethod.getReturnType(), targetMethod.getReturnType());
    200                                                        Integer newAffinity = classAffinity(sourceMethod.getReturnType(), targetMethod.getReturnType());
    201                                                        if (oldAffinity!=null && (newAffinity==null || oldAffinity.intValue()<newAffinity.intValue())) {
    202                                                                continue;
    203                                                        }
    204
    205                                                        classMethods[candidateIndex] = candidateMethod; // return method back
    206                                                }
    207
    208                                                candidateMethod=sourceMethod;
    209                                                candidateIndex=j;
    210                                        }
    211                                }
    212                        }
    213
    214                        ret = false;
    215                        if (candidateMethod!=null) {
    216                                methodMap.put(targetMethod, candidateMethod);
    217                                interfaceMethods[i] = null;
    218                        }
    219
    220                }
    221                return ret;
    222        }
    223
    224        /**
    225         * Calculates how close is subclass to superclass in class hierarchy.
    226         * @param subClass
    227         * @param superClass
    228         * @return affinity, or null if classes don't belong to the same class hierarchy.
    229         */
    230        public static Integer classAffinity(Class subClass, final Class superClass) {
    231                if (superClass.equals(subClass)) {
    232                        return 0;
    233                }
    234
    235                if (superClass.isAssignableFrom(subClass)) {
    236                        final int[] caffinity={0};
    237                        new ClassHierarchyVisitable(subClass).accept(new Visitor() {
    238
    239                                public boolean visit(Object target) {
    240                                        if (target.equals(superClass)) {
    241                                                return false;
    242                                        }
    243
    244                                        caffinity[0]++;
    245                                        return true;
    246                                }
    247
    248                        });
    249                        return new Integer(caffinity[0]);
    250                }
    251
    252                return null;
    253        }
    254
    255        /**
    256         * @param cl1
    257         * @param cl2
    258         * @return Child classloader or null if classloaders are not related
    259         */
    260        public static ClassLoader getChildClassLoader(ClassLoader cl1, ClassLoader cl2) {
    261
    262                if (cl1==null) {
    263                        return cl2;
    264                }
    265                if (cl2==null) {
    266                        return cl1;
    267                }
    268                for (ClassLoader cl = cl1; cl!=null && cl!=cl.getParent(); cl=cl.getParent()) {
    269                        if (cl2.equals(cl)) {
    270                                return cl1;
    271                        }
    272                }
    273                for (ClassLoader cl = cl2; cl!=null && cl!=cl.getParent(); cl=cl.getParent()) {
    274                        if (cl1.equals(cl)) {
    275                                return cl2;
    276                        }
    277                }
    278                return null;
    279        }
    280
    281        private static Map<Class, Class[]> interfaceMap=new HashMap<Class, Class[]>();
    282
    283        /**
    284         * @param sourceClass
    285         * @return all interfaces implemented by this class
    286         */
    287        public static Class[] getClassInterfaces(Class sourceClass) {
    288                synchronized (interfaceMap) {
    289                        Class[] ret=interfaceMap.get(sourceClass);
    290                        if (ret==null) {
    291                                Set<Class> set=new HashSet<Class>();
    292                                getClassInterfaces(sourceClass, set);
    293                                ret = new ArrayList<Class>(set).toArray(new Class[set.size()]);
    294                                interfaceMap.put(sourceClass, ret);
    295                        }
    296                        return ret;
    297                }
    298        }
    299
    300        /**
    301         * @param sourceClass
    302         */
    303        private static void getClassInterfaces(Class sourceClass, Collection<Class> set) {
    304                if (sourceClass!=null) {
    305                        Class[] interfaces = sourceClass.getInterfaces();
    306                        for (int i=0; interfaces!=null && i<interfaces.length; i++) {
    307                                getClassInterfaces(interfaces[i]);
    308                        }
    309                        getClassInterfaces(sourceClass.getSuperclass(), set);
    310                        set.addAll(Arrays.asList(interfaces));
    311                }
    312        }
    313
    314
    315}