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