2 * Copyright (C) 2010 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.tools.layoutlib.create;
20 import static org.junit.Assert.assertEquals;
21 import static org.junit.Assert.assertFalse;
22 import static org.junit.Assert.assertNotNull;
23 import static org.junit.Assert.assertSame;
24 import static org.junit.Assert.assertTrue;
25 import static org.junit.Assert.fail;
27 import com.android.tools.layoutlib.create.dataclass.ClassWithNative;
28 import com.android.tools.layoutlib.create.dataclass.OuterClass;
29 import com.android.tools.layoutlib.create.dataclass.OuterClass.InnerClass;
31 import org.junit.Before;
32 import org.junit.Test;
33 import org.objectweb.asm.ClassReader;
34 import org.objectweb.asm.ClassVisitor;
35 import org.objectweb.asm.ClassWriter;
37 import java.io.IOException;
38 import java.io.PrintWriter;
39 import java.io.StringWriter;
40 import java.lang.annotation.Annotation;
41 import java.lang.reflect.Constructor;
42 import java.lang.reflect.InvocationTargetException;
43 import java.lang.reflect.Method;
44 import java.lang.reflect.Modifier;
45 import java.util.HashMap;
46 import java.util.HashSet;
48 import java.util.Map.Entry;
51 public class DelegateClassAdapterTest {
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();
61 public void setUp() throws Exception {
63 mLog.setVerbose(true); // capture debug error too
67 * Tests that a class not being modified still works.
69 @SuppressWarnings("unchecked")
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));
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.
85 // Now process it but tell the delegate to not modify any method
86 ClassWriter cw = new ClassWriter(0 /*flags*/);
88 HashSet<String> delegateMethods = new HashSet<String>();
89 String internalClassName = NATIVE_CLASS_NAME.replace('.', '/');
90 DelegateClassAdapter cv = new DelegateClassAdapter(
91 mLog, cw, internalClassName, delegateMethods);
93 ClassReader cr = new ClassReader(NATIVE_CLASS_NAME);
94 cr.accept(cv, 0 /* flags */);
96 // Load the generated class in a different class loader and try it again
98 ClassLoader2 cl2 = null;
100 cl2 = new ClassLoader2() {
102 public void testModifiedInstance() throws Exception {
103 Class<?> clazz2 = loadClass(NATIVE_CLASS_NAME);
104 Object i2 = clazz2.newInstance();
106 assertEquals(42, callAdd(i2, 20, 22));
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
114 assertEquals(UnsatisfiedLinkError.class, e.getCause().getClass());
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);
125 cl2.add(NATIVE_CLASS_NAME, cw);
126 cl2.testModifiedInstance();
127 } catch (Throwable t) {
128 throw dumpGeneratedClass(t, cl2);
133 * {@link DelegateMethodAdapter} does not support overriding constructors yet,
134 * so this should fail with an {@link UnsupportedOperationException}.
136 * Although not tested here, the message of the exception should contain the
137 * constructor signature.
139 @Test(expected=UnsupportedOperationException.class)
140 public void testConstructorsNotSupported() throws IOException {
141 ClassWriter cw = new ClassWriter(0 /*flags*/);
143 String internalClassName = NATIVE_CLASS_NAME.replace('.', '/');
145 HashSet<String> delegateMethods = new HashSet<String>();
146 delegateMethods.add("<init>");
147 DelegateClassAdapter cv = new DelegateClassAdapter(
148 mLog, cw, internalClassName, delegateMethods);
150 ClassReader cr = new ClassReader(NATIVE_CLASS_NAME);
151 cr.accept(cv, 0 /* flags */);
155 public void testDelegateNative() throws Throwable {
156 ClassWriter cw = new ClassWriter(0 /*flags*/);
157 String internalClassName = NATIVE_CLASS_NAME.replace('.', '/');
159 HashSet<String> delegateMethods = new HashSet<String>();
160 delegateMethods.add(DelegateClassAdapter.ALL_NATIVES);
161 DelegateClassAdapter cv = new DelegateClassAdapter(
162 mLog, cw, internalClassName, delegateMethods);
164 ClassReader cr = new ClassReader(NATIVE_CLASS_NAME);
165 cr.accept(cv, 0 /* flags */);
167 // Load the generated class in a different class loader and try it
168 ClassLoader2 cl2 = null;
170 cl2 = new ClassLoader2() {
172 public void testModifiedInstance() throws Exception {
173 Class<?> clazz2 = loadClass(NATIVE_CLASS_NAME);
174 Object i2 = clazz2.newInstance();
177 // Use reflection to access inner methods
178 assertEquals(42, callAdd(i2, 20, 22));
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]);
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());
193 cl2.add(NATIVE_CLASS_NAME, cw);
194 cl2.testModifiedInstance();
195 } catch (Throwable t) {
196 throw dumpGeneratedClass(t, cl2);
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");
206 // Generate the delegate for the outer class.
207 ClassWriter cwOuter = new ClassWriter(0 /*flags*/);
208 String outerClassName = OUTER_CLASS_NAME.replace('.', '/');
209 DelegateClassAdapter cvOuter = new DelegateClassAdapter(
210 mLog, cwOuter, outerClassName, delegateMethods);
211 ClassReader cr = new ClassReader(OUTER_CLASS_NAME);
212 cr.accept(cvOuter, 0 /* flags */);
214 // Generate the delegate for the inner class.
215 ClassWriter cwInner = new ClassWriter(0 /*flags*/);
216 String innerClassName = INNER_CLASS_NAME.replace('.', '/');
217 DelegateClassAdapter cvInner = new DelegateClassAdapter(
218 mLog, cwInner, innerClassName, delegateMethods);
219 cr = new ClassReader(INNER_CLASS_NAME);
220 cr.accept(cvInner, 0 /* flags */);
222 // Load the generated classes in a different class loader and try them
223 ClassLoader2 cl2 = null;
225 cl2 = new ClassLoader2() {
227 public void testModifiedInstance() throws Exception {
229 // Check the outer class
230 Class<?> outerClazz2 = loadClass(OUTER_CLASS_NAME);
231 Object o2 = outerClazz2.newInstance();
234 // The original Outer.get returns 1+10+20,
235 // but the delegate makes it return 4+10+20
236 assertEquals(4+10+20, callGet(o2, 10, 20));
238 // Check the inner class. Since it's not a static inner class, we need
239 // to use the hidden constructor that takes the outer class as first parameter.
240 Class<?> innerClazz2 = loadClass(INNER_CLASS_NAME);
241 Constructor<?> innerCons = innerClazz2.getConstructor(
242 new Class<?>[] { outerClazz2 });
243 Object i2 = innerCons.newInstance(new Object[] { o2 });
246 // The original Inner.get returns 3+10+20,
247 // but the delegate makes it return 6+10+20
248 assertEquals(6+10+20, callGet(i2, 10, 20));
251 cl2.add(OUTER_CLASS_NAME, cwOuter.toByteArray());
252 cl2.add(INNER_CLASS_NAME, cwInner.toByteArray());
253 cl2.testModifiedInstance();
254 } catch (Throwable t) {
255 throw dumpGeneratedClass(t, cl2);
262 * A class loader than can define and instantiate our modified classes.
264 * The trick here is that this class loader will test our <em>modified</em> version
265 * of the classes, the one with the delegate calls.
267 * Trying to do so in the original class loader generates all sort of link issues because
268 * there are 2 different definitions of the same class name. This class loader will
269 * define and load the class when requested by name and provide helpers to access the
270 * instance methods via reflection.
272 private abstract class ClassLoader2 extends ClassLoader {
274 private final Map<String, byte[]> mClassDefs = new HashMap<String, byte[]>();
276 public ClassLoader2() {
280 public ClassLoader2 add(String className, byte[] definition) {
281 mClassDefs.put(className, definition);
285 public ClassLoader2 add(String className, ClassWriter rewrittenClass) {
286 mClassDefs.put(className, rewrittenClass.toByteArray());
290 private Set<Entry<String, byte[]>> getByteCode() {
291 return mClassDefs.entrySet();
294 @SuppressWarnings("unused")
296 protected Class<?> findClass(String name) throws ClassNotFoundException {
298 return super.findClass(name);
299 } catch (ClassNotFoundException e) {
301 byte[] def = mClassDefs.get(name);
303 // Load the modified ClassWithNative from its bytes representation.
304 return defineClass(name, def, 0, def.length);
308 // Load everything else from the original definition into the new class loader.
309 ClassReader cr = new ClassReader(name);
310 ClassWriter cw = new ClassWriter(0);
312 byte[] bytes = cw.toByteArray();
313 return defineClass(name, bytes, 0, bytes.length);
315 } catch (IOException ioe) {
316 throw new RuntimeException(ioe);
322 * Accesses {@link OuterClass#get()} or {@link InnerClass#get() }via reflection.
324 public int callGet(Object instance, int a, long b) throws Exception {
325 Method m = instance.getClass().getMethod("get",
326 new Class<?>[] { int.class, long.class } );
328 Object result = m.invoke(instance, new Object[] { a, b });
329 return ((Integer) result).intValue();
333 * Accesses {@link ClassWithNative#add(int, int)} via reflection.
335 public int callAdd(Object instance, int a, int b) throws Exception {
336 Method m = instance.getClass().getMethod("add",
337 new Class<?>[] { int.class, int.class });
339 Object result = m.invoke(instance, new Object[] { a, b });
340 return ((Integer) result).intValue();
344 * Accesses {@link ClassWithNative#callNativeInstance(int, double, Object[])}
347 public int callCallNativeInstance(Object instance, int a, double d, Object[] o)
349 Method m = instance.getClass().getMethod("callNativeInstance",
350 new Class<?>[] { int.class, double.class, Object[].class });
352 Object result = m.invoke(instance, new Object[] { a, d, o });
353 return ((Integer) result).intValue();
356 public abstract void testModifiedInstance() throws Exception;
360 * For debugging, it's useful to dump the content of the generated classes
361 * along with the exception that was generated.
363 * However to make it work you need to pull in the org.objectweb.asm.util.TraceClassVisitor
364 * class and associated utilities which are found in the ASM source jar. Since we don't
365 * want that dependency in the source code, we only put it manually for development and
366 * access the TraceClassVisitor via reflection if present.
368 * @param t The exception thrown by {@link ClassLoader2#testModifiedInstance()}
369 * @param cl2 The {@link ClassLoader2} instance with the generated bytecode.
370 * @return Either original {@code t} or a new wrapper {@link Throwable}
372 private Throwable dumpGeneratedClass(Throwable t, ClassLoader2 cl2) {
374 // For debugging, dump the bytecode of the class in case of unexpected error
375 // if we can find the TraceClassVisitor class.
376 Class<?> tcvClass = Class.forName("org.objectweb.asm.util.TraceClassVisitor");
378 StringBuilder sb = new StringBuilder();
379 sb.append('\n').append(t.getClass().getCanonicalName());
380 if (t.getMessage() != null) {
381 sb.append(": ").append(t.getMessage());
384 for (Entry<String, byte[]> entry : cl2.getByteCode()) {
385 String className = entry.getKey();
386 byte[] bytes = entry.getValue();
388 StringWriter sw = new StringWriter();
389 PrintWriter pw = new PrintWriter(sw);
390 // next 2 lines do: TraceClassVisitor tcv = new TraceClassVisitor(pw);
391 Constructor<?> cons = tcvClass.getConstructor(new Class<?>[] { pw.getClass() });
392 Object tcv = cons.newInstance(new Object[] { pw });
393 ClassReader cr2 = new ClassReader(bytes);
394 cr2.accept((ClassVisitor) tcv, 0 /* flags */);
396 sb.append("\nBytecode dump: <").append(className).append(">:\n")
397 .append(sw.toString());
400 // Re-throw exception with new message
401 RuntimeException ex = new RuntimeException(sb.toString(), t);
403 } catch (Throwable ignore) {
404 // In case of problem, just throw the original exception as-is.