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.Arrays;
008 import java.util.Collection;
009 import java.util.HashMap;
010 import java.util.HashSet;
011 import java.util.Map;
012 import java.util.Set;
013
014 import com.hammurapi.util.ClassHierarchyVisitable;
015 import com.hammurapi.util.Visitor;
016
017
018
019 /**
020 * Creates converters which use "duck" typing.
021 * @author Pavel
022 *
023 */
024 public 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 }