/*
 * Copyright 2002,2003,2004 The Apache Software Foundation
 *
 *  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 net.sf.cglib.proxy;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import net.sf.cglib.CodeGenTestCase;
import net.sf.cglib.core.AbstractClassGenerator;
import net.sf.cglib.core.DefaultNamingPolicy;
import net.sf.cglib.core.NamingPolicy;
import net.sf.cglib.core.Predicate;
import net.sf.cglib.core.ReflectUtils;
import net.sf.cglib.reflect.FastClass;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

/**
 *@author     Juozas Baliuka <a href="mailto:baliuka@mwm.lt">
 *      baliuka@mwm.lt</a>
 *@version    $Id: TestEnhancer.java,v 1.58 2012/07/27 16:02:49 baliuka Exp $
 */
public class TestEnhancer extends CodeGenTestCase {
    private static final MethodInterceptor TEST_INTERCEPTOR = new TestInterceptor();
    
    private static final Class [] EMPTY_ARG = new Class[]{};
    
    private boolean invokedProtectedMethod = false;
    
    private boolean invokedPackageMethod   = false;
    
    private boolean invokedAbstractMethod  = false;
    
    public TestEnhancer(String testName) {
        super(testName);
    }
    
    
    
    public static Test suite() {
        return new TestSuite(TestEnhancer.class);
    }
    
    public static void main(String args[]) {
        String[] testCaseName = {TestEnhancer.class.getName()};
        junit.textui.TestRunner.main(testCaseName);
    }

    public void testEnhance()throws Throwable{
        
        java.util.Vector vector1 = (java.util.Vector)Enhancer.create(
        java.util.Vector.class,
        new Class[]{java.util.List.class}, TEST_INTERCEPTOR );
        
        java.util.Vector vector2  = (java.util.Vector)Enhancer.create(
        java.util.Vector.class,
        new Class[]{java.util.List.class}, TEST_INTERCEPTOR );
        
        
        
        
        assertTrue("Cache failed",vector1.getClass() == vector2.getClass());
    }
    
   
    public void testMethods()throws Throwable{
        
        MethodInterceptor interceptor =
        new TestInterceptor(){
            
            public Object afterReturn(  Object obj, Method method,
            Object args[],
            boolean invokedSuper, Object retValFromSuper,
            java.lang.Throwable e )throws java.lang.Throwable{
                
                int mod =  method.getModifiers();
                
                if( Modifier.isProtected( mod ) ){
                    invokedProtectedMethod = true;
                }
                
                if( Modifier.isAbstract(mod) ){
                    invokedAbstractMethod = true;
                }
                
                
                if( ! ( Modifier.isProtected( mod ) || Modifier.isPublic( mod ) )){
                    invokedPackageMethod = true;
                }
                
                return retValFromSuper;//return the same as supper
            }
            
        };
        
        
        Source source =  (Source)Enhancer.create(
        Source.class,
        null,interceptor );
        
        source.callAll();
        assertTrue("protected", invokedProtectedMethod );
        assertTrue("package", invokedPackageMethod );
        assertTrue("abstract", invokedAbstractMethod );
    }
    
    public void testEnhanced()throws Throwable{
        
        Source source =  (Source)Enhancer.create(
        Source.class,
        null, TEST_INTERCEPTOR );
        
        
        TestCase.assertTrue("enhance", Source.class != source.getClass() );
        
    }

    public void testFinalizeNotProxied() throws Throwable {
        Source source = (Source) Enhancer.create(
                Source.class,
                null, TEST_INTERCEPTOR);

        try {
            Method finalize = source.getClass().getDeclaredMethod("finalize");
            assertNull("CGLIB should enhanced object should not declare finalize() method so proxy objects are not eligible for finalization, thus faster", finalize);
        } catch(NoSuchMethodException e) {
            // expected
        }
    }

    public void testEnhanceObject() throws Throwable {
        EA obj = new EA();
        EA save = obj;
        obj.setName("herby");
        EA proxy = (EA)Enhancer.create( EA.class,  new DelegateInterceptor(save) );
     
        assertEquals("proxy.getName()", "herby", proxy.getName());

        Factory factory = (Factory)proxy;
        assertEquals("((EA)factory.newInstance(factory.getCallbacks())).getName()", "herby", ((EA)factory.newInstance(factory.getCallbacks())).getName());
    }

    class DelegateInterceptor implements MethodInterceptor {
      Object delegate;
        DelegateInterceptor(Object delegate){
          this.delegate = delegate;
        }
        public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args, MethodProxy proxy) throws Throwable {
            return proxy.invoke(delegate,args);
        }
        
    }
    public void testEnhanceObjectDelayed() throws Throwable {
        
        DelegateInterceptor mi = new DelegateInterceptor(null);
        EA proxy = (EA)Enhancer.create( EA.class, mi);
        EA obj = new EA();
        obj.setName("herby");
        mi.delegate = obj;
       assertTrue(proxy.getName().equals("herby"));
    }
    
    
    public void testTypes()throws Throwable{
        
        Source source =  (Source)Enhancer.create(
        Source.class,
        null, TEST_INTERCEPTOR );
        TestCase.assertTrue("intType",   1   == source.intType(1));
        TestCase.assertTrue("longType",  1L  == source.longType(1L));
        TestCase.assertTrue("floatType", 1.1f  == source.floatType(1.1f));
        TestCase.assertTrue("doubleType",1.1 == source.doubleType(1.1));
        TestCase.assertEquals("objectType","1", source.objectType("1") );
        TestCase.assertEquals("objectType","",  source.toString() );
        source.arrayType( new int[]{} );    
        
    }
    

    public void testModifiers()throws Throwable{
        
        Source source =  (Source)Enhancer.create(
        Source.class,
        null, TEST_INTERCEPTOR );
        
        Class enhancedClass = source.getClass();
        
        assertTrue("isProtected" , Modifier.isProtected( enhancedClass.getDeclaredMethod("protectedMethod", EMPTY_ARG ).getModifiers() ));
        int mod =  enhancedClass.getDeclaredMethod("packageMethod", EMPTY_ARG ).getModifiers() ;
        assertTrue("isPackage" , !( Modifier.isProtected(mod)|| Modifier.isPublic(mod) ) );
        
        //not sure about this (do we need it for performace ?)
        assertTrue("isFinal" ,  Modifier.isFinal( mod ) );
        
        mod =  enhancedClass.getDeclaredMethod("synchronizedMethod", EMPTY_ARG ).getModifiers() ;
        assertTrue("isSynchronized" ,  !Modifier.isSynchronized( mod ) );
        
        
    }
    
    public void testObject()throws Throwable{
        
        Object source =  Enhancer.create(
        null,
        null, TEST_INTERCEPTOR );
        
        assertTrue("parent is object",
        source.getClass().getSuperclass() == Object.class  );
        
    }

    public void testSystemClassLoader()throws Throwable{
        
        Object source =  enhance(
        null,
        null, TEST_INTERCEPTOR , ClassLoader.getSystemClassLoader());
        source.toString();
        assertTrue("SystemClassLoader",
        source.getClass().getClassLoader()
        == ClassLoader.getSystemClassLoader()  );
        
    }
    
    
    
    public void testCustomClassLoader()throws Throwable{
        
        ClassLoader custom = new ClassLoader(this.getClass().getClassLoader()){};
        
        Object source =  enhance( null, null, TEST_INTERCEPTOR, custom);
        source.toString();
        assertTrue("Custom classLoader", source.getClass().getClassLoader() == custom  );
        
        custom = new ClassLoader(){};
        
        source =  enhance( null, null, TEST_INTERCEPTOR, custom);
        source.toString();
        assertTrue("Custom classLoader", source.getClass().getClassLoader() == custom  );
        
        
    }

    public void testProxyClassReuseAcrossGC() throws InterruptedException {
        String proxyClassName = null;
        for (int i = 0; i < 50; i++) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(Source.class);
            enhancer.setCallbackFilter(new CallbackFilter() {
                public int accept(Method method) {
                    return 0;
                }

                @Override
                public boolean equals(Object obj) {
                    return true;
                }

                @Override
                public int hashCode() {
                    return 0;
                }
            });
            enhancer.setInterfaces(new Class[]{Serializable.class});
            enhancer.setCallback(new InvocationHandler() {
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    if (method.getDeclaringClass() != Object.class
                            && method.getReturnType() == String.class) {
                        return null;
                    } else {
                        throw new RuntimeException("Do not know what to do.");
                    }
                }
            });
            Source proxy = (Source) enhancer.create();
            String actualProxyClassName = proxy.getClass().getName();
            if (proxyClassName == null) {
                proxyClassName = actualProxyClassName;
            } else {
                assertEquals("GC iteration " + i + ", proxy class should survive GC and be reused even across GC",
                        proxyClassName, actualProxyClassName);
            }
            System.gc();
        }
    }

    /**
     * Verifies that the cache in {@link AbstractClassGenerator} SOURCE doesn't
     * leak class definitions of classloaders that are no longer used.
     */
    public void testSourceCleanAfterClassLoaderDispose() throws Throwable {
        ClassLoader custom = new ClassLoader(this.getClass().getClassLoader()) {

            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                if (EA.class.getName().equals(name)) {
                    InputStream classStream = this.getClass().getResourceAsStream("/net/sf/cglib/proxy/EA.class");
                    byte[] classBytes;
                    try {
                        classBytes = toByteArray(classStream);
                        return this.defineClass(null, classBytes, 0, classBytes.length);
                    } catch (IOException e) {
                        return super.loadClass(name);
                    }
                } else {
                    return super.loadClass(name);
                }
            }
        };

        PhantomReference<ClassLoader> clRef = new PhantomReference<ClassLoader>(custom,
                new ReferenceQueue<ClassLoader>());

        buildAdvised(custom);
        custom = null;

        for (int i = 0; i < 10; ++i) {
            System.gc();
            Thread.sleep(100);
            if (clRef.isEnqueued()) {
                break;
            }
        }
        assertTrue("CGLIB should allow classloaders to be evicted. PhantomReference<ClassLoader> was not cleared after 10 gc cycles," +
                "thus it is likely some cache is preventing the class loader to be garbage collected", clRef.isEnqueued());

    }

    protected Object buildAdvised(ClassLoader custom)
            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        final Class<?> eaClassFromCustomClassloader = custom.loadClass(EA.class.getName());

        CallbackFilter callbackFilter = new CallbackFilter() {
            Object advised = eaClassFromCustomClassloader.newInstance();

            public int accept(Method method) {
                return 0;
            }

        };

        // Need to test both orders: "null classloader first" and "null last" just in case
        Object source_ = enhance(eaClassFromCustomClassloader, null, callbackFilter, TEST_INTERCEPTOR, null);
        Object source = enhance(eaClassFromCustomClassloader, null, callbackFilter, TEST_INTERCEPTOR, custom);
        assertSame("Same proxy class is expected since Enhancer with null (default) ClassLoader should use " +
                        " target class.getClassLoader(), thus the same cache key instance should be reused",
                source.getClass(), source_.getClass()
        );

        Object source2 = enhance(eaClassFromCustomClassloader, null, callbackFilter, TEST_INTERCEPTOR, custom);
        assertSame("enhance should return cached Enhancer when calling with same parameters",
                source.getClass(), source2.getClass()
        );

        Object source2_ = enhance(eaClassFromCustomClassloader, null, callbackFilter, TEST_INTERCEPTOR, null);
        assertSame("Same proxy class is expected since Enhancer with null (default) ClassLoader should use " +
                " target class.getClassLoader(), thus the same cache key instance should be reused",
                source.getClass(), source2_.getClass()
        );

        Object source3 = enhance(eaClassFromCustomClassloader, null, null, TEST_INTERCEPTOR, custom);
        assertNotSame("enhance should return different instance when callbackFilter differs",
                source.getClass(), source3.getClass()
        );

        return source;
    }

    private static byte[] toByteArray(InputStream input) throws IOException {
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        byte[] buffer = new byte[4096];
        int n = 0;

        while (-1 != (n = input.read(buffer))) {
            output.write(buffer, 0, n);
        }

        return output.toByteArray();

    }

    private static class TestFilter implements CallbackFilter {
        private final int pk;

        TestFilter(int pk) {
            this.pk = pk;
        }

        public int accept(Method method) {
            return 0;
        }

        @Override
        public int hashCode() {
            return 1; // Make sure Enhancer uses equals, not just hashCode alone
        }

        @Override
        public boolean equals(Object obj) {
            return obj instanceof TestFilter && ((TestFilter) obj).pk == pk;
        }
    }

    public void testCallbackFilterEqualsVsClassReuse() {
        Callback[] callbacks = new Callback[]{NoOp.INSTANCE};
        Object a = Enhancer.create(Source.class, null, new TestFilter(1), callbacks);
        Object b = Enhancer.create(Source.class, null, new TestFilter(1), callbacks);
        assertSame("Using the same as per .equal() CallbackFilter, thus Enhancer should reuse the same proxy class",
                a.getClass(), b.getClass());
    }

    public void testCallbackFilterNotEqualsVsClassReuse() {
        Callback[] callbacks = new Callback[]{NoOp.INSTANCE};
        Object a = Enhancer.create(Source.class, null, new TestFilter(1), callbacks);
        Object b = Enhancer.create(Source.class, null, new TestFilter(2), callbacks);
        assertNotSame("Using the different CallbackFilter instances, thus Enhancer should generate new proxy class",
                a.getClass(), b.getClass());
    }

    public void testRuntimException()throws Throwable{
    
        Source source =  (Source)Enhancer.create(
        Source.class,
        null, TEST_INTERCEPTOR );
        
        try{
            
            source.throwIndexOutOfBoundsException();
            fail("must throw an exception");
            
        }catch( IndexOutOfBoundsException ok  ){
            
        }
    
    }
    
  static abstract class CastTest{
     CastTest(){} 
    abstract int getInt();
  }
  
  class CastTestInterceptor implements MethodInterceptor{
     
      public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args, MethodProxy proxy) throws Throwable {
          return new Short((short)0);
      }
      
  }  
  
    
  public void testCast()throws Throwable{
    
    CastTest castTest =  (CastTest)Enhancer.create(CastTest.class, null, new  CastTestInterceptor());
  
    assertTrue(castTest.getInt() == 0);
    
  }
  
   public void testABC() throws Throwable{
       Enhancer.create(EA.class, null, TEST_INTERCEPTOR);
       Enhancer.create(EC1.class, null, TEST_INTERCEPTOR).toString();
       ((EB)Enhancer.create(EB.class, null, TEST_INTERCEPTOR)).finalTest();
       assertTrue("abstract method",( (EC1)Enhancer.create(EC1.class,
                     null, TEST_INTERCEPTOR) ).compareTo( new EC1() ) == -1 );
       Enhancer.create(ED.class, null, TEST_INTERCEPTOR).toString();
       Enhancer.create(ClassLoader.class, null, TEST_INTERCEPTOR).toString();
   }

    public static class AroundDemo {
        public String getFirstName() {
            return "Chris";
        }
        public String getLastName() {
            return "Nokleberg";
        }
    }

    public void testAround() throws Throwable {
        AroundDemo demo = (AroundDemo)Enhancer.create(AroundDemo.class, null, new MethodInterceptor() {
                public Object intercept(Object obj, Method method, Object[] args,
                                           MethodProxy proxy) throws Throwable {
                    if (method.getName().equals("getFirstName")) {
                        return "Christopher";
                    }
                    return proxy.invokeSuper(obj, args);
                }
            });
        assertTrue(demo.getFirstName().equals("Christopher"));
        assertTrue(demo.getLastName().equals("Nokleberg"));
    }
 
    
    public static interface TestClone extends Cloneable{
     public Object clone()throws java.lang.CloneNotSupportedException;

    }
    public static class TestCloneImpl implements TestClone{
     public Object clone()throws java.lang.CloneNotSupportedException{
         return super.clone();
     }
    }

    public void testClone() throws Throwable{
    
      TestClone testClone = (TestClone)Enhancer.create( TestCloneImpl.class,
                                                          TEST_INTERCEPTOR );
      assertTrue( testClone.clone() != null );  
      
            
      testClone = (TestClone)Enhancer.create( TestClone.class,
         new MethodInterceptor(){
      
           public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
                        MethodProxy proxy) throws Throwable{
                     return  proxy.invokeSuper(obj, args);
           }
  
      
      } );

      assertTrue( testClone.clone() != null );  
      
      
    }
    
    public static interface FinalA {
        void foo();
    }

    public static class FinalB implements FinalA {
        final public void foo() { }
    }

    public void testFinal() throws Throwable {
        ((FinalA)Enhancer.create(FinalB.class, TEST_INTERCEPTOR)).foo();
    }

    public static interface ConflictA {
        int foo();
    }

    public static interface ConflictB {
        String foo();
    }

    public void testConflict() throws Throwable {
        Object foo =
            Enhancer.create(Object.class, new Class[]{ ConflictA.class, ConflictB.class }, TEST_INTERCEPTOR);
        ((ConflictA)foo).foo();
        ((ConflictB)foo).foo();
    }

    // TODO: make this work again
     public void testArgInit() throws Throwable{

         Enhancer e = new Enhancer();
         e.setSuperclass(ArgInit.class);
         e.setCallbackType(MethodInterceptor.class);
         Class f = e.createClass();
         ArgInit a = (ArgInit)ReflectUtils.newInstance(f,
                                                       new Class[]{ String.class },
                                                       new Object[]{ "test" });
         assertEquals("test", a.toString());
         ((Factory)a).setCallback(0, TEST_INTERCEPTOR);
         assertEquals("test", a.toString());

         Callback[] callbacks = new Callback[]{ TEST_INTERCEPTOR };
         ArgInit b = (ArgInit)((Factory)a).newInstance(new Class[]{ String.class },
                                                       new Object[]{ "test2" },
                                                       callbacks);
         assertEquals("test2", b.toString());
         try{
             ((Factory)a).newInstance(new Class[]{  String.class, String.class },
                                      new Object[]{"test"},
                                      callbacks);
             fail("must throw exception");
         }catch( IllegalArgumentException iae ){
         
         }
    }

    public static class Signature {
        public int interceptor() {
            return 42;
        }
    }

    public void testSignature() throws Throwable {
        Signature sig = (Signature)Enhancer.create(Signature.class, TEST_INTERCEPTOR);
        assertTrue(((Factory)sig).getCallback(0) == TEST_INTERCEPTOR);
        assertTrue(sig.interceptor() == 42);
    }

    public abstract static class AbstractMethodCallInConstructor {
        public AbstractMethodCallInConstructor() {
            foo();
        }
    
        public abstract void foo();
    }

    public void testAbstractMethodCallInConstructor() throws Throwable {
        AbstractMethodCallInConstructor obj = (AbstractMethodCallInConstructor)
            Enhancer.create(AbstractMethodCallInConstructor.class,
                     TEST_INTERCEPTOR);
        obj.foo();
    }

    public void testProxyIface() throws Throwable {
        final DI1 other = new DI1() {
                public String herby() {
                    return "boop";
                }
            };
        DI1 d = (DI1)Enhancer.create(DI1.class, new MethodInterceptor() {
                public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
                                        MethodProxy proxy) throws Throwable {
                    return proxy.invoke(other, args);
                }
            });
        assertTrue("boop".equals(d.herby()));
    }

    static class NamingPolicyDummy {}

    public void testNamingPolicy() throws Throwable {
      Enhancer e = new Enhancer();
      e.setSuperclass(NamingPolicyDummy.class);
      e.setUseCache(false);
      e.setUseFactory(false);
      e.setNamingPolicy(new DefaultNamingPolicy() {
        public String getTag() {
          return "ByHerby";
        }
          public String toString() {
            return getTag();
          }
      });
      e.setCallbackType(MethodInterceptor.class);
      Class proxied = e.createClass();
      final boolean[] ran = new boolean[1];
      Enhancer.registerStaticCallbacks(proxied, new Callback[]{
        new MethodInterceptor() {
          public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            ran[0] = true;
            assertTrue(proxy.getSuperFastClass().getClass().getName().indexOf("$FastClassByHerby$") >= 0);
            return proxy.invokeSuper(obj, args);
          }
        }
      });
      NamingPolicyDummy dummy = (NamingPolicyDummy) proxied.newInstance();
      dummy.toString();
      assertTrue(ran[0]);
    }
    
    public void testBadNamingPolicyStillReservesNames() throws Throwable {
      Enhancer e = new Enhancer();
      e.setUseCache(false);
      e.setCallback(NoOp.INSTANCE);
      e.setClassLoader(new ClassLoader(this.getClass().getClassLoader()){});
      e.setNamingPolicy(new NamingPolicy() {      
        public String getClassName(String prefix, String source, Object key, Predicate names) {
          return "net.sf.cglib.empty.Object$$ByDerby$$123";
        }
      });
      Class proxied = e.create().getClass();
      final String name = proxied.getCanonicalName();
      final boolean[] ran = new boolean[1];
      e.setNamingPolicy(new NamingPolicy() {
        public String getClassName(String prefix, String source, Object key, Predicate names) {
          ran[0] = true;
          assertTrue(names.evaluate(name));
          return name + "45"; 
        }
      });
      Class proxied2 = e.create().getClass();
      assertTrue(ran[0]);
      assertEquals(name + "45", proxied2.getCanonicalName());
    }

    /**
     * In theory, every sane implementation of {@link NamingPolicy} should check if the class name is occupied,
     * however, in practice there are implementations in the wild that just return whatever they feel is good.
     *
     * @throws Throwable if something wrong happens
     */
    public void testNamingPolicyThatReturnsConstantNames() throws Throwable {
      Enhancer e = new Enhancer();
      final String desiredClassName = "net.sf.cglib.empty.Object$$42";
      e.setCallback(NoOp.INSTANCE);
      e.setClassLoader(new ClassLoader(this.getClass().getClassLoader()){});
      e.setNamingPolicy(new NamingPolicy() {
        public String getClassName(String prefix, String source, Object key, Predicate names) {
          return desiredClassName;
        }
      });
      Class proxied = e.create().getClass();
      assertEquals("Class name should match the one returned by NamingPolicy", desiredClassName, proxied.getName());
    }

    public static Object enhance(Class cls, Class interfaces[], Callback callback, ClassLoader loader) {
        Enhancer e = new Enhancer();
        e.setSuperclass(cls);
        e.setInterfaces(interfaces);
        e.setCallback(callback);
        e.setClassLoader(loader);
        return e.create();
    }

    public static Object enhance(Class cls, Class interfaces[], CallbackFilter callbackFilter, Callback callback, ClassLoader loader) {
        Enhancer e = new Enhancer();
        e.setSuperclass(cls);
        e.setInterfaces(interfaces);
        e.setCallbackFilter(callbackFilter);
        e.setCallback(callback);
        e.setClassLoader(loader);
        return e.create();
    }

    public interface PublicClone extends Cloneable {
        Object clone() throws CloneNotSupportedException;
    }

    public void testNoOpClone() throws Exception {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(PublicClone.class);
        enhancer.setCallback(NoOp.INSTANCE);
        ((PublicClone)enhancer.create()).clone();
    }

    public void testNoFactory() throws Exception {
        noFactoryHelper();
        noFactoryHelper();
    }

    private void noFactoryHelper() {
        MethodInterceptor mi = new MethodInterceptor() {
            public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                return "Foo";
            }
        };
        Enhancer enhancer = new Enhancer();
        enhancer.setUseFactory(false);
        enhancer.setSuperclass(AroundDemo.class);
        enhancer.setCallback(mi);
        AroundDemo obj = (AroundDemo)enhancer.create();
        assertTrue(obj.getFirstName().equals("Foo"));
        assertTrue(!(obj instanceof Factory));
    }

    interface MethDec {
        void foo();
    }
    
    abstract static class MethDecImpl implements MethDec {
    }

    public void testMethodDeclarer() throws Exception {
        final boolean[] result = new boolean[]{ false };
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(MethDecImpl.class);
        enhancer.setCallback(new MethodInterceptor() {
            public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                
                result[0] = method.getDeclaringClass().getName().equals(MethDec.class.getName());
                return null;
            }
        });
        ((MethDecImpl)enhancer.create()).foo();
        assertTrue(result[0]);
    }


    interface ClassOnlyX { }
    public void testClassOnlyFollowedByInstance() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(ClassOnlyX.class);
        enhancer.setCallbackType(NoOp.class);
        Class type = enhancer.createClass();

        enhancer = new Enhancer();
        enhancer.setSuperclass(ClassOnlyX.class);
        enhancer.setCallback(NoOp.INSTANCE);
        Object instance = enhancer.create();

        assertTrue(instance instanceof ClassOnlyX);
        assertEquals("types of enhancer.createClass() and enhancer.create().getClass() should match", type, instance.getClass());
    }

     public void testSql() {
         Enhancer.create(null, new Class[]{ java.sql.PreparedStatement.class }, TEST_INTERCEPTOR);
     }

    public void testEquals() throws Exception {
        final boolean[] result = new boolean[]{ false };
        EqualsInterceptor intercept = new EqualsInterceptor();
        Object obj = Enhancer.create(null, intercept);
        obj.equals(obj);
        assertTrue(intercept.called);
    }

    public static class EqualsInterceptor implements MethodInterceptor {
        final static Method EQUALS_METHOD = ReflectUtils.findMethod("Object.equals(Object)");
        boolean called;

        public Object intercept(Object obj,
                                Method method,
                                Object[] args,
                                MethodProxy proxy) throws Throwable {
            if (method.equals(EQUALS_METHOD)) {
                return proxy.invoke(this, args);
            } else {
                return proxy.invokeSuper(obj, args);
            }
        }

        public boolean equals(Object other) {
            called = true;
            return super.equals(other);
        }
    }

    private static interface ExceptionThrower {
        void throwsThrowable(int arg) throws Throwable;
        void throwsException(int arg) throws Exception;
        void throwsNothing(int arg);
    }

    private static class MyThrowable extends Throwable { }
    private static class MyException extends Exception { }
    private static class MyRuntimeException extends RuntimeException { }
    
    public void testExceptions() {
        Enhancer e = new Enhancer();
        e.setSuperclass(ExceptionThrower.class);
        e.setCallback(new MethodInterceptor() {
            public Object intercept(Object obj,
                                    Method method,
                                    Object[] args,
                                    MethodProxy proxy) throws Throwable {
                switch (((Integer)args[0]).intValue()) {
                case 1:
                    throw new MyThrowable();
                case 2:
                    throw new MyException();
                case 3:
                    throw new MyRuntimeException();
                default:
                    return null;
                }
            }
        });
        ExceptionThrower et = (ExceptionThrower)e.create();
        try { et.throwsThrowable(1); } catch (MyThrowable t) { } catch (Throwable t) { fail(); }
        try { et.throwsThrowable(2); } catch (MyException t) { } catch (Throwable t) { fail(); }
        try { et.throwsThrowable(3); } catch (MyRuntimeException t) { } catch (Throwable t) { fail(); }

        try { et.throwsException(1); } catch (Throwable t) { assertTrue(t instanceof MyThrowable); }
        try { et.throwsException(2); } catch (MyException t) { } catch (Throwable t) { fail(); }
        try { et.throwsException(3); } catch (MyRuntimeException t) { } catch (Throwable t) { fail(); }
        try { et.throwsException(4); } catch (Throwable t) { fail(); }

        try { et.throwsNothing(1); } catch (Throwable t) { assertTrue(t instanceof MyThrowable); }
        try { et.throwsNothing(2); } catch (Exception t) { assertTrue(t instanceof MyException); }
        try { et.throwsNothing(3); } catch (MyRuntimeException t) { } catch (Throwable t) { fail(); }
        try { et.throwsNothing(4); } catch (Throwable t) { fail(); }
    }

    public void testUnusedCallback() {
        Enhancer e = new Enhancer();
        e.setCallbackTypes(new Class[]{ MethodInterceptor.class, NoOp.class });
        e.setCallbackFilter(new CallbackFilter() {
            public int accept(Method method) {
                return 0;
            }
        });
        e.createClass();
    }

    private static ArgInit newArgInit(Class clazz, String value) {
        return (ArgInit)ReflectUtils.newInstance(clazz,
                                                 new Class[]{ String.class },
                                                 new Object[]{ value });
    }

    private static class StringValue
    implements MethodInterceptor
    {
        private String value;

        public StringValue(String value) {
            this.value = value;
        }
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) {
            return value;
        }
    }

    public void testRegisterCallbacks()
    throws InterruptedException
    {
         Enhancer e = new Enhancer();
         e.setSuperclass(ArgInit.class);
         e.setCallbackType(MethodInterceptor.class);
         e.setUseFactory(false);
         final Class clazz = e.createClass();

         assertTrue(!Factory.class.isAssignableFrom(clazz));
         assertEquals("test", newArgInit(clazz, "test").toString());

         Enhancer.registerCallbacks(clazz, new Callback[]{ new StringValue("fizzy") });
         assertEquals("fizzy", newArgInit(clazz, "test").toString());
         assertEquals("fizzy", newArgInit(clazz, "test").toString());

         Enhancer.registerCallbacks(clazz, new Callback[]{ null });
         assertEquals("test", newArgInit(clazz, "test").toString());

         Enhancer.registerStaticCallbacks(clazz, new Callback[]{ new StringValue("soda") });
         assertEquals("test", newArgInit(clazz, "test").toString());

         Enhancer.registerCallbacks(clazz, null);
         assertEquals("soda", newArgInit(clazz, "test").toString());
         
         Thread thread = new Thread(){
             public void run() {
                 assertEquals("soda", newArgInit(clazz, "test").toString());
             }
         };
         thread.start();
         thread.join();

         // clean-up static callback
         Enhancer.registerStaticCallbacks(clazz, null);
         assertEquals("test", newArgInit(clazz, "test").toString());
    }
    
   public void perform(ClassLoader loader) throws Exception{
    
           enhance( Source.class , null, TEST_INTERCEPTOR, loader);
    
    }
    
  
    public void testCallbackHelper() {
        final ArgInit delegate = new ArgInit("helper");
        Class sc = ArgInit.class;
        Class[] interfaces = new Class[]{ DI1.class, DI2.class };

        CallbackHelper helper = new CallbackHelper(sc, interfaces) {
            protected Object getCallback(final Method method) {
                return new FixedValue() {
                    public Object loadObject() {
                        return "You called method " + method.getName();
                    }
                };
            }
        };

        Enhancer e = new Enhancer();
        e.setSuperclass(sc);
        e.setInterfaces(interfaces);
        e.setCallbacks(helper.getCallbacks());
        e.setCallbackFilter(helper);

        ArgInit proxy = (ArgInit)e.create(new Class[]{ String.class }, new Object[]{ "whatever" });
        assertEquals("You called method toString", proxy.toString());
        assertEquals("You called method herby", ((DI1)proxy).herby());
        assertEquals("You called method derby", ((DI2)proxy).derby());
    }

   
    
    public void testSerialVersionUID() throws Exception {
        Long suid = new Long(0xABBADABBAD00L);

        Enhancer e = new Enhancer();
        e.setSerialVersionUID(suid);
        e.setCallback(NoOp.INSTANCE);
        Object obj = e.create();

        Field field = obj.getClass().getDeclaredField("serialVersionUID");
        field.setAccessible(true);
        assertEquals(suid, field.get(obj));
    }

    interface ReturnTypeA { int foo(String x); }
    interface ReturnTypeB { String foo(String x); }
    public void testMethodsDifferingByReturnTypeOnly() throws IOException {
        Enhancer e = new Enhancer();
        e.setInterfaces(new Class[]{ ReturnTypeA.class, ReturnTypeB.class });
        e.setCallback(new MethodInterceptor() {
            public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                if (method.getReturnType().equals(String.class))
                    return "hello";
                return new Integer(42);
            }
        });
        Object obj = e.create();
        assertEquals(42, ((ReturnTypeA)obj).foo("foo"));
        assertEquals("hello", ((ReturnTypeB)obj).foo("foo"));
        assertEquals(-1, FastClass.create(obj.getClass()).getIndex("foo", new Class[]{ String.class }));
    }


    public static class ConstructorCall
    {
        private String x;
        public ConstructorCall() {
            x = toString();
        }
        public String toString() {
            return "foo";
        }
    }
    
    public void testInterceptDuringConstruction() {
        FixedValue fixedValue = new FixedValue() {
            public Object loadObject() {
                return "bar";
            }
        };

        Enhancer e = new Enhancer();
        e.setSuperclass(ConstructorCall.class);
        e.setCallback(fixedValue);
        assertEquals("bar", ((ConstructorCall)e.create()).x);

        e = new Enhancer();
        e.setSuperclass(ConstructorCall.class);
        e.setCallback(fixedValue);
        e.setInterceptDuringConstruction(false);
        assertEquals("foo", ((ConstructorCall)e.create()).x);
    }
    
    
    
   void assertThreadLocalCallbacks(Class cls)throws Exception{
        
        Field field = cls.getDeclaredField("CGLIB$THREAD_CALLBACKS");
        field.setAccessible(true);
        
        assertNull(((ThreadLocal) field.get(null)).get());
    }
    
    public void testThreadLocalCleanup1()throws Exception{
        
        Enhancer e = new Enhancer();
        e.setUseCache(false);    
        e.setCallbackType(NoOp.class);
        Class cls = e.createClass();
        
        assertThreadLocalCallbacks(cls);
        
      
        

    }
    
    
    public void testThreadLocalCleanup2()throws Exception{
        
        Enhancer e = new Enhancer();
        e.setCallback(NoOp.INSTANCE);
        Object obj = e.create();
        
        assertThreadLocalCallbacks(obj.getClass());
        
        

    }
    
    public void testThreadLocalCleanup3()throws Exception{
        
        Enhancer e = new Enhancer();
        e.setCallback(NoOp.INSTANCE);
        Factory obj = (Factory) e.create();
        obj.newInstance(NoOp.INSTANCE);
        
        assertThreadLocalCallbacks(obj.getClass());
        
        

    }
    
    public void testUseCache() throws Exception {
        Enhancer noCache = new Enhancer();
        noCache.setUseCache(false);
        noCache.setSuperclass(Foo.class);
        noCache.setCallback(NoOp.INSTANCE);
        Class<?> a = noCache.create().getClass();
        Class<?> b = noCache.create().getClass();
        assertNotSame(a, b);
        
        Enhancer withCache = new Enhancer();
        withCache.setUseCache(true);
        withCache.setSuperclass(Foo.class);
        withCache.setCallback(NoOp.INSTANCE);
        Class<?> c = withCache.create().getClass();
        Class<?> d = withCache.create().getClass();
        assertNotSame(a, c);
        assertNotSame(b, c);
        assertSame(c, d);
    }
    
    static class Foo {
      Foo() {}
    }
    
    public void testBridgeForcesInvokeVirtual() {
        List<Class> retTypes = new ArrayList<Class>();
        List<Class> paramTypes = new ArrayList<Class>();
        Interceptor interceptor = new Interceptor(retTypes, paramTypes);

        Enhancer e = new Enhancer();
        e.setSuperclass(Impl.class);
        e.setCallbackFilter(new CallbackFilter() {
            public int accept(Method method) {
                return method.getDeclaringClass() != Object.class ? 0 : 1;
            }
        });
        e.setCallbacks(new Callback[] { interceptor, NoOp.INSTANCE });
        // We expect the bridge ('ret') to be called & forward us to the non-bridge 'erased'
        Interface intf = (Interface)e.create();
        intf.aMethod(null);
        // Make sure the right things got called in the right order:
        assertEquals(Arrays.asList(RetType.class, ErasedType.class), retTypes);
        
        // Validate calling the refined just gives us that.
        retTypes.clear();
        Impl impl = (Impl)intf;
        impl.aMethod((Refined)null);
        assertEquals(Arrays.asList(Refined.class), retTypes);
        
        // When calling from the impl, we are dispatched directly to the non-bridge,
        // because that's just how it works.
        retTypes.clear();
        impl.aMethod((RetType)null);
        assertEquals(Arrays.asList(ErasedType.class), retTypes);
        
        // Do a whole bunch of checks for the other methods too
        
        paramTypes.clear();
        intf.intReturn(null);
        assertEquals(Arrays.asList(RetType.class, ErasedType.class), paramTypes);
        
        paramTypes.clear();
        intf.voidReturn(null);
        assertEquals(Arrays.asList(RetType.class, ErasedType.class), paramTypes);
        
        paramTypes.clear();
        intf.widenReturn(null);
        assertEquals(Arrays.asList(RetType.class, ErasedType.class), paramTypes);
    }
    
    public void testBridgeForcesInvokeVirtualEvenWithoutInterceptingBridge() {
        List<Class> retTypes = new ArrayList<Class>();
        Interceptor interceptor = new Interceptor(retTypes);

        Enhancer e = new Enhancer();
        e.setSuperclass(Impl.class);
        e.setCallbackFilter(new CallbackFilter() {
            public int accept(Method method) {
              // Ideally this would be:
              // return !method.isBridge() && method.getDeclaringClass() != Object.class ? 0 : 1;
              // But Eclipse sometimes labels the wrong things as bridge methods, so we're more
              // explicit:
              return method.getDeclaringClass() != Object.class
                  && method.getReturnType() != RetType.class ? 0 : 1;
            }
        });
        e.setCallbacks(new Callback[] { interceptor, NoOp.INSTANCE });
        // We expect the bridge ('ret') to be called & forward us to non-bridge ('erased'),
        // and we only intercept on the non-bridge.
        Interface intf = (Interface)e.create();
        intf.aMethod(null);
        assertEquals(Arrays.asList(ErasedType.class), retTypes);
        
        // Validate calling the refined just gives us that.
        retTypes.clear();
        Impl impl = (Impl)intf;
        impl.aMethod((Refined)null);
        assertEquals(Arrays.asList(Refined.class), retTypes);
        
        // Make sure we still get our non-bride interception if we didn't intercept the bridge.
        retTypes.clear();
        impl.aMethod((RetType)null);
        assertEquals(Arrays.asList(ErasedType.class), retTypes);
    }

    public void testReverseBridge() {
        List<Class> retTypes = new ArrayList<Class>();
        Interceptor interceptor = new Interceptor(retTypes);

        Enhancer e = new Enhancer();
        e.setSuperclass(ReverseImpl.class);
        e.setCallbackFilter(new CallbackFilter() {
            public int accept(Method method) {
                return method.getDeclaringClass() != Object.class ? 0 : 1;
            }
        });
        e.setCallbacks(new Callback[] { interceptor, NoOp.INSTANCE });
        // We expect the bridge ('erased') to be called & forward us to 'ret' (non-bridge)
        ReverseSuper superclass = (ReverseSuper)e.create();
        superclass.aMethod(null, null, null, null);
        assertEquals(Arrays.asList(ErasedType.class, RetType.class), retTypes);
        
        // Calling the Refined type gives us just that.
        retTypes.clear();
        ReverseImpl impl2 = (ReverseImpl)superclass;
        impl2.aMethod(null, (Refined)null, null, null);
        assertEquals(Arrays.asList(Refined.class), retTypes);

        retTypes.clear();
        impl2.aMethod(null, (RetType)null, null, null);
        assertEquals(Arrays.asList(RetType.class), retTypes);
    }
    
    public void testBridgeForMoreViz() {
        List<Class> retTypes = new ArrayList<Class>();
        List<Class> paramTypes = new ArrayList<Class>();
        Interceptor interceptor = new Interceptor(retTypes, paramTypes);

        Enhancer e = new Enhancer();
        e.setSuperclass(PublicViz.class);
        e.setCallbackFilter(new CallbackFilter() {
            public int accept(Method method) {
                return method.getDeclaringClass() != Object.class ? 0 : 1;
            }
        });
        e.setCallbacks(new Callback[] { interceptor, NoOp.INSTANCE });

        VizIntf intf = (VizIntf)e.create();
        intf.aMethod(null);
        assertEquals(Arrays.asList(Concrete.class), paramTypes);
    }

    public void testBridgeParameterCheckcast() throws Exception {

    // If the compiler used for Z omits the bridge method, and X is compiled with javac,
    // javac will generate an invokespecial bridge in X.

    // public interface I<A, B> {
    //   public A f(B b);
    // }
    // public abstract class Z<U extends Number> implements I<U, Long> {
    //   public U f(Long id) {
    //     return null;
    //   }
    // }
    // public class X extends Z<Integer> {}

    final Map<String, byte[]> classes = new HashMap<String, byte[]>();

    {
      ClassWriter cw = new ClassWriter(0);
      cw.visit(
          49,
          Opcodes.ACC_PUBLIC | Opcodes.ACC_ABSTRACT | Opcodes.ACC_INTERFACE,
          "I",
          "<A:Ljava/lang/Object;B:Ljava/lang/Object;>Ljava/lang/Object;",
          "java/lang/Object",
          null);
      {
        MethodVisitor mv =
            cw.visitMethod(
                Opcodes.ACC_PUBLIC | Opcodes.ACC_ABSTRACT,
                "f",
                "(Ljava/lang/Object;)Ljava/lang/Object;",
                "(TB;)TA;",
                null);
        mv.visitEnd();
      }
      cw.visitEnd();
      classes.put("I.class", cw.toByteArray());
    }
    {
      ClassWriter cw = new ClassWriter(0);
      cw.visit(
          49,
          Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER | Opcodes.ACC_ABSTRACT,
          "Z",
          "<U:Ljava/lang/Number;>Ljava/lang/Object;LI<TU;Ljava/lang/String;>;",
          "java/lang/Object",
          new String[] {"I"});
      {
        MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
        mv.visitCode();
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(1, 1);
        mv.visitEnd();
      }
      {
        MethodVisitor mv =
            cw.visitMethod(
                Opcodes.ACC_PUBLIC,
                "f",
                "(Ljava/lang/String;)Ljava/lang/Number;",
                "(Ljava/lang/String;)TU;",
                null);
        mv.visitCode();
        mv.visitInsn(Opcodes.ACONST_NULL);
        mv.visitInsn(Opcodes.ARETURN);
        mv.visitMaxs(1, 2);
        mv.visitEnd();
      }
      cw.visitEnd();
      classes.put("Z.class", cw.toByteArray());
    }
    {
      ClassWriter cw = new ClassWriter(0);
      cw.visit(
          49, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER, "X", "LZ<Ljava/lang/Integer;>;", "Z", null);
      {
        MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
        mv.visitCode();
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "Z", "<init>", "()V", false);
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(1, 1);
        mv.visitEnd();
      }
      {
        MethodVisitor mv =
            cw.visitMethod(
                Opcodes.ACC_PUBLIC | Opcodes.ACC_BRIDGE | Opcodes.ACC_SYNTHETIC,
                "f",
                "(Ljava/lang/Object;)Ljava/lang/Object;",
                null,
                null);
        mv.visitCode();
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitVarInsn(Opcodes.ALOAD, 1);
        mv.visitTypeInsn(Opcodes.CHECKCAST, "java/lang/String");
        mv.visitMethodInsn(
            Opcodes.INVOKESPECIAL, "Z", "f", "(Ljava/lang/String;)Ljava/lang/Number;", false);
        mv.visitInsn(Opcodes.ARETURN);
        mv.visitMaxs(2, 2);
        mv.visitEnd();
      }

      cw.visitEnd();

      classes.put("X.class", cw.toByteArray());
    }

    ClassLoader classLoader =
        new ClassLoader(getClass().getClassLoader()) {
          @Override
          public InputStream getResourceAsStream(String name) {
            InputStream is = super.getResourceAsStream(name);
            if (is != null) {
              return is;
            }
            if (classes.containsKey(name)) {
              return new ByteArrayInputStream(classes.get(name));
            }
            return null;
          }

          public Class findClass(String name) throws ClassNotFoundException {
            byte[] ba = classes.get(name.replace('.', '/') + ".class");
            if (ba != null) {
              return defineClass(name, ba, 0, ba.length);
            }
            throw new ClassNotFoundException(name);
          }
        };

    List<Class> retTypes = new ArrayList<Class>();
    List<Class> paramTypes = new ArrayList<Class>();
    Interceptor interceptor = new Interceptor(retTypes, paramTypes);

    Enhancer e = new Enhancer();
    e.setClassLoader(classLoader);
    e.setSuperclass(classLoader.loadClass("X"));
    e.setCallback(interceptor);

    Object c = e.create();

    for (Method m : c.getClass().getDeclaredMethods()) {
      if (m.getName().equals("f") && m.getReturnType().equals(Object.class)) {
        m.invoke(c, new Object[] {null});
      }
    }

    // f(Object)Object should bridge to f(Number)String
    assertEquals(Arrays.asList(Object.class, Number.class), retTypes);
    assertEquals(Arrays.asList(Object.class, String.class), paramTypes);
  }

    static class ErasedType {}
    static class RetType extends ErasedType {}
    static class Refined extends RetType {}
    
    static abstract class Superclass<T extends ErasedType> {
        // Check narrowing return value & parameters
        public T aMethod(T t) { return null; }
        // Check void return value
        public void voidReturn(T t) { }
        // Check primitive return value
        public int intReturn(T t) { return 1; }
        // Check widening return value
        public RetType widenReturn(T t) { return null; }
    }
    public interface Interface { // the usage of the interface forces the bridge
        RetType aMethod(RetType obj);
        void voidReturn(RetType obj);
        int intReturn(RetType obj);
        // a wider type than in superclass
        ErasedType widenReturn(RetType obj);
    }
    public static class Impl extends Superclass<RetType> implements Interface {
        // An even more narrowed type, just to make sure
        // it doesn't confuse us.
        public Refined aMethod(Refined obj) { return null; }
    }
    
    // Another set of classes -- this time with the bridging in reverse,
    // to make sure that if we define the concrete type, a bridge
    // is created to call it from an erased type.
    static abstract class ReverseSuper<T extends ErasedType> {
        // the various parameters are to make sure we only
        // change signature when we have to -- only 'c' goes
        // from ErasedType -> RetType
        public T aMethod(Concrete b, T c, RetType d, ErasedType e) { return null; }
    }
    static class Concrete {}
    static class ReverseImpl extends ReverseSuper<RetType> {
        public Refined aMethod(Concrete b, Refined c, RetType d, ErasedType e) { return null; }
        public RetType aMethod(Concrete b, RetType c, RetType d, ErasedType e) { return null; }
    }
    
    public interface VizIntf {
        public void aMethod(Concrete a);
    }
    static abstract class PackageViz implements VizIntf {
        public void aMethod(Concrete e) {  }
    }
    // inherits aMethod from PackageViz, but bridges to make it
    // publicly accessible.  the bridge here has the same
    // target signature, so it absolutely requires invokespecial,
    // otherwise we recurse forever.
    public static class PublicViz extends PackageViz implements VizIntf {}
    
    private static class Interceptor implements MethodInterceptor {
        private final List<Class> retList;
        private final List<Class> paramList;
        
        public Interceptor(List<Class> retList) {
            this(retList, new ArrayList<Class>());
        }
        
        public Interceptor(List<Class> retList, List<Class> paramList) {
            this.retList = retList;
            this.paramList = paramList;
        }

        public Object intercept(Object obj, Method method, Object[] args,
                MethodProxy proxy) throws Throwable {
            retList.add(method.getReturnType());
            if (method.getParameterTypes().length > 0) {
                paramList.add(method.getParameterTypes()[0]);
            }
            return proxy.invokeSuper(obj, args);
        }
    }
    
}
