001 package com.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 com.hammurapi.util.Context;
012 import com.hammurapi.util.MutableContext;
013
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 private static final String SET = "set";
024
025 private static final String GET = "get";
026
027 /**
028 * Returns source object unchanged
029 */
030 private static ConverterClosure<?,?> ZERO_CONVERTER = new ConverterClosure<Object,Object>() {
031
032 public Object convert(Object source) {
033 return source;
034 }
035
036 };
037
038 /**
039 * Contains [sourceClass, targetClass] -> ProxyConverter(targetMethod -> sourceMethod) mapping.
040 */
041 private static Map<Collection<Class<?>>, Object> converterMap = new HashMap<Collection<Class<?>>, Object>();
042
043 /**
044 * Converts object to target interface using dynamic proxy.
045 * @author Pavel Vlasov
046 *
047 */
048 private static class ProxyConverter<S, T> implements ConverterClosure<S, T> {
049
050 /**
051 * Maps target methods to source methods. Unmapped methods
052 * are invoked directly (e.g. equals() or if method in both source and target belongs
053 * to the same interface (partial overlap)).
054 */
055 private Map<Method,Method> methodMap;
056
057 private Class<?>[] targetInterfaces;
058
059 private ClassLoader classLoader;
060
061 public ProxyConverter(Class<T> targetInterface, Map<Method,Method> methodMap, ClassLoader classLoader) {
062 this.methodMap = methodMap;
063 this.targetInterfaces = new Class[] {targetInterface};
064 this.classLoader = classLoader;
065 }
066
067 @SuppressWarnings("unchecked")
068 public T convert(final S source) {
069
070 InvocationHandler ih = new InvocationHandler() {
071
072 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
073 Method sourceMethod = (Method) (methodMap==null ? null : methodMap.get(method));
074 if (sourceMethod==null) {
075 return method.invoke(source, args);
076 }
077
078 if (method.getName().startsWith(GET)) {
079 Object ret = sourceMethod.invoke(source, new Object[] {method.getName().substring(GET.length())});
080 return ConvertingService.convert(ret, method.getReturnType());
081 }
082
083 return sourceMethod.invoke(source, new Object[] {method.getName().substring(SET.length()), args[0]});
084 }
085
086 };
087
088 return (T) Proxy.newProxyInstance(classLoader, targetInterfaces, ih);
089 }
090
091 }
092
093 /**
094 * @param sourceClass
095 * @param targetInterface
096 * @return Converter which can "duck-type" instance of source class to target interface or null if conversion is not possible.
097 * Methods are mapped as follows: return types shall be compatible, arguments shall be compatible, exception declarations are ignored.
098 */
099 @SuppressWarnings("unchecked")
100 public static <S, T> ConverterClosure<S, T> getConverter(Class<S> sourceClass, Class<T> targetInterface) {
101 if (targetInterface.isAssignableFrom(sourceClass)) {
102 return (ConverterClosure<S, T>) ZERO_CONVERTER;
103 }
104
105 Collection<Class<?>> key=new ArrayList<Class<?>>();
106 key.add(sourceClass);
107 key.add(targetInterface);
108 synchronized (converterMap) {
109 Object value = converterMap.get(key);
110 if (Boolean.FALSE.equals(value)) {
111 return null;
112 }
113
114 if (!Context.class.isAssignableFrom(sourceClass)) {
115 converterMap.put(key, Boolean.FALSE); // To indicate that we tried and failed
116 return null;
117 }
118
119 Method getter;
120 try {
121 getter = Context.class.getMethod("lookup", new Class[] {String.class});
122 } catch (SecurityException e) {
123 throw new ConversionException(e);
124 } catch (NoSuchMethodException e) {
125 throw new ConversionException(e);
126 }
127
128 Method setter;
129 try {
130 setter = MutableContext.class.isAssignableFrom(sourceClass) ? MutableContext.class.getMethod("bind", new Class[] {String.class, Object.class}) : null;
131 } catch (SecurityException e) {
132 throw new ConversionException(e);
133 } catch (NoSuchMethodException e) {
134 throw new ConversionException(e);
135 }
136
137 if (value==null) {
138 Method[] targetMethods = targetInterface.getMethods();
139
140 Map<Method, Method> methodMap = new HashMap<Method, Method>();
141
142 for (int i=0, l=targetMethods.length; i<l; ++i) {
143 if (Object.class.equals(targetMethods[i].getDeclaringClass())) {
144 continue;
145 }
146
147 Method targetMethod = targetMethods[i];
148 if (targetMethod.getName().startsWith("get")
149 && targetMethod.getParameterTypes().length==0) {
150 methodMap.put(targetMethod, getter);
151 } else if (targetMethod.getName().startsWith("set")
152 && targetMethod.getParameterTypes().length==1
153 && setter!=null) {
154 methodMap.put(targetMethod, setter);
155 } else {
156 converterMap.put(key, Boolean.FALSE); // To indicate that we tried and failed
157 return null;
158 }
159 }
160
161 ClassLoader cl = getChildClassLoader(sourceClass.getClassLoader(), targetInterface.getClassLoader());
162 if (cl==null) {
163 converterMap.put(key, Boolean.FALSE); // To indicate that we tried and failed
164 return null;
165 }
166
167 value = new ProxyConverter<S, T>(targetInterface, methodMap.isEmpty() ? null : methodMap, cl);
168 converterMap.put(key, value);
169 }
170 return (ConverterClosure<S, T>) value;
171 }
172 }
173
174 /**
175 * @param cl1
176 * @param cl2
177 * @return Child classloader or null if classloaders are not related
178 */
179 private static ClassLoader getChildClassLoader(ClassLoader cl1, ClassLoader cl2) {
180 if (cl1==null) {
181 return cl2;
182 }
183 if (cl2==null) {
184 return cl1;
185 }
186 for (ClassLoader cl = cl1; cl!=null && cl!=cl.getParent(); cl=cl.getParent()) {
187 if (cl2.equals(cl)) {
188 return cl1;
189 }
190 }
191 for (ClassLoader cl = cl2; cl!=null && cl!=cl.getParent(); cl=cl.getParent()) {
192 if (cl1.equals(cl)) {
193 return cl2;
194 }
195 }
196 return null;
197 }
198 }