001 package biz.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 biz.hammurapi.util.ClassHierarchyVisitable; 012 import biz.hammurapi.util.Visitor; 013 014 015 /** 016 * Creates converters which use "duck" typing. 017 * @author Pavel 018 * 019 */ 020 public class DuckConverterFactory { 021 022 /** 023 * Returns source object unchanged 024 */ 025 private static ConverterClosure ZERO_CONVERTER = new ConverterClosure() { 026 027 public Object convert(Object source) { 028 return source; 029 } 030 031 }; 032 033 /** 034 * Contains [sourceClass, targetClass] -> ProxyConverter(targetMethod -> sourceMethod) mapping. 035 */ 036 private static Map converterMap = new HashMap(); 037 038 private static class ProxyConverter implements ConverterClosure { 039 040 /** 041 * Maps target methods to source methods. Unmapped methods 042 * are invoked directly (e.g. equals() or if method in both source and target belongs 043 * to the same interface (partial overlap)). 044 */ 045 private Map methodMap; 046 047 private Class[] targetInterfaces; 048 049 private ClassLoader classLoader; 050 051 public ProxyConverter(Class targetInterface, Map methodMap, ClassLoader classLoader) { 052 this.methodMap = methodMap; 053 this.targetInterfaces = new Class[] {targetInterface}; 054 this.classLoader = classLoader; 055 } 056 057 public Object convert(final Object source) { 058 059 InvocationHandler ih = new FilterInvocationHandler() { 060 061 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 062 Method sourceMethod = (Method) (methodMap==null ? null : methodMap.get(method)); 063 return sourceMethod==null ? method.invoke(source, args) : sourceMethod.invoke(source, args); 064 } 065 066 public Object getMaster() { 067 return source; 068 } 069 070 }; 071 072 return Proxy.newProxyInstance(classLoader, targetInterfaces, ih); 073 } 074 075 } 076 077 /** 078 * @param sourceClass 079 * @param targetInterface 080 * @param lenient If true converter is created even not all interface methods could be mapped to class methods. 081 * @return Converter which can "duck-type" instance of source class to target interface or null if conversion is not possible. 082 * Methods are mapped as follows: return types shall be compatible, arguments shall be compatible, exception declarations are ignored. 083 */ 084 public static ConverterClosure getConverter(Class sourceClass, Class targetInterface, boolean lenient) { 085 if (targetInterface.isAssignableFrom(sourceClass)) { 086 return ZERO_CONVERTER; 087 } 088 089 Collection key=new ArrayList(); 090 key.add(sourceClass); 091 key.add(targetInterface); 092 key.add(lenient ? Boolean.TRUE : Boolean.FALSE); 093 synchronized (converterMap) { 094 Object value = converterMap.get(key); 095 if (Boolean.FALSE.equals(value)) { 096 return null; 097 } 098 099 if (value==null) { 100 Method[] targetMethods = targetInterface.getMethods(); 101 Method[] sourceMethods = sourceClass.getMethods(); 102 Map methodMap = new HashMap(); 103 104 if (!(duckMap(targetMethods, sourceMethods, methodMap) || lenient)) { 105 converterMap.put(key, Boolean.FALSE); // Strict mapping - to indicate that we tried and failed. 106 return null; 107 } 108 109 ClassLoader cl = getChildClassLoader(sourceClass.getClassLoader(), targetInterface.getClassLoader()); 110 if (cl==null) { 111 converterMap.put(key, Boolean.FALSE); // To indicate that we tried and failed 112 return null; 113 } 114 115 value = new ProxyConverter(targetInterface, methodMap.isEmpty() ? null : methodMap, cl); 116 converterMap.put(key, value); 117 } 118 return (ConverterClosure) value; 119 } 120 } 121 122 /** 123 * Duck maps source (interface methods) to compatible target (class) methods. 124 * @param interfaceMethods Interface methods. 125 * @param classMethods Class methods 126 * @param methodMap Method map (interface to class methods). 127 * @return true if all source methods have been mapped. 128 */ 129 public static boolean duckMap(Class theInterface, Class theClass, Map methodMap) { 130 return duckMap(theInterface.getMethods(), theClass.getMethods(), methodMap); 131 } 132 133 /** 134 * Duck maps source (interface methods) to compatible target (class) methods. 135 * Mapped interface entries are set to null. 136 * @param interfaceMethods Interface methods. 137 * @param classMethods Class methods 138 * @param methodMap Method map (interface to class methods). 139 * @return true if all source methods have been mapped. 140 */ 141 public static boolean duckMap(Method[] interfaceMethods, Method[] classMethods, Map methodMap) { 142 boolean ret = true; 143 144 Z: 145 for (int i=0, l=interfaceMethods.length; i<l; ++i) { 146 if (interfaceMethods[i] == null) { 147 continue; // Method was mapped in previous invocations. 148 } 149 150 if (Object.class.equals(interfaceMethods[i].getDeclaringClass())) { 151 continue; 152 } 153 154 Method targetMethod = interfaceMethods[i]; 155 int candidateIndex=-1; 156 Method candidateMethod = null; 157 158 Y: 159 for (int j=0, m=classMethods.length; j<m; ++j) { 160 Method sourceMethod = classMethods[j]; 161 if (sourceMethod!=null) { 162 if (targetMethod.equals(sourceMethod)) { // No mapping necessary 163 continue Z; 164 } 165 166 if (targetMethod.getName().equals(sourceMethod.getName()) 167 && targetMethod.getParameterTypes().length == sourceMethod.getParameterTypes().length) { 168 // Check for compatibility 169 170 // Return type shall "widen" 171 if (!(targetMethod.getReturnType().isAssignableFrom(sourceMethod.getReturnType()) 172 || void.class.equals(targetMethod.getReturnType()))) { 173 continue; 174 } 175 176 Class[] targetParameterTypes = targetMethod.getParameterTypes(); 177 Class[] sourceParameterTypes = sourceMethod.getParameterTypes(); 178 for (int k=0, n=targetParameterTypes.length; k<n; ++k) { 179 if (!sourceParameterTypes[k].isAssignableFrom(targetParameterTypes[k])) { 180 continue Y; 181 } 182 } 183 184 if (candidateMethod!=null) { 185 186 Class[] candidateParameterTypes = candidateMethod.getParameterTypes(); 187 for (int k=0, n=sourceParameterTypes.length; k<n; ++k) { 188 Integer oldAffinity = classAffinity(targetParameterTypes[k], candidateParameterTypes[k]); 189 Integer newAffinity = classAffinity(targetParameterTypes[k], sourceParameterTypes[k]); 190 if (oldAffinity!=null && (newAffinity==null || oldAffinity.intValue()<newAffinity.intValue())) { 191 continue Y; 192 } 193 } 194 195 Integer oldAffinity = classAffinity(candidateMethod.getReturnType(), targetMethod.getReturnType()); 196 Integer newAffinity = classAffinity(sourceMethod.getReturnType(), targetMethod.getReturnType()); 197 if (oldAffinity!=null && (newAffinity==null || oldAffinity.intValue()<newAffinity.intValue())) { 198 continue; 199 } 200 201 classMethods[candidateIndex] = candidateMethod; // return method back 202 } 203 204 candidateMethod=sourceMethod; 205 candidateIndex=j; 206 } 207 } 208 } 209 210 ret = false; 211 if (candidateMethod!=null) { 212 methodMap.put(targetMethod, candidateMethod); 213 interfaceMethods[i] = null; 214 } 215 216 } 217 return ret; 218 } 219 220 /** 221 * Calculates how close is subclass to superclass in class hierarchy. 222 * @param subClass 223 * @param superClass 224 * @return affinity, or Integer.MAX_VALUE if classes don't belong to the same class hierarchy. 225 */ 226 public static Integer classAffinity(Class subClass, final Class superClass) { 227 if (superClass.isAssignableFrom(subClass)) { 228 final int[] caffinity={0}; 229 new ClassHierarchyVisitable(subClass).accept(new Visitor() { 230 231 public boolean visit(Object target) { 232 if (target.equals(superClass)) { 233 return false; 234 } 235 236 caffinity[0]++; 237 return true; 238 } 239 240 }); 241 return new Integer(caffinity[0]); 242 } 243 244 return null; 245 } 246 247 /** 248 * @param cl1 249 * @param cl2 250 * @return Child classloader or null if classloaders are not related 251 */ 252 public static ClassLoader getChildClassLoader(ClassLoader cl1, ClassLoader cl2) { 253 254 if (cl1==null) { 255 return cl2; 256 } 257 if (cl2==null) { 258 return cl1; 259 } 260 for (ClassLoader cl = cl1; cl!=null && cl!=cl.getParent(); cl=cl.getParent()) { 261 if (cl2.equals(cl)) { 262 return cl1; 263 } 264 } 265 for (ClassLoader cl = cl2; cl!=null && cl!=cl.getParent(); cl=cl.getParent()) { 266 if (cl1.equals(cl)) { 267 return cl2; 268 } 269 } 270 return null; 271 } 272 273 }