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 ? reflectionParam.indexOf("[]") : -1;
213         if (jdiffParamEndOffset != -1 && reflectionParamEndOffset != -1) {
214             jdiffParam = jdiffParam.substring(0, jdiffParamEndOffset);
215             reflectionParam = reflectionParam.substring(0, reflectionParamEndOffset);
216             return typeComparator.compare(jdiffParam, reflectionParam);
217         }
218 
219         return false;
220     }
221 
222     /**
223      * Finds the reflected method specified by the method description.
224      *
225      * @param runtimeClass the class in which to search.
226      * @param method description of the method to find
227      * @param mismatchReasons a map from rejected method to the reason it was rejected, only
228      *     contains methods with the same name.
229      * @return the reflected method, or null if not found.
230      */
findMatchingMethod( Class<?> runtimeClass, JDiffMethod method, Map<Method, String> mismatchReasons)231     static Method findMatchingMethod(
232             Class<?> runtimeClass, JDiffMethod method, Map<Method, String> mismatchReasons) {
233 
234         // Search through the class to find the methods just in case the method was actually
235         // declared in a superclass which is not part of the API and so was made to appear as if
236         // it was declared in each of the hidden class' subclasses. Cannot use getMethods() as that
237         // will only return public methods and the API includes protected methods.
238         Class<?> currentClass = runtimeClass;
239         while (currentClass != null) {
240             Method[] reflectedMethods = currentClass.getDeclaredMethods();
241 
242             for (Method reflectedMethod : reflectedMethods) {
243                 // If the method names aren't equal, the methods can't match.
244                 if (!method.mName.equals(reflectedMethod.getName())) {
245                     continue;
246                 }
247 
248                 if (matchesSignature(method, reflectedMethod, mismatchReasons)) {
249                     return reflectedMethod;
250                 }
251             }
252 
253             currentClass = currentClass.getSuperclass();
254         }
255 
256         return null;
257     }
258 
259     /**
260      * Checks if the two types of methods are the same.
261      *
262      * @param jDiffMethod the jDiffMethod to compare
263      * @param reflectedMethod the reflected method to compare
264      * @param mismatchReasons map from method to reason it did not match, used when reporting
265      *     missing methods.
266      * @return true, if both methods are the same
267      */
matchesSignature(JDiffMethod jDiffMethod, Method reflectedMethod, Map<Method, String> mismatchReasons)268     static boolean matchesSignature(JDiffMethod jDiffMethod, Method reflectedMethod,
269             Map<Method, String> mismatchReasons) {
270         // If the method is a bridge then use a special comparator for comparing types as
271         // bridge methods created for generic methods may not have generic signatures.
272         // See b/123558763 for more information.
273         TypeComparator typeComparator = reflectedMethod.isBridge()
274                 ? BridgeTypeComparator.INSTANCE : DefaultTypeComparator.INSTANCE;
275 
276         String jdiffReturnType = jDiffMethod.mReturnType;
277         String reflectionReturnType = typeToString(reflectedMethod.getGenericReturnType());
278 
279         // Next, compare the return types of the two methods.  If
280         // they aren't equal, the methods can't match.
281         if (!typeComparator.compare(jdiffReturnType, reflectionReturnType)) {
282             mismatchReasons.put(reflectedMethod,
283                     String.format("return type mismatch: expected %s, found %s", jdiffReturnType,
284                             reflectionReturnType));
285             return false;
286         }
287 
288         List<String> jdiffParamList = jDiffMethod.mParamList;
289         Type[] params = reflectedMethod.getGenericParameterTypes();
290 
291         // Next, check the method parameters.  If they have different
292         // parameter lengths, the two methods can't match.
293         if (jdiffParamList.size() != params.length) {
294             mismatchReasons.put(reflectedMethod,
295                     String.format("parameter list length mismatch: expected %s, found %s",
296                             jdiffParamList.size(),
297                             params.length));
298             return false;
299         }
300 
301         boolean piecewiseParamsMatch = true;
302 
303         // Compare method parameters piecewise and return true if they all match.
304         for (int i = 0; i < jdiffParamList.size(); i++) {
305             piecewiseParamsMatch &= compareParam(jdiffParamList.get(i), params[i], typeComparator);
306         }
307         if (piecewiseParamsMatch) {
308             return true;
309         }
310 
311         /* NOTE: There are cases where piecewise method parameter checking
312          * fails even though the strings are equal, so compare entire strings
313          * against each other. This is not done by default to avoid a
314          * TransactionTooLargeException.
315          * Additionally, this can fail anyway due to extra
316          * information dug up by reflection.
317          *
318          * TODO: fix parameter equality checking and reflection matching
319          * See https://b.corp.google.com/issues/27726349
320          */
321 
322         StringBuilder reflectedMethodParams = new StringBuilder("");
323         StringBuilder jdiffMethodParams = new StringBuilder("");
324 
325         String sep = "";
326         for (int i = 0; i < jdiffParamList.size(); i++) {
327             jdiffMethodParams.append(sep).append(jdiffParamList.get(i));
328             reflectedMethodParams.append(sep).append(params[i].getTypeName());
329             sep = ", ";
330         }
331 
332         String jDiffFName = jdiffMethodParams.toString();
333         String refName = reflectedMethodParams.toString();
334 
335         boolean signatureMatches = jDiffFName.equals(refName);
336         if (!signatureMatches) {
337             mismatchReasons.put(reflectedMethod,
338                     String.format("parameter signature mismatch: expected (%s), found (%s)",
339                             jDiffFName,
340                             refName));
341         }
342 
343         return signatureMatches;
344     }
345 
346     /**
347      * Converts WildcardType array into a jdiff compatible string..
348      * This is a helper function for typeToString.
349      *
350      * @param types array of types to format.
351      * @return the jdiff formatted string.
352      */
concatWildcardTypes(Type[] types)353     private static String concatWildcardTypes(Type[] types) {
354         StringBuilder sb = new StringBuilder();
355         int elementNum = 0;
356         for (Type t : types) {
357             sb.append(typeToString(t));
358             if (++elementNum < types.length) {
359                 sb.append(" & ");
360             }
361         }
362         return sb.toString();
363     }
364 
365     /**
366      * Converts a Type into a jdiff compatible String.  The returned
367      * types from this function should match the same Strings that
368      * jdiff is providing to us.
369      *
370      * @param type the type to convert.
371      * @return the jdiff formatted string.
372      */
typeToString(Type type)373     public static String typeToString(Type type) {
374         if (type instanceof ParameterizedType) {
375             ParameterizedType pt = (ParameterizedType) type;
376 
377             StringBuilder sb = new StringBuilder();
378             sb.append(typeToString(pt.getRawType()));
379             sb.append("<");
380 
381             int elementNum = 0;
382             Type[] types = pt.getActualTypeArguments();
383             for (Type t : types) {
384                 sb.append(typeToString(t));
385                 if (++elementNum < types.length) {
386                     // Must match separator used in
387                     // android.signature.cts.KtHelper.toDefaultTypeString.
388                     sb.append(",");
389                 }
390             }
391 
392             sb.append(">");
393             return sb.toString();
394         } else if (type instanceof TypeVariable) {
395             return ((TypeVariable<?>) type).getName();
396         } else if (type instanceof Class) {
397             return ((Class<?>) type).getCanonicalName();
398         } else if (type instanceof GenericArrayType) {
399             String typeName = typeToString(((GenericArrayType) type).getGenericComponentType());
400             return typeName + "[]";
401         } else if (type instanceof WildcardType) {
402             WildcardType wt = (WildcardType) type;
403             Type[] lowerBounds = wt.getLowerBounds();
404             if (lowerBounds.length == 0) {
405                 String name = "? extends " + concatWildcardTypes(wt.getUpperBounds());
406 
407                 // Special case for ?
408                 if (name.equals("? extends java.lang.Object")) {
409                     return "?";
410                 } else {
411                     return name;
412                 }
413             } else {
414                 String name = concatWildcardTypes(wt.getUpperBounds()) +
415                         " super " +
416                         concatWildcardTypes(wt.getLowerBounds());
417                 // Another special case for ?
418                 name = name.replace("java.lang.Object", "?");
419                 return name;
420             }
421         } else {
422             throw new RuntimeException("Got an unknown java.lang.Type");
423         }
424     }
425 
426     private final static Pattern REPEATING_ANNOTATION_PATTERN =
427             Pattern.compile("@.*\\(value=\\[(.*)\\]\\)");
428 
hasMatchingAnnotation(AnnotatedElement elem, String annotationSpec)429     public static boolean hasMatchingAnnotation(AnnotatedElement elem, String annotationSpec) {
430         for (Annotation a : elem.getAnnotations()) {
431             if (a.toString().equals(annotationSpec)) {
432                 return true;
433             }
434             // It could be a repeating annotation. In that case, a.toString() returns
435             // "@MyAnnotation$Container(value=[@MyAnnotation(A), @MyAnnotation(B)])"
436             // Then, iterate over @MyAnnotation(A) and @MyAnnotation(B).
437             Matcher m = REPEATING_ANNOTATION_PATTERN.matcher(a.toString());
438             if (m.matches()) {
439                 for (String token : m.group(1).split(", ")) {
440                     if (token.equals(annotationSpec)) {
441                         return true;
442                     }
443                 }
444             }
445         }
446         return false;
447     }
448 
449     /**
450      * Returns a list of constructors which are annotated with the given annotation class.
451      */
getAnnotatedConstructors(Class<?> clazz, String annotationSpec)452     public static Set<Constructor<?>> getAnnotatedConstructors(Class<?> clazz,
453             String annotationSpec) {
454         Set<Constructor<?>> result = new HashSet<>();
455         if (annotationSpec != null) {
456             for (Constructor<?> c : clazz.getDeclaredConstructors()) {
457                 if (hasMatchingAnnotation(c, annotationSpec)) {
458                     // TODO(b/71630695): currently, some API members are not annotated, because
459                     // a member is automatically added to the API set if it is in a class with
460                     // annotation and it is not @hide. <member>.getDeclaringClass().
461                     // isAnnotationPresent(annotationClass) won't help because it will then
462                     // incorrectly include non-API members which are marked as @hide;
463                     // @hide isn't visible at runtime. Until the issue is fixed, we should
464                     // omit those automatically added API members from the test.
465                     result.add(c);
466                 }
467             }
468         }
469         return result;
470     }
471 
472     /**
473      * Returns a list of methods which are annotated with the given annotation class.
474      */
getAnnotatedMethods(Class<?> clazz, String annotationSpec)475     public static Set<Method> getAnnotatedMethods(Class<?> clazz, String annotationSpec) {
476         Set<Method> result = new HashSet<>();
477         if (annotationSpec != null) {
478             for (Method m : clazz.getDeclaredMethods()) {
479                 if (hasMatchingAnnotation(m, annotationSpec)) {
480                     // TODO(b/71630695): see getAnnotatedConstructors for details
481                     result.add(m);
482                 }
483             }
484         }
485         return result;
486     }
487 
488     /**
489      * Returns a list of fields which are annotated with the given annotation class.
490      */
getAnnotatedFields(Class<?> clazz, String annotationSpec)491     public static Set<Field> getAnnotatedFields(Class<?> clazz, String annotationSpec) {
492         Set<Field> result = new HashSet<>();
493         if (annotationSpec != null) {
494             for (Field f : clazz.getDeclaredFields()) {
495                 if (hasMatchingAnnotation(f, annotationSpec)) {
496                     // TODO(b/71630695): see getAnnotatedConstructors for details
497                     result.add(f);
498                 }
499             }
500         }
501         return result;
502     }
503 
isInAnnotatedClass(Member m, String annotationSpec)504     private static boolean isInAnnotatedClass(Member m, String annotationSpec) {
505         Class<?> clazz = m.getDeclaringClass();
506         do {
507             if (hasMatchingAnnotation(clazz, annotationSpec)) {
508                 return true;
509             }
510         } while ((clazz = clazz.getDeclaringClass()) != null);
511         return false;
512     }
513 
isAnnotatedOrInAnnotatedClass(Field field, String annotationSpec)514     public static boolean isAnnotatedOrInAnnotatedClass(Field field, String annotationSpec) {
515         if (annotationSpec == null) {
516             return true;
517         }
518         return hasMatchingAnnotation(field, annotationSpec)
519                 || isInAnnotatedClass(field, annotationSpec);
520     }
521 
isAnnotatedOrInAnnotatedClass(Constructor<?> constructor, String annotationSpec)522     public static boolean isAnnotatedOrInAnnotatedClass(Constructor<?> constructor,
523             String annotationSpec) {
524         if (annotationSpec == null) {
525             return true;
526         }
527         return hasMatchingAnnotation(constructor, annotationSpec)
528                 || isInAnnotatedClass(constructor, annotationSpec);
529     }
530 
isAnnotatedOrInAnnotatedClass(Method method, String annotationSpec)531     public static boolean isAnnotatedOrInAnnotatedClass(Method method, String annotationSpec) {
532         if (annotationSpec == null) {
533             return true;
534         }
535         return hasMatchingAnnotation(method, annotationSpec)
536                 || isInAnnotatedClass(method, annotationSpec);
537     }
538 
isOverridingAnnotatedMethod(Method method, String annotationSpec)539     public static boolean isOverridingAnnotatedMethod(Method method, String annotationSpec) {
540         Class<?> clazz = method.getDeclaringClass();
541         while (!(clazz = clazz.getSuperclass()).equals(Object.class)) {
542             try {
543                 Method overriddenMethod;
544                 overriddenMethod = clazz.getDeclaredMethod(method.getName(),
545                         method.getParameterTypes());
546                 if (overriddenMethod != null) {
547                     return isAnnotatedOrInAnnotatedClass(overriddenMethod, annotationSpec);
548                 }
549             } catch (NoSuchMethodException e) {
550                 continue;
551             } catch (SecurityException e) {
552                 throw new RuntimeException(
553                         "Error while searching for overridden method. " + method.toString(), e);
554             }
555         }
556         return false;
557     }
558 
findRequiredClass(JDiffClassDescription classDescription, ClassProvider classProvider)559     static Class<?> findRequiredClass(JDiffClassDescription classDescription,
560             ClassProvider classProvider) {
561         try {
562             return findMatchingClass(classDescription, classProvider);
563         } catch (ClassNotFoundException e) {
564             LogHelper.loge("ClassNotFoundException for " + classDescription.getAbsoluteClassName(), e);
565             return null;
566         }
567     }
568 
569     /**
570      * Compare the string representation of types for equality.
571      */
572     interface TypeComparator {
compare(String apiType, String reflectedType)573         boolean compare(String apiType, String reflectedType);
574     }
575 
576     /**
577      * Compare the types using their default signature, i.e. generic for generic methods, otherwise
578      * basic types.
579      */
580     static class DefaultTypeComparator implements TypeComparator {
581         static final TypeComparator INSTANCE = new DefaultTypeComparator();
582         @Override
compare(String apiType, String reflectedType)583         public boolean compare(String apiType, String reflectedType) {
584             return apiType.equals(reflectedType);
585         }
586     }
587 
588     /**
589      * Comparator for the types of bridge methods.
590      *
591      * <p>Bridge methods may not have generic signatures so compare as for
592      * {@link DefaultTypeComparator}, but if they do not match and the api type is
593      * generic then fall back to comparing their raw types.
594      */
595     static class BridgeTypeComparator implements TypeComparator {
596         static final TypeComparator INSTANCE = new BridgeTypeComparator();
597         @Override
compare(String apiType, String reflectedType)598         public boolean compare(String apiType, String reflectedType) {
599             if (DefaultTypeComparator.INSTANCE.compare(apiType, reflectedType)) {
600                 return true;
601             }
602 
603             // If the method is a bridge method and the return types are generic then compare the
604             // non generic types as bridge methods do not have generic types.
605             int index = apiType.indexOf('<');
606             if (index != -1) {
607                 String rawReturnType = apiType.substring(0, index);
608                 return rawReturnType.equals(reflectedType);
609             }
610             return false;
611         }
612     }
613 }
614