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}