summaryrefslogtreecommitdiff
path: root/libraries/compatibility-common-util/src/com/android/compatibility/common/util/BusinessLogicExecutor.java
blob: 8905fce3a5f0097297ca4e99ed17f51d49cd505c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package com.android.compatibility.common.util;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.AssumptionViolatedException;

/**
 * Resolves methods provided by the BusinessLogicService and invokes them
 */
public abstract class BusinessLogicExecutor {

    protected static final String LOG_TAG = "BusinessLogicExecutor";

    /** String representations of the String class and String[] class */
    protected static final String STRING_CLASS = "java.lang.String";
    protected static final String STRING_ARRAY_CLASS = "[Ljava.lang.String;";

    private static final String REDACTED_PLACEHOLDER = "[redacted]";
    /* List of regexes indicating a method arg should be redacted in the logs */
    protected List<String> mRedactionRegexes = new ArrayList<>();

    /**
     * Execute a business logic condition.
     * @param method the name of the method to invoke. Must include fully qualified name of the
     * enclosing class, followed by '.', followed by the name of the method
     * @param args the string arguments to supply to the method
     * @return the return value of the method invoked
     * @throws RuntimeException when failing to resolve or invoke the condition method
     */
    public boolean executeCondition(String method, String... args) {
        logDebug("Executing condition: %s", formatExecutionString(method, args));
        try {
            return (Boolean) invokeMethod(method, args);
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException |
                InvocationTargetException | NoSuchMethodException e) {
            throw new RuntimeException(String.format(
                    "BusinessLogic: Failed to invoke condition method %s with args: %s", method,
                    Arrays.toString(args)), e);
        }
    }

    /**
     * Execute a business logic action.
     * @param method the name of the method to invoke. Must include fully qualified name of the
     * enclosing class, followed by '.', followed by the name of the method
     * @param args the string arguments to supply to the method
     * @throws RuntimeException when failing to resolve or invoke the action method
     */
    public void executeAction(String method, String... args) {
        logDebug("Executing action: %s", formatExecutionString(method, args));
        try {
            invokeMethod(method, args);
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException |
                NoSuchMethodException e) {
            throw new RuntimeException(String.format(
                    "BusinessLogic: Failed to invoke action method %s with args: %s", method,
                    Arrays.toString(args)), e);
        } catch (InvocationTargetException e) {
            // This action throws an exception, so throw the original exception (e.g.
            // AssertionFailedError) for a more readable stacktrace.
            Throwable t = e.getCause();
            if (AssumptionViolatedException.class.isInstance(t)) {
                // This is an assumption failure (registered as a "pass") so don't wrap this
                // throwable in a RuntimeException
                throw (AssumptionViolatedException) t;
            } else {
                RuntimeException re = new RuntimeException(t.getMessage(), t.getCause());
                re.setStackTrace(t.getStackTrace());
                throw re;
            }
        }
    }

    /**
     * Format invocation information as "method(args[0], args[1], ...)".
     */
    protected abstract String formatExecutionString(String method, String... args);

    /** Substitute sensitive information with REDACTED_PLACEHOLDER if necessary. */
    protected String[] formatArgs(String[] args) {
        List<String> formattedArgs = new ArrayList<>();
        for (String arg : args) {
            formattedArgs.add(formatArg(arg));
        }
        return formattedArgs.toArray(new String[0]);
    }

    private String formatArg(String arg) {
        for (String regex : mRedactionRegexes) {
            Pattern pattern = Pattern.compile(regex);
            Matcher matcher = pattern.matcher(arg);
            if (matcher.find()) {
                return REDACTED_PLACEHOLDER;
            }
        }
        return arg;
    }

    /**
     * Execute a business logic method.
     * @param method the name of the method to invoke. Must include fully qualified name of the
     * enclosing class, followed by '.', followed by the name of the method
     * @param args the string arguments to supply to the method
     * @return the return value of the method invoked (type Boolean if method is a condition)
     * @throws RuntimeException when failing to resolve or invoke the method
     */
    protected Object invokeMethod(String method, String... args) throws ClassNotFoundException,
            IllegalAccessException, InstantiationException, InvocationTargetException,
            NoSuchMethodException {
        // Method names served by the BusinessLogic service should assume format
        // classname.methodName, but also handle format classname#methodName since test names use
        // this format
        int index = (method.indexOf('#') == -1) ? method.lastIndexOf('.') : method.indexOf('#');
        if (index == -1) {
            throw new RuntimeException(String.format("BusinessLogic: invalid method name "
                    + "\"%s\". Method string must include fully qualified class name. "
                    + "For example, \"com.android.packagename.ClassName.methodName\".", method));
        }
        String className = method.substring(0, index);
        Class cls = Class.forName(className);
        Object obj = null;
        if (getTestObject() != null && cls.isAssignableFrom(getTestObject().getClass())) {
            // The given method is a member of the test class, use the known test class instance
            obj = getTestObject();
        } else {
            // Only instantiate a new object if we don't already have one.
            // Otherwise the class could have been an interface which isn't instantiatable.
            obj = cls.getDeclaredConstructor().newInstance();
        }
        ResolvedMethod rm = getResolvedMethod(cls, method.substring(index + 1), args);
        return rm.invoke(obj);
    }

    /**
     * Log information with whichever logging mechanism is available to the instance. This varies
     * from host-side to device-side, so implementations are left to subclasses.
     * See {@link String.format(String, Object...)} for parameter information.
     */
    public abstract void logInfo(String format, Object... args);

    /**
     * Log debugging information to the host or device logs (depending on implementation).
     * See {@link String.format(String, Object...)} for parameter information.
     */
    public abstract void logDebug(String format, Object... args);

    /**
     * Get the test object. This method is left abstract, since non-abstract subclasses will set
     * the test object in the constructor.
     * @return the test case instance
     */
    protected abstract Object getTestObject();

    /**
     * Get the method and list of arguments corresponding to the class, method name, and proposed
     * argument values, in the form of a {@link ResolvedMethod} object. This object stores all
     * information required to successfully invoke the method. getResolvedMethod is left abstract,
     * since argument types differ between device-side (e.g. Context) and host-side
     * (e.g. ITestDevice) implementations of this class.
     * @param cls the Class to which the method belongs
     * @param methodName the name of the method to invoke
     * @param args the string arguments to use when invoking the method
     * @return a {@link ResolvedMethod}
     * @throws ClassNotFoundException
     */
    protected abstract ResolvedMethod getResolvedMethod(Class cls, String methodName,
            String... args) throws ClassNotFoundException;

    /**
     * Retrieve all methods within a class that match a given name
     * @param cls the class
     * @param name the method name
     * @return a list of method objects
     */
    protected List<Method> getMethodsWithName(Class cls, String name) {
        List<Method> methodList = new ArrayList<>();
        for (Method m : cls.getMethods()) {
            if (name.equals(m.getName())) {
                methodList.add(m);
            }
        }
        return methodList;
    }

    /**
     * Helper class for storing a method object, and a list of arguments to use when invoking the
     * method. The class is also equipped with an "invoke" method for convenience.
     */
    protected static class ResolvedMethod {
        private Method mMethod;
        List<Object> mArgs;

        public ResolvedMethod(Method method) {
            mMethod = method;
            mArgs = new ArrayList<>();
        }

        /** Add an argument to the argument list for this instance */
        public void addArg(Object arg) {
            mArgs.add(arg);
        }

        /** Invoke the stored method with the stored args on a given object */
        public Object invoke(Object instance) throws IllegalAccessException,
                InvocationTargetException {
            return mMethod.invoke(instance, mArgs.toArray());
        }
    }
}