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