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