    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}