001/**
002 * Copyright (c) 2010-2019 Mark Allen, Norbert Bartels.
003 *
004 * Permission is hereby granted, free of charge, to any person obtaining a copy
005 * of this software and associated documentation files (the "Software"), to deal
006 * in the Software without restriction, including without limitation the rights
007 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
008 * copies of the Software, and to permit persons to whom the Software is
009 * furnished to do so, subject to the following conditions:
010 *
011 * The above copyright notice and this permission notice shall be included in
012 * all copies or substantial portions of the Software.
013 *
014 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
015 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
016 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
017 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
018 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
019 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
020 * THE SOFTWARE.
021 */
022package com.restfb.util;
023
024import com.restfb.exception.FacebookJsonMappingException;
025
026import static java.lang.String.format;
027import static java.util.Collections.*;
028
029import java.lang.annotation.Annotation;
030import java.lang.reflect.*;
031import java.util.*;
032
033/**
034 * A collection of reflection-related utility methods.
035 * 
036 * @author <a href="http://restfb.com">Mark Allen</a>
037 * @author Igor Kabiljo
038 * @author Scott Hernandez
039 * @since 1.6
040 */
041public final class ReflectionUtils {
042  /**
043   * In-memory shared cache of reflection data for {@link #findFieldsWithAnnotation(Class, Class)}.
044   */
045  private static final Map<ClassAnnotationCacheKey, List<?>> FIELDS_WITH_ANNOTATION_CACHE =
046      synchronizedMap(new HashMap<ClassAnnotationCacheKey, List<?>>());
047
048  /**
049   * In-memory shared cache of reflection data for {@link #findMethodsWithAnnotation(Class, Class)}.
050   */
051  private static final Map<ClassAnnotationCacheKey, List<Method>> METHODS_WITH_ANNOTATION_CACHE =
052      synchronizedMap(new HashMap<ClassAnnotationCacheKey, List<Method>>());
053
054  /**
055   * Prevents instantiation.
056   */
057  private ReflectionUtils() {
058    // prevent instantiation
059  }
060
061  /**
062   * Is the given {@code object} a primitive type or wrapper for a primitive type?
063   * 
064   * @param object
065   *          The object to check for primitive-ness.
066   * @return {@code true} if {@code object} is a primitive type or wrapper for a primitive type, {@code false}
067   *         otherwise.
068   */
069  public static boolean isPrimitive(Object object) {
070    if (object == null) {
071      return false;
072    }
073
074    Class<?> type = object.getClass();
075
076    return object instanceof String || (object instanceof Integer || Integer.TYPE.equals(type))
077        || (object instanceof Boolean || Boolean.TYPE.equals(type))
078        || (object instanceof Long || Long.TYPE.equals(type)) || (object instanceof Double || Double.TYPE.equals(type))
079        || (object instanceof Float || Float.TYPE.equals(type)) || (object instanceof Byte || Byte.TYPE.equals(type))
080        || (object instanceof Short || Short.TYPE.equals(type))
081        || (object instanceof Character || Character.TYPE.equals(type));
082  }
083
084  /**
085   * Finds fields on the given {@code type} and all of its superclasses annotated with annotations of type
086   * {@code annotationType}.
087   * 
088   * @param <T>
089   *          The annotation type.
090   * @param type
091   *          The target type token.
092   * @param annotationType
093   *          The annotation type token.
094   * @return A list of field/annotation pairs.
095   */
096  public static <T extends Annotation> List<FieldWithAnnotation<T>> findFieldsWithAnnotation(Class<?> type,
097      Class<T> annotationType) {
098    ClassAnnotationCacheKey cacheKey = new ClassAnnotationCacheKey(type, annotationType);
099
100    @SuppressWarnings("unchecked")
101    List<FieldWithAnnotation<T>> cachedResults =
102        (List<FieldWithAnnotation<T>>) FIELDS_WITH_ANNOTATION_CACHE.get(cacheKey);
103
104    if (cachedResults != null) {
105      return cachedResults;
106    }
107
108    List<FieldWithAnnotation<T>> fieldsWithAnnotation = new ArrayList<>();
109
110    // Walk all superclasses looking for annotated fields until we hit Object
111    while (!Object.class.equals(type) && type != null) {
112      for (Field field : type.getDeclaredFields()) {
113        T annotation = field.getAnnotation(annotationType);
114        if (annotation != null) {
115          fieldsWithAnnotation.add(new FieldWithAnnotation<>(field, annotation));
116        }
117
118      }
119
120      type = type.getSuperclass();
121    }
122
123    fieldsWithAnnotation = unmodifiableList(fieldsWithAnnotation);
124    FIELDS_WITH_ANNOTATION_CACHE.put(cacheKey, fieldsWithAnnotation);
125    return fieldsWithAnnotation;
126  }
127
128  /**
129   * Finds methods on the given {@code type} and all of its superclasses annotated with annotations of type
130   * {@code annotationType}.
131   * <p>
132   * These results are cached to mitigate performance overhead.
133   * 
134   * @param <T>
135   *          The annotation type.
136   * @param type
137   *          The target type token.
138   * @param annotationType
139   *          The annotation type token.
140   * @return A list of methods with the given annotation.
141   * @since 1.6.11
142   */
143  public static <T extends Annotation> List<Method> findMethodsWithAnnotation(Class<?> type, Class<T> annotationType) {
144    ClassAnnotationCacheKey cacheKey = new ClassAnnotationCacheKey(type, annotationType);
145    List<Method> cachedResults = METHODS_WITH_ANNOTATION_CACHE.get(cacheKey);
146
147    if (cachedResults != null) {
148      return cachedResults;
149    }
150
151    List<Method> methodsWithAnnotation = new ArrayList<>();
152
153    // Walk all superclasses looking for annotated methods until we hit Object
154    while (!Object.class.equals(type)) {
155      for (Method method : type.getDeclaredMethods()) {
156        T annotation = method.getAnnotation(annotationType);
157
158        if (annotation != null) {
159          methodsWithAnnotation.add(method);
160        }
161      }
162
163      type = type.getSuperclass();
164    }
165
166    methodsWithAnnotation = unmodifiableList(methodsWithAnnotation);
167    METHODS_WITH_ANNOTATION_CACHE.put(cacheKey, methodsWithAnnotation);
168    return methodsWithAnnotation;
169  }
170
171  /**
172   * For a given {@code field}, get its first parameterized type argument.
173   * <p>
174   * For example, a field of type {@code List<Long>} would have a first type argument of {@code Long.class}.
175   * <p>
176   * If the field has no type arguments, {@code null} is returned.
177   * 
178   * @param field
179   *          The field to check.
180   * @return The field's first parameterized type argument, or {@code null} if none exists.
181   */
182  public static Class<?> getFirstParameterizedTypeArgument(Field field) {
183    return getParameterizedTypeArgument(field, 0);
184  }
185
186  /**
187   * For a given {@code field}, get its second parameterized type argument.
188   *
189   * If the field has no type arguments, {@code null} is returned.
190   *
191   * @param field
192   *          The field to check.
193   * @return The field's second parameterized type argument, or {@code null} if none exists.
194   */
195  public static Class<?> getSecondParameterizedTypeArgument(Field field) {
196    return getParameterizedTypeArgument(field, 1);
197  }
198
199  private static Class<?> getParameterizedTypeArgument(Field field, int i) {
200    Type type = field.getGenericType();
201    if (!(type instanceof ParameterizedType)) {
202      return null;
203    }
204
205    ParameterizedType parameterizedType = (ParameterizedType) type;
206    Type firstTypeArgument = parameterizedType.getActualTypeArguments()[i];
207    return (firstTypeArgument instanceof Class) ? (Class<?>) firstTypeArgument : null;
208  }
209
210  /**
211   * Gets all accessor methods for the given {@code clazz}.
212   * 
213   * @param clazz
214   *          The class for which accessors are extracted.
215   * @return All accessor methods for the given {@code clazz}.
216   */
217  public static List<Method> getAccessors(Class<?> clazz) {
218    if (clazz == null) {
219      throw new IllegalArgumentException("The 'clazz' parameter cannot be null.");
220    }
221
222    List<Method> methods = new ArrayList<>();
223    for (Method method : clazz.getMethods()) {
224      String methodName = method.getName();
225      if (!"getClass".equals(methodName) && !"hashCode".equals(methodName) && method.getReturnType() != null
226          && !Void.class.equals(method.getReturnType()) && method.getParameterTypes().length == 0
227          && ((methodName.startsWith("get") && methodName.length() > 3)
228              || (methodName.startsWith("is") && methodName.length() > 2)
229              || (methodName.startsWith("has") && methodName.length() > 3))) {
230        methods.add(method);
231      }
232    }
233
234    // Order the methods alphabetically by name
235    sort(methods, new Comparator<Method>() {
236      /**
237       * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
238       */
239      @Override
240      public int compare(Method method1, Method method2) {
241        return method1.getName().compareTo(method2.getName());
242      }
243    });
244
245    return unmodifiableList(methods);
246  }
247
248  /**
249   * Reflection-based implementation of {@link Object#toString()}.
250   * 
251   * @param object
252   *          The object to convert to a string representation.
253   * @return A string representation of {@code object}.
254   * @throws IllegalStateException
255   *           If an error occurs while performing reflection operations.
256   */
257  public static String toString(Object object) {
258    StringBuilder buffer = new StringBuilder(object.getClass().getSimpleName());
259    buffer.append("[");
260
261    boolean first = true;
262
263    for (Method method : getAccessors(object.getClass())) {
264      if (first) {
265        first = false;
266      } else {
267        buffer.append(" ");
268      }
269
270      try {
271        String methodName = method.getName();
272        int offset = methodName.startsWith("is") ? 2 : 3;
273        methodName = methodName.substring(offset, offset + 1).toLowerCase() + methodName.substring(offset + 1);
274
275        buffer.append(methodName);
276        buffer.append("=");
277
278        if (!method.isAccessible()) {
279          method.setAccessible(true);
280        }
281
282        // Accessors are guaranteed to take no parameters and return a value
283        buffer.append(method.invoke(object));
284      } catch (Exception e) {
285        throw new IllegalStateException("Unable to reflectively invoke " + method + " on " + object.getClass(), e);
286      }
287    }
288
289    buffer.append("]");
290    return buffer.toString();
291  }
292
293  /**
294   * Reflection-based implementation of {@link Object#hashCode()}.
295   * 
296   * @param object
297   *          The object to hash.
298   * @return A hashcode for {@code object}.
299   * @throws IllegalStateException
300   *           If an error occurs while performing reflection operations.
301   */
302  public static int hashCode(Object object) {
303    if (object == null) {
304      return 0;
305    }
306
307    int hashCode = 17;
308
309    for (Method method : getAccessors(object.getClass())) {
310      try {
311        if (!method.isAccessible()) {
312          method.setAccessible(true);
313        }
314
315        Object result = method.invoke(object);
316        if (result != null) {
317          hashCode = hashCode * 31 + result.hashCode();
318        }
319      } catch (Exception e) {
320        throw new IllegalStateException("Unable to reflectively invoke " + method + " on " + object, e);
321      }
322    }
323
324    return hashCode;
325  }
326
327  /**
328   * Reflection-based implementation of {@link Object#equals(Object)}.
329   * 
330   * @param object1
331   *          One object to compare.
332   * @param object2
333   *          Another object to compare.
334   * @return {@code true} if the objects are equal, {@code false} otherwise.
335   * @throws IllegalStateException
336   *           If an error occurs while performing reflection operations.
337   */
338  public static boolean equals(Object object1, Object object2) {
339    if (object1 == null && object2 == null) {
340      return true;
341    }
342    if (!(object1 != null && object2 != null)) {
343      return false;
344    }
345
346    // Bail if the classes aren't at least one-way assignable to each other
347    if (!(object1.getClass().isInstance(object2) || object2.getClass().isInstance(object1))) {
348      return false;
349    }
350
351    // Only compare accessors that are present in both classes
352    Set<Method> accessorMethodsIntersection = new HashSet<>(getAccessors(object1.getClass()));
353    accessorMethodsIntersection.retainAll(getAccessors(object2.getClass()));
354
355    for (Method method : accessorMethodsIntersection) {
356      try {
357        if (!method.isAccessible()) {
358          method.setAccessible(true);
359        }
360
361        Object result1 = method.invoke(object1);
362        Object result2 = method.invoke(object2);
363        if (result1 == null && result2 == null) {
364          continue;
365        }
366        if (!(result1 != null && result2 != null)) {
367          return false;
368        }
369        if (!result1.equals(result2)) {
370          return false;
371        }
372      } catch (Exception e) {
373        throw new IllegalStateException("Unable to reflectively invoke " + method, e);
374      }
375    }
376
377    return true;
378  }
379
380  /**
381   * Creates a new instance of the given {@code type}.
382   * <p>
383   *
384   *
385   * @param <T>
386   *          Java type to map to.
387   * @param type
388   *          Type token.
389   * @return A new instance of {@code type}.
390   * @throws FacebookJsonMappingException
391   *           If an error occurs when creating a new instance ({@code type} is inaccessible, doesn't have a no-arg
392   *           constructor, etc.)
393   */
394  public static <T> T createInstance(Class<T> type) {
395    String errorMessage = "Unable to create an instance of " + type
396            + ". Please make sure that if it's a nested class, is marked 'static'. "
397            + "It should have a no-argument constructor.";
398
399    try {
400      Constructor<T> defaultConstructor = type.getDeclaredConstructor();
401
402      if (defaultConstructor == null) {
403        throw new FacebookJsonMappingException("Unable to find a default constructor for " + type);
404      }
405
406      // Allows protected, private, and package-private constructors to be
407      // invoked
408      defaultConstructor.setAccessible(true);
409      return defaultConstructor.newInstance();
410    } catch (Exception e) {
411      throw new FacebookJsonMappingException(errorMessage, e);
412    }
413  }
414
415  /**
416   * A field/annotation pair.
417   * 
418   * @author <a href="http://restfb.com">Mark Allen</a>
419   */
420  public static class FieldWithAnnotation<T extends Annotation> {
421    /**
422     * A field.
423     */
424    private Field field;
425
426    /**
427     * An annotation on the field.
428     */
429    private T annotation;
430
431    /**
432     * Creates a field/annotation pair.
433     * 
434     * @param field
435     *          A field.
436     * @param annotation
437     *          An annotation on the field.
438     */
439    public FieldWithAnnotation(Field field, T annotation) {
440      this.field = field;
441      this.annotation = annotation;
442    }
443
444    /**
445     * Gets the field.
446     * 
447     * @return The field.
448     */
449    public Field getField() {
450      return field;
451    }
452
453    /**
454     * Gets the annotation on the field.
455     * 
456     * @return The annotation on the field.
457     */
458    public T getAnnotation() {
459      return annotation;
460    }
461
462    @Override
463    public String toString() {
464      return format("Field %s.%s (%s): %s", field.getDeclaringClass().getName(), field.getName(), field.getType(),
465        annotation);
466    }
467  }
468
469  /**
470   * Cache key composed of a class and annotation pair. Used by {@link ReflectionUtils#FIELDS_WITH_ANNOTATION_CACHE}.
471   * 
472   * @author Igor Kabiljo
473   */
474  private static final class ClassAnnotationCacheKey {
475    /**
476     * Class component of this cache key.
477     */
478    private final Class<?> clazz;
479
480    /**
481     * Annotation component of this cache key.
482     */
483    private final Class<? extends Annotation> annotation;
484
485    /**
486     * Creates a cache key with the given {@code clazz}/@{code annotation} pair.
487     * 
488     * @param clazz
489     *          Class component of this cache key.
490     * @param annotation
491     *          Annotation component of this cache key.
492     */
493    private ClassAnnotationCacheKey(Class<?> clazz, Class<? extends Annotation> annotation) {
494      this.clazz = clazz;
495      this.annotation = annotation;
496    }
497
498    /**
499     * @see java.lang.Object#hashCode()
500     */
501    @Override
502    public int hashCode() {
503      final int prime = 31;
504      int result = 1;
505      result = prime * result + (annotation == null ? 0 : annotation.hashCode());
506      result = prime * result + (clazz == null ? 0 : clazz.hashCode());
507      return result;
508    }
509
510    /**
511     * @see java.lang.Object#equals(java.lang.Object)
512     */
513    @Override
514    public boolean equals(Object obj) {
515      if (this == obj) {
516        return true;
517      }
518      if (obj == null) {
519        return false;
520      }
521      if (getClass() != obj.getClass()) {
522        return false;
523      }
524
525      ClassAnnotationCacheKey other = (ClassAnnotationCacheKey) obj;
526
527      if (annotation == null) {
528        if (other.annotation != null) {
529          return false;
530        }
531      } else if (!annotation.equals(other.annotation)) {
532        return false;
533      }
534
535      if (clazz == null) {
536        if (other.clazz != null) {
537          return false;
538        }
539      } else if (!clazz.equals(other.clazz)) {
540        return false;
541      }
542
543      return true;
544    }
545  }
546}