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<String, String> testMap = new HashMap<String, String>();
024 * testMap.put("a", "A");
025 *
026 * //Create delegate method that calls the 'clear' method for HashMap.
027 * DelegateMethod<Map, Void> method = new DelegateMethod<Map, Void>(Map.class,
028 * "clear");
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 }