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}