001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 *     http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014package org.gbif.ws.util.spring;
015
016import java.lang.reflect.Constructor;
017import java.lang.reflect.Field;
018import java.lang.reflect.InvocationTargetException;
019import java.lang.reflect.Method;
020import java.lang.reflect.Modifier;
021import java.util.Arrays;
022import java.util.LinkedList;
023import java.util.List;
024
025/**
026 * Simple utility class for working with the reflection API and handling
027 * reflection exceptions.
028 * <p>Only intended for internal use.
029 *
030 * @author Juergen Hoeller
031 * @author Rob Harrop
032 * @author Rod Johnson
033 * @author Costin Leau
034 * @author Sam Brannen
035 * @since 1.2.2
036 */
037public abstract class ReflectionUtils {
038
039  /**
040   * Attempt to find a {@link Field field} on the supplied {@link Class} with
041   * the supplied <code>name</code> and {@link Class type}. Searches all
042   * superclasses up to {@link Object}.
043   *
044   * @param clazz the class to introspect
045   * @param name  the name of the field
046   * @param type  the type of the field
047   *
048   * @return the corresponding Field object, or <code>null</code> if not found
049   *
050   * @throws IllegalArgumentException if the class or field type is
051   *                                  <code>null</code> or if the field name is <em>empty</em>
052   */
053  public static Field findField(final Class clazz, final String name, final Class type) {
054    Assert.notNull(clazz, "The 'class to introspect' supplied to findField() can not be null.");
055    Assert.hasText(name, "The field name supplied to findField() can not be empty.");
056    Assert.notNull(type, "The field type supplied to findField() can not be null.");
057    Class searchType = clazz;
058    while (!Object.class.equals(searchType) && searchType != null) {
059      final Field[] fields = searchType.getDeclaredFields();
060      for (int i = 0; i < fields.length; i++) {
061        Field field = fields[i];
062        if (name.equals(field.getName()) && type.equals(field.getType())) {
063          return field;
064        }
065      }
066      searchType = searchType.getSuperclass();
067    }
068    return null;
069  }
070
071  /**
072   * Set the field represented by the supplied {@link Field field object} on
073   * the specified {@link Object target object} to the specified
074   * <code>value</code>. In accordance with
075   * {@link Field#set(Object, Object)} semantics, the new value is
076   * automatically unwrapped if the underlying field has a primitive type.
077   * <p>Thrown exceptions are handled via a call to
078   * {@link #handleReflectionException(Exception)}.
079   *
080   * @param field  the field to set
081   * @param target the target object on which to set the field
082   * @param value  the value to set; may be <code>null</code>
083   */
084  public static void setField(Field field, Object target, Object value) {
085    try {
086      field.set(target, value);
087    } catch (IllegalAccessException ex) {
088      handleReflectionException(ex);
089      throw new IllegalStateException(
090          "Unexpected reflection exception - " + ex.getClass().getName() + ": " + ex.getMessage());
091    }
092  }
093
094  /**
095   * Attempt to find a {@link Method} on the supplied class with the supplied name
096   * and no parameters. Searches all superclasses up to <code>Object</code>.
097   * <p>Returns <code>null</code> if no {@link Method} can be found.
098   *
099   * @param clazz the class to introspect
100   * @param name  the name of the method
101   *
102   * @return the Method object, or <code>null</code> if none found
103   */
104  public static Method findMethod(Class clazz, String name) {
105    return findMethod(clazz, name, new Class[0]);
106  }
107
108  /**
109   * Attempt to find a {@link Method} on the supplied class with the supplied name
110   * and parameter types. Searches all superclasses up to <code>Object</code>.
111   * <p>Returns <code>null</code> if no {@link Method} can be found.
112   *
113   * @param clazz      the class to introspect
114   * @param name       the name of the method
115   * @param paramTypes the parameter types of the method
116   *
117   * @return the Method object, or <code>null</code> if none found
118   */
119  public static Method findMethod(Class clazz, String name, Class[] paramTypes) {
120    Assert.notNull(clazz, "Class must not be null");
121    Assert.notNull(name, "Method name must not be null");
122    Class searchType = clazz;
123    while (!Object.class.equals(searchType) && searchType != null) {
124      Method[] methods =
125          (searchType.isInterface() ? searchType.getMethods() : searchType.getDeclaredMethods());
126      for (int i = 0; i < methods.length; i++) {
127        Method method = methods[i];
128        if (name.equals(method.getName())
129            && Arrays.equals(paramTypes, method.getParameterTypes())) {
130          return method;
131        }
132      }
133      searchType = searchType.getSuperclass();
134    }
135    return null;
136  }
137
138  /**
139   * Invoke the specified {@link Method} against the supplied target object
140   * with no arguments. The target object can be <code>null</code> when
141   * invoking a static {@link Method}.
142   * <p>Thrown exceptions are handled via a call to {@link #handleReflectionException}.
143   *
144   * @param method the method to invoke
145   * @param target the target object to invoke the method on
146   *
147   * @return the invocation result, if any
148   *
149   * @see #invokeMethod(java.lang.reflect.Method, Object, Object[])
150   */
151  public static Object invokeMethod(Method method, Object target) {
152    return invokeMethod(method, target, null);
153  }
154
155  /**
156   * Invoke the specified {@link Method} against the supplied target object
157   * with the supplied arguments. The target object can be <code>null</code>
158   * when invoking a static {@link Method}.
159   * <p>Thrown exceptions are handled via a call to {@link #handleReflectionException}.
160   *
161   * @param method the method to invoke
162   * @param target the target object to invoke the method on
163   * @param args   the invocation arguments (may be <code>null</code>)
164   *
165   * @return the invocation result, if any
166   *
167   * @see #invokeMethod(java.lang.reflect.Method, Object, Object[])
168   */
169  public static Object invokeMethod(Method method, Object target, Object[] args) {
170    try {
171      return method.invoke(target, args);
172    } catch (IllegalAccessException ex) {
173      handleReflectionException(ex);
174      throw new IllegalStateException(
175          "Unexpected reflection exception - " + ex.getClass().getName() + ": " + ex.getMessage());
176    } catch (InvocationTargetException ex) {
177      handleReflectionException(ex);
178      throw new IllegalStateException(
179          "Unexpected reflection exception - " + ex.getClass().getName() + ": " + ex.getMessage());
180    }
181  }
182
183  /**
184   * Handle the given reflection exception. Should only be called if
185   * no checked exception is expected to be thrown by the target method.
186   * <p>Throws the underlying RuntimeException or Error in case of an
187   * InvocationTargetException with such a root cause. Throws an
188   * IllegalStateException with an appropriate message else.
189   *
190   * @param ex the reflection exception to handle
191   */
192  public static void handleReflectionException(Exception ex) {
193    if (ex instanceof NoSuchMethodException) {
194      throw new IllegalStateException("Method not found: " + ex.getMessage());
195    }
196    if (ex instanceof IllegalAccessException) {
197      throw new IllegalStateException("Could not access method: " + ex.getMessage());
198    }
199    if (ex instanceof InvocationTargetException) {
200      handleInvocationTargetException((InvocationTargetException) ex);
201    }
202    throw new IllegalStateException(
203        "Unexpected reflection exception - " + ex.getClass().getName() + ": " + ex.getMessage());
204  }
205
206  /**
207   * Handle the given invocation target exception. Should only be called if
208   * no checked exception is expected to be thrown by the target method.
209   * <p>Throws the underlying RuntimeException or Error in case of such
210   * a root cause. Throws an IllegalStateException else.
211   *
212   * @param ex the invocation target exception to handle
213   */
214  public static void handleInvocationTargetException(InvocationTargetException ex) {
215    if (ex.getTargetException() instanceof RuntimeException) {
216      throw (RuntimeException) ex.getTargetException();
217    }
218    if (ex.getTargetException() instanceof Error) {
219      throw (Error) ex.getTargetException();
220    }
221    throw new IllegalStateException(
222        "Unexpected exception thrown by method - "
223            + ex.getTargetException().getClass().getName()
224            + ": "
225            + ex.getTargetException().getMessage());
226  }
227
228  /**
229   * Rethrow the given {@link Throwable exception}, which is presumably the
230   * <em>target exception</em> of an {@link InvocationTargetException}.
231   * Should only be called if no checked exception is expected to be thrown by
232   * the target method.
233   * <p> Rethrows the underlying exception cast to an {@link Exception} or
234   * {@link Error} if appropriate; otherwise, throws an
235   * {@link IllegalStateException}.
236   *
237   * @param ex the exception to rethrow
238   *
239   * @throws Exception the rethrown exception (in case of a checked exception)
240   */
241  public static void rethrowException(Throwable ex) throws Exception {
242    if (ex instanceof Exception) {
243      throw (Exception) ex;
244    }
245    if (ex instanceof Error) {
246      throw (Error) ex;
247    }
248    throw new IllegalStateException(
249        "Unexpected exception thrown by method - "
250            + ex.getClass().getName()
251            + ": "
252            + ex.getMessage());
253  }
254
255  /**
256   * Determine whether the given method explicitly declares the given exception
257   * or one of its superclasses, which means that an exception of that type
258   * can be propagated as-is within a reflective invocation.
259   *
260   * @param method        the declaring method
261   * @param exceptionType the exception to throw
262   *
263   * @return <code>true</code> if the exception can be thrown as-is;
264   *         <code>false</code> if it needs to be wrapped
265   */
266  public static boolean declaresException(Method method, Class exceptionType) {
267    Assert.notNull(method, "Method must not be null");
268    Class[] declaredExceptions = method.getExceptionTypes();
269    for (int i = 0; i < declaredExceptions.length; i++) {
270      Class declaredException = declaredExceptions[i];
271      if (declaredException.isAssignableFrom(exceptionType)) {
272        return true;
273      }
274    }
275    return false;
276  }
277
278  /**
279   * Determine whether the given field is a "public static final" constant.
280   *
281   * @param field the field to check
282   */
283  public static boolean isPublicStaticFinal(Field field) {
284    int modifiers = field.getModifiers();
285    return (Modifier.isPublic(modifiers)
286        && Modifier.isStatic(modifiers)
287        && Modifier.isFinal(modifiers));
288  }
289
290  /**
291   * Make the given field accessible, explicitly setting it accessible if necessary.
292   * The <code>setAccessible(true)</code> method is only called when actually necessary,
293   * to avoid unnecessary conflicts with a JVM SecurityManager (if active).
294   *
295   * @param field the field to make accessible
296   *
297   * @see java.lang.reflect.Field#setAccessible
298   */
299  public static void makeAccessible(Field field) {
300    if (!Modifier.isPublic(field.getModifiers())
301        || !Modifier.isPublic(field.getDeclaringClass().getModifiers())) {
302      field.setAccessible(true);
303    }
304  }
305
306  /**
307   * Make the given method accessible, explicitly setting it accessible if necessary.
308   * The <code>setAccessible(true)</code> method is only called when actually necessary,
309   * to avoid unnecessary conflicts with a JVM SecurityManager (if active).
310   *
311   * @param method the method to make accessible
312   *
313   * @see java.lang.reflect.Method#setAccessible
314   */
315  public static void makeAccessible(Method method) {
316    if (!Modifier.isPublic(method.getModifiers())
317        || !Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
318      method.setAccessible(true);
319    }
320  }
321
322  /**
323   * Make the given constructor accessible, explicitly setting it accessible if necessary.
324   * The <code>setAccessible(true)</code> method is only called when actually necessary,
325   * to avoid unnecessary conflicts with a JVM SecurityManager (if active).
326   *
327   * @param ctor the constructor to make accessible
328   *
329   * @see java.lang.reflect.Constructor#setAccessible
330   */
331  public static void makeAccessible(Constructor ctor) {
332    if (!Modifier.isPublic(ctor.getModifiers())
333        || !Modifier.isPublic(ctor.getDeclaringClass().getModifiers())) {
334      ctor.setAccessible(true);
335    }
336  }
337
338  /**
339   * Perform the given callback operation on all matching methods of the
340   * given class and superclasses.
341   * <p>The same named method occurring on subclass and superclass will
342   * appear twice, unless excluded by a {@link MethodFilter}.
343   *
344   * @param targetClass class to start looking at
345   * @param mc          the callback to invoke for each method
346   *
347   * @see #doWithMethods(Class, MethodCallback, MethodFilter)
348   */
349  public static void doWithMethods(Class targetClass, MethodCallback mc)
350      throws IllegalArgumentException {
351    doWithMethods(targetClass, mc, null);
352  }
353
354  /**
355   * Perform the given callback operation on all matching methods of the
356   * given class and superclasses.
357   * <p>The same named method occurring on subclass and superclass will
358   * appear twice, unless excluded by the specified {@link MethodFilter}.
359   *
360   * @param targetClass class to start looking at
361   * @param mc          the callback to invoke for each method
362   * @param mf          the filter that determines the methods to apply the callback to
363   */
364  public static void doWithMethods(Class targetClass, MethodCallback mc, MethodFilter mf)
365      throws IllegalArgumentException {
366
367    // Keep backing up the inheritance hierarchy.
368    do {
369      Method[] methods = targetClass.getDeclaredMethods();
370      for (int i = 0; i < methods.length; i++) {
371        if (mf != null && !mf.matches(methods[i])) {
372          continue;
373        }
374        try {
375          mc.doWith(methods[i]);
376        } catch (IllegalAccessException ex) {
377          throw new IllegalStateException(
378              "Shouldn't be illegal to access method '" + methods[i].getName() + "': " + ex);
379        }
380      }
381      targetClass = targetClass.getSuperclass();
382    } while (targetClass != null);
383  }
384
385  /**
386   * Get all declared methods on the leaf class and all superclasses.
387   * Leaf class methods are included first.
388   */
389  public static Method[] getAllDeclaredMethods(Class leafClass) throws IllegalArgumentException {
390    final List list = new LinkedList();
391    doWithMethods(
392        leafClass,
393        new MethodCallback() {
394          @Override
395          public void doWith(Method m) {
396            list.add(m);
397          }
398        });
399    return (Method[]) list.toArray(new Method[list.size()]);
400  }
401
402  /**
403   * Invoke the given callback on all fields in the target class,
404   * going up the class hierarchy to get all declared fields.
405   *
406   * @param targetClass the target class to analyze
407   * @param fc          the callback to invoke for each field
408   */
409  public static void doWithFields(Class targetClass, FieldCallback fc)
410      throws IllegalArgumentException {
411    doWithFields(targetClass, fc, null);
412  }
413
414  /**
415   * Invoke the given callback on all fields in the target class,
416   * going up the class hierarchy to get all declared fields.
417   *
418   * @param targetClass the target class to analyze
419   * @param fc          the callback to invoke for each field
420   * @param ff          the filter that determines the fields to apply the callback to
421   */
422  public static void doWithFields(Class targetClass, FieldCallback fc, FieldFilter ff)
423      throws IllegalArgumentException {
424
425    // Keep backing up the inheritance hierarchy.
426    do {
427      // Copy each field declared on this class unless it's static or file.
428      Field[] fields = targetClass.getDeclaredFields();
429      for (int i = 0; i < fields.length; i++) {
430        // Skip static and final fields.
431        if (ff != null && !ff.matches(fields[i])) {
432          continue;
433        }
434        try {
435          fc.doWith(fields[i]);
436        } catch (IllegalAccessException ex) {
437          throw new IllegalStateException(
438              "Shouldn't be illegal to access field '" + fields[i].getName() + "': " + ex);
439        }
440      }
441      targetClass = targetClass.getSuperclass();
442    } while (targetClass != null && targetClass != Object.class);
443  }
444
445  /**
446   * Given the source object and the destination, which must be the same class
447   * or a subclass, copy all fields, including inherited fields. Designed to
448   * work on objects with public no-arg constructors.
449   *
450   * @throws IllegalArgumentException if the arguments are incompatible
451   */
452  public static void shallowCopyFieldState(final Object src, final Object dest)
453      throws IllegalArgumentException {
454    if (src == null) {
455      throw new IllegalArgumentException("Source for field copy cannot be null");
456    }
457    if (dest == null) {
458      throw new IllegalArgumentException("Destination for field copy cannot be null");
459    }
460    if (!src.getClass().isAssignableFrom(dest.getClass())) {
461      throw new IllegalArgumentException(
462          "Destination class ["
463              + dest.getClass().getName()
464              + "] must be same or subclass as source class ["
465              + src.getClass().getName()
466              + "]");
467    }
468    doWithFields(
469        src.getClass(),
470        new ReflectionUtils.FieldCallback() {
471          @Override
472          public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
473            makeAccessible(field);
474            Object srcValue = field.get(src);
475            field.set(dest, srcValue);
476          }
477        },
478        ReflectionUtils.COPYABLE_FIELDS);
479  }
480
481  /**
482   * Action to take on each method.
483   */
484  public static interface MethodCallback {
485
486    /**
487     * Perform an operation using the given method.
488     *
489     * @param method the method to operate on
490     */
491    void doWith(Method method) throws IllegalArgumentException, IllegalAccessException;
492  }
493
494  /**
495   * Callback optionally used to method fields to be operated on by a method callback.
496   */
497  public static interface MethodFilter {
498
499    /**
500     * Determine whether the given method matches.
501     *
502     * @param method the method to check
503     */
504    boolean matches(Method method);
505  }
506
507  /**
508   * Callback interface invoked on each field in the hierarchy.
509   */
510  public static interface FieldCallback {
511
512    /**
513     * Perform an operation using the given field.
514     *
515     * @param field the field to operate on
516     */
517    void doWith(Field field) throws IllegalArgumentException, IllegalAccessException;
518  }
519
520  /**
521   * Callback optionally used to filter fields to be operated on by a field callback.
522   */
523  public static interface FieldFilter {
524
525    /**
526     * Determine whether the given field matches.
527     *
528     * @param field the field to check
529     */
530    boolean matches(Field field);
531  }
532
533  /**
534   * Pre-built FieldFilter that matches all non-static, non-final fields.
535   */
536  public static FieldFilter COPYABLE_FIELDS =
537      new FieldFilter() {
538        @Override
539        public boolean matches(Field field) {
540          return !(Modifier.isStatic(field.getModifiers())
541              || Modifier.isFinal(field.getModifiers()));
542        }
543      };
544}