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