| 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.Arrays; |
| 8 | import java.util.Collection; |
| 9 | import java.util.HashMap; |
| 10 | import java.util.HashSet; |
| 11 | import java.util.Map; |
| 12 | import java.util.Set; |
| 13 | |
| 14 | import com.hammurapi.common.ClassHierarchyVisitable; |
| 15 | import com.hammurapi.common.Visitor; |
| 16 | |
| 17 | |
| 18 | |
| 19 | /** |
| 20 | * Creates converters which use "duck" typing. |
| 21 | * @author Pavel |
| 22 | * |
| 23 | */ |
| 24 | public class DuckConverterFactory { |
| 25 | |
| 26 | /** |
| 27 | * Returns source object unchanged |
| 28 | */ |
| 29 | private static ConverterClosure ZERO_CONVERTER = new ConverterClosure() { |
| 30 | |
| 31 | public Object convert(Object source) { |
| 32 | return source; |
| 33 | } |
| 34 | |
| 35 | }; |
| 36 | |
| 37 | /** |
| 38 | * Contains [sourceClass, targetClass] -> ProxyConverter(targetMethod -> sourceMethod) mapping. |
| 39 | */ |
| 40 | private static Map<Collection, Object> converterMap = new HashMap<Collection, Object>(); |
| 41 | |
| 42 | private static class ProxyConverter implements ConverterClosure { |
| 43 | |
| 44 | /** |
| 45 | * Maps target methods to source methods. Unmapped methods |
| 46 | * are invoked directly (e.g. equals() or if method in both source and target belongs |
| 47 | * to the same interface (partial overlap)). |
| 48 | */ |
| 49 | private Map methodMap; |
| 50 | |
| 51 | private Class[] targetInterfaces; |
| 52 | |
| 53 | private ClassLoader classLoader; |
| 54 | |
| 55 | public ProxyConverter(Class targetInterface, Map methodMap, ClassLoader classLoader) { |
| 56 | this.methodMap = methodMap; |
| 57 | this.targetInterfaces = new Class[] {targetInterface}; |
| 58 | this.classLoader = classLoader; |
| 59 | } |
| 60 | |
| 61 | public Object convert(final Object source) { |
| 62 | |
| 63 | InvocationHandler ih = new FilterInvocationHandler() { |
| 64 | |
| 65 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { |
| 66 | Method sourceMethod = (Method) (methodMap==null ? null : methodMap.get(method)); |
| 67 | return sourceMethod==null ? method.invoke(source, args) : sourceMethod.invoke(source, args); |
| 68 | } |
| 69 | |
| 70 | public Object getMaster() { |
| 71 | return source; |
| 72 | } |
| 73 | |
| 74 | }; |
| 75 | |
| 76 | return Proxy.newProxyInstance(classLoader, targetInterfaces, ih); |
| 77 | } |
| 78 | |
| 79 | } |
| 80 | |
| 81 | /** |
| 82 | * @param sourceClass |
| 83 | * @param targetInterface |
| 84 | * @param lenient If true converter is created even not all interface methods could be mapped to class methods. |
| 85 | * @return Converter which can "duck-type" instance of source class to target interface or null if conversion is not possible. |
| 86 | * Methods are mapped as follows: return types shall be compatible, arguments shall be compatible, exception declarations are ignored. |
| 87 | */ |
| 88 | public static ConverterClosure getConverter(Class sourceClass, Class targetInterface, boolean lenient) { |
| 89 | if (targetInterface.isAssignableFrom(sourceClass)) { |
| 90 | return ZERO_CONVERTER; |
| 91 | } |
| 92 | |
| 93 | Collection key=new ArrayList(); |
| 94 | key.add(sourceClass); |
| 95 | key.add(targetInterface); |
| 96 | key.add(lenient ? Boolean.TRUE : Boolean.FALSE); |
| 97 | synchronized (converterMap) { |
| 98 | Object value = converterMap.get(key); |
| 99 | 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 | } |