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.config.MutableContext;
013    import biz.hammurapi.config.RuntimeConfigurationException;
014    
015    
016    /**
017     * Creates converters from Context and MutableContext to interfaces which have only setters and getters (beans)
018     * @author Pavel
019     *
020     */
021    public class ContextConverterFactory {
022            
023            /**
024             * Returns source object unchanged
025             */
026            private static ConverterClosure ZERO_CONVERTER = new ConverterClosure() {
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 ConverterClosure {
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                                            if (sourceMethod==null) {
065                                                    return method.invoke(source, args);
066                                            }
067    
068                                            if (method.getName().startsWith("get")) {
069                                                    Object ret = sourceMethod.invoke(source, new Object[] {method.getName().substring("get".length())});                                            
070                                                    return ConvertingService.convert(ret, method.getReturnType());                                          
071                                            }
072                                            
073                                            return sourceMethod.invoke(source, new Object[] {method.getName().substring("set".length()), args[0]});
074                                    }
075                                    
076                            };
077                            
078                            return Proxy.newProxyInstance(classLoader, targetInterfaces, ih);
079                    }
080                    
081            }
082    
083            /**
084             * @param sourceClass
085             * @param targetInterface
086             * @return Converter which can "duck-type" instance of source class to target interface or null if conversion is not possible.
087             * Methods are mapped as follows: return types shall be compatible, arguments shall be compatible, exception declarations are ignored.
088             */
089            public static ConverterClosure getConverter(Class sourceClass, Class targetInterface) {
090                    if (targetInterface.isAssignableFrom(sourceClass)) {
091                            return ZERO_CONVERTER;
092                    }
093                    
094                    Collection key=new ArrayList();
095                    key.add(sourceClass);
096                    key.add(targetInterface);
097                    synchronized (converterMap) {
098                            Object value = converterMap.get(key);
099                            if (Boolean.FALSE.equals(value)) {
100                                    return null;
101                            }
102                            
103                            if (!Context.class.isAssignableFrom(sourceClass)) {
104                                    converterMap.put(key, Boolean.FALSE); // To indicate that we tried and failed
105                                    return null;                            
106                            }
107                            
108                            Method getter;
109                            try {
110                                    getter = Context.class.getMethod("get", new Class[] {String.class});
111                            } catch (SecurityException e) {
112                                    throw new RuntimeConfigurationException(e);
113                            } catch (NoSuchMethodException e) {
114                                    throw new RuntimeConfigurationException(e);
115                            }
116                            
117                            Method setter;
118                            try {
119                                    setter = MutableContext.class.isAssignableFrom(sourceClass) ? MutableContext.class.getMethod("set", new Class[] {String.class, Object.class}) : null;
120                            } catch (SecurityException e) {
121                                    throw new RuntimeConfigurationException(e);
122                            } catch (NoSuchMethodException e) {
123                                    throw new RuntimeConfigurationException(e);
124                            }                       
125                                                    
126                            if (value==null) {
127                                    Method[] targetMethods = targetInterface.getMethods();
128                                    
129                                    Map methodMap = new HashMap();
130                                    
131                                    for (int i=0, l=targetMethods.length; i<l; ++i) {
132                                            if (Object.class.equals(targetMethods[i].getDeclaringClass())) { 
133                                                    continue;
134                                            }
135                                                                                    
136                                            Method targetMethod = targetMethods[i];
137                                            if (targetMethod.getName().startsWith("get") 
138                                                            && targetMethod.getParameterTypes().length==0) {
139                                                    methodMap.put(targetMethod, getter);
140                                            } else if (targetMethod.getName().startsWith("set") 
141                                                            && targetMethod.getParameterTypes().length==1
142                                                            && setter!=null) {
143                                                    methodMap.put(targetMethod, setter);
144                                            } else {
145                                                    converterMap.put(key, Boolean.FALSE); // To indicate that we tried and failed
146                                                    return null;                                                                            
147                                            }
148                                    }
149                                    
150                                    ClassLoader cl = getChildClassLoader(sourceClass.getClassLoader(), targetInterface.getClassLoader());
151                                    if (cl==null) {
152                                            converterMap.put(key, Boolean.FALSE); // To indicate that we tried and failed
153                                            return null;                                    
154                                    }
155                                    
156                                    value = new ProxyConverter(targetInterface, methodMap.isEmpty() ? null : methodMap, cl);
157                                    converterMap.put(key, value);
158                            }
159                            return (ConverterClosure) value;
160                    }
161            }
162    
163            /**
164             * @param cl1
165             * @param cl2
166             * @return Child classloader or null if classloaders are not related
167             */
168            private static ClassLoader getChildClassLoader(ClassLoader cl1, ClassLoader cl2) {
169                    if (cl1==null) {
170                            return cl2;
171                    }
172                    if (cl2==null) {
173                            return cl1;
174                    }
175                    for (ClassLoader cl = cl1; cl!=null && cl!=cl.getParent(); cl=cl.getParent()) {
176                            if (cl2.equals(cl)) {
177                                    return cl1;
178                            }
179                    }
180                    for (ClassLoader cl = cl2; cl!=null && cl!=cl.getParent(); cl=cl.getParent()) {
181                            if (cl1.equals(cl)) {
182                                    return cl2;
183                            }
184                    }
185                    return null;
186            }               
187    }