001package com.hammurapi.convert;
002
003import java.lang.reflect.InvocationHandler;
004import java.lang.reflect.Method;
005import java.lang.reflect.Proxy;
006import java.util.ArrayList;
007import java.util.Collection;
008import java.util.HashMap;
009import java.util.Map;
010
011import com.hammurapi.common.Context;
012import com.hammurapi.common.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 */
021public 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}