DelegateClassAdapterTest.java revision 282e181b58cf72b6ca770dc7ca5f91f135444502
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.tools.layoutlib.create;
18
19
20import static org.junit.Assert.assertEquals;
21import static org.junit.Assert.assertFalse;
22import static org.junit.Assert.assertNotNull;
23import static org.junit.Assert.assertSame;
24import static org.junit.Assert.assertTrue;
25import static org.junit.Assert.fail;
26
27import com.android.tools.layoutlib.create.dataclass.ClassWithNative;
28import com.android.tools.layoutlib.create.dataclass.OuterClass;
29import com.android.tools.layoutlib.create.dataclass.OuterClass.InnerClass;
30
31import org.junit.Before;
32import org.junit.Test;
33import org.objectweb.asm.ClassReader;
34import org.objectweb.asm.ClassVisitor;
35import org.objectweb.asm.ClassWriter;
36
37import java.io.IOException;
38import java.io.PrintWriter;
39import java.io.StringWriter;
40import java.lang.annotation.Annotation;
41import java.lang.reflect.Constructor;
42import java.lang.reflect.InvocationTargetException;
43import java.lang.reflect.Method;
44import java.lang.reflect.Modifier;
45import java.util.HashMap;
46import java.util.HashSet;
47import java.util.Map;
48import java.util.Map.Entry;
49import java.util.Set;
50
51public class DelegateClassAdapterTest {
52
53    private MockLog mLog;
54
55    private static final String NATIVE_CLASS_NAME = ClassWithNative.class.getCanonicalName();
56    private static final String OUTER_CLASS_NAME = OuterClass.class.getCanonicalName();
57    private static final String INNER_CLASS_NAME = OuterClass.class.getCanonicalName() + "$" +
58                                                   InnerClass.class.getSimpleName();
59
60    @Before
61    public void setUp() throws Exception {
62        mLog = new MockLog();
63        mLog.setVerbose(true); // capture debug error too
64    }
65
66    /**
67     * Tests that a class not being modified still works.
68     */
69    @SuppressWarnings("unchecked")
70    @Test
71    public void testNoOp() throws Throwable {
72        // create an instance of the class that will be modified
73        // (load the class in a distinct class loader so that we can trash its definition later)
74        ClassLoader cl1 = new ClassLoader(this.getClass().getClassLoader()) { };
75        Class<ClassWithNative> clazz1 = (Class<ClassWithNative>) cl1.loadClass(NATIVE_CLASS_NAME);
76        ClassWithNative instance1 = clazz1.newInstance();
77        assertEquals(42, instance1.add(20, 22));
78        try {
79            instance1.callNativeInstance(10, 3.1415, new Object[0] );
80            fail("Test should have failed to invoke callTheNativeMethod [1]");
81        } catch (UnsatisfiedLinkError e) {
82            // This is expected to fail since the native method is not implemented.
83        }
84
85        // Now process it but tell the delegate to not modify any method
86        ClassWriter cw = new ClassWriter(0 /*flags*/);
87
88        HashSet<String> delegateMethods = new HashSet<String>();
89        String internalClassName = NATIVE_CLASS_NAME.replace('.', '/');
90        DelegateClassAdapter cv = new DelegateClassAdapter(
91                mLog, cw, internalClassName, delegateMethods);
92
93        ClassReader cr = new ClassReader(NATIVE_CLASS_NAME);
94        cr.accept(cv, 0 /* flags */);
95
96        // Load the generated class in a different class loader and try it again
97
98        ClassLoader2 cl2 = null;
99        try {
100            cl2 = new ClassLoader2() {
101                @Override
102                public void testModifiedInstance() throws Exception {
103                    Class<?> clazz2 = loadClass(NATIVE_CLASS_NAME);
104                    Object i2 = clazz2.newInstance();
105                    assertNotNull(i2);
106                    assertEquals(42, callAdd(i2, 20, 22));
107
108                    try {
109                        callCallNativeInstance(i2, 10, 3.1415, new Object[0]);
110                        fail("Test should have failed to invoke callTheNativeMethod [2]");
111                    } catch (InvocationTargetException e) {
112                        // This is expected to fail since the native method has NOT been
113                        // overridden here.
114                        assertEquals(UnsatisfiedLinkError.class, e.getCause().getClass());
115                    }
116
117                    // Check that the native method does NOT have the new annotation
118                    Method[] m = clazz2.getDeclaredMethods();
119                    assertEquals("native_instance", m[2].getName());
120                    assertTrue(Modifier.isNative(m[2].getModifiers()));
121                    Annotation[] a = m[2].getAnnotations();
122                    assertEquals(0, a.length);
123                }
124            };
125            cl2.add(NATIVE_CLASS_NAME, cw);
126            cl2.testModifiedInstance();
127        } catch (Throwable t) {
128            throw dumpGeneratedClass(t, cl2);
129        }
130    }
131
132    /**
133     * {@link DelegateMethodAdapter2} does not support overriding constructors yet,
134     * so this should fail with an {@link UnsupportedOperationException}.
135     *
136     * Although not tested here, the message of the exception should contain the
137     * constructor signature.
138     */
139    @Test(expected=UnsupportedOperationException.class)
140    public void testConstructorsNotSupported() throws IOException {
141        ClassWriter cw = new ClassWriter(0 /*flags*/);
142
143        String internalClassName = NATIVE_CLASS_NAME.replace('.', '/');
144
145        HashSet<String> delegateMethods = new HashSet<String>();
146        delegateMethods.add("<init>");
147        DelegateClassAdapter cv = new DelegateClassAdapter(
148                mLog, cw, internalClassName, delegateMethods);
149
150        ClassReader cr = new ClassReader(NATIVE_CLASS_NAME);
151        cr.accept(cv, 0 /* flags */);
152    }
153
154    @Test
155    public void testDelegateNative() throws Throwable {
156        ClassWriter cw = new ClassWriter(0 /*flags*/);
157        String internalClassName = NATIVE_CLASS_NAME.replace('.', '/');
158
159        HashSet<String> delegateMethods = new HashSet<String>();
160        delegateMethods.add(DelegateClassAdapter.ALL_NATIVES);
161        DelegateClassAdapter cv = new DelegateClassAdapter(
162                mLog, cw, internalClassName, delegateMethods);
163
164        ClassReader cr = new ClassReader(NATIVE_CLASS_NAME);
165        cr.accept(cv, 0 /* flags */);
166
167        // Load the generated class in a different class loader and try it
168        ClassLoader2 cl2 = null;
169        try {
170            cl2 = new ClassLoader2() {
171                @Override
172                public void testModifiedInstance() throws Exception {
173                    Class<?> clazz2 = loadClass(NATIVE_CLASS_NAME);
174                    Object i2 = clazz2.newInstance();
175                    assertNotNull(i2);
176
177                    // Use reflection to access inner methods
178                    assertEquals(42, callAdd(i2, 20, 22));
179
180                     Object[] objResult = new Object[] { null };
181                     int result = callCallNativeInstance(i2, 10, 3.1415, objResult);
182                     assertEquals((int)(10 + 3.1415), result);
183                     assertSame(i2, objResult[0]);
184
185                     // Check that the native method now has the new annotation and is not native
186                     Method[] m = clazz2.getDeclaredMethods();
187                     assertEquals("native_instance", m[2].getName());
188                     assertFalse(Modifier.isNative(m[2].getModifiers()));
189                     Annotation[] a = m[2].getAnnotations();
190                     assertEquals("LayoutlibDelegate", a[0].annotationType().getSimpleName());
191                }
192            };
193            cl2.add(NATIVE_CLASS_NAME, cw);
194            cl2.testModifiedInstance();
195        } catch (Throwable t) {
196            throw dumpGeneratedClass(t, cl2);
197        }
198    }
199
200    @Test
201    public void testDelegateInner() throws Throwable {
202        // We'll delegate the "get" method of both the inner and outer class.
203        HashSet<String> delegateMethods = new HashSet<String>();
204        delegateMethods.add("get");
205        delegateMethods.add("privateMethod");
206
207        // Generate the delegate for the outer class.
208        ClassWriter cwOuter = new ClassWriter(0 /*flags*/);
209        String outerClassName = OUTER_CLASS_NAME.replace('.', '/');
210        DelegateClassAdapter cvOuter = new DelegateClassAdapter(
211                mLog, cwOuter, outerClassName, delegateMethods);
212        ClassReader cr = new ClassReader(OUTER_CLASS_NAME);
213        cr.accept(cvOuter, 0 /* flags */);
214
215        // Generate the delegate for the inner class.
216        ClassWriter cwInner = new ClassWriter(0 /*flags*/);
217        String innerClassName = INNER_CLASS_NAME.replace('.', '/');
218        DelegateClassAdapter cvInner = new DelegateClassAdapter(
219                mLog, cwInner, innerClassName, delegateMethods);
220        cr = new ClassReader(INNER_CLASS_NAME);
221        cr.accept(cvInner, 0 /* flags */);
222
223        // Load the generated classes in a different class loader and try them
224        ClassLoader2 cl2 = null;
225        try {
226            cl2 = new ClassLoader2() {
227                @Override
228                public void testModifiedInstance() throws Exception {
229
230                    // Check the outer class
231                    Class<?> outerClazz2 = loadClass(OUTER_CLASS_NAME);
232                    Object o2 = outerClazz2.newInstance();
233                    assertNotNull(o2);
234
235                    // The original Outer.get returns 1+10+20,
236                    // but the delegate makes it return 4+10+20
237                    assertEquals(4+10+20, callGet(o2, 10, 20));
238                    assertEquals(1+10+20, callGet_Original(o2, 10, 20));
239
240                    // The original Outer has a private method that is
241                    // delegated. We should be able to call both the delegate
242                    // and the original (which is now public).
243                    assertEquals("outerPrivateMethod",
244                                 callMethod(o2, "privateMethod_Original", false /*makePublic*/));
245
246                    // The original method is private, so by default we can't access it
247                    boolean gotIllegalAccessException = false;
248                    try {
249                         callMethod(o2, "privateMethod", false /*makePublic*/);
250                    } catch(IllegalAccessException e) {
251                        gotIllegalAccessException = true;
252                    }
253                    assertTrue(gotIllegalAccessException);
254                    // Try again, but now making it accessible
255                    assertEquals("outerPrivate_Delegate",
256                            callMethod(o2, "privateMethod", true /*makePublic*/));
257
258                    // Check the inner class. Since it's not a static inner class, we need
259                    // to use the hidden constructor that takes the outer class as first parameter.
260                    Class<?> innerClazz2 = loadClass(INNER_CLASS_NAME);
261                    Constructor<?> innerCons = innerClazz2.getConstructor(
262                                                                new Class<?>[] { outerClazz2 });
263                    Object i2 = innerCons.newInstance(new Object[] { o2 });
264                    assertNotNull(i2);
265
266                    // The original Inner.get returns 3+10+20,
267                    // but the delegate makes it return 6+10+20
268                    assertEquals(6+10+20, callGet(i2, 10, 20));
269                    assertEquals(3+10+20, callGet_Original(i2, 10, 20));
270                }
271            };
272            cl2.add(OUTER_CLASS_NAME, cwOuter.toByteArray());
273            cl2.add(INNER_CLASS_NAME, cwInner.toByteArray());
274            cl2.testModifiedInstance();
275        } catch (Throwable t) {
276            throw dumpGeneratedClass(t, cl2);
277        }
278    }
279
280    //-------
281
282    /**
283     * A class loader than can define and instantiate our modified classes.
284     * <p/>
285     * The trick here is that this class loader will test our <em>modified</em> version
286     * of the classes, the one with the delegate calls.
287     * <p/>
288     * Trying to do so in the original class loader generates all sort of link issues because
289     * there are 2 different definitions of the same class name. This class loader will
290     * define and load the class when requested by name and provide helpers to access the
291     * instance methods via reflection.
292     */
293    private abstract class ClassLoader2 extends ClassLoader {
294
295        private final Map<String, byte[]> mClassDefs = new HashMap<String, byte[]>();
296
297        public ClassLoader2() {
298            super(null);
299        }
300
301        public ClassLoader2 add(String className, byte[] definition) {
302            mClassDefs.put(className, definition);
303            return this;
304        }
305
306        public ClassLoader2 add(String className, ClassWriter rewrittenClass) {
307            mClassDefs.put(className, rewrittenClass.toByteArray());
308            return this;
309        }
310
311        private Set<Entry<String, byte[]>> getByteCode() {
312            return mClassDefs.entrySet();
313        }
314
315        @SuppressWarnings("unused")
316        @Override
317        protected Class<?> findClass(String name) throws ClassNotFoundException {
318            try {
319                return super.findClass(name);
320            } catch (ClassNotFoundException e) {
321
322                byte[] def = mClassDefs.get(name);
323                if (def != null) {
324                    // Load the modified ClassWithNative from its bytes representation.
325                    return defineClass(name, def, 0, def.length);
326                }
327
328                try {
329                    // Load everything else from the original definition into the new class loader.
330                    ClassReader cr = new ClassReader(name);
331                    ClassWriter cw = new ClassWriter(0);
332                    cr.accept(cw, 0);
333                    byte[] bytes = cw.toByteArray();
334                    return defineClass(name, bytes, 0, bytes.length);
335
336                } catch (IOException ioe) {
337                    throw new RuntimeException(ioe);
338                }
339            }
340        }
341
342        /**
343         * Accesses {@link OuterClass#get} or {@link InnerClass#get}via reflection.
344         */
345        public int callGet(Object instance, int a, long b) throws Exception {
346            Method m = instance.getClass().getMethod("get",
347                    new Class<?>[] { int.class, long.class } );
348
349            Object result = m.invoke(instance, new Object[] { a, b });
350            return ((Integer) result).intValue();
351        }
352
353        /**
354         * Accesses the "_Original" methods for {@link OuterClass#get}
355         * or {@link InnerClass#get}via reflection.
356         */
357        public int callGet_Original(Object instance, int a, long b) throws Exception {
358            Method m = instance.getClass().getMethod("get_Original",
359                    new Class<?>[] { int.class, long.class } );
360
361            Object result = m.invoke(instance, new Object[] { a, b });
362            return ((Integer) result).intValue();
363        }
364
365        /**
366         * Accesses the any declared method that takes no parameter via reflection.
367         */
368        @SuppressWarnings("unchecked")
369        public <T> T callMethod(Object instance, String methodName, boolean makePublic) throws Exception {
370            Method m = instance.getClass().getDeclaredMethod(methodName, (Class<?>[])null);
371
372            boolean wasAccessible = m.isAccessible();
373            if (makePublic && !wasAccessible) {
374                m.setAccessible(true);
375            }
376
377            Object result = m.invoke(instance, (Object[])null);
378
379            if (makePublic && !wasAccessible) {
380                m.setAccessible(false);
381            }
382
383            return (T) result;
384        }
385
386        /**
387         * Accesses {@link ClassWithNative#add(int, int)} via reflection.
388         */
389        public int callAdd(Object instance, int a, int b) throws Exception {
390            Method m = instance.getClass().getMethod("add",
391                    new Class<?>[] { int.class, int.class });
392
393            Object result = m.invoke(instance, new Object[] { a, b });
394            return ((Integer) result).intValue();
395        }
396
397        /**
398         * Accesses {@link ClassWithNative#callNativeInstance(int, double, Object[])}
399         * via reflection.
400         */
401        public int callCallNativeInstance(Object instance, int a, double d, Object[] o)
402                throws Exception {
403            Method m = instance.getClass().getMethod("callNativeInstance",
404                    new Class<?>[] { int.class, double.class, Object[].class });
405
406            Object result = m.invoke(instance, new Object[] { a, d, o });
407            return ((Integer) result).intValue();
408        }
409
410        public abstract void testModifiedInstance() throws Exception;
411    }
412
413    /**
414     * For debugging, it's useful to dump the content of the generated classes
415     * along with the exception that was generated.
416     *
417     * However to make it work you need to pull in the org.objectweb.asm.util.TraceClassVisitor
418     * class and associated utilities which are found in the ASM source jar. Since we don't
419     * want that dependency in the source code, we only put it manually for development and
420     * access the TraceClassVisitor via reflection if present.
421     *
422     * @param t The exception thrown by {@link ClassLoader2#testModifiedInstance()}
423     * @param cl2 The {@link ClassLoader2} instance with the generated bytecode.
424     * @return Either original {@code t} or a new wrapper {@link Throwable}
425     */
426    private Throwable dumpGeneratedClass(Throwable t, ClassLoader2 cl2) {
427        try {
428            // For debugging, dump the bytecode of the class in case of unexpected error
429            // if we can find the TraceClassVisitor class.
430            Class<?> tcvClass = Class.forName("org.objectweb.asm.util.TraceClassVisitor");
431
432            StringBuilder sb = new StringBuilder();
433            sb.append('\n').append(t.getClass().getCanonicalName());
434            if (t.getMessage() != null) {
435                sb.append(": ").append(t.getMessage());
436              }
437
438            for (Entry<String, byte[]> entry : cl2.getByteCode()) {
439                String className = entry.getKey();
440                byte[] bytes = entry.getValue();
441
442                StringWriter sw = new StringWriter();
443                PrintWriter pw = new PrintWriter(sw);
444                // next 2 lines do: TraceClassVisitor tcv = new TraceClassVisitor(pw);
445                Constructor<?> cons = tcvClass.getConstructor(new Class<?>[] { pw.getClass() });
446                Object tcv = cons.newInstance(new Object[] { pw });
447                ClassReader cr2 = new ClassReader(bytes);
448                cr2.accept((ClassVisitor) tcv, 0 /* flags */);
449
450                sb.append("\nBytecode dump: <").append(className).append(">:\n")
451                  .append(sw.toString());
452            }
453
454            // Re-throw exception with new message
455            RuntimeException ex = new RuntimeException(sb.toString(), t);
456            return ex;
457        } catch (Throwable ignore) {
458            // In case of problem, just throw the original exception as-is.
459            return t;
460        }
461    }
462
463}
464