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