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