001/*
002 * Copyright (c) 2010-2021 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   *
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        String methodName = method.getName();
281        int offset = methodName.startsWith("is") ? 2 : 3;
282        methodName = methodName.substring(offset, offset + 1).toLowerCase() + methodName.substring(offset + 1);
283
284        buffer.append(methodName);
285        buffer.append("=");
286
287        if (!method.isAccessible()) {
288          method.setAccessible(true);
289        }
290
291        // Accessors are guaranteed to take no parameters and return a value
292        buffer.append(method.invoke(object));
293      } catch (Exception e) {
294        throwStateException(method, object.getClass(), e);
295      }
296    }
297
298    buffer.append("]");
299    return buffer.toString();
300  }
301
302  /**
303   * Reflection-based implementation of {@link Object#hashCode()}.
304   * 
305   * @param object
306   *          The object to hash.
307   * @return A hashcode for {@code object}.
308   * @throws IllegalStateException
309   *           If an error occurs while performing reflection operations.
310   */
311  public static int hashCode(Object object) {
312    if (object == null) {
313      return 0;
314    }
315
316    int hashCode = 17;
317
318    for (Method method : getAccessors(object.getClass())) {
319      try {
320        if (!method.isAccessible()) {
321          method.setAccessible(true);
322        }
323
324        Object result = method.invoke(object);
325        if (result != null) {
326          hashCode = hashCode * 31 + result.hashCode();
327        }
328      } catch (Exception e) {
329        throwStateException(method, object, e);
330      }
331    }
332
333    return hashCode;
334  }
335
336  /**
337   * Reflection-based implementation of {@link Object#equals(Object)}.
338   * 
339   * @param object1
340   *          One object to compare.
341   * @param object2
342   *          Another object to compare.
343   * @return {@code true} if the objects are equal, {@code false} otherwise.
344   * @throws IllegalStateException
345   *           If an error occurs while performing reflection operations.
346   */
347  public static boolean equals(Object object1, Object object2) {
348    if (object1 == null && object2 == null) {
349      return true;
350    }
351    if (!(object1 != null && object2 != null)) {
352      return false;
353    }
354
355    // Bail if the classes aren't at least one-way assignable to each other
356    if (!(object1.getClass().isInstance(object2) || object2.getClass().isInstance(object1))) {
357      return false;
358    }
359
360    // Only compare accessors that are present in both classes
361    Set<Method> accessorMethodsIntersection = new HashSet<>(getAccessors(object1.getClass()));
362    accessorMethodsIntersection.retainAll(getAccessors(object2.getClass()));
363
364    for (Method method : accessorMethodsIntersection) {
365      try {
366        if (!method.isAccessible()) {
367          method.setAccessible(true);
368        }
369
370        Object result1 = method.invoke(object1);
371        Object result2 = method.invoke(object2);
372        if (result1 == null && result2 == null) {
373          continue;
374        }
375        if (!(result1 != null && result2 != null)) {
376          return false;
377        }
378        if (!result1.equals(result2)) {
379          return false;
380        }
381      } catch (Exception e) {
382        throwStateException(method, null, e);
383      }
384    }
385
386    return true;
387  }
388
389  /**
390   * Creates a new instance of the given {@code type}.
391   * <p>
392   *
393   *
394   * @param <T>
395   *          Java type to map to.
396   * @param type
397   *          Type token.
398   * @return A new instance of {@code type}.
399   * @throws FacebookJsonMappingException
400   *           If an error occurs when creating a new instance ({@code type} is inaccessible, doesn't have a no-arg
401   *           constructor, etc.)
402   */
403  public static <T> T createInstance(Class<T> type) {
404    String errorMessage = "Unable to create an instance of " + type
405        + ". Please make sure that if it's a nested class, is marked 'static'. "
406        + "It should have a no-argument constructor.";
407
408    try {
409      Constructor<T> defaultConstructor = type.getDeclaredConstructor();
410      ObjectUtil.requireNotNull(defaultConstructor,
411        () -> new FacebookJsonMappingException("Unable to find a default constructor for " + type));
412
413      // Allows protected, private, and package-private constructors to be
414      // invoked
415      defaultConstructor.setAccessible(true);
416      return defaultConstructor.newInstance();
417    } catch (Exception e) {
418      throw new FacebookJsonMappingException(errorMessage, e);
419    }
420  }
421
422  private static void throwStateException(Method method, Object obj, Exception e) {
423    throw new IllegalStateException(
424      "Unable to reflectively invoke " + method + Optional.ofNullable(obj).map(o -> " on " + o).orElse(""), e);
425  }
426
427  /**
428   * A field/annotation pair.
429   * 
430   * @author <a href="http://restfb.com">Mark Allen</a>
431   */
432  public static class FieldWithAnnotation<T extends Annotation> {
433    /**
434     * A field.
435     */
436    private final Field field;
437
438    /**
439     * An annotation on the field.
440     */
441    private final T annotation;
442
443    /**
444     * Creates a field/annotation pair.
445     * 
446     * @param field
447     *          A field.
448     * @param annotation
449     *          An annotation on the field.
450     */
451    public FieldWithAnnotation(Field field, T annotation) {
452      this.field = field;
453      this.annotation = annotation;
454    }
455
456    /**
457     * Gets the field.
458     * 
459     * @return The field.
460     */
461    public Field getField() {
462      return field;
463    }
464
465    /**
466     * Gets the annotation on the field.
467     * 
468     * @return The annotation on the field.
469     */
470    public T getAnnotation() {
471      return annotation;
472    }
473
474    @Override
475    public String toString() {
476      return format("Field %s.%s (%s): %s", field.getDeclaringClass().getName(), field.getName(), field.getType(),
477        annotation);
478    }
479  }
480
481  /**
482   * Cache key composed of a class and annotation pair. Used by {@link ReflectionUtils#FIELDS_WITH_ANNOTATION_CACHE}.
483   * 
484   * @author Igor Kabiljo
485   */
486  private static final class ClassAnnotationCacheKey {
487    /**
488     * Class component of this cache key.
489     */
490    private final Class<?> clazz;
491
492    /**
493     * Annotation component of this cache key.
494     */
495    private final Class<? extends Annotation> annotation;
496
497    /**
498     * Creates a cache key with the given {@code clazz}/@{code annotation} pair.
499     * 
500     * @param clazz
501     *          Class component of this cache key.
502     * @param annotation
503     *          Annotation component of this cache key.
504     */
505    private ClassAnnotationCacheKey(Class<?> clazz, Class<? extends Annotation> annotation) {
506      this.clazz = clazz;
507      this.annotation = annotation;
508    }
509
510    /**
511     * @see java.lang.Object#hashCode()
512     */
513    @Override
514    public int hashCode() {
515      final int prime = 31;
516      int result = 1;
517      result = prime * result + (annotation == null ? 0 : annotation.hashCode());
518      result = prime * result + (clazz == null ? 0 : clazz.hashCode());
519      return result;
520    }
521
522    /**
523     * @see java.lang.Object#equals(java.lang.Object)
524     */
525    @Override
526    public boolean equals(Object obj) {
527      if (this == obj) {
528        return true;
529      }
530      if (obj == null) {
531        return false;
532      }
533      if (getClass() != obj.getClass()) {
534        return false;
535      }
536
537      ClassAnnotationCacheKey other = (ClassAnnotationCacheKey) obj;
538
539      if (annotation == null) {
540        if (other.annotation != null) {
541          return false;
542        }
543      } else if (!annotation.equals(other.annotation)) {
544        return false;
545      }
546
547      if (clazz == null) {
548        return other.clazz == null;
549      } else
550        return clazz.equals(other.clazz);
551    }
552  }
553}