Home | History | Annotate | Download | only in bytecode
      1 package com.xtremelabs.robolectric.bytecode;
      2 
      3 import com.xtremelabs.robolectric.RobolectricConfig;
      4 import com.xtremelabs.robolectric.internal.RealObject;
      5 import com.xtremelabs.robolectric.util.I18nException;
      6 import com.xtremelabs.robolectric.util.Join;
      7 import javassist.CannotCompileException;
      8 import javassist.CtClass;
      9 import javassist.CtField;
     10 import javassist.NotFoundException;
     11 
     12 import java.lang.annotation.Annotation;
     13 import java.lang.reflect.Array;
     14 import java.lang.reflect.Constructor;
     15 import java.lang.reflect.Field;
     16 import java.lang.reflect.InvocationTargetException;
     17 import java.lang.reflect.Method;
     18 import java.lang.reflect.Modifier;
     19 import java.util.ArrayList;
     20 import java.util.Arrays;
     21 import java.util.HashMap;
     22 import java.util.List;
     23 import java.util.Map;
     24 
     25 public 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