1 /*
2  * Copyright (C) 2022 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 
17 package android.car.test;
18 
19 import android.annotation.Nullable;
20 import android.util.Log;
21 
22 import java.lang.reflect.Field;
23 import java.lang.reflect.Member;
24 import java.lang.reflect.Method;
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.List;
28 import java.util.Objects;
29 
30 // TODO(b/242571576): move this class to com.android.compatibility.common.util
31 
32 /**
33  * Helper class used primarily to validate values used on
34  * {code @com.android.compatibility.common.util.ApiTest}.
35  */
36 public final class ApiHelper {
37 
38     private static final String TAG = ApiHelper.class.getSimpleName();
39     private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
40 
41     /**
42      * Resolves an API to the proper member (method or field).
43      */
44     @Nullable
resolve(String api)45     public static Member resolve(String api) {
46         Objects.requireNonNull(api);
47 
48         Member member = null;
49         ClassNotFoundException classNotFoundException = null;
50         try {
51             // Try method first, as it's the most common case...
52             member = getMethod(api);
53             if (member != null) {
54                 return member;
55             }
56         } catch (ClassNotFoundException e) {
57             classNotFoundException = e;
58         }
59 
60         // ...then field
61         if (member == null) {
62             if (api.contains("$")) {
63                 // See note below
64                 return null;
65             }
66             member = getField(api);
67         }
68         // ...then special cases
69         if (member == null && api.contains("#")) {
70             // TODO(b/242571576): From Java's point of view, a field from an inner class like:
71             //  android.car.CarVersion$VERSION_CODES#TIRAMISU_0
72             // is valid, but the python API parser is expecting
73             //  android.car.CarVersion.VERSION_CODES#TIRAMISU_0
74             int index = api.lastIndexOf('.');
75             // TODO(b/242571576): it would fail if API was like Class.INNER_1.INNER_2.Field
76             String fixed = api.substring(0, index) + "$" + api.substring(index + 1, api.length());
77             member = getField(fixed);
78         }
79 
80         if (member == null) {
81             if (classNotFoundException != null) {
82                 Log.w(TAG, "Could not resolve API " + api + ": " + classNotFoundException);
83             } else {
84                 Log.w(TAG, "Could not resolve API " + api + "; check log tag " + TAG
85                         + " for more details");
86             }
87         }
88         return member;
89     }
90 
91     @Nullable
getMethod(String fullyQualifiedMethodName)92     private static Method getMethod(String fullyQualifiedMethodName) throws ClassNotFoundException {
93         // TODO(b/242571576): improve it to:
94         // - use regex
95         // - handle methods from CREATOR
96         // - support fields from inner classes like car.PlatformVersion$VERSION_CODES#TIRAMISU_0
97 
98         int classSeparator = fullyQualifiedMethodName.indexOf('#');
99         if (classSeparator == -1) {
100             return null;
101         }
102         String className = fullyQualifiedMethodName.substring(0, classSeparator);
103         String methodSignature = fullyQualifiedMethodName.substring(classSeparator + 1,
104                 fullyQualifiedMethodName.length());
105         if (DBG) {
106             Log.d(TAG, "getMethod(" + fullyQualifiedMethodName + "): class=" + className
107                     + ", signature=" + methodSignature);
108         }
109 
110         Class<?> clazz = Class.forName(className);
111         String methodName = methodSignature;
112         if (methodSignature.contains("(") && methodSignature.endsWith(")")) {
113             int openingIndex = methodSignature.indexOf('(');
114             methodName = methodSignature.substring(0, openingIndex);
115             String types = methodSignature.substring(openingIndex + 1,
116                     methodSignature.length() - 1);
117             String[] paramTypesNames = types.split(",");
118             if (DBG) {
119                 Log.d(TAG, "Method name after stripping (): " + methodName + ". Types: "
120                         + Arrays.toString(paramTypesNames));
121             }
122             return getMethodWithParameters(clazz, methodName, paramTypesNames);
123         }
124         return getMethodWithoutParameters(clazz, methodName);
125     }
126 
127     @Nullable
getMethodWithParameters(Class<?> clazz, String methodName, String[] paramTypesNames)128     private static Method getMethodWithParameters(Class<?> clazz, String methodName,
129             String[] paramTypesNames) {
130         if (DBG) {
131             Log.d(TAG, "getMethod(" + clazz  + ", " + methodName + ", "
132                     + Arrays.toString(paramTypesNames) + ")");
133         }
134         // Need to interact trough all methods, otherwise it would be harder to handle java.lang
135         // param types. For example:
136         // - classes like String would need to be prefixed by "java.lang."
137         // - primitive types would need to be handled case by case
138         // Besides, the ApiTest syntax doesn't check for FQCN (for example, it should be just
139         // "Handler" instead of "android.os.Handler");
140         for (String paramTypeName : paramTypesNames) {
141             if (paramTypeName.contains(".")) {
142                 return null;
143             }
144         }
145 
146         Method[] allMethods = clazz.getDeclaredMethods();
147         method:
148         for (Method method : allMethods) {
149             if (DBG) {
150                 Log.v(TAG, "Trying method :"  + method);
151             }
152             if (!method.getName().equals(methodName)) {
153                 continue;
154             }
155             Class<?>[] paramTypes = method.getParameterTypes();
156             if (paramTypes.length != paramTypesNames.length) {
157                 continue;
158             }
159             for (int i = 0; i < paramTypes.length; i++) {
160                 String expected = paramTypesNames[i].trim();
161                 String actual = paramTypes[i].getCanonicalName();
162                 if (DBG) {
163                     Log.d(TAG, "Checking param #" + i + ": expected=" + expected + ", actual="
164                             + actual);
165                 }
166                 if (!actual.endsWith(expected)) {
167                     continue method;
168                 }
169             }
170             if (DBG) {
171                 Log.d(TAG, "Found method :"  + method);
172             }
173             return method;
174         }
175         return null;
176     }
177 
178     @Nullable
getMethodWithoutParameters(Class<?> clazz, String methodName)179     private static Method getMethodWithoutParameters(Class<?> clazz, String methodName) {
180         if (DBG) {
181             Log.d(TAG, "Getting method without params: " + methodName);
182         }
183         List<Method> methods = new ArrayList<>();
184         for (Method method : clazz.getDeclaredMethods()) {
185             if (methodName.equals(method.getName())) {
186                 if (DBG) {
187                     Log.d(TAG, "Found " + methodName + ": " + method);
188                 }
189                 methods.add(method);
190             }
191         }
192         if (methods.size() == 1) {
193             return methods.get(0);
194         }
195         if (DBG) {
196             if (methods.isEmpty()) {
197                 Log.d(TAG, "No method named " + methodName + " on " + clazz);
198             } else {
199                 Log.d(TAG, "Found " + methods.size() + " methods on " + clazz + ": " + methods);
200             }
201         }
202         return null;
203     }
204 
205     @Nullable
getField(String fullyQualifiedFieldName)206     private static Field getField(String fullyQualifiedFieldName) {
207         int classSeparator = fullyQualifiedFieldName.indexOf('#');
208         if (classSeparator == -1) {
209             return null;
210         }
211         String className = fullyQualifiedFieldName.substring(0, classSeparator);
212         String fieldName = fullyQualifiedFieldName.substring(classSeparator + 1,
213                 fullyQualifiedFieldName.length());
214         if (DBG) {
215             Log.d(TAG, "getField(" + fullyQualifiedFieldName + "): class=" + className
216                     + ", field=" + fieldName);
217         }
218         Class<?> clazz = null;
219         try {
220             clazz = Class.forName(className);
221             if (clazz != null) {
222                 return clazz.getDeclaredField(fieldName);
223             }
224         } catch (Exception e) {
225             Log.d(TAG, "getField(" + fullyQualifiedFieldName + ") failed: " + e);
226             if (DBG && clazz != null) {
227                 Log.d(TAG, "Fields of " + clazz + ": "
228                         + Arrays.toString(clazz.getDeclaredFields()));
229             }
230         }
231         return null;
232     }
233 
ApiHelper()234     private ApiHelper() {
235         throw new UnsupportedOperationException("provides only static methods");
236     }
237 }
238