1 package org.robolectric.internal.bytecode; 2 3 import static java.lang.invoke.MethodHandles.constant; 4 import static java.lang.invoke.MethodHandles.dropArguments; 5 import static java.lang.invoke.MethodHandles.foldArguments; 6 import static java.lang.invoke.MethodHandles.identity; 7 import static java.lang.invoke.MethodType.methodType; 8 9 import java.lang.invoke.MethodHandle; 10 import java.lang.invoke.MethodHandles; 11 import java.lang.invoke.MethodType; 12 import java.lang.reflect.Array; 13 import java.lang.reflect.Field; 14 import java.lang.reflect.InvocationTargetException; 15 import java.lang.reflect.Method; 16 import java.lang.reflect.Modifier; 17 import java.util.ArrayList; 18 import java.util.Collections; 19 import java.util.HashMap; 20 import java.util.LinkedHashMap; 21 import java.util.List; 22 import java.util.Map; 23 import java.util.concurrent.ConcurrentHashMap; 24 import org.robolectric.annotation.Implementation; 25 import org.robolectric.annotation.Implements; 26 import org.robolectric.annotation.RealObject; 27 import org.robolectric.util.Function; 28 import org.robolectric.util.ReflectionHelpers; 29 30 public class ShadowWrangler implements ClassHandler { 31 public static final Function<Object, Object> DO_NOTHING_HANDLER = new Function<Object, Object>() { 32 @Override 33 public Object call(Class<?> theClass, Object value, Object[] params) { 34 return null; 35 } 36 }; 37 public static final Plan DO_NOTHING_PLAN = new Plan() { 38 @Override 39 public Object run(Object instance, Object roboData, Object[] params) throws Exception { 40 return null; 41 } 42 43 @Override 44 public String describe() { 45 return "do nothing"; 46 } 47 }; 48 public static final Plan CALL_REAL_CODE_PLAN = null; 49 public static final MethodHandle CALL_REAL_CODE = null; 50 public static final MethodHandle DO_NOTHING = constant(Void.class, null).asType(methodType(void.class)); 51 private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); 52 private static final boolean STRIP_SHADOW_STACK_TRACES = true; 53 private static final ShadowConfig NO_SHADOW_CONFIG = new ShadowConfig(Object.class.getName(), true, false, false, -1, -1); 54 static final Object NO_SHADOW = new Object(); 55 private static final MethodHandle NO_SHADOW_HANDLE = constant(Object.class, NO_SHADOW); 56 private final ShadowMap shadowMap; 57 private final Interceptors interceptors; 58 private final int apiLevel; 59 private final Map<Class, MetaShadow> metaShadowMap = new HashMap<>(); 60 private final Map<String, Plan> planCache = 61 Collections.synchronizedMap(new LinkedHashMap<String, Plan>() { 62 @Override 63 protected boolean removeEldestEntry(Map.Entry<String, Plan> eldest) { 64 return size() > 500; 65 } 66 }); 67 private final Map<Class, ShadowConfig> shadowConfigCache = new ConcurrentHashMap<>(); 68 private final ClassValue<ShadowConfig> shadowConfigs = new ClassValue<ShadowConfig>() { 69 @Override protected ShadowConfig computeValue(Class<?> type) { 70 return shadowMap.get(type); 71 } 72 }; 73 74 public ShadowWrangler(ShadowMap shadowMap, int apiLevel, Interceptors interceptors) { 75 this.shadowMap = shadowMap; 76 this.apiLevel = apiLevel; 77 this.interceptors = interceptors; 78 } 79 80 public static Class<?> loadClass(String paramType, ClassLoader classLoader) { 81 Class primitiveClass = RoboType.findPrimitiveClass(paramType); 82 if (primitiveClass != null) return primitiveClass; 83 84 int arrayLevel = 0; 85 while (paramType.endsWith("[]")) { 86 arrayLevel++; 87 paramType = paramType.substring(0, paramType.length() - 2); 88 } 89 90 Class<?> clazz = RoboType.findPrimitiveClass(paramType); 91 if (clazz == null) { 92 try { 93 clazz = classLoader.loadClass(paramType); 94 } catch (ClassNotFoundException e) { 95 throw new RuntimeException(e); 96 } 97 } 98 99 while (arrayLevel-- > 0) { 100 clazz = Array.newInstance(clazz, 0).getClass(); 101 } 102 103 return clazz; 104 } 105 106 @Override 107 public void classInitializing(Class clazz) { 108 Class<?> shadowClass = findDirectShadowClass(clazz); 109 if (shadowClass != null) { 110 try { 111 Method method = shadowClass.getDeclaredMethod(ShadowConstants.STATIC_INITIALIZER_METHOD_NAME); 112 if (!Modifier.isStatic(method.getModifiers())) { 113 throw new RuntimeException(shadowClass.getName() + "." + method.getName() + " is not static"); 114 } 115 method.setAccessible(true); 116 method.invoke(null); 117 } catch (NoSuchMethodException e) { 118 RobolectricInternals.performStaticInitialization(clazz); 119 } catch (InvocationTargetException | IllegalAccessException e) { 120 throw new RuntimeException(e); 121 } 122 } else { 123 RobolectricInternals.performStaticInitialization(clazz); 124 } 125 } 126 127 @Override 128 public Object initializing(Object instance) { 129 return createShadowFor(instance); 130 } 131 132 @Override 133 public Plan methodInvoked(String signature, boolean isStatic, Class<?> theClass) { 134 if (planCache.containsKey(signature)) { 135 return planCache.get(signature); 136 } 137 Plan plan = calculatePlan(signature, isStatic, theClass); 138 planCache.put(signature, plan); 139 return plan; 140 } 141 142 @Override public MethodHandle findShadowMethod(Class<?> caller, String name, MethodType type, 143 boolean isStatic) throws IllegalAccessException { 144 ShadowConfig shadowConfig = shadowConfigs.get(caller); 145 if (shadowConfig == null) return CALL_REAL_CODE; 146 147 ClassLoader classLoader = caller.getClassLoader(); 148 MethodType actualType = isStatic ? type : type.dropParameterTypes(0, 1); 149 Method method = findShadowMethod(classLoader, shadowConfig, name, actualType.parameterArray()); 150 if (method == null) { 151 return shadowConfig.callThroughByDefault ? CALL_REAL_CODE : DO_NOTHING; 152 } 153 154 Class<?> declaredShadowedClass = getShadowedClass(method); 155 if (declaredShadowedClass.equals(Object.class)) { 156 // e.g. for equals(), hashCode(), toString() 157 return CALL_REAL_CODE; 158 } 159 160 boolean shadowClassMismatch = !declaredShadowedClass.equals(caller); 161 if (shadowClassMismatch && !shadowConfig.inheritImplementationMethods) { 162 return CALL_REAL_CODE; 163 } else { 164 MethodHandle mh = LOOKUP.unreflect(method); 165 166 // Robolectric doesn't actually look for static, this for example happens 167 // in MessageQueue.nativeInit() which used to be void non-static in 4.2. 168 if (!isStatic && Modifier.isStatic(method.getModifiers())) { 169 return dropArguments(mh, 0, Object.class); 170 } else { 171 return mh; 172 } 173 } 174 } 175 176 private Plan calculatePlan(String signature, boolean isStatic, Class<?> theClass) { 177 final InvocationProfile invocationProfile = new InvocationProfile(signature, isStatic, theClass.getClassLoader()); 178 ShadowConfig shadowConfig = getShadowConfig(theClass); 179 180 if (shadowConfig == null || !shadowConfig.supportsSdk(apiLevel)) { 181 return CALL_REAL_CODE_PLAN; 182 } else { 183 try { 184 final ClassLoader classLoader = theClass.getClassLoader(); 185 Class<?>[] types = invocationProfile.getParamClasses(classLoader); 186 Method shadowMethod = findShadowMethod(classLoader, shadowConfig, invocationProfile.methodName, types); 187 if (shadowMethod == null) { 188 return shadowConfig.callThroughByDefault 189 ? CALL_REAL_CODE_PLAN 190 : strict(invocationProfile) ? CALL_REAL_CODE_PLAN : DO_NOTHING_PLAN; 191 } 192 193 final Class<?> declaredShadowedClass = getShadowedClass(shadowMethod); 194 195 if (declaredShadowedClass.equals(Object.class)) { 196 // e.g. for equals(), hashCode(), toString() 197 return CALL_REAL_CODE_PLAN; 198 } 199 200 boolean shadowClassMismatch = !declaredShadowedClass.equals(invocationProfile.clazz); 201 if (shadowClassMismatch && (!shadowConfig.inheritImplementationMethods || strict(invocationProfile))) { 202 return CALL_REAL_CODE_PLAN; 203 } else { 204 return new ShadowMethodPlan(shadowMethod); 205 } 206 } catch (ClassNotFoundException e) { 207 throw new RuntimeException(e); 208 } 209 } 210 } 211 212 private Method findShadowMethod(ClassLoader classLoader, ShadowConfig config, String name, Class<?>[] types) { 213 Class<?> shadowClass; 214 try { 215 shadowClass = Class.forName(config.shadowClassName, false, classLoader); 216 } catch (ClassNotFoundException e) { 217 throw new IllegalStateException(e); 218 } 219 220 return findShadowMethod(config, shadowClass, name, types); 221 } 222 223 private Method findShadowMethod(ShadowConfig config, Class<?> shadowClass, String name, Class<?>[] types) { 224 Method method = findShadowMethodInternal(shadowClass, name, types); 225 226 if (method == null && config.looseSignatures) { 227 Class<?>[] genericTypes = MethodType.genericMethodType(types.length).parameterArray(); 228 method = findShadowMethodInternal(shadowClass, name, genericTypes); 229 } 230 231 Class<?> superclass; 232 if (method == null && config.inheritImplementationMethods && (superclass = shadowClass.getSuperclass()) != null) { 233 return findShadowMethod(config, superclass, name, types); 234 } 235 236 return method; 237 } 238 239 @SuppressWarnings("ReferenceEquality") 240 private ShadowConfig getShadowConfig(Class clazz) { 241 ShadowConfig shadowConfig = shadowConfigCache.get(clazz); 242 if (shadowConfig == null) { 243 shadowConfig = shadowMap.get(clazz); 244 shadowConfigCache.put(clazz, shadowConfig == null ? NO_SHADOW_CONFIG : shadowConfig); 245 return shadowConfig; 246 } else { 247 return (shadowConfig == NO_SHADOW_CONFIG) ? null : shadowConfig; 248 } 249 } 250 251 private boolean isAndroidSupport(InvocationProfile invocationProfile) { 252 return invocationProfile.clazz.getName().startsWith("android.support"); 253 } 254 255 private boolean strict(InvocationProfile invocationProfile) { 256 return isAndroidSupport(invocationProfile) || invocationProfile.isDeclaredOnObject(); 257 } 258 259 private Method findShadowMethodInternal(Class<?> shadowClass, String methodName, Class<?>[] paramClasses) { 260 try { 261 Method method = shadowClass.getDeclaredMethod(methodName, paramClasses); 262 method.setAccessible(true); 263 Implementation implementation = getImplementationAnnotation(method); 264 return matchesSdk(implementation) ? method : null; 265 266 // todo: allow per-version overloading 267 // if (method == null) { 268 // String methodPrefix = name + "$$"; 269 // for (Method candidateMethod : shadowClass.getMethods()) { 270 // if (candidateMethod.getName().startsWith(methodPrefix)) { 271 // 272 // } 273 // } 274 // } 275 276 } catch (NoSuchMethodException e) { 277 return null; 278 } 279 } 280 281 private boolean matchesSdk(Implementation implementation) { 282 return implementation.minSdk() <= apiLevel && (implementation.maxSdk() == -1 || implementation.maxSdk() >= apiLevel); 283 } 284 285 private Class<?> getShadowedClass(Method shadowMethod) { 286 Class<?> shadowingClass = shadowMethod.getDeclaringClass(); 287 if (shadowingClass.equals(Object.class)) { 288 return Object.class; 289 } 290 291 Implements implementsAnnotation = shadowingClass.getAnnotation(Implements.class); 292 if (implementsAnnotation == null) { 293 throw new RuntimeException(shadowingClass + " has no @" + Implements.class.getSimpleName() + " annotation"); 294 } 295 String shadowedClassName = implementsAnnotation.className(); 296 if (shadowedClassName.isEmpty()) { 297 return implementsAnnotation.value(); 298 } else { 299 try { 300 return shadowingClass.getClassLoader().loadClass(shadowedClassName); 301 } catch (ClassNotFoundException e) { 302 throw new RuntimeException(e); 303 } 304 } 305 } 306 307 private Implementation getImplementationAnnotation(Method method) { 308 if (method == null) { 309 return null; 310 } 311 Implementation implementation = method.getAnnotation(Implementation.class); 312 return implementation == null 313 ? ReflectionHelpers.defaultsFor(Implementation.class) 314 : implementation; 315 } 316 317 @Override 318 public Object intercept(String signature, Object instance, Object[] params, Class theClass) throws Throwable { 319 final MethodSignature methodSignature = MethodSignature.parse(signature); 320 return interceptors.getInterceptionHandler(methodSignature).call(theClass, instance, params); 321 } 322 323 @Override 324 public <T extends Throwable> T stripStackTrace(T throwable) { 325 if (STRIP_SHADOW_STACK_TRACES) { 326 List<StackTraceElement> stackTrace = new ArrayList<>(); 327 328 String previousClassName = null; 329 String previousMethodName = null; 330 String previousFileName = null; 331 332 for (StackTraceElement stackTraceElement : throwable.getStackTrace()) { 333 String methodName = stackTraceElement.getMethodName(); 334 String className = stackTraceElement.getClassName(); 335 String fileName = stackTraceElement.getFileName(); 336 337 if (methodName.equals(previousMethodName) 338 && className.equals(previousClassName) 339 && fileName != null && fileName.equals(previousFileName) 340 && stackTraceElement.getLineNumber() < 0) { 341 continue; 342 } 343 344 if (className.equals(ShadowMethodPlan.class.getName())) { 345 continue; 346 } 347 348 if (methodName.startsWith(ShadowConstants.ROBO_PREFIX)) { 349 methodName = methodName.substring(ShadowConstants.ROBO_PREFIX.length()); 350 stackTraceElement = new StackTraceElement(className, methodName, 351 stackTraceElement.getFileName(), stackTraceElement.getLineNumber()); 352 } 353 354 if (className.startsWith("sun.reflect.") || className.startsWith("java.lang.reflect.")) { 355 continue; 356 } 357 358 stackTrace.add(stackTraceElement); 359 360 previousClassName = className; 361 previousMethodName = methodName; 362 previousFileName = fileName; 363 } 364 throwable.setStackTrace(stackTrace.toArray(new StackTraceElement[stackTrace.size()])); 365 } 366 return throwable; 367 } 368 369 public Object createShadowFor(Object instance) { 370 String shadowClassName = getShadowClassName(instance.getClass()); 371 372 if (shadowClassName == null) return NO_SHADOW; 373 374 try { 375 Class<?> shadowClass = loadClass(shadowClassName, instance.getClass().getClassLoader()); 376 Object shadow = shadowClass.getDeclaredConstructor().newInstance(); 377 injectRealObjectOn(shadow, shadowClass, instance); 378 379 return shadow; 380 } catch (InstantiationException | IllegalAccessException | NoSuchMethodException 381 | InvocationTargetException e) { 382 throw new RuntimeException("Could not instantiate shadow, missing public empty constructor.", e); 383 } 384 } 385 386 @Override public MethodHandle getShadowCreator(Class<?> caller) { 387 String shadowClassName = getShadowClassNameInvoke(caller); 388 389 if (shadowClassName == null) return dropArguments(NO_SHADOW_HANDLE, 0, caller); 390 391 try { 392 Class<?> shadowClass = Class.forName(shadowClassName, false, caller.getClassLoader()); 393 MethodHandle constructor = LOOKUP.findConstructor(shadowClass, methodType(void.class)); 394 MetaShadow metaShadow = getMetaShadow(shadowClass); 395 396 MethodHandle mh = identity(shadowClass); // (instance) 397 mh = dropArguments(mh, 1, caller); // (instance) 398 for (Field field : metaShadow.realObjectFields) { 399 MethodHandle setter = LOOKUP.unreflectSetter(field); 400 MethodType setterType = mh.type().changeReturnType(void.class); 401 mh = foldArguments(mh, setter.asType(setterType)); 402 } 403 mh = foldArguments(mh, constructor); // (shadow, instance) 404 405 return mh; // (instance) 406 } catch (NoSuchMethodException | IllegalAccessException e) { 407 throw new RuntimeException("Could not instantiate shadow, missing public empty constructor.", e); 408 } catch (ClassNotFoundException e) { 409 throw new RuntimeException("Could not instantiate shadow", e); 410 } 411 } 412 413 private String getShadowClassNameInvoke(Class<?> cl) { 414 Class clazz = cl; 415 ShadowConfig shadowConfig = null; 416 while (shadowConfig == null && clazz != null) { 417 shadowConfig = shadowConfigs.get(clazz); 418 clazz = clazz.getSuperclass(); 419 } 420 return shadowConfig == null ? null : shadowConfig.shadowClassName; 421 } 422 423 private String getShadowClassName(Class<?> cl) { 424 Class clazz = cl; 425 ShadowConfig shadowConfig = null; 426 while ((shadowConfig == null || !shadowConfig.supportsSdk(apiLevel)) && clazz != null) { 427 shadowConfig = getShadowConfig(clazz); 428 clazz = clazz.getSuperclass(); 429 } 430 return shadowConfig == null ? null : shadowConfig.shadowClassName; 431 } 432 433 private void injectRealObjectOn(Object shadow, Class<?> shadowClass, Object instance) { 434 MetaShadow metaShadow = getMetaShadow(shadowClass); 435 for (Field realObjectField : metaShadow.realObjectFields) { 436 writeField(shadow, instance, realObjectField); 437 } 438 } 439 440 private MetaShadow getMetaShadow(Class<?> shadowClass) { 441 synchronized (metaShadowMap) { 442 if (!metaShadowMap.containsKey(shadowClass)) { 443 metaShadowMap.put(shadowClass, new MetaShadow(shadowClass)); 444 } 445 return metaShadowMap.get(shadowClass); 446 } 447 } 448 449 private Class<?> findDirectShadowClass(Class<?> originalClass) { 450 ShadowConfig shadowConfig = getShadowConfig(originalClass); 451 if (shadowConfig == null || !shadowConfig.supportsSdk(apiLevel)) { 452 return null; 453 } 454 return loadClass(shadowConfig.shadowClassName, originalClass.getClassLoader()); 455 } 456 457 private static void writeField(Object target, Object value, Field realObjectField) { 458 try { 459 realObjectField.set(target, value); 460 } catch (IllegalAccessException e) { 461 throw new RuntimeException(e); 462 } 463 } 464 465 private static class ShadowMethodPlan implements Plan { 466 private final Method shadowMethod; 467 468 public ShadowMethodPlan(Method shadowMethod) { 469 this.shadowMethod = shadowMethod; 470 } 471 472 @Override 473 public Object run(Object instance, Object roboData, Object[] params) throws Throwable { 474 //noinspection UnnecessaryLocalVariable 475 Object shadow = roboData; 476 try { 477 return shadowMethod.invoke(shadow, params); 478 } catch (IllegalArgumentException e) { 479 throw new IllegalArgumentException("attempted to invoke " + shadowMethod 480 + (shadow == null ? "" : " on instance of " + shadow.getClass() + ", but " + shadow.getClass().getSimpleName() + " doesn't extend " + shadowMethod.getDeclaringClass().getSimpleName())); 481 } catch (InvocationTargetException e) { 482 throw e.getCause(); 483 } 484 } 485 486 @Override 487 public String describe() { 488 return shadowMethod.toString(); 489 } 490 } 491 492 private static class MetaShadow { 493 final List<Field> realObjectFields = new ArrayList<>(); 494 495 public MetaShadow(Class<?> shadowClass) { 496 while (shadowClass != null) { 497 for (Field field : shadowClass.getDeclaredFields()) { 498 if (field.isAnnotationPresent(RealObject.class)) { 499 if (Modifier.isStatic(field.getModifiers())) { 500 String message = "@RealObject must be on a non-static field, " + shadowClass; 501 System.err.println(message); 502 throw new IllegalArgumentException(message); 503 } 504 field.setAccessible(true); 505 realObjectFields.add(field); 506 } 507 } 508 shadowClass = shadowClass.getSuperclass(); 509 } 510 } 511 } 512 } 513