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