/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.layoutlib.create;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.android.tools.layoutlib.create.dataclass.ClassWithNative;
import com.android.tools.layoutlib.create.dataclass.OuterClass;
import com.android.tools.layoutlib.create.dataclass.OuterClass.InnerClass;
import org.junit.Before;
import org.junit.Test;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public class DelegateClassAdapterTest {
private MockLog mLog;
private static final String NATIVE_CLASS_NAME = ClassWithNative.class.getCanonicalName();
private static final String OUTER_CLASS_NAME = OuterClass.class.getCanonicalName();
private static final String INNER_CLASS_NAME = OuterClass.class.getCanonicalName() + "$" +
InnerClass.class.getSimpleName();
@Before
public void setUp() throws Exception {
mLog = new MockLog();
mLog.setVerbose(true); // capture debug error too
}
/**
* Tests that a class not being modified still works.
*/
@SuppressWarnings("unchecked")
@Test
public void testNoOp() throws Throwable {
// create an instance of the class that will be modified
// (load the class in a distinct class loader so that we can trash its definition later)
ClassLoader cl1 = new ClassLoader(this.getClass().getClassLoader()) { };
Class<ClassWithNative> clazz1 = (Class<ClassWithNative>) cl1.loadClass(NATIVE_CLASS_NAME);
ClassWithNative instance1 = clazz1.newInstance();
assertEquals(42, instance1.add(20, 22));
try {
instance1.callNativeInstance(10, 3.1415, new Object[0] );
fail("Test should have failed to invoke callTheNativeMethod [1]");
} catch (UnsatisfiedLinkError e) {
// This is expected to fail since the native method is not implemented.
}
// Now process it but tell the delegate to not modify any method
ClassWriter cw = new ClassWriter(0 /*flags*/);
HashSet<String> delegateMethods = new HashSet<String>();
String internalClassName = NATIVE_CLASS_NAME.replace('.', '/');
DelegateClassAdapter cv = new DelegateClassAdapter(
mLog, cw, internalClassName, delegateMethods);
ClassReader cr = new ClassReader(NATIVE_CLASS_NAME);
cr.accept(cv, 0 /* flags */);
// Load the generated class in a different class loader and try it again
ClassLoader2 cl2 = null;
try {
cl2 = new ClassLoader2() {
@Override
public void testModifiedInstance() throws Exception {
Class<?> clazz2 = loadClass(NATIVE_CLASS_NAME);
Object i2 = clazz2.newInstance();
assertNotNull(i2);
assertEquals(42, callAdd(i2, 20, 22));
try {
callCallNativeInstance(i2, 10, 3.1415, new Object[0]);
fail("Test should have failed to invoke callTheNativeMethod [2]");
} catch (InvocationTargetException e) {
// This is expected to fail since the native method has NOT been
// overridden here.
assertEquals(UnsatisfiedLinkError.class, e.getCause().getClass());
}
// Check that the native method does NOT have the new annotation
Method[] m = clazz2.getDeclaredMethods();
Method nativeInstanceMethod = null;
for (Method method : m) {
if ("native_instance".equals(method.getName())) {
nativeInstanceMethod = method;
break;
}
}
assertNotNull(nativeInstanceMethod);
assertTrue(Modifier.isNative(nativeInstanceMethod.getModifiers()));
Annotation[] a = nativeInstanceMethod.getAnnotations();
assertEquals(0, a.length);
}
};
cl2.add(NATIVE_CLASS_NAME, cw);
cl2.testModifiedInstance();
} catch (Throwable t) {
throw dumpGeneratedClass(t, cl2);
}
}
/**
* {@link DelegateMethodAdapter} does not support overriding constructors yet,
* so this should fail with an {@link UnsupportedOperationException}.
*
* Although not tested here, the message of the exception should contain the
* constructor signature.
*/
@Test(expected=UnsupportedOperationException.class)
public void testConstructorsNotSupported() throws IOException {
ClassWriter cw = new ClassWriter(0 /*flags*/);
String internalClassName = NATIVE_CLASS_NAME.replace('.', '/');
HashSet<String> delegateMethods = new HashSet<String>();
delegateMethods.add("<init>");
DelegateClassAdapter cv = new DelegateClassAdapter(
mLog, cw, internalClassName, delegateMethods);
ClassReader cr = new ClassReader(NATIVE_CLASS_NAME);
cr.accept(cv, 0 /* flags */);
}
@Test
public void testDelegateNative() throws Throwable {
ClassWriter cw = new ClassWriter(0 /*flags*/);
String internalClassName = NATIVE_CLASS_NAME.replace('.', '/');
HashSet<String> delegateMethods = new HashSet<String>();
delegateMethods.add(DelegateClassAdapter.ALL_NATIVES);
DelegateClassAdapter cv = new DelegateClassAdapter(
mLog, cw, internalClassName, delegateMethods);
ClassReader cr = new ClassReader(NATIVE_CLASS_NAME);
cr.accept(cv, 0 /* flags */);
// Load the generated class in a different class loader and try it
ClassLoader2 cl2 = null;
try {
cl2 = new ClassLoader2() {
@Override
public void testModifiedInstance() throws Exception {
Class<?> clazz2 = loadClass(NATIVE_CLASS_NAME);
Object i2 = clazz2.newInstance();
assertNotNull(i2);
// Use reflection to access inner methods
assertEquals(42, callAdd(i2, 20, 22));
Object[] objResult = new Object[] { null };
int result = callCallNativeInstance(i2, 10, 3.1415, objResult);
assertEquals((int)(10 + 3.1415), result);
assertSame(i2, objResult[0]);
// Check that the native method now has the new annotation and is not native
Method[] m = clazz2.getDeclaredMethods();
Method nativeInstanceMethod = null;
for (Method method : m) {
if ("native_instance".equals(method.getName())) {
nativeInstanceMethod = method;
break;
}
}
assertNotNull(nativeInstanceMethod);
assertFalse(Modifier.isNative(nativeInstanceMethod.getModifiers()));
Annotation[] a = nativeInstanceMethod.getAnnotations();
assertEquals("LayoutlibDelegate", a[0].annotationType().getSimpleName());
}
};
cl2.add(NATIVE_CLASS_NAME, cw);
cl2.testModifiedInstance();
} catch (Throwable t) {
throw dumpGeneratedClass(t, cl2);
}
}
@Test
public void testDelegateInner() throws Throwable {
// We'll delegate the "get" method of both the inner and outer class.
HashSet<String> delegateMethods = new HashSet<String>();
delegateMethods.add("get");
delegateMethods.add("privateMethod");
// Generate the delegate for the outer class.
ClassWriter cwOuter = new ClassWriter(0 /*flags*/);
String outerClassName = OUTER_CLASS_NAME.replace('.', '/');
DelegateClassAdapter cvOuter = new DelegateClassAdapter(
mLog, cwOuter, outerClassName, delegateMethods);
ClassReader cr = new ClassReader(OUTER_CLASS_NAME);
cr.accept(cvOuter, 0 /* flags */);
// Generate the delegate for the inner class.
ClassWriter cwInner = new ClassWriter(0 /*flags*/);
String innerClassName = INNER_CLASS_NAME.replace('.', '/');
DelegateClassAdapter cvInner = new DelegateClassAdapter(
mLog, cwInner, innerClassName, delegateMethods);
cr = new ClassReader(INNER_CLASS_NAME);
cr.accept(cvInner, 0 /* flags */);
// Load the generated classes in a different class loader and try them
ClassLoader2 cl2 = null;
try {
cl2 = new ClassLoader2() {
@Override
public void testModifiedInstance() throws Exception {
// Check the outer class
Class<?> outerClazz2 = loadClass(OUTER_CLASS_NAME);
Object o2 = outerClazz2.newInstance();
assertNotNull(o2);
// The original Outer.get returns 1+10+20,
// but the delegate makes it return 4+10+20
assertEquals(4+10+20, callGet(o2, 10, 20));
assertEquals(1+10+20, callGet_Original(o2, 10, 20));
// The original Outer has a private method,
// so by default we can't access it.
boolean gotIllegalAccessException = false;
try {
callMethod(o2, "privateMethod", false /*makePublic*/);
} catch(IllegalAccessException e) {
gotIllegalAccessException = true;
}
assertTrue(gotIllegalAccessException);
// The private method from original Outer has been
// delegated. The delegate generated should have the
// same access.
gotIllegalAccessException = false;
try {
assertEquals("outerPrivateMethod",
callMethod(o2, "privateMethod_Original", false /*makePublic*/));
} catch (IllegalAccessException e) {
gotIllegalAccessException = true;
}
assertTrue(gotIllegalAccessException);
// Check the inner class. Since it's not a static inner class, we need
// to use the hidden constructor that takes the outer class as first parameter.
Class<?> innerClazz2 = loadClass(INNER_CLASS_NAME);
Constructor<?> innerCons = innerClazz2.getConstructor(outerClazz2);
Object i2 = innerCons.newInstance(o2);
assertNotNull(i2);
// The original Inner.get returns 3+10+20,
// but the delegate makes it return 6+10+20
assertEquals(6+10+20, callGet(i2, 10, 20));
assertEquals(3+10+20, callGet_Original(i2, 10, 20));
}
};
cl2.add(OUTER_CLASS_NAME, cwOuter.toByteArray());
cl2.add(INNER_CLASS_NAME, cwInner.toByteArray());
cl2.testModifiedInstance();
} catch (Throwable t) {
throw dumpGeneratedClass(t, cl2);
}
}
//-------
/**
* A class loader than can define and instantiate our modified classes.
* <p/>
* The trick here is that this class loader will test our <em>modified</em> version
* of the classes, the one with the delegate calls.
* <p/>
* Trying to do so in the original class loader generates all sort of link issues because
* there are 2 different definitions of the same class name. This class loader will
* define and load the class when requested by name and provide helpers to access the
* instance methods via reflection.
*/
private abstract class ClassLoader2 extends ClassLoader {
private final Map<String, byte[]> mClassDefs = new HashMap<String, byte[]>();
public ClassLoader2() {
super(null);
}
public ClassLoader2 add(String className, byte[] definition) {
mClassDefs.put(className, definition);
return this;
}
public ClassLoader2 add(String className, ClassWriter rewrittenClass) {
mClassDefs.put(className, rewrittenClass.toByteArray());
return this;
}
private Set<Entry<String, byte[]>> getByteCode() {
return mClassDefs.entrySet();
}
@SuppressWarnings("unused")
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
return super.findClass(name);
} catch (ClassNotFoundException e) {
byte[] def = mClassDefs.get(name);
if (def != null) {
// Load the modified ClassWithNative from its bytes representation.
return defineClass(name, def, 0, def.length);
}
try {
// Load everything else from the original definition into the new class loader.
ClassReader cr = new ClassReader(name);
ClassWriter cw = new ClassWriter(0);
cr.accept(cw, 0);
byte[] bytes = cw.toByteArray();
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
}
/**
* Accesses {@link OuterClass#get} or {@link InnerClass#get}via reflection.
*/
public int callGet(Object instance, int a, long b) throws Exception {
Method m = instance.getClass().getMethod("get",
int.class, long.class);
Object result = m.invoke(instance, a, b);
return (Integer) result;
}
/**
* Accesses the "_Original" methods for {@link OuterClass#get}
* or {@link InnerClass#get}via reflection.
*/
public int callGet_Original(Object instance, int a, long b) throws Exception {
Method m = instance.getClass().getMethod("get_Original",
int.class, long.class);
Object result = m.invoke(instance, a, b);
return (Integer) result;
}
/**
* Accesses the any declared method that takes no parameter via reflection.
*/
@SuppressWarnings("unchecked")
public <T> T callMethod(Object instance, String methodName, boolean makePublic) throws Exception {
Method m = instance.getClass().getDeclaredMethod(methodName, (Class<?>[])null);
boolean wasAccessible = m.isAccessible();
if (makePublic && !wasAccessible) {
m.setAccessible(true);
}
Object result = m.invoke(instance, (Object[])null);
if (makePublic && !wasAccessible) {
m.setAccessible(false);
}
return (T) result;
}
/**
* Accesses {@link ClassWithNative#add(int, int)} via reflection.
*/
public int callAdd(Object instance, int a, int b) throws Exception {
Method m = instance.getClass().getMethod("add",
int.class, int.class);
Object result = m.invoke(instance, a, b);
return (Integer) result;
}
/**
* Accesses {@link ClassWithNative#callNativeInstance(int, double, Object[])}
* via reflection.
*/
public int callCallNativeInstance(Object instance, int a, double d, Object[] o)
throws Exception {
Method m = instance.getClass().getMethod("callNativeInstance",
int.class, double.class, Object[].class);
Object result = m.invoke(instance, a, d, o);
return (Integer) result;
}
public abstract void testModifiedInstance() throws Exception;
}
/**
* For debugging, it's useful to dump the content of the generated classes
* along with the exception that was generated.
*
* However to make it work you need to pull in the org.objectweb.asm.util.TraceClassVisitor
* class and associated utilities which are found in the ASM source jar. Since we don't
* want that dependency in the source code, we only put it manually for development and
* access the TraceClassVisitor via reflection if present.
*
* @param t The exception thrown by {@link ClassLoader2#testModifiedInstance()}
* @param cl2 The {@link ClassLoader2} instance with the generated bytecode.
* @return Either original {@code t} or a new wrapper {@link Throwable}
*/
private Throwable dumpGeneratedClass(Throwable t, ClassLoader2 cl2) {
try {
// For debugging, dump the bytecode of the class in case of unexpected error
// if we can find the TraceClassVisitor class.
Class<?> tcvClass = Class.forName("org.objectweb.asm.util.TraceClassVisitor");
StringBuilder sb = new StringBuilder();
sb.append('\n').append(t.getClass().getCanonicalName());
if (t.getMessage() != null) {
sb.append(": ").append(t.getMessage());
}
for (Entry<String, byte[]> entry : cl2.getByteCode()) {
String className = entry.getKey();
byte[] bytes = entry.getValue();
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
// next 2 lines do: TraceClassVisitor tcv = new TraceClassVisitor(pw);
Constructor<?> cons = tcvClass.getConstructor(pw.getClass());
Object tcv = cons.newInstance(pw);
ClassReader cr2 = new ClassReader(bytes);
cr2.accept((ClassVisitor) tcv, 0 /* flags */);
sb.append("\nBytecode dump: <").append(className).append(">:\n")
.append(sw.toString());
}
// Re-throw exception with new message
return new RuntimeException(sb.toString(), t);
} catch (Throwable ignore) {
// In case of problem, just throw the original exception as-is.
return t;
}
}
}