xref: /aosp_15_r20/external/robolectric/shadowapi/src/main/java/org/robolectric/util/ReflectionHelpers.java (revision e6ba16074e6af37d123cb567d575f496bf0a58ee)
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