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