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