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 17 package com.android.compatibility.common.util; 18 19 import com.google.errorprone.annotations.CanIgnoreReturnValue; 20 21 import org.junit.AssumptionViolatedException; 22 23 import java.lang.reflect.InvocationTargetException; 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.regex.Matcher; 29 import java.util.regex.Pattern; 30 31 /** 32 * Resolves methods provided by the BusinessLogicService and invokes them 33 */ 34 public abstract class BusinessLogicExecutor { 35 36 protected static final String LOG_TAG = "BusinessLogicExecutor"; 37 38 /** String representations of the String class and String[] class */ 39 protected static final String STRING_CLASS = "java.lang.String"; 40 protected static final String STRING_ARRAY_CLASS = "[Ljava.lang.String;"; 41 42 private static final String REDACTED_PLACEHOLDER = "[redacted]"; 43 /* List of regexes indicating a method arg should be redacted in the logs */ 44 protected List<String> mRedactionRegexes = new ArrayList<>(); 45 46 /** 47 * Execute a business logic condition. 48 * @param method the name of the method to invoke. Must include fully qualified name of the 49 * enclosing class, followed by '.', followed by the name of the method 50 * @param args the string arguments to supply to the method 51 * @return the return value of the method invoked 52 * @throws RuntimeException when failing to resolve or invoke the condition method 53 */ executeCondition(String method, String... args)54 public boolean executeCondition(String method, String... args) { 55 logDebug("Executing condition: %s", formatExecutionString(method, args)); 56 try { 57 return (Boolean) invokeMethod(method, args); 58 } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | 59 InvocationTargetException | NoSuchMethodException e) { 60 throw new RuntimeException(String.format( 61 "BusinessLogic: Failed to invoke condition method %s with args: %s", method, 62 Arrays.toString(args)), e); 63 } 64 } 65 66 /** 67 * Execute a business logic action. 68 * @param method the name of the method to invoke. Must include fully qualified name of the 69 * enclosing class, followed by '.', followed by the name of the method 70 * @param args the string arguments to supply to the method 71 * @throws RuntimeException when failing to resolve or invoke the action method 72 */ executeAction(String method, String... args)73 public void executeAction(String method, String... args) { 74 logDebug("Executing action: %s", formatExecutionString(method, args)); 75 try { 76 invokeMethod(method, args); 77 } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | 78 NoSuchMethodException e) { 79 throw new RuntimeException(String.format( 80 "BusinessLogic: Failed to invoke action method %s with args: %s", method, 81 Arrays.toString(args)), e); 82 } catch (InvocationTargetException e) { 83 // This action throws an exception, so throw the original exception (e.g. 84 // AssertionFailedError) for a more readable stacktrace. 85 Throwable t = e.getCause(); 86 if (AssumptionViolatedException.class.isInstance(t)) { 87 // This is an assumption failure (registered as a "pass") so don't wrap this 88 // throwable in a RuntimeException 89 throw (AssumptionViolatedException) t; 90 } else { 91 RuntimeException re = new RuntimeException(t.getMessage(), t.getCause()); 92 re.setStackTrace(t.getStackTrace()); 93 throw re; 94 } 95 } 96 } 97 98 /** 99 * Format invocation information as "method(args[0], args[1], ...)". 100 */ formatExecutionString(String method, String... args)101 protected abstract String formatExecutionString(String method, String... args); 102 103 /** Substitute sensitive information with REDACTED_PLACEHOLDER if necessary. */ formatArgs(String[] args)104 protected String[] formatArgs(String[] args) { 105 List<String> formattedArgs = new ArrayList<>(); 106 for (String arg : args) { 107 formattedArgs.add(formatArg(arg)); 108 } 109 return formattedArgs.toArray(new String[0]); 110 } 111 formatArg(String arg)112 private String formatArg(String arg) { 113 for (String regex : mRedactionRegexes) { 114 Pattern pattern = Pattern.compile(regex); 115 Matcher matcher = pattern.matcher(arg); 116 if (matcher.find()) { 117 return REDACTED_PLACEHOLDER; 118 } 119 } 120 return arg; 121 } 122 123 /** 124 * Execute a business logic method. 125 * 126 * @param method the name of the method to invoke. Must include fully qualified name of the 127 * enclosing class, followed by '.', followed by the name of the method 128 * @param args the string arguments to supply to the method 129 * @return the return value of the method invoked (type Boolean if method is a condition) 130 * @throws RuntimeException when failing to resolve or invoke the method 131 */ 132 @CanIgnoreReturnValue invokeMethod(String method, String... args)133 protected Object invokeMethod(String method, String... args) 134 throws ClassNotFoundException, IllegalAccessException, InstantiationException, 135 InvocationTargetException, NoSuchMethodException { 136 // Method names served by the BusinessLogic service should assume format 137 // classname.methodName, but also handle format classname#methodName since test names use 138 // this format 139 int index = (method.indexOf('#') == -1) ? method.lastIndexOf('.') : method.indexOf('#'); 140 if (index == -1) { 141 throw new RuntimeException(String.format("BusinessLogic: invalid method name " 142 + "\"%s\". Method string must include fully qualified class name. " 143 + "For example, \"com.android.packagename.ClassName.methodName\".", method)); 144 } 145 String className = method.substring(0, index); 146 Class cls = Class.forName(className); 147 Object obj = null; 148 if (getTestObject() != null && cls.isAssignableFrom(getTestObject().getClass())) { 149 // The given method is a member of the test class, use the known test class instance 150 obj = getTestObject(); 151 } else { 152 // Only instantiate a new object if we don't already have one. 153 // Otherwise the class could have been an interface which isn't instantiatable. 154 obj = cls.getDeclaredConstructor().newInstance(); 155 } 156 ResolvedMethod rm = getResolvedMethod(cls, method.substring(index + 1), args); 157 return rm.invoke(obj); 158 } 159 160 /** 161 * Log information with whichever logging mechanism is available to the instance. This varies 162 * from host-side to device-side, so implementations are left to subclasses. 163 * See {@link String.format(String, Object...)} for parameter information. 164 */ logInfo(String format, Object... args)165 public abstract void logInfo(String format, Object... args); 166 167 /** 168 * Log debugging information to the host or device logs (depending on implementation). 169 * See {@link String.format(String, Object...)} for parameter information. 170 */ logDebug(String format, Object... args)171 public abstract void logDebug(String format, Object... args); 172 173 /** 174 * Get the test object. This method is left abstract, since non-abstract subclasses will set 175 * the test object in the constructor. 176 * @return the test case instance 177 */ getTestObject()178 protected abstract Object getTestObject(); 179 180 /** 181 * Get the method and list of arguments corresponding to the class, method name, and proposed 182 * argument values, in the form of a {@link ResolvedMethod} object. This object stores all 183 * information required to successfully invoke the method. getResolvedMethod is left abstract, 184 * since argument types differ between device-side (e.g. Context) and host-side 185 * (e.g. ITestDevice) implementations of this class. 186 * @param cls the Class to which the method belongs 187 * @param methodName the name of the method to invoke 188 * @param args the string arguments to use when invoking the method 189 * @return a {@link ResolvedMethod} 190 * @throws ClassNotFoundException 191 */ getResolvedMethod(Class cls, String methodName, String... args)192 protected abstract ResolvedMethod getResolvedMethod(Class cls, String methodName, 193 String... args) throws ClassNotFoundException; 194 195 /** 196 * Retrieve all methods within a class that match a given name 197 * @param cls the class 198 * @param name the method name 199 * @return a list of method objects 200 */ getMethodsWithName(Class cls, String name)201 protected List<Method> getMethodsWithName(Class cls, String name) { 202 List<Method> methodList = new ArrayList<>(); 203 for (Method m : cls.getMethods()) { 204 if (name.equals(m.getName())) { 205 methodList.add(m); 206 } 207 } 208 return methodList; 209 } 210 211 /** 212 * Helper class for storing a method object, and a list of arguments to use when invoking the 213 * method. The class is also equipped with an "invoke" method for convenience. 214 */ 215 protected static class ResolvedMethod { 216 private Method mMethod; 217 List<Object> mArgs; 218 ResolvedMethod(Method method)219 public ResolvedMethod(Method method) { 220 mMethod = method; 221 mArgs = new ArrayList<>(); 222 } 223 224 /** Add an argument to the argument list for this instance */ addArg(Object arg)225 public void addArg(Object arg) { 226 mArgs.add(arg); 227 } 228 229 /** Invoke the stored method with the stored args on a given object */ invoke(Object instance)230 public Object invoke(Object instance) throws IllegalAccessException, 231 InvocationTargetException { 232 return mMethod.invoke(instance, mArgs.toArray()); 233 } 234 } 235 } 236