| 1 | package com.hammurapi.convert; |
| 2 | |
| 3 | import java.lang.reflect.InvocationHandler; |
| 4 | import java.lang.reflect.Method; |
| 5 | import java.lang.reflect.Proxy; |
| 6 | import java.util.ArrayList; |
| 7 | import java.util.Collection; |
| 8 | import java.util.HashMap; |
| 9 | import java.util.Map; |
| 10 | |
| 11 | import com.hammurapi.common.Context; |
| 12 | import com.hammurapi.common.MutableContext; |
| 13 | |
| 14 | |
| 15 | |
| 16 | /** |
| 17 | * Creates converters from Context and MutableContext to interfaces which have only setters and getters (beans) |
| 18 | * @author Pavel |
| 19 | * |
| 20 | */ |
| 21 | public class ContextConverterFactory { |
| 22 | |
| 23 | private static final String SET = "set"; |
| 24 | |
| 25 | private static final String GET = "get"; |
| 26 | |
| 27 | /** |
| 28 | * Returns source object unchanged |
| 29 | */ |
| 30 | private static ConverterClosure<?,?> ZERO_CONVERTER = new ConverterClosure<Object,Object>() { |
| 31 | |
| 32 | public Object convert(Object source) { |
| 33 | return source; |
| 34 | } |
| 35 | |
| 36 | }; |
| 37 | |
| 38 | /** |
| 39 | * Contains [sourceClass, targetClass] -> ProxyConverter(targetMethod -> sourceMethod) mapping. |
| 40 | */ |
| 41 | private static Map<Collection<Class<?>>, Object> converterMap = new HashMap<Collection<Class<?>>, Object>(); |
| 42 | |
| 43 | /** |
| 44 | * Converts object to target interface using dynamic proxy. |
| 45 | * @author Pavel Vlasov |
| 46 | * |
| 47 | */ |
| 48 | private static class ProxyConverter<S, T> implements ConverterClosure<S, T> { |
| 49 | |
| 50 | /** |
| 51 | * Maps target methods to source methods. Unmapped methods |
| 52 | * are invoked directly (e.g. equals() or if method in both source and target belongs |
| 53 | * to the same interface (partial overlap)). |
| 54 | */ |
| 55 | private Map<Method,Method> methodMap; |
| 56 | |
| 57 | private Class<?>[] targetInterfaces; |
| 58 | |
| 59 | private ClassLoader classLoader; |
| 60 | |
| 61 | public ProxyConverter(Class<T> targetInterface, Map<Method,Method> methodMap, ClassLoader classLoader) { |
| 62 | this.methodMap = methodMap; |
| 63 | this.targetInterfaces = new Class[] {targetInterface}; |
| 64 | this.classLoader = classLoader; |
| 65 | } |
| 66 | |
| 67 | @SuppressWarnings("unchecked") |
| 68 | public T convert(final S source) { |
| 69 | |
| 70 | InvocationHandler ih = new InvocationHandler() { |
| 71 | |
| 72 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { |
| 73 | Method sourceMethod = (Method) (methodMap==null ? null : methodMap.get(method)); |
| 74 | if (sourceMethod==null) { |
| 75 | return method.invoke(source, args); |
| 76 | } |
| 77 | |
| 78 | if (method.getName().startsWith(GET)) { |
| 79 | Object ret = sourceMethod.invoke(source, new Object[] {method.getName().substring(GET.length())}); |
| 80 | return ConvertingService.convert(ret, method.getReturnType()); |
| 81 | } |
| 82 | |
| 83 | return sourceMethod.invoke(source, new Object[] {method.getName().substring(SET.length()), args[0]}); |
| 84 | } |
| 85 | |
| 86 | }; |
| 87 | |
| 88 | return (T) Proxy.newProxyInstance(classLoader, targetInterfaces, ih); |
| 89 | } |
| 90 | |
| 91 | } |
| 92 | |
| 93 | /** |
| 94 | * @param sourceClass |
| 95 | * @param targetInterface |
| 96 | * @return Converter which can "duck-type" instance of source class to target interface or null if conversion is not possible. |
| 97 | * Methods are mapped as follows: return types shall be compatible, arguments shall be compatible, exception declarations are ignored. |
| 98 | */ |
| 99 | @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 | } |