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 *****************************************************************************/
010package org.picocontainer.gems.util;
011
012import java.lang.reflect.InvocationTargetException;
013import java.lang.reflect.Method;
014import java.lang.reflect.Modifier;
015import 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 */
041public 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}