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