001    package com.hammurapi.convert;
002    
003    import java.lang.reflect.InvocationHandler;
004    import java.lang.reflect.InvocationTargetException;
005    import java.lang.reflect.Method;
006    import java.lang.reflect.Proxy;
007    import java.util.ArrayList;
008    import java.util.Arrays;
009    import java.util.Collection;
010    import java.util.HashMap;
011    import java.util.Iterator;
012    import java.util.List;
013    import java.util.Map;
014    
015    /**
016     * Mixes interfaces from multiple objects into one proxy.
017     * @author Pavel
018     *
019     */
020    public class Mixer {
021            
022            /**
023             * Mixes-in implementation of interfaces into class. I.e. it creates a proxy
024             * which implements all object's interfaces plus interfaces from parameters.
025             * E.g. class has methods compatible with interfaces, but doesn't implement 
026             * the interface itself. This is very similar to duck typing.
027             * This method doesn't check for method compatibility, it simply routes 
028             * interface methods to the object. It is useful when only a sub-set of interface
029             * methods is used by the calling code, so the code would work even if strict 
030             * conversion failed.
031             * @param object
032             * @param interfaces
033             * @return
034             */
035            public static Object mixIn(final Object object, Class<?>... interfaces) {
036                    List<Class<?>> additional = new ArrayList<Class<?>>();
037                    for (int i = 0; interfaces!=null && i<interfaces.length; ++i) {
038                            if (!interfaces[i].isInstance(object)) {
039                                    additional.add(interfaces[i]);
040                            }
041                    }
042                    
043                    if (additional.isEmpty()) {
044                            return object;
045                    }
046                    
047                    Class<?> objectClass = object.getClass();
048                    Class<?>[] objectInterfaces = DuckConverterFactory.getClassInterfaces(objectClass);
049                    Class<?>[] proxyInterfaces = new Class[objectInterfaces.length+additional.size()];
050                    ClassLoader classLoader = null;
051                    for (int i=0; i<objectInterfaces.length; ++i) {
052                            proxyInterfaces[i] = objectInterfaces[i];
053                            classLoader = DuckConverterFactory.getChildClassLoader(classLoader, objectInterfaces[i].getClassLoader());
054                    }
055                    
056                    final Map<Method, Method> methodMap = new HashMap<Method, Method>();                
057                    Iterator<Class<?>> it = additional.iterator();
058                    for (int i=objectInterfaces.length; it.hasNext(); ++i) {
059                            proxyInterfaces[i] = it.next();
060                            classLoader = DuckConverterFactory.getChildClassLoader(classLoader, proxyInterfaces[i].getClassLoader());
061                            DuckConverterFactory.duckMap(proxyInterfaces[i], objectClass, methodMap);
062                    }
063                    
064                    return Proxy.newProxyInstance(
065                                    classLoader == null ? objectClass.getClassLoader() : classLoader, 
066                                    proxyInterfaces,
067                                    new FilterInvocationHandler() {
068    
069                                            public Object invoke(
070                                                            Object proxy, 
071                                                            Method method,
072                                                            Object[] args) throws Throwable {
073    
074                                                    Method mappedMethod = methodMap.get(method);
075                                                    if (mappedMethod ==null) {
076                                                            return method.invoke(object, args);
077                                                    }
078                                                    
079    //                                              CompositeConverter converter = CompositeConverter.getDefaultConverter();
080                                                    Object[] convertedArgs;
081                                                    if (args==null) {
082                                                            convertedArgs = null;
083                                                    } else {
084                                                            Class<?>[] parameterTypes = mappedMethod.getParameterTypes();
085                                                            convertedArgs = new Object[args.length];
086                                                            for (int i=0; i<convertedArgs.length; ++i) {
087                                                                    convertedArgs[i] = ConvertingService.convert(args[i], parameterTypes[i]);
088                                                            }
089                                                    }
090                                                    return mappedMethod.invoke(object, args);
091                                            }
092    
093                                            public Object getMaster() {
094                                                    return object;
095                                            }
096                                            
097                                    });
098            }
099            
100            /**
101             * Creates a proxy which routes invocations to master unless one of interceptors
102             * has a method with matching signature. Only interface methods are routed
103             * to interceptors.
104             * 
105             * Traditional approach to intercepting in Java is to create filters/wrappers.
106             * This approach might be problematic if the filter master instances might be of 
107             * different subclasses of the base filtered type and if the subclassed functionality
108             * must be propagated through the filter.
109             * 
110             * Returned proxy invokes only interceptor's method. It is responsiblity of the
111             * interceptor to call master's method if needed. 
112             * 
113             * Interceptor methods are matched to master methods based on name and parameter
114             * types equality (as in overriding). Return type and exception types are not
115             * taken in consideration.
116             * 
117             * @param master
118             * @param interceptors
119             * @return
120             */
121            @SuppressWarnings("unchecked")
122            public static Object addInterceptors(final Object master, Object... interceptors) {
123                    if (master == null) {
124                            return null;
125                    }
126                    
127                    // Array of collections.
128                    Collection<Method>[] interceptorMethods = new Collection[interceptors.length];
129                    for (int i=0; i<interceptors.length; ++i) {
130                            Class<? extends Object> interceptorClass = interceptors[i].getClass();
131                            Method[] iMethods = interceptorClass.getMethods();
132                            interceptorMethods[i] = new ArrayList<Method>();
133                            for (int m=0; m<iMethods.length; ++m) {
134                                    Method interceptorMethod = iMethods[m];
135                                    
136                                    if (!interceptorMethod.getDeclaringClass().equals(Object.class)) {
137                                            interceptorMethods[i].add(interceptorMethod);
138                                    }
139                            }
140                    }
141                    
142                    class InterceptorEntry {
143                            Object object;
144                            Method method;
145                            
146                            InterceptorEntry(Object object, Method method) {
147                                    super();
148                                    this.object = object;
149                                    this.method = method;
150                            }                       
151                            
152                            Object invoke(Object[] args) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
153                                    return method.invoke(object, args);
154                            }
155                    }
156                    
157                    Class<?>[] interfaces = DuckConverterFactory.getClassInterfaces(master.getClass());
158                    final Map<Method, InterceptorEntry> interceptionMap = new HashMap<Method, InterceptorEntry>();
159                    for (int interfaceIdx=0; interfaceIdx<interfaces.length; ++interfaceIdx) {
160                            Method[] methods = interfaces[interfaceIdx].getMethods();
161                            Z: for (int methodIdx = 0; methodIdx < methods.length; ++methodIdx) {
162                                    Method masterMethod = methods[methodIdx];
163                                    if (!masterMethod.getDeclaringClass().equals(Object.class)) {
164                                            for (int interceptorIdx =0; interceptorIdx<interceptors.length; ++interceptorIdx) {
165                                                    Iterator<?> interceptorMethodIterator = interceptorMethods[interceptorIdx].iterator();                                            
166                                                    Y: while (interceptorMethodIterator.hasNext()) {
167                                                            Method interceptorMethod = (Method) interceptorMethodIterator.next();
168                                                            if (masterMethod.getName().equals(interceptorMethod.getName())) {
169                                                                    Class<?>[] masterParameterTypes = masterMethod.getParameterTypes();
170                                                                    Class<?>[] interceptorParameterTypes = interceptorMethod.getParameterTypes();
171                                                                    if (masterParameterTypes.length!=interceptorParameterTypes.length) {
172                                                                            continue;
173                                                                    }
174                                                                    for (int prmIdx = 0; prmIdx<masterParameterTypes.length; ++prmIdx) {
175                                                                            if (!masterParameterTypes[prmIdx].equals(interceptorParameterTypes[prmIdx])) {
176                                                                                    continue Y;
177                                                                            }
178                                                                    }
179                                                                    
180                                                                    System.out.println("~~~~~ Mapped method "+masterMethod+" to interceptor "+interceptorIdx);
181                                                                    interceptionMap.put(masterMethod, new InterceptorEntry(interceptors[interceptorIdx], interceptorMethod));
182                                                                    continue Z;
183                                                            }
184                                                    }
185                                            }
186                                    }
187                            }
188                    }
189                    
190                    return Proxy.newProxyInstance(
191                                    Object.class.getClassLoader(), 
192                                    interfaces,
193                                    new FilterInvocationHandler() {
194    
195                                            public Object invoke(
196                                                            Object proxy, 
197                                                            Method method,
198                                                            Object[] args) throws Throwable {
199    
200                                                    InterceptorEntry ie = interceptionMap.get(method);                                              
201                                                    return ie==null ? method.invoke(master, args) : ie.invoke(args);
202                                            }
203    
204                                            public Object getMaster() {
205                                                    return master;
206                                            }
207                                            
208                                    });
209                                    
210            }
211                    
212            /**
213             * Creates a proxy which implements given interfaces and routes invocations
214             * to appropriate object. If object is not found for a particular interface,
215             * then invocation is routed to the first object.
216             * @param object
217             * @param interfaces
218             * @return
219             */
220            @SuppressWarnings("unchecked")
221            public static Object combine(final Object[] objects, Class<?>[] interfaces) {
222                    if (objects==null || objects.length==0) {
223                            return null;
224                    }
225                    
226                    boolean firstDoesIt = true;
227                    for (int i=0; i<interfaces.length; ++i) {
228                            if (!interfaces[i].isInstance(objects[0])) {
229                                    firstDoesIt = false;
230                                    break;
231                            }
232                    }
233                    
234                    if (firstDoesIt) {
235                            return objects[0];
236                    }
237    
238                    if (objects.length==1) {
239                            return mixIn(objects[0], interfaces);
240                    }
241                    
242                    List<Method> allInterfaceMethods = new ArrayList<Method>();
243                    for (int i=0; i<interfaces.length; ++i) {
244                            allInterfaceMethods.addAll(Arrays.asList(interfaces[i].getMethods()));
245                    }
246                    
247                    Method[] aim = allInterfaceMethods.toArray(new Method[allInterfaceMethods.size()]);
248                    
249                    final Map<Method, Method>[] methodMaps = new Map[objects.length];
250                    for (int i=0; i<objects.length; ++i) {
251                            methodMaps[i] = new HashMap<Method, Method>();
252                            DuckConverterFactory.duckMap(aim, objects[i].getClass().getMethods(), methodMaps[i]);
253                    }
254                    
255                    ClassLoader classLoader  = null;
256                    for (int i=0; i<objects.length; ++i) {
257                            classLoader = DuckConverterFactory.getChildClassLoader(classLoader, objects[i].getClass().getClassLoader());
258                    }
259                    for (int i=0; i<interfaces.length; ++i) {
260                            classLoader = DuckConverterFactory.getChildClassLoader(classLoader, interfaces[i].getClassLoader());
261                    }
262                    
263                    return Proxy.newProxyInstance(
264                                    classLoader == null ? objects[0].getClass().getClassLoader() : classLoader, 
265                                    interfaces, 
266                                    new InvocationHandler() {
267    
268                                            public Object invoke(
269                                                            Object proxy, 
270                                                            Method method,
271                                                            Object[] args) throws Throwable {
272                                                    
273                                                    Class<?> declaringClass = method.getDeclaringClass();
274                                                    for (int i=0; i<objects.length; ++i) {                                                       
275                                                            if (declaringClass.isInstance(objects[i])) {
276                                                                    return method.invoke(objects[i], args);
277                                                            }
278                                                            Method mappedMethod = (Method) methodMaps[i].get(method);
279                                                            if (mappedMethod!=null) {
280                                                                    return mappedMethod.invoke(objects[i], args);
281                                                            }
282                                                    }
283                                                    
284                                                    return method.invoke(objects[0], args);
285                                            }
286                                            
287                                    });
288            }
289            
290            /**
291             * Mixes interfaces from multiple objects into one proxy.
292             * It can be used to "mix-in" interface into another interface, e.g.
293             * mix-in application-specific interfaces into HttpServletRequest.
294             * Different interfaces can be mixed-in depending on context, which is 
295             * difficult to achieve with wrapping.
296             * @param objects
297             * @return
298             */
299            public static Object mix(final Object... objects) {
300                    if (objects==null || objects.length==0) {
301                            return null;
302                    }
303                    
304                    if (objects.length==1) {
305                            return objects[0];
306                    }
307                    
308                    
309                    // Maps interface to implementation.
310                    final Map<Class<?>, Object> interfaceMap = new HashMap<Class<?>, Object>();
311                    List<Class<?>> interfaces = new ArrayList<Class<?>>();
312                    ClassLoader classLoader = null;
313                    boolean onlyFirstInterfaces = false;
314                    
315                    for (int i=0; i<objects.length; ++i) {
316                            Class<?> sourceClass = objects[i].getClass();
317                            classLoader=DuckConverterFactory.getChildClassLoader(classLoader, sourceClass.getClassLoader());
318                            Class<?>[] cia = DuckConverterFactory.getClassInterfaces(sourceClass);
319                            for (int j=0; j<cia.length; ++j) {
320                                    Class<?> classInterface = cia[j];
321                                    if (!interfaceMap.containsKey(classInterface)) {
322                                            onlyFirstInterfaces = i==0;
323                                            interfaceMap.put(classInterface, objects[i]);
324                                            interfaces.add(classInterface);
325                                    }
326                            }
327                    }
328    
329                    // All interfaces are implemented by the first object, i.e. other objects add nothing.
330                    if (onlyFirstInterfaces) {
331                            return objects[0];
332                    }
333                    
334                    return Proxy.newProxyInstance(
335                                    classLoader == null ? objects[0].getClass().getClassLoader() : classLoader,
336                                                    interfaces.toArray(new Class[interfaces.size()]), 
337                                                    new InvocationHandler() {
338    
339                                                            public Object invoke(
340                                                                            Object proxy, 
341                                                                            Method method,
342                                                                            Object[] args) throws Throwable {
343                                                                    
344                                                                    Object target = interfaceMap.get(method.getDeclaringClass());
345                                                                    return method.invoke(target==null ? objects[0] : target, args);
346                                                            }
347                                            
348                                    });
349            }
350            
351    }