/*
 Copyright 2010 Sun Microsystems, Inc.
 All rights reserved. Use is subject to license terms.

 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; version 2 of the License.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
*/
/*
 * MyLoadUnloadTest.java
 */

package test;

import java.lang.ref.WeakReference;

import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;

import java.io.PrintWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.FileNotFoundException;

import java.net.URL;
import java.net.URI;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URLClassLoader;

import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * A test for loading, calling into, and unloading a native library.
 *
 * Under the JNI specification, a native library is loaded by the class loader
 * of that class which called the method System.loadLibrary(String).  In a
 * Java VM, a library cannot be loaded into more than one class loader
 * (since JDK 1.2, to preserve the separation of name-spaces).  A library is
 * unloaded when the class loader containing the library is garbage collected.
 *
 * Therefore, the task of loading of the native library has to be carried
 * out by newly created class loader instances that are separate from the
 * loader of this test class.  This implies that this test class must not
 * statically refer to the code calling into the native library, for this
 * would load the native library by this class loader when running this test,
 * and any subsequent attempt to load the library by another class loader
 * instance would fail (with an UnsatisfiedLinkError).
 *
 * Hence, this class tests the loading and unloading of a native library by
 * executing the following cycle at least twice:
 * <ol>
 * <li> create a test class loader instance,
 * <li> using reflection, instruct the test class loader to load the a
 *      native library and run Java code calling into the native library,
 * <li> clear any (strong) references to the test class loader
 * <li> invoke the system's garbage collector to finalize the test class
 *      loader and unload the library.
 * </ol>
 */
public class MyLoadUnloadTest {

    /**
     *  The stream to write messages to.
     */
    static protected final PrintWriter out = new PrintWriter(System.out, true);

    /**
     *  The stream to write error messages to.
     */
    static protected final PrintWriter err = new PrintWriter(System.err, true);

    // ensure that asserts are enabled
    static {
        boolean assertsEnabled = false;
        assert assertsEnabled = true; // intentional side effect
        if (!assertsEnabled) {
            throw new RuntimeException("Asserts must be enabled for this test to be effective!");
        }
    }
    
    static final String pprefixes_prop
        = "com.mysql.jtie.test.MyLoadUnloadTest.target_package_prefixes";
    static final String cname_prop
        = "com.mysql.jtie.test.MyLoadUnloadTest.target_class_name";
    static final String mname_prop
        = "com.mysql.jtie.test.MyLoadUnloadTest.target_method_name";

    /**
     * A weak reference to a class loader loading the native library.
     */
    static protected WeakReference<ClassLoader> cl = null;
    
    /**
     *  Shortcut to the Runtime.
     */
    static private final Runtime rt = Runtime.getRuntime();

    /**
     * A ClassLoader that loads classes from certain packages by itself
     * not delegating loading of these to the parent class loader.
     */
    // this loader is only used for debugging this test
    static class SelfishFileClassLoader extends ClassLoader {

        public SelfishFileClassLoader() {
            out.println("--> SelfishFileClassLoader()");
            out.println("    this = " + this);
            out.println("    getParent() = " + getParent());
            out.println("<-- SelfishFileClassLoader()");
        }
        
        private ByteBuffer getClassData(String name)
            throws ClassNotFoundException {
            out.println("--> SelfishFileClassLoader.getClassData(String)");

            // for simplicity of code, only support loading classes from files;
            // an alternative approach to collect all bytes using
            //   InputStream ClassLoader.getSystemResourceAsStream(String)
            //   ReadableByteChannel Channels.newChannel(InputStream)
            //   int ReadableByteChannel.read(ByteBuffer)
            // requires multiple copying within nested loops, since the total
            // number of bytes to be read is not known in advance.

            String rname = name.replace('.', '/') + ".class";

            // locate the class data
            final URL url = ClassLoader.getSystemResource(rname);
            if (url == null) {
                final String msg = ("no definition of the class "
                                    + rname + " could be found.");
                throw new ClassNotFoundException(msg);
            }
            out.println("    url = " + url);

            // convert into a URI
            final URI uri;
            try {
                uri = url.toURI();
            } catch (URISyntaxException ex) {
                final String msg = ("the URL " + url + " is not formatted"
                                    + "strictly according to RFC2396 and "
                                    + "cannot be converted to a URI.");
                throw new ClassNotFoundException(msg, ex);
            }

            // convert into a pathname
            final File f;
            try {
                f = new File(uri);
            } catch (IllegalArgumentException ex) {
                final String msg = ("the system-dependent URI " + uri
                                    + " cannot be converted into a pathname.");
                throw new ClassNotFoundException(msg, ex);
            }

            // check accessibility
            if (!f.canRead()) {
                final String msg = ("cannot read file " + f);
                throw new ClassNotFoundException(msg);
            }
            out.println("    can read file " + f);
            final long nn = f.length();
            out.println("    f.length = " + nn);

            // open a FileInputStream
            final FileInputStream fis;
            try {
                fis = new FileInputStream(f);
            } catch (FileNotFoundException ex) {
                final String msg = ("the file " + f
                                    + " does not exist, is a directory, or"
                                    + " cannot be opened for reading.");
                throw new ClassNotFoundException(msg, ex);
            }
            
            // get the stream's FileChannel
            final FileChannel fc = fis.getChannel();
            assert (fc != null);
            out.println("    fc = " + fc);

            // allocate a ByteBuffer and read file's content
            final ByteBuffer bb;
            try {
                // not worth mapping file as ByteBuffer
                //   final MappedByteBuffer mbb
                //       = fc.map(MapMode.READ_ONLY, 0, fc.size());
                // unclear performance gain but platform-dependent behaviour

                // retrieve the file's size
                final long n;
                try {
                    n = fc.size();
                } catch (IOException ex) {
                    final String msg = ("cannot get size of file " + f);
                    throw new ClassNotFoundException(msg, ex);
                }
                assert (n == nn);
                
                // allocate a ByteBuffer
                if (n > Integer.MAX_VALUE) {
                    final String msg = ("size of file " + f
                                        + " larger than Integer.MAX_VALUE.");
                    throw new ClassFormatError(msg);
                }
                bb = ByteBuffer.allocateDirect((int)n);

                // read file's content into a ByteBuffer
                try {
                    int k;
                    while ((k = fc.read(bb)) > 0) {
                        out.println("    read " + k + " bytes");
                    }
                } catch (IOException ex) {
                    final String msg = ("cannot read file " + f);
                    throw new ClassNotFoundException(msg, ex);
                }
                assert (bb.remaining() == 0);
                bb.rewind();
            } finally {
                try {
                    fc.close();
                } catch (IOException ex) {
                    final String msg = ("cannot close FileChannel " + fc);
                    throw new ClassNotFoundException(msg, ex);
                }
                try {
                    fis.close();
                } catch (IOException ex) {
                    final String msg = ("cannot close FileInputStream " + fis);
                    throw new ClassNotFoundException(msg, ex);
                }
            }

            out.println("<-- SelfishFileClassLoader.getClassData(String)");
            return bb;
        }

        // Under the Java ClassLoader delegation model, subclasses are
        // encouraged to override findClass(), whose default implementation
        // throws a ClassNotFoundException, rather than loadClass().
        // However, for the purpose of ensuring that certain classes are only
        // loaded by child class loaders, it is not sufficient to override
        // findClass(), which is invoked by loadClass() only after it has
        // delegated loading to the parent class loader with no success.
        protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
            out.println("--> SelfishFileClassLoader.loadClass(String, boolean)");

            // check if the class has already been loaded
            Class<?> cls = findLoadedClass(name);
            if (cls != null) {
                out.println("    already loaded " + cls);
                return cls;
            }

            //cls = super.findClass(name);
            //cls = super.loadClass(name, resolve);

            // load test's classes by this ClassLoader, delegate others
            if (name.startsWith("test")
                || name.startsWith("myjapi")
                || name.startsWith("jtie")) {
                out.println("    self:    loading " + name + " ...");
                final ByteBuffer bb = getClassData(name);
                cls = defineClass(name, bb, null);
            } else {
                out.println("    parent:  loading " + name + " ...");
                cls = getParent().loadClass(name);
            }
            assert (cls != null);

            out.println("    ... loaded " + cls
                        + " <" + cls.getClassLoader() + ">");

            // link class if requested
            if (resolve) {
                out.println("    linking " + cls + " ...");
                resolveClass(cls);
                out.println("    ... linked " + cls);
            }

            out.println("<-- SelfishFileClassLoader.loadClass(String, boolean)");
            return cls;
        }
    }

    /**
     * A ClassLoader selectively blocking the delegation of the loading of
     * classes to parent class loaders.
     */
    static class FilterClassLoader extends ClassLoader {

        // list of package prefixes not to be loaded by parent class loaders
        protected final String[] prefixes;

        public FilterClassLoader(String[] prefixes) {
            out.println("--> FilterClassLoader()");
            out.println("    this = " + this);
            out.println("    getParent() = " + getParent());
            out.println("    prefixes[] = {");
            for (int i = 0; i < prefixes.length; i++) {
                out.println("        \"" + prefixes[i] + "\"");
            }
            out.println("    }");
            this.prefixes = prefixes;
            out.println("<-- FilterClassLoader()");
        }

        // should never be called, since only invoked by loadClass()
        protected Class<?> findClass(String name)
            throws ClassNotFoundException {
            assert (false) : "should never be called";
            throw new ClassNotFoundException();
        }

        // selectively load classes by parent class loaders 
        protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
            out.println("--> FilterClassLoader.loadClass(String, boolean)");
            Class<?> cls = null;

            // this loader does not find and load any classes by itself
            assert (findLoadedClass(name) == null);

            // give up on loading if class matches any of the prefixes
            for (int i = prefixes.length - 1; i >= 0; i--) {
                if (name.startsWith(prefixes[i])) {
                    out.println("    redirect loading " + name);
                    out.println("<<< FilterClassLoader.loadClass(String, boolean)");
                    throw new ClassNotFoundException();
                }
            }

            // delegate loading of class to parent class loaders
            out.println("    delegate loading " + name);
            cls = getParent().loadClass(name);
            assert (cls != null);
            //out.println("    ... loaded " + cls
            //            + " <" + cls.getClassLoader() + ">");

            // link class if requested
            if (resolve) {
                out.println("    linking " + cls + " ...");
                resolveClass(cls);
                out.println("    ... linked " + cls);
            }

            out.println("<-- FilterClassLoader.loadClass(String, boolean)");
            return cls;
        }
    }

    /**
     * A URLClassLoader with tracing capabilities for debugging.
     */
    static class MyURLClassLoader extends URLClassLoader {

        public MyURLClassLoader(URL[] urls, ClassLoader parent) {
            super(urls, parent);
            out.println("--> MyURLClassLoader(URL[], ClassLoader)");
            out.println("    this = " + this);
            out.println("    getParent() = " + getParent());
            out.println("    urls[] = {");
            for (int i = 0; i < urls.length; i++) {
                out.println("        " + urls[i]);
            }
            out.println("    }");
            out.println("<-- MyURLClassLoader(URL[], ClassLoader)");
        }

        protected Class<?> findClass(String name)
            throws ClassNotFoundException {
            out.println("--> MyURLClassFinder.findClass(String, boolean)");
            out.println("    finding " + name + " ...");
            Class<?> cls = super.findClass(name);
            out.println("    ... found " + cls
                        + " <" + cls.getClassLoader() + ">");
            out.println("<-- MyURLClassFinder.findClass(String, boolean)");
            return cls;
        }

        protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
            out.println("--> MyURLClassLoader.loadClass(String, boolean)");
            out.println("    loading " + name + " ...");
            Class<?> cls = super.loadClass(name, resolve);
            out.println("    ... loaded " + cls
                        + " <" + cls.getClassLoader() + ">");
            out.println("<-- MyURLClassLoader.loadClass(String, boolean)");
            return cls;
        }
    }

    /**
     * Attempts to run the JVM's Garbage Collector.
     */
    static private URL[] classPathURLs() throws MalformedURLException {
        final String cp = System.getProperty("java.class.path");
        assert (cp != null) : ("classpath = '" + cp + "'");
        final String[] s = cp.split(File.pathSeparator);
        final URL[] urls = new URL[s.length];
        for (int i = 0; i < s.length; i++) {
            urls[i] = new File(s[i]).toURI().toURL();
        }
        return urls;
    }
    
    static public void test0()
        throws ClassNotFoundException, NoSuchMethodException,
        IllegalAccessException, InvocationTargetException,
        MalformedURLException, InstantiationException {
        out.flush();
        out.println("--> MyLoadUnloadTest.test0()");

        out.println();
        out.println("    MyLoadUnloadTest.class.getClassLoader() = "
                    + MyLoadUnloadTest.class.getClassLoader());
        out.println("    ClassLoader.getSystemClassLoader() = "
                    +  ClassLoader.getSystemClassLoader());

        // read properties specifying the test to run
        final String pprefixes = System.getProperty(pprefixes_prop,
                                                    "test.,myjapi.");
        final String[] pprefix = pprefixes.split(",");
        final String cname = System.getProperty(cname_prop, "test.MyJapiTest");
        final String mname = System.getProperty(mname_prop, "test");
        out.println();
        out.println("    using properties");
        out.println("        pprefixes = '" + pprefixes + "'");
        out.println("        cname = '" + cname + "'");
        out.println("        mname = '" + mname + "'");

        // set up class loaders
        out.println();
        out.println("    create FilterClassLoader ...");
        final ClassLoader fcl = new FilterClassLoader(pprefix);
        out.println("    ... created " + fcl);

        out.println();
        out.println("    create URLClassLoader ...");
        final URL[] urls = classPathURLs();
        //final ClassLoader ucl = new MyURLClassLoader(urls, fcl);
        final ClassLoader ucl = new URLClassLoader(urls, fcl);
        out.println("    ... created " + ucl);

        // store class loader in a global but weak reference
        cl = new WeakReference<ClassLoader>(ucl);

        // run test
        out.println();
        out.println("    load class ...");
        Class cls = ucl.loadClass(cname);
        out.println("    ... loaded " + cls
                    + " <" + cls.getClassLoader() + ">");

        out.println();
        out.println("    get method: " + mname + " ...");
        Method m = cls.getMethod("test");
        out.println("    ... got method: " + m);

        out.println();
        out.println("    create instance: ...");
        Object o = cls.newInstance();
        out.println("    ... created instance: " + o);
        
        out.println();
        out.println("    invoke method: " + m + " ...");
        m.invoke(o);
        out.println("    ... invoked method: " + m);
        
        out.println();
        out.println("<-- MyLoadUnloadTest.test0()");
        out.flush();
    }

    /**
     * Attempts to run the JVM's Garbage Collector.
     */
    static private void gc() {
        out.println("--> MyLoadUnloadTest.gc()");
        out.println("    jvm mem: " + (rt.totalMemory() - rt.freeMemory())/1024
                    + " KiB [" + rt.totalMemory()/1024 + " KiB]");
        // empirically determined limit after which no further
        // reduction in memory usage has been observed
        final int nFullGCs = 100;
        //final int nFullGCs = 10;
        for (int i = 0; i < nFullGCs; i++) {
            long oldfree;
            long newfree = rt.freeMemory();
            do {
                // help I/O appear in sync between Java/native
                synchronized (MyLoadUnloadTest.class) {
                    oldfree = newfree;
                    out.println("    rt.gc()");
                    out.flush();
                    err.flush();

                    rt.runFinalization();
                    rt.gc();
                    newfree = rt.freeMemory();
                }
            } while (newfree > oldfree);
        }
        out.println("    jvm mem: " + (rt.totalMemory() - rt.freeMemory())/1024
                    + " KiB [" + rt.totalMemory()/1024 + " KiB]");
        out.println("<-- MyLoadUnloadTest.gc()");
    }

    static public void test()
        throws ClassNotFoundException, NoSuchMethodException,
        IllegalAccessException, InvocationTargetException,
        MalformedURLException, InstantiationException {
        out.flush();
        out.println("--> MyLoadUnloadTest.test()");

        for (int i = 0; i < 3; i++) {
            // create a class loader, load native library, and run test
            test0();

            // garbage collect class loader that loaded native library
            gc();

            // if the class loader with the native library has not been
            // garbage collected, for instance, due to a strong caching
            // reference, the next test invocation will fail with, e.g.:
            //   java.lang.UnsatisfiedLinkError: Native Library <libmyjapi>
            //   already loaded in another classloader
            if (cl.get() != null) {
                out.println("!!! the class loader with the native library"
                            + " has not been garbage collected (for instance,"
                            + " due to a strong caching reference)!");
                break;
            }
        }
        
        out.println();
        out.println("<-- MyLoadUnloadTest.test()");
        out.flush();
    }

    static public void main(String[] args)
        throws Exception {
        out.println("--> MyLoadUnloadTest.main()");

        test();

        // help I/O appear in sync between Java/native
        synchronized (MyLoadUnloadTest.class) {
            //try {
            //    MyLoadUnloadTest.class.wait(1000);
            //} catch (InterruptedException ex) {
            //    // ignore
            //}
            out.flush();
            err.flush();
        }
        
        out.println();
        out.println("<-- MyLoadUnloadTest.main()");
    }
}
