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