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 invokation 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