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