1 /*
2 * Copyright 2006-2016 The JGUIraffe Team.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License")
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package net.sf.jguiraffe.di;
17
18 import java.lang.reflect.AccessibleObject;
19 import java.lang.reflect.Constructor;
20 import java.lang.reflect.Method;
21 import java.util.Arrays;
22 import java.util.Iterator;
23 import java.util.List;
24
25 import org.apache.commons.beanutils.PropertyUtils;
26 import org.apache.commons.lang.ClassUtils;
27
28 /**
29 * <p>
30 * A helper class providing some more complex functionality related to
31 * reflection.
32 * </p>
33 * <p>
34 * This class builds on top of {@link ReflectionUtils} which implements
35 * low-level utility methods for various operations related to reflection.
36 * {@code InvocationHelper} in contrast focuses on some more advanced
37 * operations. It deals with stuff like finding suitable methods and
38 * constructors to be invoked, getting and setting properties, and data type
39 * conversions (this is delegated to an instance of {@link ConversionHelper}
40 * which is maintained by this class).
41 * </p>
42 * <p>
43 * Implementation note: This class is thread-safe.
44 * </p>
45 *
46 * @author Oliver Heger
47 * @version $Id: InvocationHelper.java 205 2012-01-29 18:29:57Z oheger $
48 */
49 public class InvocationHelper
50 {
51 /** Constant for a format string for generating a method signature. */
52 private static final String METHOD_SIGNATURE = "%s.%s(%s), arguments: %s";
53
54 /** Constant for the name of a method representing a constructor. */
55 private static final String CONSTR_METHOD_NAME = "<init>";
56
57 /** Stores the associated {@code ConversionHelper} instance. */
58 private final ConversionHelper conversionHelper;
59
60 /**
61 * Creates a new instance of {@code InvocationHelper}. A default
62 * {@code ConversionHelper} instance is set.
63 */
64 public InvocationHelper()
65 {
66 this(null);
67 }
68
69 /**
70 * Creates a new instance of {@code InvocationHelper} and initializes it
71 * with the given {@code ConversionHelper} instance. If no helper object is
72 * provided, a new default instance is created.
73 *
74 * @param convHlp the {@code ConversionHelper}
75 */
76 public InvocationHelper(ConversionHelper convHlp)
77 {
78 conversionHelper = (convHlp != null) ? convHlp : new ConversionHelper();
79 }
80
81 /**
82 * Returns the {@code ConversionHelper} instance associated with this
83 * object.
84 *
85 * @return the {@code ConversionHelper}
86 */
87 public ConversionHelper getConversionHelper()
88 {
89 return conversionHelper;
90 }
91
92 /**
93 * Determines the method to be called given the name, the parameter types,
94 * and the arguments. This method delegates to
95 * {@link ReflectionUtils#findMethods(Class, String, Class[], boolean)} to
96 * find a single method that matches the specified method signature. It also
97 * takes the specified method arguments into account in order to find a
98 * unique match. If this is not possible, an exception is thrown.
99 *
100 * @param targetClass the class on which to invoke the method
101 * @param methodName the name of the method to be invoked
102 * @param parameterTypes an array with the known parameter types (may be
103 * <b>null</b> or contain <b>null</b> entries representing wild
104 * cards)
105 * @param args an array with the method arguments
106 * @return the method to be invoked
107 * @throws InjectionException if the method cannot be determined
108 * @throws IllegalArgumentException if the target class is <b>null</b>
109 */
110 public Method findUniqueMethod(Class<?> targetClass, String methodName,
111 Class<?>[] parameterTypes, Object[] args)
112 {
113 List<Method> methods =
114 findMethods(targetClass, methodName, parameterTypes, args, true);
115 if (methods.isEmpty())
116 {
117 // try again with relaxed type checking
118 methods =
119 findMethods(targetClass, methodName, parameterTypes, args,
120 false);
121 if (methods.isEmpty())
122 {
123 throw nonUniqueMethodException(methods, targetClass,
124 methodName, parameterTypes, args);
125 }
126 }
127
128 if (methods.size() > 1)
129 {
130 // apply the types of the current call arguments
131 Class<?>[] callTypes =
132 parameterTypesFromArguments(parameterTypes, args);
133 methods =
134 findMethods(targetClass, methodName, callTypes, args, false);
135 }
136
137 if (methods.size() != 1)
138 {
139 throw nonUniqueMethodException(methods, targetClass, methodName,
140 parameterTypes, args);
141 }
142
143 return methods.get(0);
144 }
145
146 /**
147 * Tries to match a single constructor of the specified target class given
148 * the parameter types and the call arguments. This method works analogously
149 * to {@link #findUniqueMethod(Class, String, Class[], Object[])}, but it
150 * searches for a matching constructor.
151 *
152 * @param <T> the type of the target class
153 * @param targetClass the class on which to invoke the method
154 * @param parameterTypes an array with the known parameter types (may be
155 * <b>null</b> or contain <b>null</b> entries representing wild
156 * cards)
157 * @param args an array with the call arguments
158 * @return the unique constructor matching the specified criteria
159 * @throws InjectionException if no unique constructor can be determined
160 * @throws IllegalArgumentException if the target class is <b>null</b>
161 */
162 public <T> Constructor<T> findUniqueConstructor(Class<T> targetClass,
163 Class<?>[] parameterTypes, Object[] args)
164 {
165 // This code is pretty similar to the one of findUniqueMethod().
166 // Unfortunately, it cannot easily be generalized. This is because
167 // Method and Constructor do not share a common super class that allows
168 // access to the parameter types.
169 List<Constructor<T>> constrs =
170 findConstructors(targetClass, parameterTypes, args, true);
171 if (constrs.isEmpty())
172 {
173 constrs =
174 findConstructors(targetClass, parameterTypes, args, false);
175 if (constrs.isEmpty())
176 {
177 throw nonUniqueMethodException(constrs, targetClass,
178 CONSTR_METHOD_NAME, parameterTypes, args);
179 }
180 }
181
182 if (constrs.size() > 1)
183 {
184 Class<?>[] callTypes =
185 parameterTypesFromArguments(parameterTypes, args);
186 constrs = findConstructors(targetClass, callTypes, args, false);
187 }
188
189 if (constrs.size() != 1)
190 {
191 throw nonUniqueMethodException(constrs, targetClass,
192 CONSTR_METHOD_NAME, parameterTypes, args);
193 }
194
195 return constrs.get(0);
196 }
197
198 /**
199 * Invokes the specified static method and returns its result.
200 *
201 * @param targetClass the target class on which to invoke the method (must
202 * not be <b>null</b>)
203 * @param methodName the name of the method
204 * @param parameterTypes an array with the known parameter types (may be
205 * <b>null</b> or contain <b>null</b> entries representing wild
206 * cards)
207 * @param args an array with the method arguments
208 * @return the result returned by the method
209 * @throws InjectionException if an exception occurs
210 * @throws IllegalArgumentException if the target class is undefined
211 */
212 public Object invokeStaticMethod(Class<?> targetClass, String methodName,
213 Class<?>[] parameterTypes, Object[] args)
214 {
215 return invokeMethod(targetClass, null, methodName, parameterTypes, args);
216 }
217
218 /**
219 * Invokes a method on the specified object. The object must not be
220 * <b>null</b> because the target class is obtained from it. Then this
221 * implementation delegates to
222 * {@link #invokeMethod(Class, Object, String, Class[], Object[])}.
223 *
224 * @param instance the instance on which to invoke the method
225 * @param methodName the name of the method to be invoked
226 * @param parameterTypes an array with the known parameter types (may be
227 * <b>null</b> or contain <b>null</b> entries representing wild
228 * cards)
229 * @param args an array with the method arguments
230 * @return the result returned by the method
231 * @throws InjectionException if an exception occurs
232 * @throws IllegalArgumentException if the instance is undefined
233 */
234 public Object invokeInstanceMethod(Object instance, String methodName,
235 Class<?>[] parameterTypes, Object[] args)
236 {
237 return invokeMethod(null, instance, methodName, parameterTypes, args);
238 }
239
240 /**
241 * Invokes a method. This is the most generic form of invoking a method. It
242 * works with both static and instance methods. This method delegates to
243 * {@link #findUniqueMethod(Class, String, Class[], Object[])} to find the
244 * method to be invoked. Then it performs necessary type conversions of the
245 * parameters. Eventually, it invokes the method. Using this method it is
246 * possible to invoke a specific method in a given class, even if the
247 * parameter types are not or only partly known. All possible exceptions
248 * (e.g. no unique method is found, the parameters cannot be converted,
249 * reflection-related exceptions) are thrown as {@link InjectionException}
250 * exceptions.
251 *
252 * @param targetClass the target class on which to invoke the method (may be
253 * <b>null</b> if an object instance is provided)
254 * @param instance the instance on which to invoke the method (may be
255 * <b>null</b> for static methods)
256 * @param methodName the name of the method to be invoked
257 * @param parameterTypes an array with the known parameter types (may be
258 * <b>null</b> or contain <b>null</b> entries representing wild
259 * cards)
260 * @param args an array with the method arguments
261 * @return the result returned by the method
262 * @throws InjectionException if an exception occurs
263 * @throws IllegalArgumentException if a required parameter is missing or
264 * the specification of the method to be called is invalid
265 */
266 public Object invokeMethod(Class<?> targetClass, Object instance,
267 String methodName, Class<?>[] parameterTypes, Object[] args)
268 {
269 Class<?> clsToInvoke = getClassToInvoke(targetClass, instance);
270 Method method =
271 findUniqueMethod(clsToInvoke, methodName, parameterTypes, args);
272 Object[] convertedArgs =
273 convertArguments(method.getParameterTypes(), args);
274 return ReflectionUtils.invokeMethod(method, instance, convertedArgs);
275 }
276
277 /**
278 * Invokes a constructor. This method delegates to
279 * {@link #findUniqueConstructor(Class, Class[], Object[])} to obtain the
280 * constructor to be invoked. Then it performs necessary type conversions of
281 * the parameters. Eventually, it invokes the constructor and returns the
282 * newly created instance. Using this method it is possible to invoke a
283 * specific constructor of a given class, even if the parameter types are
284 * not or only partly known. All possible exceptions (e.g. no unique method
285 * is found, the parameters cannot be converted, reflection-related
286 * exceptions) are thrown as {@link InjectionException} exceptions.
287 *
288 * @param <T> the type of the target class
289 * @param targetClass the target class on which the constructor is to be
290 * invoked
291 * @param parameterTypes an array with the known parameter types (may be
292 * <b>null</b> or contain <b>null</b> entries representing wild
293 * cards)
294 * @param args an array with the method arguments
295 * @return the newly created instance
296 * @throws InjectionException if an exception occurs
297 * @throws IllegalArgumentException if the specification of the constructor
298 * is invalid
299 */
300 public <T> T invokeConstructor(Class<T> targetClass,
301 Class<?>[] parameterTypes, Object[] args)
302 {
303 Constructor<T> ctor =
304 findUniqueConstructor(targetClass, parameterTypes, args);
305 Object[] convertedArgs =
306 convertArguments(ctor.getParameterTypes(), args);
307 return ReflectionUtils.invokeConstructor(ctor, convertedArgs);
308 }
309
310 /**
311 * Sets a property of the given bean. This method obtains the set method for
312 * the property in question. If necessary, type conversion is performed.
313 * Then the set method is invoked so that the new value of the property is
314 * written. Occurring exceptions are redirected either as
315 * {@link InjectionException} (if they are related to reflection operations)
316 * or as {@code IllegalArgumentException} if they are related to the
317 * parameters passed to this method.
318 *
319 * @param bean the bean on which to set the property
320 * @param property the name of the property to be set
321 * @param value the value of the property
322 * @throws InjectionException if an error occurs related to reflection
323 * @throws IllegalArgumentException if invalid arguments are passed in
324 */
325 public void setProperty(Object bean, String property, Object value)
326 {
327 Class<?> propertyType = getPropertyType(bean, property);
328 Object convertedValue =
329 getConversionHelper().convert(propertyType, value);
330 ReflectionUtils.setProperty(bean, property, convertedValue);
331 }
332
333 /**
334 * Performs necessary type conversions before invoking a method. This method
335 * is called before a method or a constructor is invoked. The passed in
336 * parameter types are the actual parameters of the method to be invoked,
337 * the arguments are the ones passed by the caller. They may require a type
338 * conversion. This implementation performs this conversion if necessary.
339 *
340 * @param parameterTypes the array with the parameter types
341 * @param args the array with the call arguments
342 * @return an array with the converted arguments
343 * @throws InjectionException if a conversion fails
344 */
345 protected Object[] convertArguments(Class<?>[] parameterTypes, Object[] args)
346 {
347 assert arrayLength(parameterTypes) == arrayLength(args) : "Different array lengths!";
348 Object[] results = null;
349
350 for (int i = 0; i < parameterTypes.length; i++)
351 {
352 Object convValue =
353 getConversionHelper().convert(parameterTypes[i], args[i]);
354
355 if (convValue != args[i])
356 {
357 if (results == null)
358 {
359 // lazy create results array
360 results = new Object[args.length];
361 System.arraycopy(args, 0, results, 0, args.length);
362 }
363 results[i] = convValue;
364 }
365 }
366
367 return (results != null) ? results : args;
368 }
369
370 /**
371 * Helper method for finding methods that match given criteria. This method
372 * delegates to {@link ReflectionUtils} for doing the lookup. Then it sorts
373 * out the hits that are incompatible with the passed in parameters.
374 * Finally, it deals with methods with identical signatures but different
375 * return types.
376 *
377 * @param targetClass the target class
378 * @param methodName the name of the method
379 * @param parameterTypes an array with parameter types
380 * @param args concrete parameters to be passed to the method
381 * @param exactMatch a flag whether an exact match is to be performed
382 * @return the list with found methods
383 */
384 private static List<Method> findMethods(Class<?> targetClass,
385 String methodName, Class<?>[] parameterTypes, Object[] args,
386 boolean exactMatch)
387 {
388 List<Method> methods =
389 ReflectionUtils.findMethods(targetClass, methodName,
390 parameterTypes, exactMatch);
391
392 for (Iterator<Method> it = methods.iterator(); it.hasNext();)
393 {
394 Method m = it.next();
395 if (!checkArgumentsForCompatibility(m.getParameterTypes(), args))
396 {
397 it.remove();
398 }
399 }
400
401 return ReflectionUtils.removeCovariantDuplicates(methods);
402 }
403
404 /**
405 * Helper method for finding constructors that match given criteria. Works
406 * analogously to
407 * {@link #findMethods(Class, String, Class[], Object[], boolean)}, but
408 * operates on constructors.
409 *
410 * @param <T> the type of the target class
411 * @param targetClass the target class
412 * @param parameterTypes an array with parameter types
413 * @param args concrete parameters to be passed to the constructor
414 * @param exactMatch a flag whether an exact match is to be performed
415 * @return the list with found constructors
416 */
417 private static <T> List<Constructor<T>> findConstructors(
418 Class<T> targetClass, Class<?>[] parameterTypes, Object[] args,
419 boolean exactMatch)
420 {
421 List<Constructor<T>> constrs =
422 ReflectionUtils.findConstructors(targetClass, parameterTypes,
423 exactMatch);
424
425 for (Iterator<Constructor<T>> it = constrs.iterator(); it.hasNext();)
426 {
427 Constructor<T> c = it.next();
428 if (!checkArgumentsForCompatibility(c.getParameterTypes(), args))
429 {
430 it.remove();
431 }
432 }
433
434 return constrs;
435 }
436
437 /**
438 * Tests whether the specified arguments are compatible with the given
439 * method signature. This method mainly checks whether a null value is
440 * assigned to a parameter of primitive type. Further type checks are not
441 * performed because there may be type conversions later.
442 *
443 * @param parameterTypes the parameter types of the method
444 * @param args the arguments to be passed to the method
445 * @return a flag whether these arguments are compatible with the method
446 */
447 private static boolean checkArgumentsForCompatibility(
448 Class<?>[] parameterTypes, Object[] args)
449 {
450 if (arrayLength(parameterTypes) != arrayLength(args))
451 {
452 // number of parameters does not match
453 return false;
454 }
455
456 for (int i = 0; i < arrayLength(parameterTypes); i++)
457 {
458 if (args[i] == null && parameterTypes[i].isPrimitive())
459 {
460 return false;
461 }
462 }
463
464 return true;
465 }
466
467 /**
468 * Derives parameter types from the concrete parameters. For each parameter
469 * type whose class is not specified the corresponding type from the
470 * argument is set - unless it is undefined, too.
471 *
472 * @param parameterTypes the array with parameter types
473 * @param args the concrete call arguments
474 * @return an array with enhanced parameter type information
475 */
476 private static Class<?>[] parameterTypesFromArguments(
477 Class<?>[] parameterTypes, Object[] args)
478 {
479 if (arrayLength(args) == 0)
480 {
481 return parameterTypes;
482 }
483
484 Class<?>[] newTypes = new Class<?>[arrayLength(args)];
485 System.arraycopy(parameterTypes, 0, newTypes, 0, newTypes.length);
486 for (int i = 0; i < newTypes.length; i++)
487 {
488 if (newTypes[i] == null && args[i] != null)
489 {
490 newTypes[i] = args[i].getClass();
491 }
492 }
493
494 return newTypes;
495 }
496
497 /**
498 * Obtains the class to be invoked from the parameters of a method
499 * invocation. If both a target class and an instance are provided, they are
500 * checked for compatibility. If a target class was specified, it is used;
501 * otherwise the class from the instance is obtained.
502 *
503 * @param targetClass the target class
504 * @param instance the object instance
505 * @return the class to be invoked
506 * @throws IllegalArgumentException if the parameters are invalid
507 */
508 private static Class<?> getClassToInvoke(Class<?> targetClass,
509 Object instance)
510 {
511 if (targetClass == null && instance == null)
512 {
513 throw new IllegalArgumentException(
514 "Neither target class nor instance provided!");
515 }
516
517 Class<?> clsToInvoke;
518 if (targetClass != null)
519 {
520 if (instance != null
521 && !ClassUtils.isAssignable(instance.getClass(),
522 targetClass))
523 {
524 throw new IllegalArgumentException("Target class "
525 + targetClass + " is not compatible with instance "
526 + instance);
527 }
528 clsToInvoke = targetClass;
529 }
530 else
531 {
532 clsToInvoke = instance.getClass();
533 }
534 return clsToInvoke;
535 }
536
537 /**
538 * Determines the type of the specified property.
539 *
540 * @param bean the bean
541 * @param property the name of the property
542 * @return the data type of this property
543 * @throws IllegalArgumentException if parameters are invalid
544 * @throws InjectionException if an error occurs
545 */
546 private static Class<?> getPropertyType(Object bean, String property)
547 {
548 Class<?> propertyType;
549 try
550 {
551 propertyType = PropertyUtils.getPropertyType(bean, property);
552 }
553 catch (IllegalArgumentException iex)
554 {
555 throw iex;
556 }
557 catch (Exception ex)
558 {
559 // any other exception related to reflection is redirected
560 throw new InjectionException(
561 "Error when determining type of property " + property
562 + " on bean " + bean, ex);
563 }
564
565 if (propertyType == null)
566 {
567 throw new InjectionException("Cannot determine type of property "
568 + property + " on bean " + bean);
569 }
570 return propertyType;
571 }
572
573 /**
574 * Helper method for generating a string representation for a method
575 * signature.
576 *
577 * @param targetClass the class the method belongs to
578 * @param methodName the name of the method
579 * @param paramTypes the array with parameter types
580 * @param args the array with call arguments
581 * @return a string representation of the method signature
582 */
583 private static String methodSignature(Class<?> targetClass,
584 String methodName, Class<?>[] paramTypes, Object[] args)
585 {
586 return String.format(METHOD_SIGNATURE, targetClass.getName(),
587 methodName, Arrays.toString(paramTypes), Arrays.toString(args));
588 }
589
590 /**
591 * Helper method for generating a meaningful exception message if no unique
592 * match for a method to be called can be found. This method checks whether
593 * 0 or more than 1 matches were found and produces a corresponding error
594 * message. The exception returned by this method also contains the
595 * signature of the desired method.
596 *
597 * @param matches a list with the found matches
598 * @param targetClass the class the method belongs to
599 * @param methodName the name of the method
600 * @param paramTypes the array with parameter types
601 * @param args the array with call arguments
602 * @return an exception reporting the error condition
603 */
604 private static InjectionException nonUniqueMethodException(
605 List<? extends AccessibleObject> matches, Class<?> targetClass,
606 String methodName, Class<?>[] paramTypes, Object[] args)
607 {
608 String msg =
609 matches.isEmpty() ? "No match found for method %s"
610 : String.format(
611 "Multiple matches found for method %%s. Matches: %s",
612 matches.toString());
613 return new InjectionException(String.format(msg,
614 methodSignature(targetClass, methodName, paramTypes, args)));
615 }
616
617 /**
618 * Null-safe method for determining the length of an array.
619 *
620 * @param ar the array in question
621 * @return the length of this array
622 */
623 private static int arrayLength(Object[] ar)
624 {
625 return (ar != null) ? ar.length : 0;
626 }
627 }