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