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