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 
17 package com.android.tools.layoutlib.create;
18 
19 
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;
26 
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;
30 import com.android.tools.layoutlib.create.dataclass.OuterClass.StaticInnerClass;
31 
32 import org.junit.Before;
33 import org.junit.Test;
34 import org.objectweb.asm.ClassReader;
35 import org.objectweb.asm.ClassVisitor;
36 import org.objectweb.asm.ClassWriter;
37 
38 import java.io.IOException;
39 import java.io.PrintWriter;
40 import java.io.StringWriter;
41 import java.lang.annotation.Annotation;
42 import java.lang.reflect.Constructor;
43 import java.lang.reflect.InvocationTargetException;
44 import java.lang.reflect.Method;
45 import java.lang.reflect.Modifier;
46 import java.util.HashMap;
47 import java.util.HashSet;
48 import java.util.Map;
49 import java.util.Map.Entry;
50 import java.util.Set;
51 
52 public 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
setUp()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
testNoOp()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     @Test
testConstructorAfterDelegate()141     public void testConstructorAfterDelegate() throws Throwable {
142         ClassWriter cw = new ClassWriter(0 /*flags*/);
143 
144         String internalClassName = NATIVE_CLASS_NAME.replace('.', '/');
145 
146         HashSet<String> delegateMethods = new HashSet<>();
147         delegateMethods.add("<init>");
148         DelegateClassAdapter cv = new DelegateClassAdapter(
149                 mLog, cw, internalClassName, delegateMethods);
150 
151         ClassReader cr = new ClassReader(NATIVE_CLASS_NAME);
152         cr.accept(cv, 0 /* flags */);
153 
154         ClassLoader2 cl2 = null;
155         try {
156             cl2 = new ClassLoader2() {
157                 @Override
158                 public void testModifiedInstance() throws Exception {
159                     Class<?> clazz2 = loadClass(NATIVE_CLASS_NAME);
160                     Object i2 = clazz2.newInstance();
161                     assertNotNull(i2);
162                     assertEquals(123, clazz2.getField("mId").getInt(i2));
163                 }
164             };
165             cl2.add(NATIVE_CLASS_NAME, cw);
166             cl2.testModifiedInstance();
167         } catch (Throwable t) {
168             throw dumpGeneratedClass(t, cl2);
169         }
170     }
171 
172     @Test
testInnerConstructorAfterDelegate()173     public void testInnerConstructorAfterDelegate() throws Throwable {
174         ClassWriter cw = new ClassWriter(0 /*flags*/);
175 
176         String internalClassName = INNER_CLASS_NAME.replace('.', '/');
177 
178         HashSet<String> delegateMethods = new HashSet<>();
179         delegateMethods.add("<init>");
180         DelegateClassAdapter cv = new DelegateClassAdapter(
181                 mLog, cw, internalClassName, delegateMethods);
182 
183         ClassReader cr = new ClassReader(INNER_CLASS_NAME);
184         cr.accept(cv, 0 /* flags */);
185 
186         ClassLoader2 cl2 = null;
187         try {
188             cl2 = new ClassLoader2() {
189                 @Override
190                 public void testModifiedInstance() throws Exception {
191                     Class<?> outerClazz2 = loadClass(OUTER_CLASS_NAME);
192                     Object o2 = outerClazz2.newInstance();
193 
194                     Class<?> clazz2 = loadClass(INNER_CLASS_NAME);
195                     Object i2 = clazz2.getConstructor(outerClazz2).newInstance(o2);
196                     assertNotNull(i2);
197                     assertEquals(98, clazz2.getField("mInnerId").getInt(i2));
198                 }
199             };
200             cl2.add(INNER_CLASS_NAME, cw);
201             cl2.testModifiedInstance();
202         } catch (Throwable t) {
203             throw dumpGeneratedClass(t, cl2);
204         }
205     }
206 
207     @Test
testStaticInnerConstructorAfterDelegate()208     public void testStaticInnerConstructorAfterDelegate() throws Throwable {
209         ClassWriter cw = new ClassWriter(0 /*flags*/);
210 
211         String internalClassName = STATIC_INNER_CLASS_NAME.replace('.', '/');
212 
213         HashSet<String> delegateMethods = new HashSet<>();
214         delegateMethods.add("<init>");
215         DelegateClassAdapter cv = new DelegateClassAdapter(
216                 mLog, cw, internalClassName, delegateMethods);
217 
218         ClassReader cr = new ClassReader(STATIC_INNER_CLASS_NAME);
219         cr.accept(cv, 0 /* flags */);
220 
221         ClassLoader2 cl2 = null;
222         try {
223             cl2 = new ClassLoader2() {
224                 @Override
225                 public void testModifiedInstance() throws Exception {
226                     Class<?> clazz2 = loadClass(STATIC_INNER_CLASS_NAME);
227                     Object i2 = clazz2.newInstance();
228                     assertNotNull(i2);
229                     assertEquals(42, clazz2.getField("mStaticInnerId").getInt(i2));
230                 }
231             };
232             cl2.add(STATIC_INNER_CLASS_NAME, cw);
233             cl2.testModifiedInstance();
234         } catch (Throwable t) {
235             throw dumpGeneratedClass(t, cl2);
236         }
237     }
238 
239     @Test
testDelegateNative()240     public void testDelegateNative() throws Throwable {
241         ClassWriter cw = new ClassWriter(0 /*flags*/);
242         String internalClassName = NATIVE_CLASS_NAME.replace('.', '/');
243 
244         HashSet<String> delegateMethods = new HashSet<>();
245         delegateMethods.add(DelegateClassAdapter.ALL_NATIVES);
246         DelegateClassAdapter cv = new DelegateClassAdapter(
247                 mLog, cw, internalClassName, delegateMethods);
248 
249         ClassReader cr = new ClassReader(NATIVE_CLASS_NAME);
250         cr.accept(cv, 0 /* flags */);
251 
252         // Load the generated class in a different class loader and try it
253         ClassLoader2 cl2 = null;
254         try {
255             cl2 = new ClassLoader2() {
256                 @Override
257                 public void testModifiedInstance() throws Exception {
258                     Class<?> clazz2 = loadClass(NATIVE_CLASS_NAME);
259                     Object i2 = clazz2.newInstance();
260                     assertNotNull(i2);
261 
262                     // Use reflection to access inner methods
263                     assertEquals(42, callAdd(i2, 20, 22));
264 
265                      Object[] objResult = new Object[] { null };
266                      int result = callCallNativeInstance(i2, 10, 3.1415, objResult);
267                      assertEquals((int)(10 + 3.1415), result);
268                      assertSame(i2, objResult[0]);
269 
270                      // Check that the native method now has the new annotation and is not native
271                      Method[] m = clazz2.getDeclaredMethods();
272                      Method nativeInstanceMethod = null;
273                      for (Method method : m) {
274                          if ("native_instance".equals(method.getName())) {
275                              nativeInstanceMethod = method;
276                              break;
277                          }
278                      }
279                      assertNotNull(nativeInstanceMethod);
280                      assertFalse(Modifier.isNative(nativeInstanceMethod.getModifiers()));
281                      Annotation[] a = nativeInstanceMethod.getAnnotations();
282                      assertEquals("LayoutlibDelegate", a[0].annotationType().getSimpleName());
283                 }
284             };
285             cl2.add(NATIVE_CLASS_NAME, cw);
286             cl2.testModifiedInstance();
287         } catch (Throwable t) {
288             throw dumpGeneratedClass(t, cl2);
289         }
290     }
291 
292     @Test
testDelegateInner()293     public void testDelegateInner() throws Throwable {
294         // We'll delegate the "get" method of both the inner and outer class.
295         HashSet<String> delegateMethods = new HashSet<>();
296         delegateMethods.add("get");
297         delegateMethods.add("privateMethod");
298 
299         // Generate the delegate for the outer class.
300         ClassWriter cwOuter = new ClassWriter(0 /*flags*/);
301         String outerClassName = OUTER_CLASS_NAME.replace('.', '/');
302         DelegateClassAdapter cvOuter = new DelegateClassAdapter(
303                 mLog, cwOuter, outerClassName, delegateMethods);
304         ClassReader cr = new ClassReader(OUTER_CLASS_NAME);
305         cr.accept(cvOuter, 0 /* flags */);
306 
307         // Generate the delegate for the inner class.
308         ClassWriter cwInner = new ClassWriter(0 /*flags*/);
309         String innerClassName = INNER_CLASS_NAME.replace('.', '/');
310         DelegateClassAdapter cvInner = new DelegateClassAdapter(
311                 mLog, cwInner, innerClassName, delegateMethods);
312         cr = new ClassReader(INNER_CLASS_NAME);
313         cr.accept(cvInner, 0 /* flags */);
314 
315         // Load the generated classes in a different class loader and try them
316         ClassLoader2 cl2 = null;
317         try {
318             cl2 = new ClassLoader2() {
319                 @Override
320                 public void testModifiedInstance() throws Exception {
321 
322                     // Check the outer class
323                     Class<?> outerClazz2 = loadClass(OUTER_CLASS_NAME);
324                     Object o2 = outerClazz2.newInstance();
325                     assertNotNull(o2);
326 
327                     // The original Outer.get returns 1+10+20,
328                     // but the delegate makes it return 4+10+20
329                     assertEquals(4+10+20, callGet(o2, 10, 20));
330                     assertEquals(1+10+20, callGet_Original(o2, 10, 20));
331 
332                     // The original Outer has a private method,
333                     // so by default we can't access it.
334                     boolean gotIllegalAccessException = false;
335                     try {
336                          callMethod(o2, "privateMethod", false /*makePublic*/);
337                     } catch(IllegalAccessException e) {
338                         gotIllegalAccessException = true;
339                     }
340                     assertTrue(gotIllegalAccessException);
341 
342                     // The private method from original Outer has been
343                     // delegated. The delegate generated should have the
344                     // same access.
345                     gotIllegalAccessException = false;
346                     try {
347                         assertEquals("outerPrivateMethod",
348                                 callMethod(o2, "privateMethod_Original", false /*makePublic*/));
349                     } catch (IllegalAccessException e) {
350                         gotIllegalAccessException = true;
351                     }
352                     assertTrue(gotIllegalAccessException);
353 
354                     // Check the inner class. Since it's not a static inner class, we need
355                     // to use the hidden constructor that takes the outer class as first parameter.
356                     Class<?> innerClazz2 = loadClass(INNER_CLASS_NAME);
357                     Constructor<?> innerCons = innerClazz2.getConstructor(outerClazz2);
358                     Object i2 = innerCons.newInstance(o2);
359                     assertNotNull(i2);
360 
361                     // The original Inner.get returns 3+10+20,
362                     // but the delegate makes it return 6+10+20
363                     assertEquals(6+10+20, callGet(i2, 10, 20));
364                     assertEquals(3+10+20, callGet_Original(i2, 10, 20));
365                 }
366             };
367             cl2.add(OUTER_CLASS_NAME, cwOuter.toByteArray());
368             cl2.add(INNER_CLASS_NAME, cwInner.toByteArray());
369             cl2.testModifiedInstance();
370         } catch (Throwable t) {
371             throw dumpGeneratedClass(t, cl2);
372         }
373     }
374 
375     @Test
testDelegateStaticInner()376     public void testDelegateStaticInner() throws Throwable {
377         // We'll delegate the "get" method of both the inner and outer class.
378         HashSet<String> delegateMethods = new HashSet<>();
379         delegateMethods.add("get");
380 
381         // Generate the delegate for the outer class.
382         ClassWriter cwOuter = new ClassWriter(0 /*flags*/);
383         String outerClassName = OUTER_CLASS_NAME.replace('.', '/');
384         DelegateClassAdapter cvOuter = new DelegateClassAdapter(
385                 mLog, cwOuter, outerClassName, delegateMethods);
386         ClassReader cr = new ClassReader(OUTER_CLASS_NAME);
387         cr.accept(cvOuter, 0 /* flags */);
388 
389         // Generate the delegate for the static inner class.
390         ClassWriter cwInner = new ClassWriter(0 /*flags*/);
391         String innerClassName = STATIC_INNER_CLASS_NAME.replace('.', '/');
392         DelegateClassAdapter cvInner = new DelegateClassAdapter(
393                 mLog, cwInner, innerClassName, delegateMethods);
394         cr = new ClassReader(STATIC_INNER_CLASS_NAME);
395         cr.accept(cvInner, 0 /* flags */);
396 
397         // Load the generated classes in a different class loader and try them
398         ClassLoader2 cl2 = null;
399         try {
400             cl2 = new ClassLoader2() {
401                 @Override
402                 public void testModifiedInstance() throws Exception {
403 
404                     // Check the outer class
405                     Class<?> outerClazz2 = loadClass(OUTER_CLASS_NAME);
406                     Object o2 = outerClazz2.newInstance();
407                     assertNotNull(o2);
408 
409                     // Check the inner class. Since it's not a static inner class, we need
410                     // to use the hidden constructor that takes the outer class as first parameter.
411                     Class<?> innerClazz2 = loadClass(STATIC_INNER_CLASS_NAME);
412                     Constructor<?> innerCons = innerClazz2.getConstructor();
413                     Object i2 = innerCons.newInstance();
414                     assertNotNull(i2);
415 
416                     // The original StaticInner.get returns 100+10+20,
417                     // but the delegate makes it return 6+10+20
418                     assertEquals(6+10+20, callGet(i2, 10, 20));
419                     assertEquals(100+10+20, callGet_Original(i2, 10, 20));
420                 }
421             };
422             cl2.add(OUTER_CLASS_NAME, cwOuter.toByteArray());
423             cl2.add(STATIC_INNER_CLASS_NAME, cwInner.toByteArray());
424             cl2.testModifiedInstance();
425         } catch (Throwable t) {
426             throw dumpGeneratedClass(t, cl2);
427         }
428     }
429 
430     //-------
431 
432     /**
433      * A class loader than can define and instantiate our modified classes.
434      * <p/>
435      * The trick here is that this class loader will test our <em>modified</em> version
436      * of the classes, the one with the delegate calls.
437      * <p/>
438      * Trying to do so in the original class loader generates all sort of link issues because
439      * there are 2 different definitions of the same class name. This class loader will
440      * define and load the class when requested by name and provide helpers to access the
441      * instance methods via reflection.
442      */
443     private abstract class ClassLoader2 extends ClassLoader {
444 
445         private final Map<String, byte[]> mClassDefs = new HashMap<>();
446 
ClassLoader2()447         public ClassLoader2() {
448             super(null);
449         }
450 
add(String className, byte[] definition)451         public ClassLoader2 add(String className, byte[] definition) {
452             mClassDefs.put(className, definition);
453             return this;
454         }
455 
add(String className, ClassWriter rewrittenClass)456         public ClassLoader2 add(String className, ClassWriter rewrittenClass) {
457             mClassDefs.put(className, rewrittenClass.toByteArray());
458             return this;
459         }
460 
getByteCode()461         private Set<Entry<String, byte[]>> getByteCode() {
462             return mClassDefs.entrySet();
463         }
464 
465         @SuppressWarnings("unused")
466         @Override
findClass(String name)467         protected Class<?> findClass(String name) throws ClassNotFoundException {
468             try {
469                 return super.findClass(name);
470             } catch (ClassNotFoundException e) {
471 
472                 byte[] def = mClassDefs.get(name);
473                 if (def != null) {
474                     // Load the modified ClassWithNative from its bytes representation.
475                     return defineClass(name, def, 0, def.length);
476                 }
477 
478                 try {
479                     // Load everything else from the original definition into the new class loader.
480                     ClassReader cr = new ClassReader(name);
481                     ClassWriter cw = new ClassWriter(0);
482                     cr.accept(cw, 0);
483                     byte[] bytes = cw.toByteArray();
484                     return defineClass(name, bytes, 0, bytes.length);
485 
486                 } catch (IOException ioe) {
487                     throw new RuntimeException(ioe);
488                 }
489             }
490         }
491 
492         /**
493          * Accesses {@link OuterClass#get} or {@link InnerClass#get}via reflection.
494          */
callGet(Object instance, int a, long b)495         public int callGet(Object instance, int a, long b) throws Exception {
496             Method m = instance.getClass().getMethod("get",
497                     int.class, long.class);
498 
499             Object result = m.invoke(instance, a, b);
500             return (Integer) result;
501         }
502 
503         /**
504          * Accesses the "_Original" methods for {@link OuterClass#get}
505          * or {@link InnerClass#get}via reflection.
506          */
callGet_Original(Object instance, int a, long b)507         public int callGet_Original(Object instance, int a, long b) throws Exception {
508             Method m = instance.getClass().getMethod("get_Original",
509                     int.class, long.class);
510 
511             Object result = m.invoke(instance, a, b);
512             return (Integer) result;
513         }
514 
515         /**
516          * Accesses the any declared method that takes no parameter via reflection.
517          */
518         @SuppressWarnings("unchecked")
callMethod(Object instance, String methodName, boolean makePublic)519         public <T> T callMethod(Object instance, String methodName, boolean makePublic) throws Exception {
520             Method m = instance.getClass().getDeclaredMethod(methodName, (Class<?>[])null);
521 
522             boolean wasAccessible = m.isAccessible();
523             if (makePublic && !wasAccessible) {
524                 m.setAccessible(true);
525             }
526 
527             Object result = m.invoke(instance, (Object[])null);
528 
529             if (makePublic && !wasAccessible) {
530                 m.setAccessible(false);
531             }
532 
533             return (T) result;
534         }
535 
536         /**
537          * Accesses {@link ClassWithNative#add(int, int)} via reflection.
538          */
callAdd(Object instance, int a, int b)539         public int callAdd(Object instance, int a, int b) throws Exception {
540             Method m = instance.getClass().getMethod("add",
541                     int.class, int.class);
542 
543             Object result = m.invoke(instance, a, b);
544             return (Integer) result;
545         }
546 
547         /**
548          * Accesses {@link ClassWithNative#callNativeInstance(int, double, Object[])}
549          * via reflection.
550          */
callCallNativeInstance(Object instance, int a, double d, Object[] o)551         public int callCallNativeInstance(Object instance, int a, double d, Object[] o)
552                 throws Exception {
553             Method m = instance.getClass().getMethod("callNativeInstance",
554                     int.class, double.class, Object[].class);
555 
556             Object result = m.invoke(instance, a, d, o);
557             return (Integer) result;
558         }
559 
testModifiedInstance()560         public abstract void testModifiedInstance() throws Exception;
561     }
562 
563     /**
564      * For debugging, it's useful to dump the content of the generated classes
565      * along with the exception that was generated.
566      *
567      * However to make it work you need to pull in the org.objectweb.asm.util.TraceClassVisitor
568      * class and associated utilities which are found in the ASM source jar. Since we don't
569      * want that dependency in the source code, we only put it manually for development and
570      * access the TraceClassVisitor via reflection if present.
571      *
572      * @param t The exception thrown by {@link ClassLoader2#testModifiedInstance()}
573      * @param cl2 The {@link ClassLoader2} instance with the generated bytecode.
574      * @return Either original {@code t} or a new wrapper {@link Throwable}
575      */
dumpGeneratedClass(Throwable t, ClassLoader2 cl2)576     private Throwable dumpGeneratedClass(Throwable t, ClassLoader2 cl2) {
577         try {
578             // For debugging, dump the bytecode of the class in case of unexpected error
579             // if we can find the TraceClassVisitor class.
580             Class<?> tcvClass = Class.forName("org.objectweb.asm.util.TraceClassVisitor");
581 
582             StringBuilder sb = new StringBuilder();
583             sb.append('\n').append(t.getClass().getCanonicalName());
584             if (t.getMessage() != null) {
585                 sb.append(": ").append(t.getMessage());
586               }
587 
588             for (Entry<String, byte[]> entry : cl2.getByteCode()) {
589                 String className = entry.getKey();
590                 byte[] bytes = entry.getValue();
591 
592                 StringWriter sw = new StringWriter();
593                 PrintWriter pw = new PrintWriter(sw);
594                 // next 2 lines do: TraceClassVisitor tcv = new TraceClassVisitor(pw);
595                 Constructor<?> cons = tcvClass.getConstructor(pw.getClass());
596                 Object tcv = cons.newInstance(pw);
597                 ClassReader cr2 = new ClassReader(bytes);
598                 cr2.accept((ClassVisitor) tcv, 0 /* flags */);
599 
600                 sb.append("\nBytecode dump: <").append(className).append(">:\n")
601                   .append(sw.toString());
602             }
603 
604             // Re-throw exception with new message
605             return new RuntimeException(sb.toString(), t);
606         } catch (Throwable ignore) {
607             // In case of problem, just throw the original exception as-is.
608             return t;
609         }
610     }
611 
612 }
613