001/*
002 * Copyright (c) 2010-2025 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  /**
220   * Retrieves the {@code i}-th parameterized type argument from the given {@code type}.
221   * <p>
222   * If the type is not parameterized or the index is out of bounds, {@code null} is returned.
223   *
224   * @param type
225   *          The generic type to inspect.
226   * @param i
227   *          The index of the parameterized type argument to retrieve (zero-based).
228   * @return The {@code i}-th parameterized type argument of the given type, or {@code null} if none exists.
229   */
230  public static Type getParameterizedTypeArgument(Type type, int i) {
231    if (!(type instanceof ParameterizedType)) return null;
232
233    Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments();
234
235    if (i < 0 || i >= typeArguments.length) return null;
236
237    return typeArguments[i];
238  }
239
240  private static Class<?> getParameterizedTypeArgument(Field field, int i) {
241    Type firstTypeArgument = getParameterizedTypeArgument(field.getGenericType(), i);
242
243    return (firstTypeArgument instanceof Class) ? (Class<?>) firstTypeArgument : null;
244  }
245
246  /**
247   * Gets all accessor methods for the given {@code clazz}.
248   * 
249   * @param clazz
250   *          The class for which accessors are extracted.
251   * @return All accessor methods for the given {@code clazz}.
252   */
253  public static List<Method> getAccessors(Class<?> clazz) {
254    ObjectUtil.requireNotNull(clazz, () -> new IllegalArgumentException("The 'clazz' parameter cannot be null."));
255
256    List<Method> methods = new ArrayList<>();
257    for (Method method : clazz.getMethods()) {
258      String methodName = method.getName();
259      if (!"getClass".equals(methodName) && !"hashCode".equals(methodName) && method.getReturnType() != null
260          && !Void.class.equals(method.getReturnType()) && method.getParameterTypes().length == 0
261          && ((methodName.startsWith("get") && methodName.length() > 3)
262              || (methodName.startsWith("is") && methodName.length() > 2)
263              || (methodName.startsWith("has") && methodName.length() > 3))) {
264        methods.add(method);
265      }
266    }
267
268    methods.sort(Comparator.comparing(Method::getName));
269
270    return unmodifiableList(methods);
271  }
272
273  /**
274   * Reflection-based implementation of {@link Object#toString()}.
275   * 
276   * @param object
277   *          The object to convert to a string representation.
278   * @return A string representation of {@code object}.
279   * @throws IllegalStateException
280   *           If an error occurs while performing reflection operations.
281   */
282  public static String toString(Object object) {
283    StringBuilder buffer = new StringBuilder(object.getClass().getSimpleName());
284    buffer.append("[");
285
286    boolean first = true;
287
288    for (Method method : getAccessors(object.getClass())) {
289      if (first) {
290        first = false;
291      } else {
292        buffer.append(" ");
293      }
294
295      try {
296        buffer.append(getMethodName(method));
297        buffer.append("=");
298
299        makeMethodAccessible(method);
300
301        // Accessors are guaranteed to take no parameters and return a value
302        buffer.append(method.invoke(object));
303      } catch (Exception e) {
304        throwStateException(method, object.getClass(), e);
305      }
306    }
307
308    buffer.append("]");
309    return buffer.toString();
310  }
311
312  private static String getMethodName(Method method) {
313    String methodName = method.getName();
314    int offset = methodName.startsWith("is") ? 2 : 3;
315    methodName = methodName.substring(offset, offset + 1).toLowerCase() + methodName.substring(offset + 1);
316    return methodName;
317  }
318
319  /**
320   * Reflection-based implementation of {@link Object#hashCode()}.
321   * 
322   * @param object
323   *          The object to hash.
324   * @return A hashcode for {@code object}.
325   * @throws IllegalStateException
326   *           If an error occurs while performing reflection operations.
327   */
328  public static int hashCode(Object object) {
329    if (object == null) {
330      return 0;
331    }
332
333    int hashCode = 17;
334
335    for (Method method : getAccessors(object.getClass())) {
336      try {
337        makeMethodAccessible(method);
338
339        Object result = method.invoke(object);
340        if (result != null) {
341          hashCode = hashCode * 31 + result.hashCode();
342        }
343      } catch (Exception e) {
344        throwStateException(method, object, e);
345      }
346    }
347
348    return hashCode;
349  }
350
351  /**
352   * Reflection-based implementation of {@link Object#equals(Object)}.
353   * 
354   * @param object1
355   *          One object to compare.
356   * @param object2
357   *          Another object to compare.
358   * @return {@code true} if the objects are equal, {@code false} otherwise.
359   * @throws IllegalStateException
360   *           If an error occurs while performing reflection operations.
361   */
362  public static boolean equals(Object object1, Object object2) {
363    if (object1 == null && object2 == null) {
364      return true;
365    }
366    if (!(object1 != null && object2 != null)) {
367      return false;
368    }
369
370    // Bail if the classes aren't at least one-way assignable to each other
371    if (!(object1.getClass().isInstance(object2) || object2.getClass().isInstance(object1))) {
372      return false;
373    }
374
375    // Only compare accessors that are present in both classes
376    Set<Method> accessorMethodsIntersection = new HashSet<>(getAccessors(object1.getClass()));
377    accessorMethodsIntersection.retainAll(getAccessors(object2.getClass()));
378
379    for (Method method : accessorMethodsIntersection) {
380      try {
381        makeMethodAccessible(method);
382
383        Object result1 = method.invoke(object1);
384        Object result2 = method.invoke(object2);
385        if (result1 == null && result2 == null) {
386          continue;
387        }
388        if (!(result1 != null && result2 != null)) {
389          return false;
390        }
391        if (!result1.equals(result2)) {
392          return false;
393        }
394      } catch (Exception e) {
395        throwStateException(method, null, e);
396      }
397    }
398
399    return true;
400  }
401
402  private static void makeMethodAccessible(Method method) {
403    if (!method.isAccessible()) {
404      method.setAccessible(true);
405    }
406  }
407
408  /**
409   * Creates a new instance of the given {@code type}.
410   * <p>
411   *
412   *
413   * @param <T>
414   *          Java type to map to.
415   * @param type
416   *          Type token.
417   * @return A new instance of {@code type}.
418   * @throws FacebookJsonMappingException
419   *           If an error occurs when creating a new instance ({@code type} is inaccessible, doesn't have a no-arg
420   *           constructor, etc.)
421   */
422  public static <T> T createInstance(Class<T> type) {
423    String errorMessage = "Unable to create an instance of " + type
424        + ". Please make sure that if it's a nested class, is marked 'static'. "
425        + "It should have a no-argument constructor.";
426
427    try {
428      Constructor<T> defaultConstructor = type.getDeclaredConstructor();
429      ObjectUtil.requireNotNull(defaultConstructor,
430        () -> new FacebookJsonMappingException("Unable to find a default constructor for " + type));
431
432      // Allows protected, private, and package-private constructors to be
433      // invoked
434      defaultConstructor.setAccessible(true);
435      return defaultConstructor.newInstance();
436    } catch (Exception e) {
437      throw new FacebookJsonMappingException(errorMessage, e);
438    }
439  }
440
441  private static void throwStateException(Method method, Object obj, Exception e) {
442    throw new IllegalStateException(
443      "Unable to reflectively invoke " + method + Optional.ofNullable(obj).map(o -> " on " + o).orElse(""), e);
444  }
445
446  /**
447   * A field/annotation pair.
448   * 
449   * @author <a href="http://restfb.com">Mark Allen</a>
450   */
451  public static class FieldWithAnnotation<T extends Annotation> {
452    /**
453     * A field.
454     */
455    private final Field field;
456
457    /**
458     * An annotation on the field.
459     */
460    private final T annotation;
461
462    /**
463     * Creates a field/annotation pair.
464     * 
465     * @param field
466     *          A field.
467     * @param annotation
468     *          An annotation on the field.
469     */
470    public FieldWithAnnotation(Field field, T annotation) {
471      this.field = field;
472      this.annotation = annotation;
473    }
474
475    /**
476     * Gets the field.
477     * 
478     * @return The field.
479     */
480    public Field getField() {
481      return field;
482    }
483
484    /**
485     * Gets the annotation on the field.
486     * 
487     * @return The annotation on the field.
488     */
489    public T getAnnotation() {
490      return annotation;
491    }
492
493    @Override
494    public String toString() {
495      return format("Field %s.%s (%s): %s", field.getDeclaringClass().getName(), field.getName(), field.getType(),
496        annotation);
497    }
498  }
499
500  /**
501   * Cache key composed of a class and annotation pair. Used by {@link ReflectionUtils#FIELDS_WITH_ANNOTATION_CACHE}.
502   * 
503   * @author Igor Kabiljo
504   */
505  private static final class ClassAnnotationCacheKey {
506    /**
507     * Class component of this cache key.
508     */
509    private final Class<?> clazz;
510
511    /**
512     * Annotation component of this cache key.
513     */
514    private final Class<? extends Annotation> annotation;
515
516    /**
517     * Creates a cache key with the given {@code clazz}/@{code annotation} pair.
518     * 
519     * @param clazz
520     *          Class component of this cache key.
521     * @param annotation
522     *          Annotation component of this cache key.
523     */
524    private ClassAnnotationCacheKey(Class<?> clazz, Class<? extends Annotation> annotation) {
525      this.clazz = clazz;
526      this.annotation = annotation;
527    }
528
529    /**
530     * @see java.lang.Object#hashCode()
531     */
532    @Override
533    public int hashCode() {
534      final int prime = 31;
535      int result = 1;
536      result = prime * result + (annotation == null ? 0 : annotation.hashCode());
537      result = prime * result + (clazz == null ? 0 : clazz.hashCode());
538      return result;
539    }
540
541    /**
542     * @see java.lang.Object#equals(java.lang.Object)
543     */
544    @Override
545    public boolean equals(Object obj) {
546      if (this == obj) {
547        return true;
548      }
549      if (obj == null) {
550        return false;
551      }
552      if (getClass() != obj.getClass()) {
553        return false;
554      }
555
556      ClassAnnotationCacheKey other = (ClassAnnotationCacheKey) obj;
557
558      if (annotation == null) {
559        if (other.annotation != null) {
560          return false;
561        }
562      } else if (!annotation.equals(other.annotation)) {
563        return false;
564      }
565
566      if (clazz == null) {
567        return other.clazz == null;
568      } else
569        return clazz.equals(other.clazz);
570    }
571  }
572}