001    /*****************************************************************************
002     * Copyright (C) PicoContainer Organization. All rights reserved.            *
003     * ------------------------------------------------------------------------- *
004     * The software in this package is published under the terms of the BSD      *
005     * style license a copy of which has been included with this distribution in *
006     * the LICENSE.txt file.                                                     *
007     *                                                                           *
008     * Original code by Centerline Computers, Inc.                               *
009     *****************************************************************************/
010    package org.picocontainer.gems.util;
011    
012    import java.lang.reflect.InvocationTargetException;
013    import java.lang.reflect.Method;
014    import java.lang.reflect.Modifier;
015    import java.util.Arrays;
016    
017    /**
018     * The DelegateMethod class has been designed in the hope of providing easier
019     * access to methods invoked via reflection. Sample:
020     * 
021     * <pre>
022     * //Sample Map
023     * HashMap&lt;String, String&gt; testMap = new HashMap&lt;String, String&gt;();
024     * testMap.put(&quot;a&quot;, &quot;A&quot;);
025     * 
026     * //Create delegate method that calls the 'clear' method for HashMap.
027     * DelegateMethod&lt;Map, Void&gt; method = new DelegateMethod&lt;Map, Void&gt;(Map.class,
028     *              &quot;clear&quot;);
029     * 
030     * //Invokes clear() on the HashMap.
031     * method.invoke(testMap);
032     * </pre>
033     * 
034     * <p>
035     * Good uses of this object are for lazy invocation of a method and integrating
036     * reflection with a vistor pattern.
037     * </p>
038     * 
039     * @author Michael Rimov
040     */
041    public class DelegateMethod<TARGET_TYPE, RETURN_TYPE> {
042    
043            /**
044             * Arguments for the method invocation.
045             */
046            private final Object[] args;
047    
048            /**
049             * The method to be invoked.
050             */
051            private final Method method;
052    
053            /**
054             * Constructs a delegate method object that will invoke method
055             * <em>methodName</em> on class <em>type</em> with the parameters
056             * specified. The object automatically searches for a suitable object to be
057             * invoked.
058             * <p>
059             * Note that this version simply grabs the
060             * <em>first<em> method that fits the parameter criteria with
061             * the specific name.  You may need to be careful if use extensive overloading.</p>
062             * <p>To specify the exact types in the method. 
063             * @param type the class of the object that should be invoked.
064             * @param methodName the name of the method that will be invoked.
065             * @param parameters the parameters to be used.
066             * @throws NoSuchMethodRuntimeException if the method is not found or parameters that match cannot be found.
067             */
068            public DelegateMethod(final Class<TARGET_TYPE> type,
069                            final String methodName, final Object... parameters)
070                            throws NoSuchMethodRuntimeException {
071                    this.args = parameters;
072                    this.method = findMatchingMethod(type.getMethods(), methodName,
073                                    parameters);
074    
075                    if (method == null) {
076                            throw new NoSuchMethodRuntimeException("Could not find method "
077                                            + methodName + " in type " + type.getName());
078                    }
079            }
080    
081            /**
082             * Constructs a DelegateMethod object with very specific argument types.
083             * 
084             * @param type
085             *            the type of the class to be examined for reflection.
086             * @param methodName
087             *            the name of the method to be invoked.
088             * @param paramTypes
089             *            specific parameter types for the method to be found.
090             * @param parameters
091             *            the parameters for method invocation.
092             * @throws NoSuchMethodRuntimeException
093             *             if the method is not found.
094             */
095            public DelegateMethod(final Class<?> type, final String methodName,
096                            final Class<?>[] paramTypes, final Object... parameters)
097                            throws NoSuchMethodRuntimeException {
098                    this.args = parameters;
099                    try {
100                            this.method = type.getMethod(methodName, paramTypes);
101                    } catch (NoSuchMethodException e) {
102                            throw new NoSuchMethodRuntimeException("Could not find method "
103                                            + methodName + " in type " + type.getName());
104                    }
105            }
106    
107            /**
108             * Constructs a method delegate with an explicit Method object.
109             * 
110             * @param targetMethod
111             * @param parameters
112             */
113            public DelegateMethod(final Method targetMethod, final Object... parameters) {
114                    this.args = parameters;
115                    this.method = targetMethod;
116            }
117    
118            /**
119             * Locates a method that fits the given parameter types.
120             * 
121             * @param methods
122             * @param methodName
123             * @param parameters
124             * @return
125             */
126            private Method findMatchingMethod(final Method[] methods,
127                            final String methodName, final Object[] parameters) {
128    
129                    // Get parameter types.
130                    Class<?>[] paramTypes = new Class[parameters.length];
131                    for (int i = 0; i < parameters.length; i++) {
132                            if (parameters[i] == null) {
133                                    paramTypes[i] = NullType.class;
134                            } else {
135                                    paramTypes[i] = parameters[i].getClass();
136                            }
137                    }
138    
139                    for (Method eachMethod : methods) {
140                            if (eachMethod.getName().equals(methodName)) {
141                                    if (isPotentialMatchingArguments(eachMethod, paramTypes)) {
142                                            return eachMethod;
143                                    }
144                            }
145                    }
146    
147                    return null;
148            }
149    
150            /**
151             * Returns true if all parameter types are assignable to the argument type.
152             * 
153             * @param eachMethod
154             *            the method we're checking.
155             * @param paramTypes
156             *            the parameter types provided as constructor arguments.
157             * @return true if the given method is a match given the parameter types.
158             */
159            private boolean isPotentialMatchingArguments(final Method eachMethod,
160                            final Class<?>[] paramTypes) {
161                    Class<?>[] argParameters = eachMethod.getParameterTypes();
162                    if (argParameters.length != paramTypes.length) {
163                            return false;
164                    }
165    
166                    for (int i = 0; i < paramTypes.length; i++) {
167                            if (paramTypes[i].getName().equals(NullType.class.getName())) {
168                                    // Nulls are allowed for any parameter.
169                                    continue;
170                            }
171    
172                            if (!argParameters[i].isAssignableFrom(paramTypes[i])) {
173                                    return false;
174                            }
175                    }
176    
177                    return true;
178            }
179    
180            /**
181             * Used for invoking static methods on the type passed into the constructor.
182             * 
183             * @return the result of the invocation. May be null if the return type is
184             *         void.
185             * @throws IllegalArgumentException
186             *             if the method being invoked is not static.
187             * @throws IllegalAccessRuntimeException
188             *             if the method being invoked is not public.
189             * @throws InvocationTargetRuntimeException
190             *             if an exception is thrown within the method being invoked.
191             */
192            public RETURN_TYPE invoke() throws IllegalArgumentException,
193                            IllegalAccessRuntimeException, InvocationTargetRuntimeException {
194                    if (!Modifier.isStatic(method.getModifiers())) {
195                            throw new IllegalArgumentException("Method "
196                                            + method.toGenericString()
197                                            + " is not static.  Use invoke(Object) instead.");
198                    }
199    
200                    return invoke(null);
201            }
202    
203            @SuppressWarnings("unchecked")
204            private RETURN_TYPE cast(final Object objectToCast) {
205                    return (RETURN_TYPE) objectToCast;
206            }
207    
208            /**
209             * Invokes the method specified in the constructor against the target
210             * specified.
211             * 
212             * @param <V>
213             *            a subclass of the type specified by the object declaration.
214             *            This allows Map delegates to operate on HashMaps etc.
215             * @param target
216             *            the target object instance to be operated upon. Unless
217             *            invoking a static method, this should not be null.
218             * @return the result of the invocation. May be null if the return type is
219             *         void.
220             * @throws IllegalArgumentException
221             *             if the method being invoked is not static and parameter
222             *             target null.
223             * @throws IllegalAccessRuntimeException
224             *             if the method being invoked is not public.
225             * @throws InvocationTargetRuntimeException
226             *             if an exception is thrown within the method being invoked.
227             */
228            public <V extends TARGET_TYPE> RETURN_TYPE invoke(final V target)
229                            throws IllegalAccessRuntimeException,
230                            InvocationTargetRuntimeException {
231                    assert args != null;
232    
233                    if (!Modifier.isStatic(method.getModifiers()) && target == null) {
234                            throw new IllegalArgumentException("Method "
235                                            + method.toGenericString()
236                                            + " is not static.  Use invoke(Object) instead.");
237                    }
238    
239                    RETURN_TYPE result;
240                    try {
241                            result = cast(method.invoke(target, args));
242                    } catch (IllegalAccessException e) {
243                            throw new IllegalAccessRuntimeException("Method "
244                                            + method.toGenericString() + " is not public.", e);
245                    } catch (InvocationTargetException e) {
246                            // Unwrap the exception. Should save confusing duplicate traces.
247                            throw new InvocationTargetRuntimeException(
248                                            "There was an error invoking " + method.toGenericString(),
249                                            e.getCause());
250                    }
251    
252                    return result;
253            }
254    
255            /** {@inheritDoc} */
256            @Override
257            public String toString() {
258                    return "DelegateMethod " + method.toGenericString()
259                                    + " with arguments: " + Arrays.deepToString(args);
260            }
261    
262            /** {@inheritDoc} */
263            @Override
264            public int hashCode() {
265                    final int prime = 31;
266                    int result = 1;
267                    result = prime * result + Arrays.hashCode(args);
268                    result = prime * result + ((method == null) ? 0 : method.hashCode());
269                    return result;
270            }
271    
272            /** {@inheritDoc} */
273            @Override
274            @SuppressWarnings("unchecked")
275            public boolean equals(final Object obj) {
276                    if (this == obj) {
277                            return true;
278                    }
279                    if (obj == null) {
280                            return false;
281                    }
282                    if (getClass() != obj.getClass()) {
283                            return false;
284                    }
285                    final DelegateMethod other = (DelegateMethod) obj;
286                    if (!Arrays.equals(args, other.args)) {
287                            return false;
288                    }
289                    if (method == null) {
290                            if (other.method != null) {
291                                    return false;
292                            }
293                    } else if (!method.equals(other.method)) {
294                            return false;
295                    }
296                    return true;
297            }
298    
299            /**
300             * Retrieves the expected return type of the delegate method.
301             * @return
302             */
303            public Class<?> getReturnType() {
304                    return method.getReturnType();
305            }
306            
307            /**
308             * Placeholder type used for comparing null parameter values.
309             * 
310             * @author Michael Rimov
311             */
312            private static final class NullType {
313    
314                    /**
315                     * This type should never be constructed.
316                     */
317                    private NullType() {
318    
319                    }
320            }
321    
322    }