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