// ========================================================================
// $Id: ContextLoader.java,v 1.37 2006/01/09 07:26:12 gregwilkins Exp $
// Copyright 1999-2004 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// 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 org.mortbay.http;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.CodeSource;
import java.security.PermissionCollection;
import java.util.Arrays;
import java.util.StringTokenizer;

import org.apache.commons.logging.Log;
import org.mortbay.log.LogFactory;
import org.mortbay.util.IO;
import org.mortbay.util.Resource;

/* ------------------------------------------------------------ */
/** ClassLoader for HttpContext.
 * Specializes URLClassLoader with some utility and file mapping
 * methods.
 *
 * This loader defaults to the 2.3 servlet spec behaviour where non
 * system classes are loaded from the classpath in preference to the
 * parent loader.  Java2 compliant loading, where the parent loader
 * always has priority, can be selected with the setJava2Complient method.
 *
 * @version $Id: ContextLoader.java,v 1.37 2006/01/09 07:26:12 gregwilkins Exp $
 * @author Greg Wilkins (gregw)
 */
public class ContextLoader extends URLClassLoader
{
    private static Log log= LogFactory.getLog(ContextLoader.class);

    private boolean _java2compliant= false;
    private ClassLoader _parent;
    private PermissionCollection _permissions;
    private String _urlClassPath;
    private HttpContext _context;
    
    /* ------------------------------------------------------------ */
    /** Constructor.
     * @param classPath Comma separated path of filenames or URLs
     * pointing to directories or jar files. Directories should end
     * with '/'.
     * @exception IOException
     */
    public ContextLoader(HttpContext context, String classPath, ClassLoader parent, PermissionCollection permisions)
        throws MalformedURLException, IOException
    {
        super(new URL[0], parent);
        _context=context;
        _permissions= permisions;
        _parent= parent;
        
        if (_parent == null)
            _parent= getSystemClassLoader();

        if (classPath == null)
        {
            _urlClassPath= "";
        }
        else
        {
            StringTokenizer tokenizer= new StringTokenizer(classPath, ",;");

            while (tokenizer.hasMoreTokens())
            {
                Resource resource= Resource.newResource(tokenizer.nextToken());
                if (log.isDebugEnabled())
                    log.debug("Path resource=" + resource);

                // Resolve file path if possible
                File file= resource.getFile();

                if (file != null)
                {
                    URL url= resource.getURL();
                    addURL(url);
                    _urlClassPath= (_urlClassPath == null) ? url.toString() : (_urlClassPath + "," + url.toString());
                }
                else
                {
                    // Add resource or expand jar/
                    if (!resource.isDirectory() && file == null)
                    {
                        InputStream in= resource.getInputStream();
                        File lib= new File(context.getTempDirectory(), "lib");
                        if (!lib.exists())
                        {
                            lib.mkdir();
                            lib.deleteOnExit();
                        }
                        File jar= File.createTempFile("Jetty-", ".jar", lib);

                        jar.deleteOnExit();
                        if (log.isDebugEnabled())
                            log.debug("Extract " + resource + " to " + jar);
                        FileOutputStream out = null;
                        try
                        {
                            out= new FileOutputStream(jar);
                            IO.copy(in, out);
                        }
                        finally
                        {
                            IO.close(out);
                        }

                        URL url= jar.toURL();
                        addURL(url);
                        _urlClassPath=
                            (_urlClassPath == null) ? url.toString() : (_urlClassPath + "," + url.toString());
                    }
                    else
                    {
                        URL url= resource.getURL();
                        addURL(url);
                        _urlClassPath=
                            (_urlClassPath == null) ? url.toString() : (_urlClassPath + "," + url.toString());
                    }
                }
            }
        }

        if (log.isDebugEnabled())
        {
            if (log.isDebugEnabled())
                log.debug("ClassPath=" + _urlClassPath);
            if (log.isDebugEnabled())
                log.debug("Permissions=" + _permissions);
            if (log.isDebugEnabled())
                log.debug("URL=" + Arrays.asList(getURLs()));
        }
    }

    /* ------------------------------------------------------------ */
    /** Set Java2 compliant status.
     * @param compliant 
     */
    public void setJava2Compliant(boolean compliant)
    {
        _java2compliant= compliant;
    }

    /* ------------------------------------------------------------ */
    public boolean isJava2Compliant()
    {
        return _java2compliant;
    }

    /* ------------------------------------------------------------ */
    public String toString()
    {
        return "ContextLoader@" + hashCode() + "(" + _urlClassPath + ")\n  --parent--> " + _parent.toString();
    }

    /* ------------------------------------------------------------ */
    public PermissionCollection getPermissions(CodeSource cs)
    {
        PermissionCollection pc= (_permissions == null) ? super.getPermissions(cs) : _permissions;
        if (log.isDebugEnabled())
            log.debug("loader.getPermissions(" + cs + ")=" + pc);
        return pc;
    }

    /* ------------------------------------------------------------ */
    public Class loadClass(String name) throws ClassNotFoundException
    {
        return loadClass(name, false);
    }

    /* ------------------------------------------------------------ */
    protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException
    {
        Class c= findLoadedClass(name);
        ClassNotFoundException ex= null;
        boolean tried_parent= false;
        if (c == null && (_java2compliant || isSystemPath(name)) && !isServerPath(name) && _parent!=null)
        {
            if (log.isTraceEnabled())
                log.trace("try loadClass " + name + " from " + _parent);
            tried_parent= true;
            try
            {
                c= _parent.loadClass(name);
                if (log.isTraceEnabled())
                    log.trace("p0 loaded " + c);
            }
            catch (ClassNotFoundException e)
            {
                ex= e;
            }
        }

        if (c == null)
        {
            if (log.isTraceEnabled())
                log.trace("try findClass " + name + " from " + _urlClassPath);
            try
            {
                c= this.findClass(name);
                if (log.isTraceEnabled())
                    log.trace("cx loaded " + c);
            }
            catch (ClassNotFoundException e)
            {
                ex= e;
            }
        }

        if (c == null && !tried_parent && !isServerPath(name) && _parent!=null)
        {
            if (log.isTraceEnabled())
                log.trace("try loadClass " + name + " from " + _parent);
            c= _parent.loadClass(name);
            if (log.isTraceEnabled())
                log.trace("p1 loaded " + c);
        }
        
        if (c == null)
            throw ex;

        if (resolve)
            resolveClass(c);

        return c;
    }

    /* ------------------------------------------------------------ */
    public URL getResource(String name)
    {
        URL url= null;
        boolean tried_parent= false;
        if (_parent!=null &&(_java2compliant || isSystemPath(name)))
        {
            if (log.isTraceEnabled())
                log.trace("try getResource " + name + " from " + _parent);
            tried_parent= true;
            url= _parent.getResource(name);
        }

        if (url == null)
        {
            if (log.isTraceEnabled())
                log.trace("try findResource " + name + " from " + _urlClassPath);
            url= this.findResource(name);

            if (url == null && name.startsWith("/"))
            {
                if (log.isDebugEnabled())
                    log.debug("HACK leading / off " + name);
                url= this.findResource(name.substring(1));
            }
        }

        if (_parent!=null && url == null && !tried_parent)
        {
            if (log.isTraceEnabled())
                log.trace("try getResource " + name + " from " + _parent);
            url= _parent.getResource(name);
        }

        if (url != null)
            if (log.isTraceEnabled())
                log.trace("found " + url);

        return url;
    }

    /* ------------------------------------------------------------ */
    public boolean isServerPath(String name)
    {
        name=name.replace('/','.');
        while(name.startsWith("."))
            name=name.substring(1);

        String[] server_classes=_context.getServerClasses();
        
        if (server_classes!=null)
        {
            for (int i=0;i<server_classes.length;i++)
            {
                boolean result=true;
                String c=server_classes[i];
                if (c.startsWith("-"))
                {
                    c=c.substring(1);
                    result=false;
                }
                
                if (c.endsWith("."))
                {
                    if (name.startsWith(c))
                        return result;
                }
                else if (name.equals(c))
                {
                    return result;
                }
            }
        }
        return false;
    }

    /* ------------------------------------------------------------ */
    public boolean isSystemPath(String name)
    {
        name=name.replace('/','.');
        while(name.startsWith("."))
            name=name.substring(1);

        String[] system_classes=_context.getSystemClasses();
        if (system_classes!=null)
        {
            for (int i=0;i<system_classes.length;i++)
            {
                boolean result=true;
                String c=system_classes[i];
                if (c.startsWith("-"))
                {
                    c=c.substring(1);
                    result=false;
                }
                
                if (c.endsWith("."))
                {
                    if (name.startsWith(c))
                        return result;
                }
                else if (name.equals(c))
                    return result;
            }
        }
        
        return false;
    }

    /* ------------------------------------------------------------ */
    public void destroy()
    {
        this._parent=null;
        this._permissions=null;
        this._urlClassPath=null;
    }
    
    /* ------------------------------------------------------------ */
    /**
     * @return Returns the serverClasses.
     */
    String[] getServerClasses()
    {
        return _context.getServerClasses();
    }
    
    /* ------------------------------------------------------------ */
    /**
     * @param serverClasses The serverClasses to set.
     */
    void setServerClasses(String[] serverClasses)
    {
        _context.setServerClasses(serverClasses);
    }
    
    /* ------------------------------------------------------------ */
    /**
     * @return Returns the systemClasses.
     */
    String[] getSystemClasses()
    {
        return _context.getSystemClasses();
    }
    
    /* ------------------------------------------------------------ */
    /**
     * @param systemClasses The systemClasses to set.
     */
    void setSystemClasses(String[] systemClasses)
    {
        _context.setSystemClasses(systemClasses);
    }
}
