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.MapContext;
013    import biz.hammurapi.config.MutableContext;
014    import biz.hammurapi.config.RuntimeConfigurationException;
015    import biz.hammurapi.metrics.Metric.Measurement;
016    
017    
018    /**
019     * Creates converters from Context and MutableContext to interfaces which have only setters and getters (beans)
020     * @author Pavel
021     *
022     */
023    public class ContextConverterFactory {
024            
025            /**
026             * Returns source object unchanged
027             */
028            private static Converter ZERO_CONVERTER = new Converter() {
029    
030                    public Object convert(Object source) {                  
031                            return source;
032                    }
033                    
034            };
035            
036            /**
037             * Contains [sourceClass, targetClass] -> ProxyConverter(targetMethod -> sourceMethod) mapping.
038             */
039            private static Map converterMap = new HashMap();
040            
041            private static class ProxyConverter implements Converter {
042    
043                    /**
044                     * Maps target methods to source methods. Unmapped methods
045                     * are invoked directly (e.g. equals() or if method in both source and target belongs
046                     * to the same interface (partial overlap)).
047                     */
048                    private Map methodMap;
049                    
050                    private Class[] targetInterfaces;
051    
052                    private ClassLoader classLoader;
053                    
054                    public ProxyConverter(Class targetInterface, Map methodMap, ClassLoader classLoader) {
055                            this.methodMap = methodMap;
056                            this.targetInterfaces = new Class[] {targetInterface};
057                            this.classLoader = classLoader;
058                    }
059                    
060                    public Object convert(final Object source) {
061                            
062                            InvocationHandler ih = new InvocationHandler() {
063    
064                                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
065                                            Method sourceMethod = (Method) (methodMap==null ? null : methodMap.get(method));
066                                            if (sourceMethod==null) {
067                                                    return method.invoke(source, args);
068                                            }
069    
070                                            if (method.getName().startsWith("get")) {
071                                                    Object ret = sourceMethod.invoke(source, new Object[] {method.getName().substring("get".length())});                                            
072                                                    return CompositeConverter.getDefaultConverter().convert(ret, method.getReturnType(), false);                                            
073                                            }
074                                            
075                                            return sourceMethod.invoke(source, new Object[] {method.getName().substring("set".length()), args[0]});
076                                    }
077                                    
078                            };
079                            
080                            return Proxy.newProxyInstance(classLoader, targetInterfaces, ih);
081                    }
082                    
083            }
084    
085            /**
086             * @param sourceClass
087             * @param targetInterface
088             * @return Converter which can "duck-type" instance of source class to target interface or null if conversion is not possible.
089             * Methods are mapped as follows: return types shall be compatible, arguments shall be compatible, exception declarations are ignored.
090             */
091            public static Converter getConverter(Class sourceClass, Class targetInterface) {
092                    if (targetInterface.isAssignableFrom(sourceClass)) {
093                            return ZERO_CONVERTER;
094                    }
095                    
096                    Collection key=new ArrayList();
097                    key.add(sourceClass);
098                    key.add(targetInterface);
099                    synchronized (converterMap) {
100                            Object value = converterMap.get(key);
101                            if (Boolean.FALSE.equals(value)) {
102                                    return null;
103                            }
104                            
105                            if (!Context.class.isAssignableFrom(sourceClass)) {
106                                    converterMap.put(key, Boolean.FALSE); // To indicate that we tried and failed
107                                    return null;                            
108                            }
109                            
110                            Method getter;
111                            try {
112                                    getter = Context.class.getMethod("get", new Class[] {String.class});
113                            } catch (SecurityException e) {
114                                    throw new RuntimeConfigurationException(e);
115                            } catch (NoSuchMethodException e) {
116                                    throw new RuntimeConfigurationException(e);
117                            }
118                            
119                            Method setter;
120                            try {
121                                    setter = MutableContext.class.isAssignableFrom(sourceClass) ? MutableContext.class.getMethod("set", new Class[] {String.class, Object.class}) : null;
122                            } catch (SecurityException e) {
123                                    throw new RuntimeConfigurationException(e);
124                            } catch (NoSuchMethodException e) {
125                                    throw new RuntimeConfigurationException(e);
126                            }                       
127                                                    
128                            if (value==null) {
129                                    Method[] targetMethods = targetInterface.getMethods();
130                                    
131                                    Map methodMap = new HashMap();
132                                    
133                                    for (int i=0, l=targetMethods.length; i<l; ++i) {
134                                            if (Object.class.equals(targetMethods[i].getDeclaringClass())) { 
135                                                    continue;
136                                            }
137                                                                                    
138                                            Method targetMethod = targetMethods[i];
139                                            if (targetMethod.getName().startsWith("get") 
140                                                            && targetMethod.getParameterTypes().length==0) {
141                                                    methodMap.put(targetMethod, getter);
142                                            } else if (targetMethod.getName().startsWith("set") 
143                                                            && targetMethod.getParameterTypes().length==1
144                                                            && setter!=null) {
145                                                    methodMap.put(targetMethod, setter);
146                                            } else {
147                                                    converterMap.put(key, Boolean.FALSE); // To indicate that we tried and failed
148                                                    return null;                                                                            
149                                            }
150                                    }
151                                    
152                                    ClassLoader cl = getChildClassLoader(sourceClass.getClassLoader(), targetInterface.getClassLoader());
153                                    if (cl==null) {
154                                            converterMap.put(key, Boolean.FALSE); // To indicate that we tried and failed
155                                            return null;                                    
156                                    }
157                                    
158                                    value = new ProxyConverter(targetInterface, methodMap.isEmpty() ? null : methodMap, cl);
159                                    converterMap.put(key, value);
160                            }
161                            return (Converter) value;
162                    }
163            }
164    
165            /**
166             * @param cl1
167             * @param cl2
168             * @return Child classloader or null if classloaders are not related
169             */
170            private static ClassLoader getChildClassLoader(ClassLoader cl1, ClassLoader cl2) {
171                    if (cl1==null) {
172                            return cl2;
173                    }
174                    if (cl2==null) {
175                            return cl1;
176                    }
177                    for (ClassLoader cl = cl1; cl!=null && cl!=cl.getParent(); cl=cl.getParent()) {
178                            if (cl2.equals(cl)) {
179                                    return cl1;
180                            }
181                    }
182                    for (ClassLoader cl = cl2; cl!=null && cl!=cl.getParent(); cl=cl.getParent()) {
183                            if (cl1.equals(cl)) {
184                                    return cl2;
185                            }
186                    }
187                    return null;
188            }
189                    
190            public static void main(String[] args) {
191                    
192                    Map testMap = new HashMap();
193                    testMap.put("Value", "33");
194                    MapContext mc = new MapContext(testMap);
195                    
196                    Converter converter = getConverter(mc.getClass(), Measurement.class);
197                    System.out.println(converter);
198                                    
199                    System.out.println(((Measurement) converter.convert(mc)).getValue());           
200            }
201                    
202    }