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.config.MutableContext; 013 import biz.hammurapi.config.RuntimeConfigurationException; 014 015 016 /** 017 * Creates converters from Context and MutableContext to interfaces which have only setters and getters (beans) 018 * @author Pavel 019 * 020 */ 021 public class ContextConverterFactory { 022 023 /** 024 * Returns source object unchanged 025 */ 026 private static ConverterClosure ZERO_CONVERTER = new ConverterClosure() { 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 ConverterClosure { 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 if (sourceMethod==null) { 065 return method.invoke(source, args); 066 } 067 068 if (method.getName().startsWith("get")) { 069 Object ret = sourceMethod.invoke(source, new Object[] {method.getName().substring("get".length())}); 070 return ConvertingService.convert(ret, method.getReturnType()); 071 } 072 073 return sourceMethod.invoke(source, new Object[] {method.getName().substring("set".length()), args[0]}); 074 } 075 076 }; 077 078 return Proxy.newProxyInstance(classLoader, targetInterfaces, ih); 079 } 080 081 } 082 083 /** 084 * @param sourceClass 085 * @param targetInterface 086 * @return Converter which can "duck-type" instance of source class to target interface or null if conversion is not possible. 087 * Methods are mapped as follows: return types shall be compatible, arguments shall be compatible, exception declarations are ignored. 088 */ 089 public static ConverterClosure getConverter(Class sourceClass, Class targetInterface) { 090 if (targetInterface.isAssignableFrom(sourceClass)) { 091 return ZERO_CONVERTER; 092 } 093 094 Collection key=new ArrayList(); 095 key.add(sourceClass); 096 key.add(targetInterface); 097 synchronized (converterMap) { 098 Object value = converterMap.get(key); 099 if (Boolean.FALSE.equals(value)) { 100 return null; 101 } 102 103 if (!Context.class.isAssignableFrom(sourceClass)) { 104 converterMap.put(key, Boolean.FALSE); // To indicate that we tried and failed 105 return null; 106 } 107 108 Method getter; 109 try { 110 getter = Context.class.getMethod("get", new Class[] {String.class}); 111 } catch (SecurityException e) { 112 throw new RuntimeConfigurationException(e); 113 } catch (NoSuchMethodException e) { 114 throw new RuntimeConfigurationException(e); 115 } 116 117 Method setter; 118 try { 119 setter = MutableContext.class.isAssignableFrom(sourceClass) ? MutableContext.class.getMethod("set", new Class[] {String.class, Object.class}) : null; 120 } catch (SecurityException e) { 121 throw new RuntimeConfigurationException(e); 122 } catch (NoSuchMethodException e) { 123 throw new RuntimeConfigurationException(e); 124 } 125 126 if (value==null) { 127 Method[] targetMethods = targetInterface.getMethods(); 128 129 Map methodMap = new HashMap(); 130 131 for (int i=0, l=targetMethods.length; i<l; ++i) { 132 if (Object.class.equals(targetMethods[i].getDeclaringClass())) { 133 continue; 134 } 135 136 Method targetMethod = targetMethods[i]; 137 if (targetMethod.getName().startsWith("get") 138 && targetMethod.getParameterTypes().length==0) { 139 methodMap.put(targetMethod, getter); 140 } else if (targetMethod.getName().startsWith("set") 141 && targetMethod.getParameterTypes().length==1 142 && setter!=null) { 143 methodMap.put(targetMethod, setter); 144 } else { 145 converterMap.put(key, Boolean.FALSE); // To indicate that we tried and failed 146 return null; 147 } 148 } 149 150 ClassLoader cl = getChildClassLoader(sourceClass.getClassLoader(), targetInterface.getClassLoader()); 151 if (cl==null) { 152 converterMap.put(key, Boolean.FALSE); // To indicate that we tried and failed 153 return null; 154 } 155 156 value = new ProxyConverter(targetInterface, methodMap.isEmpty() ? null : methodMap, cl); 157 converterMap.put(key, value); 158 } 159 return (ConverterClosure) value; 160 } 161 } 162 163 /** 164 * @param cl1 165 * @param cl2 166 * @return Child classloader or null if classloaders are not related 167 */ 168 private static ClassLoader getChildClassLoader(ClassLoader cl1, ClassLoader cl2) { 169 if (cl1==null) { 170 return cl2; 171 } 172 if (cl2==null) { 173 return cl1; 174 } 175 for (ClassLoader cl = cl1; cl!=null && cl!=cl.getParent(); cl=cl.getParent()) { 176 if (cl2.equals(cl)) { 177 return cl1; 178 } 179 } 180 for (ClassLoader cl = cl2; cl!=null && cl!=cl.getParent(); cl=cl.getParent()) { 181 if (cl1.equals(cl)) { 182 return cl2; 183 } 184 } 185 return null; 186 } 187 }