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