ShadowWrangler.java revision 718a56a01f49a2d4f17d5659d4291aa1d6991682
1package com.xtremelabs.robolectric.bytecode; 2 3import com.xtremelabs.robolectric.RobolectricConfig; 4import com.xtremelabs.robolectric.internal.Implementation; 5import com.xtremelabs.robolectric.internal.RealObject; 6import com.xtremelabs.robolectric.util.I18nException; 7import com.xtremelabs.robolectric.util.Join; 8import javassist.CannotCompileException; 9import javassist.CtClass; 10import javassist.CtField; 11import javassist.NotFoundException; 12 13import java.lang.annotation.Annotation; 14import java.lang.reflect.Array; 15import java.lang.reflect.Constructor; 16import java.lang.reflect.Field; 17import java.lang.reflect.InvocationTargetException; 18import java.lang.reflect.Method; 19import java.lang.reflect.Modifier; 20import java.util.ArrayList; 21import java.util.Arrays; 22import java.util.HashMap; 23import java.util.List; 24import java.util.Map; 25 26public class ShadowWrangler implements ClassHandler { 27 public static final String SHADOW_FIELD_NAME = "__shadow__"; 28 29 private static ShadowWrangler singleton; 30 31 public boolean debug = false; 32 private boolean strictI18n = false; 33 34 private final Map<Class, MetaShadow> metaShadowMap = new HashMap<Class, MetaShadow>(); 35 private Map<String, String> shadowClassMap = new HashMap<String, String>(); 36 private Map<Class, Field> shadowFieldMap = new HashMap<Class, Field>(); 37 private boolean logMissingShadowMethods = false; 38 39 // sorry! it really only makes sense to have one per ClassLoader anyway though [xw/hu] 40 public static ShadowWrangler getInstance() { 41 if (singleton == null) { 42 singleton = new ShadowWrangler(); 43 } 44 return singleton; 45 } 46 47 private ShadowWrangler() { 48 } 49 50 @Override 51 public void configure(RobolectricConfig robolectricConfig) { 52 strictI18n = robolectricConfig.getStrictI18n(); 53 } 54 55 @Override 56 public void instrument(CtClass ctClass) { 57 try { 58 CtClass objectClass = ctClass.getClassPool().get(Object.class.getName()); 59 try { 60 ctClass.getField(SHADOW_FIELD_NAME); 61 } catch (NotFoundException e) { 62 CtField field = new CtField(objectClass, SHADOW_FIELD_NAME, ctClass); 63 field.setModifiers(Modifier.PUBLIC); 64 ctClass.addField(field); 65 } 66 } catch (CannotCompileException e) { 67 throw new RuntimeException(e); 68 } catch (NotFoundException e) { 69 throw new RuntimeException(e); 70 } 71 } 72 73 @Override 74 public void beforeTest() { 75 shadowClassMap.clear(); 76 } 77 78 @Override 79 public void afterTest() { 80 } 81 82 public void bindShadowClass(Class<?> realClass, Class<?> shadowClass) { 83 shadowClassMap.put(realClass.getName(), shadowClass.getName()); 84 if (debug) System.out.println("shadow " + realClass + " with " + shadowClass); 85 } 86 87 @Override 88 public Object methodInvoked(Class clazz, String methodName, Object instance, String[] paramTypes, Object[] params) throws Throwable { 89 InvocationPlan invocationPlan = new InvocationPlan(clazz, methodName, instance, paramTypes); 90 if (!invocationPlan.prepare()) { 91 reportNoShadowMethodFound(clazz, methodName, paramTypes); 92 return null; 93 } 94 95 if (strictI18n && !invocationPlan.isI18nSafe()) { 96 throw new I18nException("Method " + methodName + " on class " + clazz.getName() + " is not i18n-safe."); 97 } 98 99 try { 100 return invocationPlan.getMethod().invoke(invocationPlan.getShadow(), params); 101 } catch (IllegalArgumentException e) { 102 throw new RuntimeException(invocationPlan.getShadow().getClass().getName() + " is not assignable from " + 103 invocationPlan.getDeclaredShadowClass().getName(), e); 104 } catch (InvocationTargetException e) { 105 throw stripStackTrace(e.getCause()); 106 } 107 } 108 109 private <T extends Throwable> T stripStackTrace(T throwable) { 110 List<StackTraceElement> stackTrace = new ArrayList<StackTraceElement>(); 111 for (StackTraceElement stackTraceElement : throwable.getStackTrace()) { 112 String className = stackTraceElement.getClassName(); 113 boolean isInternalCall = className.startsWith("sun.reflect.") 114 || className.startsWith("java.lang.reflect.") 115 || className.equals(ShadowWrangler.class.getName()) 116 || className.equals(RobolectricInternals.class.getName()); 117 if (!isInternalCall) { 118 stackTrace.add(stackTraceElement); 119 } 120 } 121 throwable.setStackTrace(stackTrace.toArray(new StackTraceElement[stackTrace.size()])); 122 return throwable; 123 } 124 125 private void reportNoShadowMethodFound(Class clazz, String methodName, String[] paramTypes) { 126 if (logMissingShadowMethods) { 127 System.out.println("No Shadow method found for " + clazz.getSimpleName() + "." + methodName + "(" + 128 Join.join(", ", (Object[]) paramTypes) + ")"); 129 } 130 } 131 132 public static Class<?> loadClass(String paramType, ClassLoader classLoader) { 133 Class primitiveClass = Type.findPrimitiveClass(paramType); 134 if (primitiveClass != null) return primitiveClass; 135 136 int arrayLevel = 0; 137 while (paramType.endsWith("[]")) { 138 arrayLevel++; 139 paramType = paramType.substring(0, paramType.length() - 2); 140 } 141 142 Class<?> clazz = Type.findPrimitiveClass(paramType); 143 if (clazz == null) { 144 try { 145 clazz = classLoader.loadClass(paramType); 146 } catch (ClassNotFoundException e) { 147 throw new RuntimeException(e); 148 } 149 } 150 151 while (arrayLevel-- > 0) { 152 clazz = Array.newInstance(clazz, 0).getClass(); 153 } 154 155 return clazz; 156 } 157 158 public Object shadowFor(Object instance) { 159 Field field = getShadowField(instance); 160 Object shadow = readField(instance, field); 161 162 if (shadow != null) { 163 return shadow; 164 } 165 166 String shadowClassName = getShadowClassName(instance.getClass()); 167 168 if (debug) 169 System.out.println("creating new " + shadowClassName + " as shadow for " + instance.getClass().getName()); 170 try { 171 Class<?> shadowClass = loadClass(shadowClassName, instance.getClass().getClassLoader()); 172 Constructor<?> constructor = findConstructor(instance, shadowClass); 173 if (constructor != null) { 174 shadow = constructor.newInstance(instance); 175 } else { 176 shadow = shadowClass.newInstance(); 177 } 178 field.set(instance, shadow); 179 180 injectRealObjectOn(shadow, shadowClass, instance); 181 182 return shadow; 183 } catch (InstantiationException e) { 184 throw new RuntimeException(e); 185 } catch (IllegalAccessException e) { 186 throw new RuntimeException(e); 187 } catch (InvocationTargetException e) { 188 throw new RuntimeException(e); 189 } 190 } 191 192 private void injectRealObjectOn(Object shadow, Class<?> shadowClass, Object instance) { 193 MetaShadow metaShadow = getMetaShadow(shadowClass); 194 for (Field realObjectField : metaShadow.realObjectFields) { 195 writeField(shadow, instance, realObjectField); 196 } 197 } 198 199 private MetaShadow getMetaShadow(Class<?> shadowClass) { 200 synchronized (metaShadowMap) { 201 MetaShadow metaShadow = metaShadowMap.get(shadowClass); 202 if (metaShadow == null) { 203 metaShadow = new MetaShadow(shadowClass); 204 metaShadowMap.put(shadowClass, metaShadow); 205 } 206 return metaShadow; 207 } 208 } 209 210 private String getShadowClassName(Class clazz) { 211 String shadowClassName = null; 212 while (shadowClassName == null && clazz != null) { 213 shadowClassName = shadowClassMap.get(clazz.getName()); 214 clazz = clazz.getSuperclass(); 215 } 216 return shadowClassName; 217 } 218 219 private Constructor<?> findConstructor(Object instance, Class<?> shadowClass) { 220 Class clazz = instance.getClass(); 221 222 Constructor constructor; 223 for (constructor = null; constructor == null && clazz != null; clazz = clazz.getSuperclass()) { 224 try { 225 constructor = shadowClass.getConstructor(clazz); 226 } catch (NoSuchMethodException e) { 227 // expected 228 } 229 } 230 return constructor; 231 } 232 233 private Field getShadowField(Object instance) { 234 Class clazz = instance.getClass(); 235 Field field = shadowFieldMap.get(clazz); 236 if (field == null) { 237 try { 238 field = clazz.getField(SHADOW_FIELD_NAME); 239 } catch (NoSuchFieldException e) { 240 throw new RuntimeException(instance.getClass().getName() + " has no shadow field", e); 241 } 242 shadowFieldMap.put(clazz, field); 243 } 244 return field; 245 } 246 247 public Object shadowOf(Object instance) { 248 if (instance == null) { 249 throw new NullPointerException("can't get a shadow for null"); 250 } 251 Field field = getShadowField(instance); 252 return readField(instance, field); 253 } 254 255 private Object readField(Object target, Field field) { 256 try { 257 return field.get(target); 258 } catch (IllegalAccessException e1) { 259 throw new RuntimeException(e1); 260 } 261 } 262 263 private void writeField(Object target, Object value, Field realObjectField) { 264 try { 265 realObjectField.set(target, value); 266 } catch (IllegalAccessException e) { 267 throw new RuntimeException(e); 268 } 269 } 270 271 public void logMissingInvokedShadowMethods() { 272 logMissingShadowMethods = true; 273 } 274 275 public void silence() { 276 logMissingShadowMethods = false; 277 } 278 279 private class InvocationPlan { 280 private Class clazz; 281 private ClassLoader classLoader; 282 private String methodName; 283 private Object instance; 284 private String[] paramTypes; 285 private Class<?> declaredShadowClass; 286 private Method method; 287 private Object shadow; 288 289 public InvocationPlan(Class clazz, String methodName, Object instance, String... paramTypes) { 290 this.clazz = clazz; 291 this.classLoader = clazz.getClassLoader(); 292 this.methodName = methodName; 293 this.instance = instance; 294 this.paramTypes = paramTypes; 295 } 296 297 public Class<?> getDeclaredShadowClass() { 298 return declaredShadowClass; 299 } 300 301 public Method getMethod() { 302 return method; 303 } 304 305 public Object getShadow() { 306 return shadow; 307 } 308 309 public boolean isI18nSafe() { 310 // method is loaded by another class loader. So do everything reflectively. 311 Annotation[] annos = method.getAnnotations(); 312 for (int i = 0; i < annos.length; i++) { 313 String name = annos[i].annotationType().getName(); 314 if (name.equals("com.xtremelabs.robolectric.internal.Implementation")) { 315 try { 316 Method m = (annos[i]).getClass().getMethod("i18nSafe"); 317 return (Boolean) m.invoke(annos[i]); 318 } catch (Exception e) { 319 return true; // should probably throw some other exception 320 } 321 } 322 } 323 324 return true; 325 } 326 327 public boolean prepare() { 328 Class<?>[] paramClasses = getParamClasses(); 329 330 Class<?> originalClass = loadClass(clazz.getName(), classLoader); 331 332 declaredShadowClass = findDeclaredShadowClassForMethod(originalClass, methodName, paramClasses); 333 if (declaredShadowClass == null) { 334 return false; 335 } 336 337 if (methodName.equals("<init>")) { 338 methodName = "__constructor__"; 339 } 340 341 if (instance != null) { 342 shadow = shadowFor(instance); 343 method = getMethod(shadow.getClass(), methodName, paramClasses); 344 } else { 345 shadow = null; 346 method = getMethod(findShadowClass(clazz), methodName, paramClasses); 347 } 348 349 if (method == null) { 350 if (debug) { 351 System.out.println("No method found for " + clazz + "." + methodName + "(" + Arrays.asList(paramClasses) + ") on " + declaredShadowClass.getName()); 352 } 353 return false; 354 } 355 356 if ((instance == null) != Modifier.isStatic(method.getModifiers())) { 357 throw new RuntimeException("method staticness of " + clazz.getName() + "." + methodName + " and " + declaredShadowClass.getName() + "." + method.getName() + " don't match"); 358 } 359 360 method.setAccessible(true); 361 362 return true; 363 } 364 365 private Class<?> findDeclaredShadowClassForMethod(Class<?> originalClass, String methodName, Class<?>[] paramClasses) { 366 Class<?> declaringClass = findDeclaringClassForMethod(methodName, paramClasses, originalClass); 367 return findShadowClass(declaringClass); 368 } 369 370 private Class<?> findShadowClass(Class<?> originalClass) { 371 String declaredShadowClassName = getShadowClassName(originalClass); 372 if (declaredShadowClassName == null) { 373 return null; 374 } 375 return loadClass(declaredShadowClassName, classLoader); 376 } 377 378 private Class<?> findDeclaringClassForMethod(String methodName, Class<?>[] paramClasses, Class<?> originalClass) { 379 Class<?> declaringClass; 380 if (this.methodName.equals("<init>")) { 381 declaringClass = originalClass; 382 } else { 383 Method originalMethod; 384 try { 385 originalMethod = originalClass.getDeclaredMethod(methodName, paramClasses); 386 } catch (NoSuchMethodException e) { 387 throw new RuntimeException(e); 388 } 389 declaringClass = originalMethod.getDeclaringClass(); 390 } 391 return declaringClass; 392 } 393 394 private Class<?>[] getParamClasses() { 395 Class<?>[] paramClasses = new Class<?>[paramTypes.length]; 396 397 for (int i = 0; i < paramTypes.length; i++) { 398 paramClasses[i] = loadClass(paramTypes[i], classLoader); 399 } 400 return paramClasses; 401 } 402 403 private Method getMethod(Class<?> clazz, String methodName, Class<?>[] paramClasses) { 404 Method method = null; 405 try { 406 method = clazz.getMethod(methodName, paramClasses); 407 } catch (NoSuchMethodException e) { 408 try { 409 method = clazz.getDeclaredMethod(methodName, paramClasses); 410 } catch (NoSuchMethodException e1) { 411 method = null; 412 } 413 } 414 415 if (method != null && !isOnShadowClass(method)) { 416 method = null; 417 } 418 419 return method; 420 } 421 422 private boolean isOnShadowClass(Method method) { 423 Class<?> declaringClass = method.getDeclaringClass(); 424 // why doesn't getAnnotation(com.xtremelabs.robolectric.internal.Implements) work here? It always returns null. pg 20101115 425 // It doesn't work because the method and declaringClass were loaded by the delegate class loader. Different classloaders so types don't match. mp 20110823 426 for (Annotation annotation : declaringClass.getAnnotations()) { 427 if (annotation.annotationType().toString().equals("interface com.xtremelabs.robolectric.internal.Implements")) { 428 return true; 429 } 430 } 431 return false; 432 } 433 434 @Override 435 public String toString() { 436 return "delegating to " + declaredShadowClass.getName() + "." + method.getName() 437 + "(" + Arrays.toString(method.getParameterTypes()) + ")"; 438 } 439 } 440 441 private class MetaShadow { 442 List<Field> realObjectFields = new ArrayList<Field>(); 443 444 public MetaShadow(Class<?> shadowClass) { 445 while (shadowClass != null) { 446 for (Field field : shadowClass.getDeclaredFields()) { 447 if (field.isAnnotationPresent(RealObject.class)) { 448 field.setAccessible(true); 449 realObjectFields.add(field); 450 } 451 } 452 shadowClass = shadowClass.getSuperclass(); 453 } 454 455 } 456 } 457} 458