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}