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