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