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