001    package com.hammurapi.convert;
002    
003    import java.lang.reflect.InvocationHandler;
004    import java.lang.reflect.Method;
005    import java.lang.reflect.Proxy;
006    import java.util.ArrayList;
007    import java.util.Arrays;
008    import java.util.Collection;
009    import java.util.HashMap;
010    import java.util.HashSet;
011    import java.util.Map;
012    import java.util.Set;
013    
014    import com.hammurapi.util.ClassHierarchyVisitable;
015    import com.hammurapi.util.Visitor;
016    
017    
018    
019    /**
020     * Creates converters which use "duck" typing.
021     * @author Pavel
022     *
023     */
024    public 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    }