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