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 private Constructor<?> findConstructor(Object instance, Class<?> shadowClass) { 219 Class clazz = instance.getClass(); 220 221 Constructor constructor; 222 for (constructor = null; constructor == null && clazz != null; clazz = clazz.getSuperclass()) { 223 try { 224 constructor = shadowClass.getConstructor(clazz); 225 } catch (NoSuchMethodException e) { 226 // expected 227 } 228 } 229 return constructor; 230 } 231 232 private Field getShadowField(Object instance) { 233 Class clazz = instance.getClass(); 234 Field field = shadowFieldMap.get(clazz); 235 if (field == null) { 236 try { 237 field = clazz.getField(SHADOW_FIELD_NAME); 238 } catch (NoSuchFieldException e) { 239 throw new RuntimeException(instance.getClass().getName() + " has no shadow field", e); 240 } 241 shadowFieldMap.put(clazz, field); 242 } 243 return field; 244 } 245 246 public Object shadowOf(Object instance) { 247 if (instance == null) { 248 throw new NullPointerException("can't get a shadow for null"); 249 } 250 Field field = getShadowField(instance); 251 return readField(instance, field); 252 } 253 254 private Object readField(Object target, Field field) { 255 try { 256 return field.get(target); 257 } catch (IllegalAccessException e1) { 258 throw new RuntimeException(e1); 259 } 260 } 261 262 private void writeField(Object target, Object value, Field realObjectField) { 263 try { 264 realObjectField.set(target, value); 265 } catch (IllegalAccessException e) { 266 throw new RuntimeException(e); 267 } 268 } 269 270 public void logMissingInvokedShadowMethods() { 271 logMissingShadowMethods = true; 272 } 273 274 public void silence() { 275 logMissingShadowMethods = false; 276 } 277 278 private class InvocationPlan { 279 private Class clazz; 280 private ClassLoader classLoader; 281 private String methodName; 282 private Object instance; 283 private String[] paramTypes; 284 private Class<?> declaredShadowClass; 285 private Method method; 286 private Object shadow; 287 288 public InvocationPlan(Class clazz, String methodName, Object instance, String... paramTypes) { 289 this.clazz = clazz; 290 this.classLoader = clazz.getClassLoader(); 291 this.methodName = methodName; 292 this.instance = instance; 293 this.paramTypes = paramTypes; 294 } 295 296 public Class<?> getDeclaredShadowClass() { 297 return declaredShadowClass; 298 } 299 300 public Method getMethod() { 301 return method; 302 } 303 304 public Object getShadow() { 305 return shadow; 306 } 307 308 public boolean isI18nSafe() { 309 // method is loaded by another class loader. So do everything reflectively. 310 Annotation[] annos = method.getAnnotations(); 311 for (int i = 0; i < annos.length; i++) { 312 String name = annos[i].annotationType().getName(); 313 if (name.equals("com.xtremelabs.robolectric.internal.Implementation")) { 314 try { 315 Method m = (annos[i]).getClass().getMethod("i18nSafe"); 316 return (Boolean) m.invoke(annos[i]); 317 } catch (Exception e) { 318 return true; // should probably throw some other exception 319 } 320 } 321 } 322 323 return true; 324 } 325 326 public boolean prepare() { 327 Class<?>[] paramClasses = getParamClasses(); 328 329 Class<?> originalClass = loadClass(clazz.getName(), classLoader); 330 331 declaredShadowClass = findDeclaredShadowClassForMethod(originalClass, methodName, paramClasses); 332 if (declaredShadowClass == null) { 333 return false; 334 } 335 336 if (methodName.equals("<init>")) { 337 methodName = "__constructor__"; 338 } 339 340 if (instance != null) { 341 shadow = shadowFor(instance); 342 method = getMethod(shadow.getClass(), methodName, paramClasses); 343 } else { 344 shadow = null; 345 method = getMethod(findShadowClass(clazz), methodName, paramClasses); 346 } 347 348 if (method == null) { 349 if (debug) { 350 System.out.println("No method found for " + clazz + "." + methodName + "(" + Arrays.asList(paramClasses) + ") on " + declaredShadowClass.getName()); 351 } 352 return false; 353 } 354 355 if ((instance == null) != Modifier.isStatic(method.getModifiers())) { 356 throw new RuntimeException("method staticness of " + clazz.getName() + "." + methodName + " and " + declaredShadowClass.getName() + "." + method.getName() + " don't match"); 357 } 358 359 method.setAccessible(true); 360 361 return true; 362 } 363 364 private Class<?> findDeclaredShadowClassForMethod(Class<?> originalClass, String methodName, Class<?>[] paramClasses) { 365 Class<?> declaringClass = findDeclaringClassForMethod(methodName, paramClasses, originalClass); 366 return findShadowClass(declaringClass); 367 } 368 369 private Class<?> findShadowClass(Class<?> originalClass) { 370 String declaredShadowClassName = getShadowClassName(originalClass); 371 if (declaredShadowClassName == null) { 372 return null; 373 } 374 return loadClass(declaredShadowClassName, 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