1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package android.signature.cts;
17 
18 import android.signature.cts.JDiffClassDescription.JDiffConstructor;
19 import android.signature.cts.JDiffClassDescription.JDiffMethod;
20 import java.lang.annotation.Annotation;
21 import java.lang.reflect.AnnotatedElement;
22 import java.lang.reflect.Constructor;
23 import java.lang.reflect.Field;
24 import java.lang.reflect.GenericArrayType;
25 import java.lang.reflect.Member;
26 import java.lang.reflect.Method;
27 import java.lang.reflect.Modifier;
28 import java.lang.reflect.ParameterizedType;
29 import java.lang.reflect.Type;
30 import java.lang.reflect.TypeVariable;
31 import java.lang.reflect.WildcardType;
32 import java.util.ArrayList;
33 import java.util.HashSet;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Set;
37 import java.util.regex.Matcher;
38 import java.util.regex.Pattern;
39 
40 /**
41  * Uses reflection to obtain runtime representations of elements in the API.
42  */
43 public class ReflectionHelper {
44 
45     /**
46      * Finds the reflected class for the class under test.
47      *
48      * @param classDescription the description of the class to find.
49      * @return the reflected class, or null if not found.
50      */
findMatchingClass(JDiffClassDescription classDescription, ClassProvider classProvider)51     public static Class<?> findMatchingClass(JDiffClassDescription classDescription,
52             ClassProvider classProvider) throws ClassNotFoundException {
53         // even if there are no . in the string, split will return an
54         // array of length 1
55         String shortClassName = classDescription.getShortClassName();
56         String[] classNameParts = shortClassName.split("\\.");
57         String packageName = classDescription.getPackageName();
58         String outermostClassName = packageName + "." + classNameParts[0];
59         int firstInnerClassNameIndex = 0;
60 
61         return searchForClass(classProvider, classDescription.getAbsoluteClassName(),
62                 outermostClassName, classNameParts,
63                 firstInnerClassNameIndex);
64     }
65 
searchForClass( ClassProvider classProvider, String absoluteClassName, String outermostClassName, String[] classNameParts, int outerClassNameIndex)66     private static Class<?> searchForClass(
67             ClassProvider classProvider,
68             String absoluteClassName,
69             String outermostClassName, String[] classNameParts,
70             int outerClassNameIndex) throws ClassNotFoundException {
71 
72         Class<?> clz = classProvider.getClass(outermostClassName);
73         if (clz.getCanonicalName().equals(absoluteClassName)) {
74             return clz;
75         }
76 
77         // Then it must be an inner class.
78         for (int x = outerClassNameIndex + 1; x < classNameParts.length; x++) {
79             clz = findInnerClassByName(clz, classNameParts[x]);
80             if (clz == null) {
81                 return null;
82             }
83             if (clz.getCanonicalName().equals(absoluteClassName)) {
84                 return clz;
85             }
86         }
87         return null;
88     }
89 
findMatchingClass(String absoluteClassName, ClassProvider classProvider)90     static Class<?> findMatchingClass(String absoluteClassName, ClassProvider classProvider)
91             throws ClassNotFoundException {
92 
93         String[] classNameParts = absoluteClassName.split("\\.");
94         StringBuilder builder = new StringBuilder();
95         String separator = "";
96         int start;
97         for (start = 0; start < classNameParts.length; start++) {
98             String classNamePart = classNameParts[start];
99             builder.append(separator).append(classNamePart);
100             separator = ".";
101             if (Character.isUpperCase(classNamePart.charAt(0))) {
102                 break;
103             }
104         }
105         String outermostClassName = builder.toString();
106 
107         return searchForClass(classProvider, absoluteClassName, outermostClassName, classNameParts,
108                 start);
109     }
110 
111     /**
112      * Searches the class for the specified inner class.
113      *
114      * @param clz the class to search in.
115      * @param simpleName the simpleName of the class to find
116      * @return the class being searched for, or null if it can't be found.
117      */
findInnerClassByName(Class<?> clz, String simpleName)118     private static Class<?> findInnerClassByName(Class<?> clz, String simpleName) {
119         for (Class<?> c : clz.getDeclaredClasses()) {
120             if (c.getSimpleName().equals(simpleName)) {
121                 return c;
122             }
123         }
124         return null;
125     }
126 
127     /**
128      * Searches available constructor.
129      *
130      * @param runtimeClass the class in which to search.
131      * @param jdiffDes constructor description to find.
132      * @param mismatchReasons a map from rejected constructor to the reason it was rejected.
133      * @return reflected constructor, or null if not found.
134      */
findMatchingConstructor(Class<?> runtimeClass, JDiffConstructor jdiffDes, Map<Constructor, String> mismatchReasons)135     static Constructor<?> findMatchingConstructor(Class<?> runtimeClass,
136             JDiffConstructor jdiffDes, Map<Constructor, String> mismatchReasons) {
137 
138         for (Constructor<?> c : runtimeClass.getDeclaredConstructors()) {
139             Type[] params = c.getGenericParameterTypes();
140             boolean isStaticClass = ((runtimeClass.getModifiers() & Modifier.STATIC) != 0);
141 
142             int startParamOffset = 0;
143             int numberOfParams = params.length;
144 
145             // non-static inner class -> skip implicit parent pointer
146             // as first arg
147             if (runtimeClass.isMemberClass() && !isStaticClass && params.length >= 1) {
148                 startParamOffset = 1;
149                 --numberOfParams;
150             }
151 
152             ArrayList<String> jdiffParamList = jdiffDes.mParamList;
153             if (jdiffParamList.size() == numberOfParams) {
154                 boolean isFound = true;
155                 // i counts jdiff params, j counts reflected params
156                 int i = 0;
157                 int j = startParamOffset;
158                 while (i < jdiffParamList.size()) {
159                     String expectedParameter = jdiffParamList.get(i);
160                     Type actualParameter = params[j];
161                     if (!compareParam(expectedParameter, actualParameter,
162                             DefaultTypeComparator.INSTANCE)) {
163                         mismatchReasons.put(c,
164                                 String.format("parameter %d mismatch: expected (%s), found (%s)",
165                                         i,
166                                         expectedParameter,
167                                         actualParameter));
168                         isFound = false;
169                         break;
170                     }
171                     ++i;
172                     ++j;
173                 }
174                 if (isFound) {
175                     return c;
176                 }
177             } else {
178                 mismatchReasons.put(c,
179                         String.format("parameter list length mismatch: expected %d, found %d",
180                                 jdiffParamList.size(),
181                                 params.length));
182             }
183         }
184         return null;
185     }
186 
187     /**
188      * Compares the parameter from the API and the parameter from
189      * reflection.
190      *
191      * @param jdiffParam param parsed from the API xml file.
192      * @param reflectionParamType param gotten from the Java reflection.
193      * @param typeComparator compares two types to determine if they are equal.
194      * @return True if the two params match, otherwise return false.
195      */
compareParam(String jdiffParam, Type reflectionParamType, TypeComparator typeComparator)196     private static boolean compareParam(String jdiffParam, Type reflectionParamType,
197             TypeComparator typeComparator) {
198         if (jdiffParam == null) {
199             return false;
200         }
201 
202         String reflectionParam = typeToString(reflectionParamType);
203         // Most things aren't varargs, so just do a simple compare
204         // first.
205         if (typeComparator.compare(jdiffParam, reflectionParam)) {
206             return true;
207         }
208 
209         // Check for varargs.  jdiff reports varargs as ..., while
210         // reflection reports them as []
211         int jdiffParamEndOffset = jdiffParam.indexOf("...");
212         int reflectionParamEndOffset = reflectionParam != null
213                 ? reflectionParam.lastIndexOf("[]") : -1;
214         if (jdiffParamEndOffset != -1 && reflectionParamEndOffset != -1) {
215             jdiffParam = jdiffParam.substring(0, jdiffParamEndOffset);
216             reflectionParam = reflectionParam.substring(0, reflectionParamEndOffset);
217             return typeComparator.compare(jdiffParam, reflectionParam);
218         }
219 
220         return false;
221     }
222 
223     /**
224      * Finds the reflected method specified by the method description.
225      *
226      * @param runtimeClass the class in which to search.
227      * @param method description of the method to find
228      * @param mismatchReasons a map from rejected method to the reason it was rejected, only
229      *     contains methods with the same name.
230      * @return the reflected method, or null if not found.
231      */
findMatchingMethod( Class<?> runtimeClass, JDiffMethod method, Map<Method, String> mismatchReasons)232     static Method findMatchingMethod(
233             Class<?> runtimeClass, JDiffMethod method, Map<Method, String> mismatchReasons) {
234 
235         // Search through the class to find the methods just in case the method was actually
236         // declared in a superclass which is not part of the API and so was made to appear as if
237         // it was declared in each of the hidden class' subclasses. Cannot use getMethods() as that
238         // will only return public methods and the API includes protected methods.
239         Class<?> currentClass = runtimeClass;
240         while (currentClass != null) {
241             Method[] reflectedMethods = currentClass.getDeclaredMethods();
242 
243             for (Method reflectedMethod : reflectedMethods) {
244                 // If the method names aren't equal, the methods can't match.
245                 if (!method.mName.equals(reflectedMethod.getName())) {
246                     continue;
247                 }
248 
249                 if (matchesSignature(method, reflectedMethod, mismatchReasons)) {
250                     return reflectedMethod;
251                 }
252             }
253 
254             currentClass = currentClass.getSuperclass();
255         }
256 
257         return null;
258     }
259 
260     /**
261      * Checks if the two types of methods are the same.
262      *
263      * @param jDiffMethod the jDiffMethod to compare
264      * @param reflectedMethod the reflected method to compare
265      * @param mismatchReasons map from method to reason it did not match, used when reporting
266      *     missing methods.
267      * @return true, if both methods are the same
268      */
matchesSignature(JDiffMethod jDiffMethod, Method reflectedMethod, Map<Method, String> mismatchReasons)269     static boolean matchesSignature(JDiffMethod jDiffMethod, Method reflectedMethod,
270             Map<Method, String> mismatchReasons) {
271         // If the method is a bridge then use a special comparator for comparing types as
272         // bridge methods created for generic methods may not have generic signatures.
273         // See b/123558763 for more information.
274         TypeComparator typeComparator = reflectedMethod.isBridge()
275                 ? BridgeTypeComparator.INSTANCE : DefaultTypeComparator.INSTANCE;
276 
277         String jdiffReturnType = jDiffMethod.mReturnType;
278         String reflectionReturnType = typeToString(reflectedMethod.getGenericReturnType());
279 
280         // Next, compare the return types of the two methods.  If
281         // they aren't equal, the methods can't match.
282         if (!typeComparator.compare(jdiffReturnType, reflectionReturnType)) {
283             mismatchReasons.put(reflectedMethod,
284                     String.format("return type mismatch: expected %s, found %s", jdiffReturnType,
285                             reflectionReturnType));
286             return false;
287         }
288 
289         List<String> jdiffParamList = jDiffMethod.mParamList;
290         Type[] params = reflectedMethod.getGenericParameterTypes();
291 
292         // Next, check the method parameters.  If they have different
293         // parameter lengths, the two methods can't match.
294         if (jdiffParamList.size() != params.length) {
295             mismatchReasons.put(reflectedMethod,
296                     String.format("parameter list length mismatch: expected %s, found %s",
297                             jdiffParamList.size(),
298                             params.length));
299             return false;
300         }
301 
302         boolean piecewiseParamsMatch = true;
303 
304         // Compare method parameters piecewise and return true if they all match.
305         for (int i = 0; i < jdiffParamList.size(); i++) {
306             piecewiseParamsMatch &= compareParam(jdiffParamList.get(i), params[i], typeComparator);
307         }
308         if (piecewiseParamsMatch) {
309             return true;
310         }
311 
312         /* NOTE: There are cases where piecewise method parameter checking
313          * fails even though the strings are equal, so compare entire strings
314          * against each other. This is not done by default to avoid a
315          * TransactionTooLargeException.
316          * Additionally, this can fail anyway due to extra
317          * information dug up by reflection.
318          *
319          * TODO: fix parameter equality checking and reflection matching
320          * See https://b.corp.google.com/issues/27726349
321          */
322 
323         StringBuilder reflectedMethodParams = new StringBuilder("");
324         StringBuilder jdiffMethodParams = new StringBuilder("");
325 
326         String sep = "";
327         for (int i = 0; i < jdiffParamList.size(); i++) {
328             jdiffMethodParams.append(sep).append(jdiffParamList.get(i));
329             reflectedMethodParams.append(sep).append(params[i].getTypeName());
330             sep = ", ";
331         }
332 
333         String jDiffFName = jdiffMethodParams.toString();
334         String refName = reflectedMethodParams.toString();
335 
336         boolean signatureMatches = jDiffFName.equals(refName);
337         if (!signatureMatches) {
338             mismatchReasons.put(reflectedMethod,
339                     String.format("parameter signature mismatch: expected (%s), found (%s)",
340                             jDiffFName,
341                             refName));
342         }
343 
344         return signatureMatches;
345     }
346 
347     /**
348      * Converts WildcardType array into a jdiff compatible string..
349      * This is a helper function for typeToString.
350      *
351      * @param types array of types to format.
352      * @return the jdiff formatted string.
353      */
concatWildcardTypes(Type[] types)354     private static String concatWildcardTypes(Type[] types) {
355         StringBuilder sb = new StringBuilder();
356         int elementNum = 0;
357         for (Type t : types) {
358             sb.append(typeToString(t));
359             if (++elementNum < types.length) {
360                 sb.append(" & ");
361             }
362         }
363         return sb.toString();
364     }
365 
366     /**
367      * Converts a Type into a jdiff compatible String.  The returned
368      * types from this function should match the same Strings that
369      * jdiff is providing to us.
370      *
371      * @param type the type to convert.
372      * @return the jdiff formatted string.
373      */
typeToString(Type type)374     public static String typeToString(Type type) {
375         if (type instanceof ParameterizedType) {
376             ParameterizedType pt = (ParameterizedType) type;
377 
378             StringBuilder sb = new StringBuilder();
379             sb.append(typeToString(pt.getRawType()));
380             sb.append("<");
381 
382             int elementNum = 0;
383             Type[] types = pt.getActualTypeArguments();
384             for (Type t : types) {
385                 sb.append(typeToString(t));
386                 if (++elementNum < types.length) {
387                     // Must match separator used in
388                     // android.signature.cts.KtHelper.toDefaultTypeString.
389                     sb.append(",");
390                 }
391             }
392 
393             sb.append(">");
394             return sb.toString();
395         } else if (type instanceof TypeVariable) {
396             return ((TypeVariable<?>) type).getName();
397         } else if (type instanceof Class) {
398             return ((Class<?>) type).getCanonicalName();
399         } else if (type instanceof GenericArrayType) {
400             String typeName = typeToString(((GenericArrayType) type).getGenericComponentType());
401             return typeName + "[]";
402         } else if (type instanceof WildcardType) {
403             WildcardType wt = (WildcardType) type;
404             Type[] lowerBounds = wt.getLowerBounds();
405             if (lowerBounds.length == 0) {
406                 String name = "? extends " + concatWildcardTypes(wt.getUpperBounds());
407 
408                 // Special case for ?
409                 if (name.equals("? extends java.lang.Object")) {
410                     return "?";
411                 } else {
412                     return name;
413                 }
414             } else {
415                 String name = concatWildcardTypes(wt.getUpperBounds()) +
416                         " super " +
417                         concatWildcardTypes(wt.getLowerBounds());
418                 // Another special case for ?
419                 name = name.replace("java.lang.Object", "?");
420                 return name;
421             }
422         } else {
423             throw new RuntimeException("Got an unknown java.lang.Type");
424         }
425     }
426 
427     private final static Pattern REPEATING_ANNOTATION_PATTERN =
428             Pattern.compile("@.*\\(value=\\[(.*)\\]\\)");
429 
hasMatchingAnnotation(AnnotatedElement elem, String annotationSpec)430     public static boolean hasMatchingAnnotation(AnnotatedElement elem, String annotationSpec) {
431         for (Annotation a : elem.getAnnotations()) {
432             if (a.toString().equals(annotationSpec)) {
433                 return true;
434             }
435             // It could be a repeating annotation. In that case, a.toString() returns
436             // "@MyAnnotation$Container(value=[@MyAnnotation(A), @MyAnnotation(B)])"
437             // Then, iterate over @MyAnnotation(A) and @MyAnnotation(B).
438             Matcher m = REPEATING_ANNOTATION_PATTERN.matcher(a.toString());
439             if (m.matches()) {
440                 for (String token : m.group(1).split(", ")) {
441                     if (token.equals(annotationSpec)) {
442                         return true;
443                     }
444                 }
445             }
446         }
447         return false;
448     }
449 
450     /**
451      * Returns a list of constructors which are annotated with the given annotation class.
452      */
getAnnotatedConstructors(Class<?> clazz, String annotationSpec)453     public static Set<Constructor<?>> getAnnotatedConstructors(Class<?> clazz,
454             String annotationSpec) {
455         Set<Constructor<?>> result = new HashSet<>();
456         if (annotationSpec != null) {
457             for (Constructor<?> c : clazz.getDeclaredConstructors()) {
458                 if (hasMatchingAnnotation(c, annotationSpec)) {
459                     // TODO(b/71630695): currently, some API members are not annotated, because
460                     // a member is automatically added to the API set if it is in a class with
461                     // annotation and it is not @hide. <member>.getDeclaringClass().
462                     // isAnnotationPresent(annotationClass) won't help because it will then
463                     // incorrectly include non-API members which are marked as @hide;
464                     // @hide isn't visible at runtime. Until the issue is fixed, we should
465                     // omit those automatically added API members from the test.
466                     result.add(c);
467                 }
468             }
469         }
470         return result;
471     }
472 
473     /**
474      * Returns a list of methods which are annotated with the given annotation class.
475      */
getAnnotatedMethods(Class<?> clazz, String annotationSpec)476     public static Set<Method> getAnnotatedMethods(Class<?> clazz, String annotationSpec) {
477         Set<Method> result = new HashSet<>();
478         if (annotationSpec != null) {
479             for (Method m : clazz.getDeclaredMethods()) {
480                 if (hasMatchingAnnotation(m, annotationSpec)) {
481                     // TODO(b/71630695): see getAnnotatedConstructors for details
482                     result.add(m);
483                 }
484             }
485         }
486         return result;
487     }
488 
489     /**
490      * Returns a list of fields which are annotated with the given annotation class.
491      */
getAnnotatedFields(Class<?> clazz, String annotationSpec)492     public static Set<Field> getAnnotatedFields(Class<?> clazz, String annotationSpec) {
493         Set<Field> result = new HashSet<>();
494         if (annotationSpec != null) {
495             for (Field f : clazz.getDeclaredFields()) {
496                 if (hasMatchingAnnotation(f, annotationSpec)) {
497                     // TODO(b/71630695): see getAnnotatedConstructors for details
498                     result.add(f);
499                 }
500             }
501         }
502         return result;
503     }
504 
isInAnnotatedClass(Member m, String annotationSpec)505     private static boolean isInAnnotatedClass(Member m, String annotationSpec) {
506         Class<?> clazz = m.getDeclaringClass();
507         do {
508             if (hasMatchingAnnotation(clazz, annotationSpec)) {
509                 return true;
510             }
511         } while ((clazz = clazz.getDeclaringClass()) != null);
512         return false;
513     }
514 
isAnnotatedOrInAnnotatedClass(Field field, String annotationSpec)515     public static boolean isAnnotatedOrInAnnotatedClass(Field field, String annotationSpec) {
516         if (annotationSpec == null) {
517             return true;
518         }
519         return hasMatchingAnnotation(field, annotationSpec)
520                 || isInAnnotatedClass(field, annotationSpec);
521     }
522 
isAnnotatedOrInAnnotatedClass(Constructor<?> constructor, String annotationSpec)523     public static boolean isAnnotatedOrInAnnotatedClass(Constructor<?> constructor,
524             String annotationSpec) {
525         if (annotationSpec == null) {
526             return true;
527         }
528         return hasMatchingAnnotation(constructor, annotationSpec)
529                 || isInAnnotatedClass(constructor, annotationSpec);
530     }
531 
isAnnotatedOrInAnnotatedClass(Method method, String annotationSpec)532     public static boolean isAnnotatedOrInAnnotatedClass(Method method, String annotationSpec) {
533         if (annotationSpec == null) {
534             return true;
535         }
536         return hasMatchingAnnotation(method, annotationSpec)
537                 || isInAnnotatedClass(method, annotationSpec);
538     }
539 
isOverridingAnnotatedMethod(Method method, String annotationSpec)540     public static boolean isOverridingAnnotatedMethod(Method method, String annotationSpec) {
541         Class<?> clazz = method.getDeclaringClass();
542         while (!(clazz = clazz.getSuperclass()).equals(Object.class)) {
543             try {
544                 Method overriddenMethod;
545                 overriddenMethod = clazz.getDeclaredMethod(method.getName(),
546                         method.getParameterTypes());
547                 if (overriddenMethod != null) {
548                     return isAnnotatedOrInAnnotatedClass(overriddenMethod, annotationSpec);
549                 }
550             } catch (NoSuchMethodException e) {
551                 continue;
552             } catch (SecurityException e) {
553                 throw new RuntimeException(
554                         "Error while searching for overridden method. " + method.toString(), e);
555             }
556         }
557         return false;
558     }
559 
findRequiredClass(JDiffClassDescription classDescription, ClassProvider classProvider)560     static Class<?> findRequiredClass(JDiffClassDescription classDescription,
561             ClassProvider classProvider) {
562         try {
563             return findMatchingClass(classDescription, classProvider);
564         } catch (ClassNotFoundException e) {
565             LogHelper.loge("ClassNotFoundException for " + classDescription.getAbsoluteClassName(), e);
566             return null;
567         }
568     }
569 
570     /**
571      * Compare the string representation of types for equality.
572      */
573     interface TypeComparator {
compare(String apiType, String reflectedType)574         boolean compare(String apiType, String reflectedType);
575     }
576 
577     /**
578      * Compare the types using their default signature, i.e. generic for generic methods, otherwise
579      * basic types.
580      */
581     static class DefaultTypeComparator implements TypeComparator {
582         static final TypeComparator INSTANCE = new DefaultTypeComparator();
583         @Override
compare(String apiType, String reflectedType)584         public boolean compare(String apiType, String reflectedType) {
585             return apiType.equals(reflectedType);
586         }
587     }
588 
589     /**
590      * Comparator for the types of bridge methods.
591      *
592      * <p>Bridge methods may not have generic signatures so compare as for
593      * {@link DefaultTypeComparator}, but if they do not match and the api type is
594      * generic then fall back to comparing their raw types.
595      */
596     static class BridgeTypeComparator implements TypeComparator {
597         static final TypeComparator INSTANCE = new BridgeTypeComparator();
598         @Override
compare(String apiType, String reflectedType)599         public boolean compare(String apiType, String reflectedType) {
600             if (DefaultTypeComparator.INSTANCE.compare(apiType, reflectedType)) {
601                 return true;
602             }
603 
604             // If the method is a bridge method and the return types are generic then compare the
605             // non generic types as bridge methods do not have generic types.
606             int index = apiType.indexOf('<');
607             if (index != -1) {
608                 String rawReturnType = apiType.substring(0, index);
609                 return rawReturnType.equals(reflectedType);
610             }
611             return false;
612         }
613     }
614 }
615