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