1 /* 2 * Copyright (c) 2018 Mockito contributors 3 * This program is made available under the terms of the MIT License. 4 */ 5 6 package com.android.dx.mockito.inline; 7 8 import java.lang.ref.WeakReference; 9 import java.lang.reflect.InvocationTargetException; 10 import java.lang.reflect.Method; 11 import java.lang.reflect.Modifier; 12 import java.util.ArrayList; 13 import java.util.Collection; 14 import java.util.List; 15 import java.util.Map; 16 import java.util.concurrent.Callable; 17 import java.util.function.BiConsumer; 18 import java.util.regex.Matcher; 19 import java.util.regex.Pattern; 20 21 import static com.android.dx.mockito.inline.InlineStaticMockMaker.onMethodCallDuringStubbing; 22 import static com.android.dx.mockito.inline.InlineStaticMockMaker.onMethodCallDuringVerification; 23 24 /** 25 * Backend for the method entry hooks. Checks if the hooks should cause an interception or should 26 * be ignored. 27 */ 28 class StaticMockMethodAdvice { 29 /** 30 * Pattern to decompose a instrumentedMethodWithTypeAndSignature 31 */ 32 private final static Pattern methodPattern = Pattern.compile("(.*)#(.*)\\((.*)\\)"); 33 private final Map<Object, InvocationHandlerAdapter> markersToHandler; 34 private final Map<Class, Object> classToMarker; 35 @SuppressWarnings("ThreadLocalUsage") 36 private final SelfCallInfo selfCallInfo = new SelfCallInfo(); 37 StaticMockMethodAdvice(Map<Object, InvocationHandlerAdapter> markerToHandler, Map<Class, Object> classToMarker)38 StaticMockMethodAdvice(Map<Object, InvocationHandlerAdapter> markerToHandler, Map<Class, Object> 39 classToMarker) { 40 this.markersToHandler = markerToHandler; 41 this.classToMarker = classToMarker; 42 } 43 44 /** 45 * Try to invoke the method {@code origin}. 46 * 47 * @param origin method to invoke 48 * @param arguments arguments to the method 49 * @return result of the method 50 * @throws Throwable Exception if thrown by the method 51 */ tryInvoke(Method origin, Object[] arguments)52 private static Object tryInvoke(Method origin, Object[] arguments) 53 throws Throwable { 54 try { 55 return origin.invoke(null, arguments); 56 } catch (InvocationTargetException exception) { 57 throw exception.getCause(); 58 } 59 } 60 classForTypeName(String name)61 private static Class<?> classForTypeName(String name) throws ClassNotFoundException { 62 if (name.endsWith("[]")) { 63 return Class.forName("[L" + name.substring(0, name.length() - 2) + ";"); 64 } else { 65 return Class.forName(name); 66 } 67 } 68 nameToType(String name)69 private static Class nameToType(String name) throws ClassNotFoundException { 70 switch (name) { 71 case "byte": 72 return Byte.TYPE; 73 case "short": 74 return Short.TYPE; 75 case "int": 76 return Integer.TYPE; 77 case "long": 78 return Long.TYPE; 79 case "char": 80 return Character.TYPE; 81 case "float": 82 return Float.TYPE; 83 case "double": 84 return Double.TYPE; 85 case "boolean": 86 return Boolean.TYPE; 87 case "byte[]": 88 return byte[].class; 89 case "short[]": 90 return short[].class; 91 case "int[]": 92 return int[].class; 93 case "long[]": 94 return long[].class; 95 case "char[]": 96 return char[].class; 97 case "float[]": 98 return float[].class; 99 case "double[]": 100 return double[].class; 101 case "boolean[]": 102 return boolean[].class; 103 default: 104 return classForTypeName(name); 105 } 106 } 107 108 /** 109 * Would a call to SubClass.method handled by SuperClass.method ? 110 * <p>This is the case when subclass or any intermediate parent does not override method. 111 * 112 * @param subclass Class that might have been called 113 * @param superClass Class defining the method 114 * @param methodName Name of method 115 * @param methodParameters Parameter of method 116 * @return {code true} iff the method would have be handled by superClass 117 */ isMethodDefinedBySuperClass(Class<?> subclass, Class<?> superClass, String methodName, Class<?>[] methodParameters)118 private static boolean isMethodDefinedBySuperClass(Class<?> subclass, Class<?> superClass, 119 String methodName, 120 Class<?>[] methodParameters) { 121 do { 122 if (subclass == superClass) { 123 // The method is not overridden in the subclass or any class in between subClass 124 // and superClass. 125 return true; 126 } 127 128 try { 129 subclass.getDeclaredMethod(methodName, methodParameters); 130 131 // method is overridden is sub-class. hence the call could not have handled by 132 // the super-class. 133 return false; 134 } catch (NoSuchMethodException e) { 135 subclass = subclass.getSuperclass(); 136 } 137 } while (subclass != null); 138 139 // Subclass is not a sub class of superClass 140 return false; 141 } 142 getAllSubclasses(Class<?> superClass, Collection<Class> possibleSubClasses)143 private static List<Class<?>> getAllSubclasses(Class<?> superClass, Collection<Class> 144 possibleSubClasses) { 145 ArrayList<Class<?>> subclasses = new ArrayList<>(); 146 for (Class<?> possibleSubClass : possibleSubClasses) { 147 if (superClass.isAssignableFrom(possibleSubClass)) { 148 subclasses.add(possibleSubClass); 149 } 150 } 151 152 return subclasses; 153 } 154 nativeGetCalledClassName(Thread currentThread)155 private synchronized static native String nativeGetCalledClassName(Thread currentThread); 156 getClassMethodWasCalledOn(MethodDesc methodDesc)157 private Class<?> getClassMethodWasCalledOn(MethodDesc methodDesc) throws ClassNotFoundException, 158 NoSuchMethodException { 159 Class<?> classDeclaringMethod = classForTypeName(methodDesc.className); 160 161 /* If a sub-class does not override a static method, the super-classes method is called 162 * directly. Hence 'classDeclaringMethod' will be the super class. As the mocking of 163 * this and the class actually called might be different we need to find the class that 164 * was actually called. 165 */ 166 if (Modifier.isFinal(classDeclaringMethod.getModifiers()) 167 || Modifier.isFinal(classDeclaringMethod.getDeclaredMethod(methodDesc.methodName, 168 methodDesc.methodParamTypes).getModifiers())) { 169 return classDeclaringMethod; 170 } else { 171 boolean mightBeMocked = false; 172 // if neither the defining class nor any subclass of it is mocked, no point of 173 // trying to figure out the called class as isMocked will soon be checked. 174 for (Class<?> subClass : getAllSubclasses(classDeclaringMethod, classToMarker.keySet())) { 175 if (isMethodDefinedBySuperClass(subClass, classDeclaringMethod, 176 methodDesc.methodName, methodDesc.methodParamTypes)) { 177 mightBeMocked = true; 178 break; 179 } 180 } 181 182 if (!mightBeMocked) { 183 return null; 184 } 185 186 String calledClassName = nativeGetCalledClassName(Thread.currentThread()); 187 return Class.forName(calledClassName); 188 } 189 } 190 191 /** 192 * Get the method specified by {@code methodWithTypeAndSignature}. 193 * 194 * @param ignored 195 * @param methodWithTypeAndSignature the description of the method 196 * @return method {@code methodWithTypeAndSignature} refer to 197 */ 198 @SuppressWarnings("unused") getOrigin(Object ignored, String methodWithTypeAndSignature)199 public Method getOrigin(Object ignored, String methodWithTypeAndSignature) throws Throwable { 200 MethodDesc methodDesc = new MethodDesc(methodWithTypeAndSignature); 201 202 Class clazz = getClassMethodWasCalledOn(methodDesc); 203 if (clazz == null) { 204 return null; 205 } 206 207 Object marker = classToMarker.get(clazz); 208 if (!isMocked(marker)) { 209 return null; 210 } 211 212 return Class.forName(methodDesc.className).getDeclaredMethod(methodDesc.methodName, 213 methodDesc.methodParamTypes); 214 } 215 216 /** 217 * Handle a method entry hook. 218 * 219 * @param origin method that contains the hook 220 * @param arguments arguments to the method 221 * @return A callable that can be called to get the mocked result or null if the method is not 222 * mocked. 223 */ 224 @SuppressWarnings("unused") handle(Object methodDescStr, Method origin, Object[] arguments)225 public Callable<?> handle(Object methodDescStr, Method origin, Object[] arguments) throws 226 Throwable { 227 MethodDesc methodDesc = new MethodDesc((String) methodDescStr); 228 Class clazz = getClassMethodWasCalledOn(methodDesc); 229 230 Object marker = classToMarker.get(clazz); 231 InvocationHandlerAdapter interceptor = markersToHandler.get(marker); 232 if (interceptor == null) { 233 return null; 234 } 235 236 // extended.StaticCapableStubber#whenInt 237 BiConsumer<Class<?>, Method> onStub = onMethodCallDuringStubbing.get(); 238 if (onStub != null) { 239 onStub.accept(clazz, origin); 240 } 241 242 // extended.ExtendedMockito#verifyInt 243 BiConsumer<Class<?>, Method> onVerify = onMethodCallDuringVerification.get(); 244 if (onVerify != null) { 245 onVerify.accept(clazz, origin); 246 } 247 248 return new ReturnValueWrapper(interceptor.interceptEntryHook(marker, origin, arguments, 249 new SuperMethodCall(selfCallInfo, origin, marker, arguments))); 250 } 251 252 /** 253 * Checks if an {@code marker} is a mock marker. 254 * 255 * @return {@code true} iff the marker is a mock marker 256 */ isMarker(Object marker)257 public boolean isMarker(Object marker) { 258 return markersToHandler.containsKey(marker); 259 } 260 261 /** 262 * Check if this method call should be mocked. Usually the same as {@link #isMarker(Object)} but 263 * takes into account the state of {@link #selfCallInfo} that allows to temporary disable 264 * mocking for a single method call. 265 */ isMocked(Object marker)266 public boolean isMocked(Object marker) { 267 return selfCallInfo.shouldMockMethod(marker) && isMarker(marker); 268 } 269 270 private static class MethodDesc { 271 final String className; 272 final String methodName; 273 final Class<?>[] methodParamTypes; 274 MethodDesc(String methodWithTypeAndSignature)275 private MethodDesc(String methodWithTypeAndSignature) throws ClassNotFoundException { 276 Matcher methodComponents = methodPattern.matcher(methodWithTypeAndSignature); 277 boolean wasFound = methodComponents.find(); 278 if (!wasFound) { 279 throw new IllegalArgumentException(); 280 } 281 282 className = methodComponents.group(1); 283 methodName = methodComponents.group(2); 284 String methodParamTypeNames[] = methodComponents.group(3).split(","); 285 286 ArrayList<Class<?>> methodParamTypesList = new ArrayList<>(methodParamTypeNames.length); 287 for (String methodParamName : methodParamTypeNames) { 288 if (!methodParamName.equals("")) { 289 methodParamTypesList.add(nameToType(methodParamName)); 290 } 291 } 292 methodParamTypes = methodParamTypesList.toArray(new Class<?>[]{}); 293 } 294 295 @Override toString()296 public String toString() { 297 return className + "#" + methodName; 298 } 299 } 300 301 /** 302 * Used to call the real (non mocked) method. 303 */ 304 private static class SuperMethodCall implements InvocationHandlerAdapter.SuperMethod { 305 private final SelfCallInfo selfCallInfo; 306 private final Method origin; 307 private final WeakReference<Object> marker; 308 private final Object[] arguments; 309 SuperMethodCall(SelfCallInfo selfCallInfo, Method origin, Object marker, Object[] arguments)310 private SuperMethodCall(SelfCallInfo selfCallInfo, Method origin, Object marker, 311 Object[] arguments) { 312 this.selfCallInfo = selfCallInfo; 313 this.origin = origin; 314 this.marker = new WeakReference(marker); 315 this.arguments = arguments; 316 } 317 318 /** 319 * Call the read (non mocked) method. 320 * 321 * @return Result of read method 322 * @throws Throwable thrown by the read method 323 */ 324 @Override invoke()325 public Object invoke() throws Throwable { 326 if (!Modifier.isPublic(origin.getDeclaringClass().getModifiers() 327 & origin.getModifiers())) { 328 origin.setAccessible(true); 329 } 330 331 // By setting instance in the the selfCallInfo, once single method call on this instance 332 // and thread will call the read method as isMocked will return false. 333 selfCallInfo.set(marker.get()); 334 return tryInvoke(origin, arguments); 335 } 336 337 } 338 339 /** 340 * Stores a return value of {@link #handle(Object, Method, Object[])} and returns in on 341 * {@link #call()}. 342 */ 343 private static class ReturnValueWrapper implements Callable<Object> { 344 private final Object returned; 345 ReturnValueWrapper(Object returned)346 private ReturnValueWrapper(Object returned) { 347 this.returned = returned; 348 } 349 350 @Override call()351 public Object call() { 352 return returned; 353 } 354 } 355 356 /** 357 * Used to call the original method. If a instance is {@link #set(Object)} 358 * {@link #shouldMockMethod(Object)} returns false for this instance once. 359 * <p>This is {@link ThreadLocal}, so a thread can {@link #set(Object)} and instance and then 360 * call {@link #shouldMockMethod(Object)} without interference. 361 * 362 * @see SuperMethodCall#invoke() 363 * @see #isMocked(Object) 364 */ 365 private static class SelfCallInfo extends ThreadLocal<Object> { shouldMockMethod(Object value)366 boolean shouldMockMethod(Object value) { 367 Object current = get(); 368 369 if (current == value) { 370 set(null); 371 return false; 372 } else { 373 return true; 374 } 375 } 376 } 377 } 378