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}