1 package org.robolectric.util; 2 3 import java.lang.annotation.Annotation; 4 import java.lang.reflect.Constructor; 5 import java.lang.reflect.Field; 6 import java.lang.reflect.InvocationTargetException; 7 import java.lang.reflect.Method; 8 import java.lang.reflect.Modifier; 9 import java.lang.reflect.Proxy; 10 import java.util.Collections; 11 import java.util.HashMap; 12 import java.util.Map; 13 import java.util.Optional; 14 import javax.annotation.Nullable; 15 import org.robolectric.annotation.ClassName; 16 17 /** Collection of helper methods for calling methods and accessing fields reflectively. */ 18 @SuppressWarnings(value = {"unchecked", "TypeParameterUnusedInFormals", "NewApi"}) 19 public class ReflectionHelpers { 20 21 private static final Map<String, Object> PRIMITIVE_RETURN_VALUES; 22 private static final PerfStatsCollector perfStatsCollector = PerfStatsCollector.getInstance(); 23 24 static { 25 HashMap<String, Object> map = new HashMap<>(); 26 map.put("boolean", Boolean.FALSE); 27 map.put("int", 0); 28 map.put("long", (long) 0); 29 map.put("float", (float) 0); 30 map.put("double", (double) 0); 31 map.put("short", (short) 0); 32 map.put("byte", (byte) 0); 33 PRIMITIVE_RETURN_VALUES = Collections.unmodifiableMap(map); 34 } 35 36 /** 37 * Create a proxy for the given class which returns default values for every method call. 38 * 39 * <p>0 will be returned for any primitive return types, otherwise null will be returned. 40 * 41 * @param clazz the class to provide a proxy instance of. 42 * @return a new "Null Proxy" instance of the given class. 43 */ createNullProxy(Class<T> clazz)44 public static <T> T createNullProxy(Class<T> clazz) { 45 return (T) 46 Proxy.newProxyInstance( 47 clazz.getClassLoader(), 48 new Class<?>[] {clazz}, 49 (proxy, method, args) -> PRIMITIVE_RETURN_VALUES.get(method.getReturnType().getName())); 50 } 51 52 /** 53 * Create a proxy for the given class which returns other deep proxies from all it's methods. 54 * 55 * <p>The returned object will be an instance of the given class, but all methods will return 56 * either the "default" value for primitives, or another deep proxy for non-primitive types. 57 * 58 * <p>This should be used rarely, for cases where we need to create deep proxies in order not to 59 * crash. The inner proxies are impossible to configure, so there is no way to create meaningful 60 * behavior from a deep proxy. It serves mainly to prevent Null Pointer Exceptions. 61 * 62 * @param clazz the class to provide a proxy instance of. 63 * @return a new "Deep Proxy" instance of the given class. 64 */ createDeepProxy(Class<T> clazz)65 public static <T> T createDeepProxy(Class<T> clazz) { 66 return (T) 67 Proxy.newProxyInstance( 68 clazz.getClassLoader(), 69 new Class[] {clazz}, 70 (proxy, method, args) -> { 71 if (PRIMITIVE_RETURN_VALUES.containsKey(method.getReturnType().getName())) { 72 return PRIMITIVE_RETURN_VALUES.get(method.getReturnType().getName()); 73 } else if (method.getReturnType().isInterface()) { 74 return createDeepProxy(method.getReturnType()); 75 } else { 76 return null; 77 } 78 }); 79 } 80 81 /** 82 * Create a proxy for the given class which can delegate method calls to another object. 83 * 84 * <p>If the delegate has no methods whose signature matches, a null (or 0 for primitive types) 85 * return value will be returned. 86 * 87 * @param clazz the class to provide a proxy instance of. 88 * @param delegate the object to delegate matching method calls to. A 'matching method' must have 89 * exactlu the same method name and parameter class names as the desired method. 90 * The @ClassName annotation can be applied to provide a custom class name. 91 * @return a new "Delegating Proxy" instance of the given class. 92 */ 93 public static <T> T createDelegatingProxy(Class<T> clazz, final Object delegate) { 94 final Class delegateClass = delegate.getClass(); 95 return (T) 96 Proxy.newProxyInstance( 97 clazz.getClassLoader(), 98 new Class[] {clazz}, 99 (proxy, method, args) -> { 100 try { 101 Method delegateMethod = 102 findDelegateMethod(delegateClass, method.getName(), method.getParameterTypes()); 103 delegateMethod.setAccessible(true); 104 return delegateMethod.invoke(delegate, args); 105 } catch (NoSuchMethodException e) { 106 return PRIMITIVE_RETURN_VALUES.get(method.getReturnType().getName()); 107 } catch (InvocationTargetException e) { 108 // Required to propagate the correct throwable. 109 throw e.getTargetException(); 110 } 111 }); 112 } 113 114 private static Method findDelegateMethod( 115 Class<?> delegateClass, String methodName, Class<?>[] parameterTypes) 116 throws NoSuchMethodException { 117 for (Method delegateMethod : delegateClass.getMethods()) { 118 if (delegateMethod.getName().equals(methodName) 119 && Modifier.isPublic(delegateMethod.getModifiers()) 120 && parametersMatch( 121 parameterTypes, 122 delegateMethod.getParameterTypes(), 123 delegateMethod.getParameterAnnotations())) { 124 return delegateMethod; 125 } 126 } 127 throw new NoSuchMethodException(); 128 } 129 130 private static boolean parametersMatch( 131 Class<?>[] parameterTypes, 132 Class<?>[] delegateParameterTypes, 133 Annotation[][] parameterAnnotations) { 134 if (parameterTypes.length != delegateParameterTypes.length) { 135 return false; 136 } 137 for (int i = 0; i < parameterTypes.length; i++) { 138 if (!parameterTypes[i].getName().equals(delegateParameterTypes[i].getName()) 139 && !parameterTypes[i] 140 .getName() 141 .equals(getClassNameFromAnnotation(parameterAnnotations[i]))) { 142 return false; 143 } 144 } 145 return true; 146 } 147 148 @Nullable 149 private static String getClassNameFromAnnotation(Annotation[] parameterAnnotations) { 150 for (Annotation annotation : parameterAnnotations) { 151 if (annotation.annotationType().equals(ClassName.class)) { 152 return ((ClassName) annotation).value(); 153 } 154 } 155 return null; 156 } 157 158 public static <A extends Annotation> A defaultsFor(Class<A> annotation) { 159 return annotation.cast( 160 Proxy.newProxyInstance( 161 annotation.getClassLoader(), 162 new Class[] {annotation}, 163 (proxy, method, args) -> method.getDefaultValue())); 164 } 165 166 /** 167 * Reflectively get the value of a field. 168 * 169 * @param object Target object. 170 * @param fieldName The field name. 171 * @param <R> The return type. 172 * @return Value of the field on the object. 173 */ 174 @SuppressWarnings("unchecked") 175 public static <R> R getField(final Object object, final String fieldName) { 176 try { 177 return traverseClassHierarchy( 178 object.getClass(), 179 NoSuchFieldException.class, 180 traversalClass -> { 181 Field field = traversalClass.getDeclaredField(fieldName); 182 field.setAccessible(true); 183 return (R) field.get(object); 184 }); 185 } catch (Exception e) { 186 throw new RuntimeException(e); 187 } 188 } 189 190 /** 191 * Reflectively set the value of a field. 192 * 193 * @param object Target object. 194 * @param fieldName The field name. 195 * @param fieldNewValue New value. 196 */ 197 public static void setField( 198 final Object object, final String fieldName, final Object fieldNewValue) { 199 try { 200 traverseClassHierarchy( 201 object.getClass(), 202 NoSuchFieldException.class, 203 (InsideTraversal<Void>) 204 traversalClass -> { 205 Field field = traversalClass.getDeclaredField(fieldName); 206 field.setAccessible(true); 207 field.set(object, fieldNewValue); 208 return null; 209 }); 210 } catch (Exception e) { 211 throw new RuntimeException(e); 212 } 213 } 214 215 /** 216 * Reflectively set the value of a field. 217 * 218 * @param type Target type. 219 * @param object Target object. 220 * @param fieldName The field name. 221 * @param fieldNewValue New value. 222 */ 223 public static void setField( 224 Class<?> type, final Object object, final String fieldName, final Object fieldNewValue) { 225 try { 226 Field field = type.getDeclaredField(fieldName); 227 field.setAccessible(true); 228 field.set(object, fieldNewValue); 229 } catch (Exception e) { 230 throw new RuntimeException(e); 231 } 232 } 233 234 /** 235 * Reflectively check if a class has a given field (static or non static). 236 * 237 * @param clazz Target class. 238 * @param fieldName The field name. 239 * @return boolean to indicate whether the field exists or not in clazz. 240 */ 241 public static boolean hasField(Class<?> clazz, String fieldName) { 242 try { 243 Field field = clazz.getDeclaredField(fieldName); 244 return (field != null); 245 } catch (NoSuchFieldException e) { 246 return false; 247 } 248 } 249 250 /** 251 * Reflectively get the value of a static field. 252 * 253 * @param field Field object. 254 * @param <R> The return type. 255 * @return Value of the field. 256 */ 257 @SuppressWarnings("unchecked") 258 public static <R> R getStaticField(Field field) { 259 try { 260 field.setAccessible(true); 261 return (R) field.get(null); 262 } catch (Exception e) { 263 throw new RuntimeException(e); 264 } 265 } 266 267 /** 268 * Reflectively get the value of a static field. 269 * 270 * @param clazz Target class. 271 * @param fieldName The field name. 272 * @param <R> The return type. 273 * @return Value of the field. 274 */ 275 public static <R> R getStaticField(Class<?> clazz, String fieldName) { 276 try { 277 return getStaticField(clazz.getDeclaredField(fieldName)); 278 } catch (Exception e) { 279 throw new RuntimeException(e); 280 } 281 } 282 283 /** 284 * Reflectively set the value of a static field. 285 * 286 * @param field Field object. 287 * @param fieldNewValue The new value. 288 */ 289 public static void setStaticField(Field field, Object fieldNewValue) { 290 try { 291 if ((field.getModifiers() & Modifier.FINAL) == Modifier.FINAL) { 292 throw new IllegalArgumentException("Cannot set the value of final field " + field); 293 } 294 field.setAccessible(true); 295 field.set(null, fieldNewValue); 296 } catch (Exception e) { 297 throw new RuntimeException(e); 298 } 299 } 300 301 /** 302 * Reflectively set the value of a static field. 303 * 304 * @param clazz Target class. 305 * @param fieldName The field name. 306 * @param fieldNewValue The new value. 307 */ 308 public static void setStaticField(Class<?> clazz, String fieldName, Object fieldNewValue) { 309 try { 310 setStaticField(clazz.getDeclaredField(fieldName), fieldNewValue); 311 } catch (Exception e) { 312 throw new RuntimeException(e); 313 } 314 } 315 316 /** 317 * Reflectively call an instance method on an object. 318 * 319 * @param instance Target object. 320 * @param methodName The method name to call. 321 * @param classParameters Array of parameter types and values. 322 * @param <R> The return type. 323 * @return The return value of the method. 324 */ 325 public static <R> R callInstanceMethod( 326 final Object instance, final String methodName, ClassParameter<?>... classParameters) { 327 perfStatsCollector.incrementCount( 328 String.format( 329 "ReflectionHelpers.callInstanceMethod-%s_%s", 330 instance.getClass().getName(), methodName)); 331 try { 332 final Class<?>[] classes = ClassParameter.getClasses(classParameters); 333 final Object[] values = ClassParameter.getValues(classParameters); 334 335 return traverseClassHierarchy( 336 instance.getClass(), 337 NoSuchMethodException.class, 338 traversalClass -> { 339 Method declaredMethod = traversalClass.getDeclaredMethod(methodName, classes); 340 declaredMethod.setAccessible(true); 341 return (R) declaredMethod.invoke(instance, values); 342 }); 343 } catch (InvocationTargetException e) { 344 if (e.getTargetException() instanceof RuntimeException) { 345 throw (RuntimeException) e.getTargetException(); 346 } 347 if (e.getTargetException() instanceof Error) { 348 throw (Error) e.getTargetException(); 349 } 350 throw new RuntimeException(e.getTargetException()); 351 } catch (Exception e) { 352 throw new RuntimeException(e); 353 } 354 } 355 356 /** 357 * Reflectively call an instance method on an object on a specific class. 358 * 359 * @param cl The class. 360 * @param instance Target object. 361 * @param methodName The method name to call. 362 * @param classParameters Array of parameter types and values. 363 * @param <R> The return type. 364 * @return The return value of the method. 365 */ 366 public static <R> R callInstanceMethod( 367 Class<?> cl, 368 final Object instance, 369 final String methodName, 370 ClassParameter<?>... classParameters) { 371 perfStatsCollector.incrementCount( 372 String.format("ReflectionHelpers.callInstanceMethod-%s_%s", cl.getName(), methodName)); 373 try { 374 final Class<?>[] classes = ClassParameter.getClasses(classParameters); 375 final Object[] values = ClassParameter.getValues(classParameters); 376 377 Method method = cl.getDeclaredMethod(methodName, classes); 378 method.setAccessible(true); 379 if (Modifier.isStatic(method.getModifiers())) { 380 throw new IllegalArgumentException(method + " is static"); 381 } 382 return (R) method.invoke(instance, values); 383 } catch (InvocationTargetException e) { 384 if (e.getTargetException() instanceof RuntimeException) { 385 throw (RuntimeException) e.getTargetException(); 386 } 387 if (e.getTargetException() instanceof Error) { 388 throw (Error) e.getTargetException(); 389 } 390 throw new RuntimeException(e.getTargetException()); 391 } catch (Exception e) { 392 throw new RuntimeException(e); 393 } 394 } 395 396 /** 397 * Helper method for calling a static method using a class from a custom class loader 398 * 399 * @param classLoader The ClassLoader used to load class 400 * @param fullyQualifiedClassName The full qualified class name with package name of the 401 * ClassLoader will load 402 * @param methodName The method name will be called 403 * @param classParameters The input parameters will be used for method calling 404 * @param <R> Return type of the method 405 * @return Return the value of the method 406 */ 407 public static <R> R callStaticMethod( 408 ClassLoader classLoader, 409 String fullyQualifiedClassName, 410 String methodName, 411 ClassParameter<?>... classParameters) { 412 Class<?> clazz = loadClass(classLoader, fullyQualifiedClassName); 413 return callStaticMethod(clazz, methodName, classParameters); 414 } 415 416 /** 417 * Reflectively call a static method on a class. 418 * 419 * @param clazz Target class. 420 * @param methodName The method name to call. 421 * @param classParameters Array of parameter types and values. 422 * @param <R> The return type. 423 * @return The return value of the method. 424 */ 425 @SuppressWarnings("unchecked") 426 public static <R> R callStaticMethod( 427 Class<?> clazz, String methodName, ClassParameter<?>... classParameters) { 428 perfStatsCollector.incrementCount( 429 String.format("ReflectionHelpers.callStaticMethod-%s_%s", clazz.getName(), methodName)); 430 try { 431 Class<?>[] classes = ClassParameter.getClasses(classParameters); 432 Object[] values = ClassParameter.getValues(classParameters); 433 434 Method method = clazz.getDeclaredMethod(methodName, classes); 435 method.setAccessible(true); 436 if (!Modifier.isStatic(method.getModifiers())) { 437 throw new IllegalArgumentException(method + " is not static"); 438 } 439 return (R) method.invoke(null, values); 440 } catch (InvocationTargetException e) { 441 if (e.getTargetException() instanceof RuntimeException) { 442 throw (RuntimeException) e.getTargetException(); 443 } 444 if (e.getTargetException() instanceof Error) { 445 throw (Error) e.getTargetException(); 446 } 447 throw new RuntimeException(e.getTargetException()); 448 } catch (NoSuchMethodException e) { 449 throw new RuntimeException("no such method " + clazz + "." + methodName, e); 450 } catch (Exception e) { 451 throw new RuntimeException(e); 452 } 453 } 454 455 /** 456 * Load a class. 457 * 458 * @param classLoader The class loader. 459 * @param fullyQualifiedClassName The fully qualified class name. 460 * @return The class object. 461 */ 462 public static Class<?> loadClass(ClassLoader classLoader, String fullyQualifiedClassName) { 463 try { 464 return classLoader.loadClass(fullyQualifiedClassName); 465 } catch (ClassNotFoundException e) { 466 throw new RuntimeException(e); 467 } 468 } 469 470 /** 471 * Attempt to load a class. 472 * 473 * @param classLoader The class loader. 474 * @param fullyQualifiedClassName The fully qualified class name. 475 * @return The class object, or null if class is not found. 476 */ 477 public static Optional<Class<?>> attemptLoadClass( 478 ClassLoader classLoader, String fullyQualifiedClassName) { 479 try { 480 return Optional.of(classLoader.loadClass(fullyQualifiedClassName)); 481 } catch (ClassNotFoundException e) { 482 return Optional.empty(); 483 } 484 } 485 486 /** 487 * Create a new instance of a class 488 * 489 * @param cl The class object. 490 * @param <T> The class type. 491 * @return New class instance. 492 */ 493 public static <T> T newInstance(Class<T> cl) { 494 try { 495 return cl.getDeclaredConstructor().newInstance(); 496 } catch (InstantiationException 497 | IllegalAccessException 498 | NoSuchMethodException 499 | InvocationTargetException e) { 500 throw new RuntimeException(e); 501 } 502 } 503 504 /** 505 * Reflectively call the constructor of an object. 506 * 507 * @param clazz Target class. 508 * @param classParameters Array of parameter types and values. 509 * @param <R> The return type. 510 * @return The return value of the method. 511 */ 512 public static <R> R callConstructor( 513 Class<? extends R> clazz, ClassParameter<?>... classParameters) { 514 perfStatsCollector.incrementCount("ReflectionHelpers.callConstructor-" + clazz.getName()); 515 try { 516 final Class<?>[] classes = ClassParameter.getClasses(classParameters); 517 final Object[] values = ClassParameter.getValues(classParameters); 518 519 Constructor<? extends R> constructor = clazz.getDeclaredConstructor(classes); 520 constructor.setAccessible(true); 521 return constructor.newInstance(values); 522 } catch (InstantiationException e) { 523 throw new RuntimeException("error instantiating " + clazz.getName(), e); 524 } catch (InvocationTargetException e) { 525 if (e.getTargetException() instanceof RuntimeException) { 526 throw (RuntimeException) e.getTargetException(); 527 } 528 if (e.getTargetException() instanceof Error) { 529 throw (Error) e.getTargetException(); 530 } 531 throw new RuntimeException(e.getTargetException()); 532 } catch (Exception e) { 533 throw new RuntimeException(e); 534 } 535 } 536 537 /** 538 * Reflectively check if a class has a given constructor. 539 * 540 * @param parameterTypes in order parameters of the constructor. 541 * @param clazzName Target class name. 542 * @return boolean to indicate whether the constructor exists or not on the clazz. 543 */ 544 public static boolean hasConstructor(String clazzName, Class<?>... parameterTypes) { 545 Class<?> clazz = 546 ReflectionHelpers.loadClass(Thread.currentThread().getContextClassLoader(), clazzName); 547 return hasConstructor(clazz, parameterTypes); 548 } 549 550 /** 551 * Reflectively check if a class has a given constructor. 552 * 553 * @param parameterTypes in order parameters of the constructor. 554 * @param clazz Target class. 555 * @return boolean to indicate whether the constructor exists or not on the clazz. 556 */ 557 public static boolean hasConstructor(Class<?> clazz, Class<?>... parameterTypes) { 558 for (Constructor<?> constructor : clazz.getDeclaredConstructors()) { 559 Class<?>[] paramTypes = constructor.getParameterTypes(); 560 if (paramTypes.length == parameterTypes.length) { 561 boolean match = true; 562 for (int i = 0; i < paramTypes.length; i++) { 563 if (!paramTypes[i].isAssignableFrom(parameterTypes[i])) { 564 match = false; 565 break; 566 } 567 } 568 if (match) { 569 return true; 570 } 571 } 572 } 573 return false; 574 } 575 576 private static <R, E extends Exception> R traverseClassHierarchy( 577 Class<?> targetClass, Class<? extends E> exceptionClass, InsideTraversal<R> insideTraversal) 578 throws Exception { 579 Class<?> hierarchyTraversalClass = targetClass; 580 while (true) { 581 try { 582 return insideTraversal.run(hierarchyTraversalClass); 583 } catch (Exception e) { 584 if (!exceptionClass.isInstance(e)) { 585 throw e; 586 } 587 hierarchyTraversalClass = hierarchyTraversalClass.getSuperclass(); 588 if (hierarchyTraversalClass == null) { 589 throw new RuntimeException(e); 590 } 591 } 592 } 593 } 594 595 public static Object defaultValueForType(String returnType) { 596 return PRIMITIVE_RETURN_VALUES.get(returnType); 597 } 598 599 private interface InsideTraversal<R> { 600 R run(Class<?> traversalClass) throws Exception; 601 } 602 603 /** 604 * Typed parameter used with reflective method calls. 605 * 606 * @param <V> The value of the method parameter. 607 */ 608 public static class ClassParameter<V> { 609 public final Class<? extends V> clazz; 610 public final V value; 611 612 public ClassParameter(Class<? extends V> clazz, V value) { 613 this.clazz = clazz; 614 this.value = value; 615 } 616 617 public static <V> ClassParameter<V> from(Class<? extends V> clazz, V value) { 618 return new ClassParameter<>(clazz, value); 619 } 620 621 public static ClassParameter<?>[] fromComponentLists(Class<?>[] classes, Object[] values) { 622 ClassParameter<?>[] classParameters = new ClassParameter[classes.length]; 623 for (int i = 0; i < classes.length; i++) { 624 classParameters[i] = ClassParameter.from(classes[i], values[i]); 625 } 626 return classParameters; 627 } 628 629 public static Class<?>[] getClasses(ClassParameter<?>... classParameters) { 630 Class<?>[] classes = new Class[classParameters.length]; 631 for (int i = 0; i < classParameters.length; i++) { 632 Class<?> paramClass = classParameters[i].clazz; 633 classes[i] = paramClass; 634 } 635 return classes; 636 } 637 638 public static Object[] getValues(ClassParameter<?>... classParameters) { 639 Object[] values = new Object[classParameters.length]; 640 for (int i = 0; i < classParameters.length; i++) { 641 Object paramValue = classParameters[i].value; 642 values[i] = paramValue; 643 } 644 return values; 645 } 646 } 647 } 648