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