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