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