/*
 * @(#)UrlClassLoader.java
 *
 * Copyright (C) 2000,2002-2003 Matt Albrecht
 * groboclown@users.sourceforge.net
 * http://groboutils.sourceforge.net
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a
 *  copy of this software and associated documentation files (the "Software"),
 *  to deal in the Software without restriction, including without limitation
 *  the rights to use, copy, modify, merge, publish, distribute, sublicense,
 *  and/or sell copies of the Software, and to permit persons to whom the
 *  Software is furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in
 *  all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 *  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 *  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 *  DEALINGS IN THE SOFTWARE.
 */
 

package net.sourceforge.groboutils.util.classes.v1.jdk0;

import java.io.File;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;

import java.net.URL;

import java.util.Hashtable;
import java.util.Enumeration;

import java.util.zip.ZipInputStream;
import java.util.zip.ZipEntry;


import net.sourceforge.groboutils.util.classes.v1.IUrlClassLoader;




/**
 * Class responsible for loading classes in a JDK 1.0+ version compatible
 * way.  Need to determine speed vs. size for caching Jar files that are
 * loaded over the internet.  For now, we cache stuff, in the hope that
 * the flush() will be called.
 * <P>
 * If the URL is null, or if the bytecode for a class name is not found,
 * then this will attempt to load the class from the system classloader.
 * <P>
 * NOTE: As of version 1.0.0, there is no longer a dependency upon
 * <tt>net.sourceforge.groboutils.util.io.v1</tt>
 *
 * @author  Matt Albrecht <a href="mailto:groboclown@users.sourceforge.net">groboclown@users.sourceforge.net</a>
 * @version $Date: 2003/05/06 05:35:00 $
 * @since   November 17, 2000 (GroboUtils Alpha 0.9.0)
 */
public class UrlClassLoader implements IUrlClassLoader, BytecodeSource
{
    //----------------------------
    // Public data
    
    //----------------------------
    // Private data
    
    private ArrayClassLoader m_acl = new ArrayClassLoader();
    private Hashtable m_urlSources = new Hashtable();
    
    private static final String[] JAR_EXTENTIONS =
        { ".jar", ".zip", ".war", ".sar", ".ear" };
    

    private abstract class BytecodeSourceCache implements BytecodeSource
    {
        private Hashtable m_classes = new Hashtable();
        
        public BytecodeSourceCache()
        {
            // do nothing
        }
        
        public byte[] getBytecode( String className )
        {
            byte buff[] = (byte[])this.m_classes.get( className );
            if (buff == null)
            {
                try
                {
                    buff = readBytecode( className );
                    if (buff != null)
                    {
                        this.m_classes.put( className, buff );
                    }
                }
                catch (IOException ioe)
                {
ioe.printStackTrace();
                    buff = null;
                }
                catch (SecurityException se)
                {
se.printStackTrace();
                    buff = null;
                }
            }
            return buff;
        }
        
        
        protected abstract byte[] readBytecode( String className )
                throws IOException;
    }
    
    
    
    /**
     * Loads classes from a given zip/jar file
     */
    private class JarBytecodeSource extends BytecodeSourceCache
    {
        private byte[] m_zipfile;
        
        public JarBytecodeSource( String s_url )
                throws IOException
        {
            // due to an error in the JDK 1.1 implementation of the
            // file: protocol, we must verify the URL ourself
            InputStream is = null;
            try
            {
                if (s_url.startsWith( "file:" ))
                {
                    // this is also a bit of an optimization
                    is = new FileInputStream(
                        s_url.substring( "file:".length() ) );
                }
                else
                {
                    URL url = new URL( s_url );
                    is = url.openStream();
                }
                this.m_zipfile = readByteStream( is );
            }
            finally
            {
                if (is != null)
                {
                    is.close();
                }
            }
        }
        
        protected byte[] readBytecode( String className )
                throws IOException
        {
            ByteArrayInputStream bais = new ByteArrayInputStream(
                this.m_zipfile );
            ZipInputStream zis = new ZipInputStream( bais );
            String classFile = className2jarFileName( className );
            byte buff[] = findZipEntry( classFile, zis );
            zis.close();
            bais.close();
            return buff;
        }
    }

    
    private class UrlBytecodeSource extends BytecodeSourceCache
    {
        private String m_url;
        
        public UrlBytecodeSource( String url )
        {
            this.m_url = url;
        }
        
        protected byte[] readBytecode( String className )
                throws IOException
        {
            URL url = new URL( joinClassToUrl(
                className, this.m_url ) );
            byte[] buff = null;
            IOException ex = null;
            // there may be time-out issues
            for (int i = 0; i < 3 && buff == null; ++i)
            {
                InputStream is = null;
                try
                {
                    is = url.openStream();
                    buff = readByteStream( is );
                }
                catch (IOException ioe)
                {
                    ex = ioe;
                }
                finally
                {
                    is.close();
                }
            }
            if (buff == null)
            {
                throw ex;
            }
            return buff;
        }
    }
    
    
    //----------------------------
    // constructors
    
    /**
     * Default constructor
     */
    public UrlClassLoader()
    {
        this.m_acl.setBytecodeSource( this );
    }
    
    
    //----------------------------
    // Public methods
     
    /**
     * Load the given class from the given URL.  If the URL is <tt>null</tt>,
     * then it loads the class from the default class loader.
     *
     * @param className the exact class name to load.
     * @param url the URL from which the class is loaded.  If this is
     *    <tt>null</tt>, then this loads the class from the default class
     *    loader.
     * @return the loaded Class instance, or <tt>null</tt> if the class could
     *    not be found.
     */
    public Class loadClass( String className, String url )
    {
        url = convertUrl( url );
        
        // check if this is a JAR type reference
        BytecodeSource bs = getBytecodeSource( url );
        if (bs != null)
        {
            byte bytecode[] = bs.getBytecode( className );
            
            if (bytecode != null && bytecode.length > 0)
            {
                // load the class from bytecode
                
                this.m_acl.addClass( className, bytecode );
            }
            // else - use the system class loader
            // ***** SHOULD WE RETURN NULL???? ****
        }
        Class c = null;
        try
        {
            c = this.m_acl.loadClass( className );
        }
        catch (ClassNotFoundException cnfe)
        {
            c = null;
        }
        
        return c;
    }
    
    
    /**
     * Call to flush any cache stored in the interface.  This allows for
     * a class loader to cache results, and free up memory when it is
     * not needed.
     */
    public void flush()
    {
        this.m_acl = new ArrayClassLoader();
        this.m_acl.setBytecodeSource( this );
        this.m_urlSources = new Hashtable();
    }
    
    
    public byte[] getBytecode( String classname )
    {
        Enumeration enum = this.m_urlSources.elements();
        while (enum.hasMoreElements())
        {
            BytecodeSource bs = (BytecodeSource)enum.nextElement();
            byte buff[] = bs.getBytecode( classname );
            if (buff != null)
            {
                return buff;
            }
        }
        return null;
    }
    
    
    //----------------------------
    // Protected methods
    
    
    /**
     * 
     */
    protected BytecodeSource getBytecodeSource( String url )
    {
        if (url == null)
        {
            return null;
        }
        BytecodeSource bs = (BytecodeSource)this.m_urlSources.get( url );
        if (bs == null)
        {
            // there may be time-out issues
            for (int i = 0; i < 3 && bs == null; ++i)
            {
                if (isJarURL( url ))
                {
                    try
                    {
                        bs = new JarBytecodeSource( url );
                    }
                    catch (IOException ioe)
                    {
ioe.printStackTrace();
                    }
                }
                else
                {
                    bs = new UrlBytecodeSource( url );
                }
            }
            if (bs != null)
            {
                this.m_urlSources.put( url, bs );
            }
        }
        return bs;
    }
    
    
    /**
     * Converts the given string to a fully qualified URL.  If no
     * scheme is given, then it is converted to a File scheme.
     */
    protected String convertUrl( String url )
    {
        if (url == null)
        {
            return null;
        }
//System.out.println("Converting from URL "+url);
        int pos = url.indexOf(':');
        String scheme = null;
        if (pos >= 0)
        {
            scheme = url.substring( 0, pos );
        }
        if (scheme == null || scheme.length() <= 0)
        {
            url = "file:" + getAbsoluteFilename( new File( url ) );
        }
        else
        {
            // check if the url is an existing file, since
            // a colon could be a drive or volume reference.
            File f = new File( url );
            if (f.exists())
            {
                // make this a file
                url = "file:" + getAbsoluteFilename( f );
            }
            // else assume that the given url is correct
        }

//System.out.println(" - to URL "+url);
        return url;
    }
    
    
    /**
     * @return <tt>true</tt> if the URL references a Jar-like file.
     */
    protected boolean isJarURL( String url )
    {
        int pos = url.lastIndexOf( '.' );
        if (pos < 0) return false;
        String ext = url.substring( pos );
        for (int i = JAR_EXTENTIONS.length; --i >= 0;)
        {
            if (JAR_EXTENTIONS[i].equalsIgnoreCase( ext ))
            {
                return true;
            }
        }
        return false;
    }
    
    
    /**
     * Finds the entry of the given filename in the given input stream,
     * and returns the entry as a byte array.  If the entry wasn't found,
     * then <tt>null</tt> is returned.
     */
    protected byte[] findZipEntry( String filename, ZipInputStream zis )
    {
        try
        {
            ZipEntry ze = zis.getNextEntry();
            while (ze != null)
            {
                // case must match!
                if (filename.equals( ze.getName() ))
                {
                    // size may be huge, but actually class files
                    // have a maximum size
                    int size = (int)ze.getSize();
                    byte b[];
                    if (size < 0)
                    {
                        b = readByteStream( zis );
                    }
                    else
                    {
                        b = new byte[ size ];
                        zis.read( b, 0, size );
                    }
                    return b;
                }
                ze = zis.getNextEntry();
            }
            // the entry wasn't found
            return null;
        }
        catch (IOException ioe)
        {
ioe.printStackTrace();
            return null;
        }
    }
    
    
    /**
     * Converts a class name to a file name.
     */
    protected String className2jarFileName( String className )
    {
        return className.replace( '.', '/' ) + ".class";
    }
    
    
    /**
     * Joins a classname to a URL.
     */
    protected String joinClassToUrl( String className, String url )
    {
        StringBuffer sb = new StringBuffer( url );
        sb.append('/');
        sb.append( className2jarFileName( className ) );
        return new String( sb );
    }
    
    
    /**
     * 
     */
    protected String getAbsoluteFilename( File f )
    {
        return f.getAbsolutePath().toString();
    }
    
    
    /**
     * A re-implementation of
     * <tt>net.groboclown.util.io.v1.ReadByteStream</tt>, to reduce
     * the amount of inter-package dependencies.
     */
    protected byte[] readByteStream( InputStream is )
            throws IOException
    {
        int totSize = 0;
        byte[] ret = new byte[ 0 ];
        byte[] buff = new byte[ 4096 ];
        int size = is.read( buff, 0, 4096 );
        while (size > 0)
        {
            byte[] tmp = new byte[ size + totSize ];
            System.arraycopy( ret, 0, tmp, 0, totSize );
            System.arraycopy( buff, 0, tmp, totSize, size );
            ret = tmp;
            totSize += size;
            
            size = is.read( buff, 0, 4096 );
        }
        return ret;
    }
}
 
