/*
 * Copyright (c) 1997-1999 The Java Apache Project.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. All advertising materials mentioning features or use of this
 *    software must display the following acknowledgment:
 *    "This product includes software developed by the Java Apache 
 *    Project for use in the Apache JServ servlet engine project
 *    <http://java.apache.org/>."
 *
 * 4. The names "Apache JServ", "Apache JServ Servlet Engine" and 
 *    "Java Apache Project" must not be used to endorse or promote products 
 *    derived from this software without prior written permission.
 *
 * 5. Products derived from this software may not be called "Apache JServ"
 *    nor may "Apache" nor "Apache JServ" appear in their names without 
 *    prior written permission of the Java Apache Project.
 *
 * 6. Redistributions of any form whatsoever must retain the following
 *    acknowledgment:
 *    "This product includes software developed by the Java Apache 
 *    Project for use in the Apache JServ servlet engine project
 *    <http://java.apache.org/>."
 *    
 * THIS SOFTWARE IS PROVIDED BY THE JAVA APACHE PROJECT "AS IS" AND ANY
 * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE JAVA APACHE PROJECT OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Java Apache Group. For more information
 * on the Java Apache Project and the Apache JServ Servlet Engine project,
 * please see <http://java.apache.org/>.
 *
 */

package org.apache.tomcat.loader;
//package org.apache.java.lang;

import java.io.*;
import java.lang.*;
import java.net.*;
import java.text.*;
import java.util.*;
import java.util.zip.*;
import java.security.*;

/**
 * A class loader that loads classes from directories and/or zip-format
 * file such as JAR file. It tracks the modification time of the classes
 * it loads to permit reloading through re-instantiation.
 * <P>
 * When the classloader reports its creator that one of the classes it
 * has loaded has changed on disk, it should discard the classloader
 * and create a new instance using <CODE>reinstantiate</CODE>.
 * The classes are then reloaded into the new classloader as required.
 *
 * <P>The classloader can also load resources, which are a means
 * for packaging application data such as images within a jar file
 * or directory.
 *
 * <P>The classloader always first tries to load classes and resources
 * from the system, and uses it's own path if that fails. This is also
 * done if an empty repository is passed at construction.
 *
 * <P><B>How autoreload works:</B></P>
 *
 * <P>The Java VM considers two classes the same if they have the same
 * fully-qualified name <B>and</B> if they were loaded from the same
 * <CODE>ClassLoader</CODE>.
 *
 * <P>There is no way for a classloader to 'undefine' a class once it
 * has been loaded.  However, the servlet engine can discard a
 * classloader and the classes it contains, causing the
 *
 * <P>The <CODE>JServServletManager</CODE> creates a new instance of
 * the classloader each time it detects that any of the loaded classes
 * have changed.
 *
 * <P>Before terminating, all servlets are destroyed.
 *
 * <P>According to the Java Language Specification (JLS), classes may
 * be garbage-collected when there are no longer any instances of that
 * class and the <CODE>java.lang.Class</CODE> object is finalizable.
 * It is intended that this be the case when a <CODE>JServClassLoader</CODE>
 * is discarded.
 *
 * <P>Many VM releases did not implement class garbage collection
 * properly.  In such a VM, the memory usage will continue to grow if
 * autoreloading is enable.  Running the VM with
 * <CODE>-verbosegc</CODE> (or the corresponding option for
 * non-Javasoft VMs) may give some debugging information.
 *
 * <P>It is important that the <CODE>destroy</CODE> method be
 * implemented properly, as servlets may be destroyed and
 * reinitialized several times in the life of a VM.
 *
 * @author Francis J. Lacoste
 * @author Martin Pool
 * @author Jim Heintz
 * @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a>
 * @version $Revision: 1.9.2.3 $ $Date: 2001/03/04 03:47:58 $
 * @see java.lang.ClassLoader
 */
public class AdaptiveClassLoader extends ClassLoader {
    private static final int debug=0;
    
    /**
     * Instance of the SecurityManager installed.
     */
    static protected SecurityManager sm;

    /**
     * Generation counter, incremented for each classloader as they are
     * created.
     */
    static private int generationCounter = 0;

    /**
     * Generation number of the classloader, used to distinguish between
     * different instances.
     */
    private int generation;

    /**
     * Cache of the loaded classes. This contains ClassCacheEntry keyed
     * by class names.
     */
    protected Hashtable cache;

    /**
     * The classpath which this classloader searches for class definitions.
     * Each element of the vector should be either a directory, a .zip
     * file, or a .jar file.
     * <p>
     * It may be empty when only system classes are controlled.
     */
    protected Vector repository;

    /**
     * A parent class loader for delegation of finding a class definition.
     * JDK 1.2 contains parent class loaders as part of java.lang.ClassLoader, the parent
     * being passed to a constructor, and retreived with getParent() method. For JDK 1.1
     * compatibility, we'll duplicate the 1.2 private member var.
     */
    protected ClassLoader parent;
    
    /**
     * Private class used to maintain information about the classes that
     * we loaded.
     */
    private static class ClassCacheEntry {

        /**
         * The actual loaded class
         */
        Class loadedClass;

        /**
         * The file from which this class was loaded; or null if
         * it was loaded from the system.
         */
        File origin;

        /**
         * The time at which the class was loaded from the origin
         * file, in ms since the epoch.
         */
        long lastModified;

        /**
         * Check whether this class was loaded from the system.
         */
        public boolean isSystemClass() {
            return origin == null;
        }
    }
    
    //------------------------------------------------------- Constructors

    /**
     * Creates a new class loader that will load classes from specified
     * class repositories, delegating first to the passed parent for definitions.
     *
     * @param classRepository An set of File classes indicating
     *        directories and/or zip/jar files. It may be empty when
     *        only system classes are loaded.
     * @param theParent A containing class loader for initial delegation of class
     *        definition serach.
     * @throw java.lang.IllegalArgumentException if the objects contained
     *        in the vector are not a file instance or the file is not
     *        a valid directory or a zip/jar file.
     */
    public AdaptiveClassLoader() {
	// Create the cache of loaded classes
	cache = new Hashtable();
    }

    public void setRepository( Vector classRepository ) 
	throws IllegalArgumentException
    {
	// Verify that all the repository are valid.
	Enumeration e = classRepository.elements();
	while(e.hasMoreElements()) {
	    ClassRepository cp = (ClassRepository) e.nextElement();
	    File file;
            String[] files;
            int i;

            // Check to see if element is a File instance.
            try {
                file = cp.getFile();
            } catch (ClassCastException objectIsNotFile) {
                throw new IllegalArgumentException("Object " + cp
                    + "is not a valid \"File\" instance");
            }
	    //	    org.apache.java.io.XXX
            files=SimpleFileFilter.fileOrFiles(file);
            if (files!=null)
            {
                for (i=0;i<files.length;i++)
                {
                    file=new File(files[i]);
                    // Check to see if we have proper access.
                    if (!file.exists()) {
                        throw new IllegalArgumentException("Repository "
                        + file.getAbsolutePath() + " doesn't exist!");
                    } else if (!file.canRead()) {
                        throw new IllegalArgumentException(
                        "Do not have read access for file "
                        + file.getAbsolutePath());
                    }

                    // Check that it is a directory or zip/jar file
                    if (!(file.isDirectory() || isZipOrJarArchive(file))) {
                        throw new IllegalArgumentException(
                           file.getAbsolutePath()
                           + " is not a directory or zip/jar file"
                           + " or if it's a zip/jar file then it is corrupted.");
                    }
                }
            }
        }

        // Store the class repository for use
        this.repository = classRepository;

        // Increment and store generation counter
        this.generation = generationCounter++;
    }

    public void setParent( ClassLoader p ) {
	parent=p;
    }

    void log( String s ) {
	System.out.println("AdaptiveClassLoader: " + s );
    }
    
    //------------------------------------------------------- Methods

    /**
     * Test if a file is a ZIP or JAR archive.
     *
     * @param file the file to be tested.
     * @return true if the file is a ZIP/JAR archive, false otherwise.
     */
    private boolean isZipOrJarArchive(File file) {
        boolean isArchive = true;
        ZipFile zipFile = null;

        try {
            zipFile = new ZipFile(file);
        } catch (ZipException zipCurrupted) {
            isArchive = false;
        } catch (IOException anyIOError) {
            isArchive = false;
        } finally {
            if (zipFile != null) {
                try {
                    zipFile.close();
                } catch (IOException ignored) {}
            }
        }

        return isArchive;
    }

    /**
     * Check to see if a given class should be reloaded because of a
     * modification to the original class.
     *
     * @param className The name of the class to check for modification.
     */
    public synchronized boolean shouldReload(String classname) {
	return checkExpired( classname );
    }

    protected boolean checkExpired(String classname ) {
	ClassCacheEntry entry = (ClassCacheEntry) cache.get(classname);

        if (entry == null) {
            // class wasn't even loaded
            return false;
        } else if (entry.isSystemClass()) {
            // System classes cannot be reloaded
            return false;
        } else {
            return (entry.origin.lastModified() != entry.lastModified);
        
        }
    }

    /**
     * Check whether the classloader should be reinstantiated.
     * <P>
     * The classloader must be replaced if there is any class whose
        * origin file has changed since it was last loaded.
     */
    public synchronized boolean shouldReload() {

        // Check whether any class has changed
        Enumeration e = cache.elements();
        while (e.hasMoreElements()) {
            ClassCacheEntry entry = (ClassCacheEntry) e.nextElement();

	    if( entry.loadedClass==null )
		continue;
	    if( debug>5 )
		log( "cache entry: " + entry.loadedClass.getName());
	    if (entry.isSystemClass()) continue;
	    
            // XXX: Because we want the classloader to be an accurate
            // reflection of the contents of the repository, we also
            // reload if a class origin file is now missing.  This
            // probably makes things a bit more fragile, but is OK in
            // a servlet development situation. <mbp@pharos.com.au>

            long msOrigin = entry.origin.lastModified();

            if (msOrigin == 0) {
                // class no longer exists
                return true;
            }

            if (msOrigin != entry.lastModified) {
                // class is modified
                return true;
            }
        }

        // No changes, no need to reload
        return false;
    }

    /**
     * Re-instantiate this class loader.
     * <p>
     * This method creates a new instance
     * of the class loader that will load classes form the same path
     * as this one.
     */
    public AdaptiveClassLoader reinstantiate() {
        AdaptiveClassLoader cl=new AdaptiveClassLoader();
	cl.setParent(parent); 
	cl.setRepository(repository);
	return cl;
    }

    //------------------------------------ Implementation of Classloader

    /*
     * XXX: The javadoc for java.lang.ClassLoader says that the
     * ClassLoader should cache classes so that it can handle repeated
     * requests for the same class.  On the other hand, the JLS seems
     * to imply that each classloader is only asked to load each class
     * once.  Is this a contradiction?
     *
     * Perhaps the second call only applies to classes which have been
     * garbage-collected?
     */

    /**
     * Resolves the specified name to a Class. The method loadClass()
     * is called by the virtual machine.  As an abstract method,
     * loadClass() must be defined in a subclass of ClassLoader.
     *
     * @param      name the name of the desired Class.
     * @param      resolve true if the Class needs to be resolved;
     *             false if the virtual machine just wants to determine
     *             whether the class exists or not
     * @return     the resulting Class.
     * @exception  ClassNotFoundException  if the class loader cannot
     *             find a the requested class.
     */
    protected synchronized Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        if( debug>0) log( "loadClass() " + name);
	// The class object that will be returned.
        Class c = null;

        // Use the cached value, if this class is already loaded into
        // this classloader.
        ClassCacheEntry entry = (ClassCacheEntry) cache.get(name);

        if (entry != null) {
	    if( debug>0) log( "Found in cache " + name);
            // Class found in our cache
            c = entry.loadedClass;
            if (resolve) resolveClass(c);
            return c;
        }

        // Make sure we can access this class when using a SecurityManager
        if (sm != null) {
            int i = name.lastIndexOf('.');
            if (i >= 0) {
                sm.checkPackageAccess(name.substring(0,i)); 
                sm.checkPackageDefinition(name.substring(0,i));
	    }
	}

	if (parent != null) {
	    try {
		if( debug>0) log( "loadClass() from parent " + name);
		c = parent.loadClass(name);
		if (c != null) {
		    if (resolve) resolveClass(c);
		    return c;
		}
	    } catch (ClassNotFoundException e) {
		c = null;
	    } catch (Exception e) {
		c = null;
	    }
	}

        // Attempt to load the class from the system
	if( debug>0) log( "loadClass() from system " + name);
        try {
            c = loadSystemClass(name, resolve);
            if (c != null) {
                return c;
            }
        } catch (Exception e) {
            c = null;
        }

	if( debug>0) log( "loadClass() from local repository " + name);
        // Try to load it from each repository
        Enumeration repEnum = repository.elements();

        // Cache entry.
        ClassCacheEntry classCache = new ClassCacheEntry();
        while (repEnum.hasMoreElements()) {
            byte[] classData=null;

            ClassRepository cp = (ClassRepository) repEnum.nextElement();
            File file = cp.getFile();
            try {
                if (file.isDirectory()) {
                    classData =
                        loadClassFromDirectory(file, name, classCache);
                } else {
		    classData =
                        loadClassFromZipfile(file, name, classCache);
		}
            } catch(SecurityException sex) {
                sex.printStackTrace();
                throw sex;
            } catch(IOException ioe) {
                // Error while reading in data, consider it as not found
                classData = null;
            } catch( Exception ex ) {
		ex.printStackTrace();
	    }

            if (classData != null) {
                // Define the class with a ProtectionDomain if using
		// a SecurityManager
                // Origin is set by the specific loader
                classCache.lastModified = classCache.origin.lastModified();
		if( debug>0) log( "Add to cache() " + name + " " + classCache);
		cache.put(name, classCache);
		
		if( debug>0) log( "Before define class " + name);
		try {
		    classCache.loadedClass = 
			doDefineClass(name, classData,
				      cp.getProtectionDomain());
		} catch(Throwable t ) {
		    t.printStackTrace();
		}
		if( debug>0) log( "Class defined " + classCache.loadedClass);
                // Cache the result;

                // Resolve it if necessary
		if (resolve) resolveClass(classCache.loadedClass);
                return classCache.loadedClass;
            }
        }

        // If not found in any repository
        throw new ClassNotFoundException(name);
    }

    // Override this with PD
    protected Class doDefineClass(String name, byte classData[], Object pd )
    {
	return  defineClass(name, classData, 0, classData.length);
    }
    
    /**
     * Load a class using the system classloader.
     *
     * @exception  ClassNotFoundException  if the class loader cannot
     *             find a the requested class.
     * @exception  NoClassDefFoundError  if the class loader cannot
     *             find a definition for the class.
     */
    private Class loadSystemClass(String name, boolean resolve)
        throws NoClassDefFoundError, ClassNotFoundException
    {
        Class c = findSystemClass(name);
        // Throws if not found.

//         // Add cache entry
//         ClassCacheEntry cacheEntry = new ClassCacheEntry();
//         cacheEntry.origin = null;
//         cacheEntry.loadedClass = c;
//         cacheEntry.lastModified = Long.MAX_VALUE;
//         cache.put(name, cacheEntry);

        if (resolve) resolveClass(c);

        return c;
    }

    /**
     * Tries to load the class from a directory.
     *
     * @param dir The directory that contains classes.
     * @param name The classname
     * @param cache The cache entry to set the file if successful.
     */
    private byte[] loadClassFromDirectory(File dir, String name,
        ClassCacheEntry cache)
            throws IOException
    {
        // Translate class name to file name
        String classFileName =
            name.replace('.', File.separatorChar) + ".class";

        // Check for garbage input at beginning of file name
        // i.e. ../ or similar
        if (!Character.isJavaIdentifierStart(classFileName.charAt(0))) {
            // Find real beginning of class name
            int start = 1;
            while (!Character.isJavaIdentifierStart(
                classFileName.charAt(start++)));
            classFileName = classFileName.substring(start);
        }

        File classFile = new File(dir, classFileName);

        if (classFile.exists()) {
            cache.origin = classFile;
            InputStream in = new FileInputStream(classFile);
            try {
                return loadBytesFromStream(in, (int) classFile.length());
            } finally {
                in.close();
            }
        } else {
            // Not found
            return null;
        }
    }

    /**
     * Tries to load the class from a zip file.
     *
     * @param file The zipfile that contains classes.
     * @param name The classname
     * @param cache The cache entry to set the file if successful.
     */
    private byte[] loadClassFromZipfile(File file, String name,
            ClassCacheEntry cache)
        throws IOException
    {
        // Translate class name to file name
        String classFileName = name.replace('.', '/') + ".class";

        ZipFile zipfile = new ZipFile(file);

        try {
            ZipEntry entry = zipfile.getEntry(classFileName);
	    //	    System.out.println("XXX Found " + classFileName + " " + entry + " " + entry.getSize() );
            if (entry != null) {
                cache.origin = file;
                return loadBytesFromStream(zipfile.getInputStream(entry),
                    (int) entry.getSize());
            } else {
                // Not found
                return null;
            }
        } finally {
            zipfile.close();
        }
    }

    /**
     * Loads all the bytes of an InputStream.
     */
    private byte[] loadBytesFromStream(InputStream in, int length)
        throws IOException
    {
        byte[] buf = new byte[length];
        int nRead, count = 0;

        while ((length > 0) && ((nRead = in.read(buf,count,length)) != -1)) {
            count += nRead;
            length -= nRead;
        }

        return buf;
    }

    /**
     * Get an InputStream on a given resource.  Will return null if no
     * resource with this name is found.
     * <p>
     * The JServClassLoader translate the resource's name to a file
     * or a zip entry. It looks for the resource in all its repository
     * entry.
     *
     * @see     java.lang.Class#getResourceAsStream(String)
     * @param   name    the name of the resource, to be used as is.
     * @return  an InputStream on the resource, or null if not found.
     */
    public InputStream getResourceAsStream(String name) {
        // Try to load it from the system class
        if( debug > 0 ) log( "getResourceAsStream() " + name );
        // First ask the parent class loader to fetch it, if possible
        InputStream s = null;
        if (parent != null) {
            s = parent.getResourceAsStream(name);
            if (s != null)
                return (s);
        }
        // Second ask the system class loader to fetch it from the classpath
	s = getSystemResourceAsStream(name);

        // Third, check our own repositories
        if (s == null) {
            // Try to find it from every repository
            Enumeration repEnum = repository.elements();
            while (repEnum.hasMoreElements()) {
                ClassRepository cp = (ClassRepository) repEnum.nextElement();
                File file = cp.getFile();
                if (file.isDirectory()) {
                    s = loadResourceFromDirectory(file, name);
                }
                else if(name.endsWith(".initArgs")) {
		    // XXX XXX XXX What is this ?
                    File dir = new File(file.getParent());
                    s = loadResourceFromDirectory(dir, name);
                } else {
                    s = loadResourceFromZipfile(file, name);
                }
		//		System.out.println("LOADING " + file + " "  + name + " " + s );
                if (s != null) {
                    break;
                }
            }
        }

        return s;
    }

    /**
     * Loads resource from a directory.
     */
    private InputStream loadResourceFromDirectory(File dir, String name) {
        // Name of resources are always separated by /
        String fileName = name.replace('/', File.separatorChar);
        File resFile = new File(dir, fileName);

        if (resFile.exists()) {
            try {
                return new FileInputStream(resFile);
            } catch (FileNotFoundException shouldnothappen) {
                return null;
            }
        } else {
            return null;
        }
    }

    /**
     * Loads resource from a zip file
     */
    private InputStream loadResourceFromZipfile(File file, String name) {
        ZipFile zipfile = null;
        try {
            zipfile = new ZipFile(file);
            ZipEntry entry = zipfile.getEntry(name);

            if (entry != null) {
		// workaround - the better solution is to not close the
		// zipfile !!!!
		byte[] data= loadBytesFromStream(zipfile.getInputStream(entry),
						 (int) entry.getSize());
		if(data != null) {
		    InputStream istream = new ByteArrayInputStream(data);
		    return istream;
		}
            }
        } catch(IOException e) {
        } finally {
	    // if we close the zipfile bad things will happen - we can't read the stream
	    // on some VMs
	    if ( zipfile != null ) {
                try {
                    zipfile.close();
                } catch ( IOException ignored ) {
                }
            }
        }
	// default case
	return null;
    }

    /**
     * Find a resource with a given name.  The return is a URL to the
     * resource. Doing a getContent() on the URL may return an Image,
     * an AudioClip,or an InputStream.
     * <p>
     * This classloader looks for the resource only in the directory
     * repository for this resource.
     *
     * @param   name    the name of the resource, to be used as is.
     * @return  an URL on the resource, or null if not found.
     */
    public URL getResource(String name) {
        if( debug > 0 ) log( "getResource() " + name );
        // First ask the parent class loader to fetch it, if possible
        URL u = null;
        if (parent != null) {
            u = parent.getResource(name);
            if (u != null)
                return (u);
        }
        // Second ask the system class loader to fetch it from the classpath
        u = getSystemResource(name);
        if (u != null) {
            return u;
        }

        if (name == null) {
            return null;
        }

        // Third, check our own repositories
        Enumeration repEnum = repository.elements();
        while (repEnum.hasMoreElements()) {
            ClassRepository cp = (ClassRepository) repEnum.nextElement();
            File file = cp.getFile();
            // Construct a file://-URL if the repository is a directory
            if (file.isDirectory()) {
                String fileName = name.replace('/', File.separatorChar);
                File resFile = new File(file, fileName);
                if (resFile.exists()) {
                    // Build a file:// URL form the file name
                    try {
                        return new URL("file", null, resFile.getAbsolutePath());                    	
                    } catch(java.net.MalformedURLException badurl) {
                        badurl.printStackTrace();
                        return null;
                    }
                }
            }
            else {
                // a jar:-URL *could* change even between minor releases, but
                // didn't between JVM's 1.1.6 and 1.3beta. Tested on JVM's from
                // IBM, Blackdown, Microsoft, Sun @ Windows and Sun @ Solaris
                try {
                    ZipFile zf = new ZipFile(file.getAbsolutePath());
                    ZipEntry ze = zf.getEntry(name);
                    zf.close();

                    if (ze != null) {
                        try {
                            return new URL("jar:file:" + file.getAbsolutePath() + "!/" + name);
                        } catch(java.net.MalformedURLException badurl) {
                            badurl.printStackTrace();
                            return null;
                        }
                    }
                } catch (IOException ioe) {
                    ioe.printStackTrace();
                    return null;
                }
            }   
        }

        // Not found
        return null;
    }

    public String toString() {
	return "AdaptiveClassLoader(  )";
    }

}
