ShadowWrangler.java revision 22e617961e21946fd47d3b96cc670181ccd3a2bd
1package com.xtremelabs.robolectric.bytecode;
2
3import com.xtremelabs.robolectric.RobolectricConfig;
4import com.xtremelabs.robolectric.internal.Implementation;
5import com.xtremelabs.robolectric.internal.RealObject;
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 RuntimeException ("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