// ========================================================================
// $Id: WebApplicationContext.java,v 1.136 2005/10/26 08:11:04 gregwilkins Exp $
// Copyright 2000-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.jetty.servlet;

import java.io.Externalizable;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.security.PermissionCollection;
import java.util.Collections;
import java.util.EventListener;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import org.apache.commons.logging.Log;
import org.mortbay.log.LogFactory;
import org.mortbay.http.HttpException;
import org.mortbay.http.HttpHandler;
import org.mortbay.http.HttpRequest;
import org.mortbay.http.HttpResponse;
import org.mortbay.http.UserRealm;
import org.mortbay.jetty.Server;
import org.mortbay.util.JarResource;
import org.mortbay.util.LazyList;
import org.mortbay.util.Loader;
import org.mortbay.util.MultiException;
import org.mortbay.util.Resource;

/* ------------------------------------------------------------ */
/** Standard web.xml configured HttpContext.
 *
 * This specialization of HttpContext uses the standardized web.xml
 * to describe a web application and configure the handlers for the
 * HttpContext.
 *
 * If a file named web-jetty.xml or jetty-web.xml is found in the
 * WEB-INF directory it is applied to the context using the
 * XmlConfiguration format.
 *
 * A single WebApplicationHandler instance is used to provide
 * security, filter, sevlet and resource handling.
 *
 * @see org.mortbay.jetty.servlet.WebApplicationHandler
 * @version $Id: WebApplicationContext.java,v 1.136 2005/10/26 08:11:04 gregwilkins Exp $
 * @author Greg Wilkins (gregw)
 */
public class WebApplicationContext extends ServletHttpContext implements Externalizable
{
    private static Log log= LogFactory.getLog(WebApplicationContext.class);

    /* ------------------------------------------------------------ */
    private String _defaultsDescriptor= "org/mortbay/jetty/servlet/webdefault.xml";
    private String _war;
    private boolean _extract;
    private boolean _ignorewebjetty;
    private boolean _distributable;
    private Configuration[] _configurations;
    private String[] _configurationClassNames;

    private transient Map _resourceAliases;
    private transient Resource _webApp;
    private transient Resource _webInf;
    private transient WebApplicationHandler _webAppHandler;
    private transient Object _contextListeners;
    private transient Map _errorPages;

    /* ------------------------------------------------------------ */
    /** Constructor. 
     */
    public WebApplicationContext()
    {
    }

    /* ------------------------------------------------------------ */
    /** Constructor. 
     * @param webApp The Web application directory or WAR file.
     */
    public WebApplicationContext(String webApp)
    {
        _war= webApp;
    }

    /* ------------------------------------------------------------ */
    public void writeExternal(java.io.ObjectOutput out) throws java.io.IOException
    {
        out.writeObject(getContextPath());
        out.writeObject(getVirtualHosts());
        HttpHandler[] handlers= getHandlers();
        for (int i= 0; i < handlers.length; i++)
        {
            if (handlers[i] instanceof WebApplicationHandler)
                break;
            out.writeObject(handlers[i]);
        }
        out.writeObject(getAttributes());
        out.writeBoolean(isRedirectNullPath());
        out.writeInt(getMaxCachedFileSize());
        out.writeInt(getMaxCacheSize());
        out.writeBoolean(getStatsOn());
        out.writeObject(getPermissions());
        out.writeBoolean(isClassLoaderJava2Compliant());

        out.writeObject(_defaultsDescriptor);
        out.writeObject(_war);
        out.writeBoolean(_extract);
        out.writeBoolean(_ignorewebjetty);
        out.writeBoolean(_distributable);
        
        out.writeObject(_configurationClassNames);
    }

    /* ------------------------------------------------------------ */
    public void readExternal(java.io.ObjectInput in) throws java.io.IOException, ClassNotFoundException
    {
        setContextPath((String)in.readObject());
        setVirtualHosts((String[])in.readObject());
        Object o= in.readObject();

        while (o instanceof HttpHandler)
        {
            addHandler((HttpHandler)o);
            o= in.readObject();
        }
        setAttributes((Map)o);
        setRedirectNullPath(in.readBoolean());
        setMaxCachedFileSize(in.readInt());
        setMaxCacheSize(in.readInt());
        setStatsOn(in.readBoolean());
        setPermissions((PermissionCollection)in.readObject());
        setClassLoaderJava2Compliant(in.readBoolean());

        _defaultsDescriptor= (String)in.readObject();
        _war= (String)in.readObject();
        _extract= in.readBoolean();
        _ignorewebjetty= in.readBoolean();
        _distributable= in.readBoolean();
        _configurationClassNames=(String[])in.readObject();
    }

    

    /* ------------------------------------------------------------ */
    public void setConfigurationClassNames (String[] configurationClassNames)
    {
        if (null != configurationClassNames)
        {
            _configurationClassNames = new String[configurationClassNames.length];
            System.arraycopy (configurationClassNames, 0, _configurationClassNames, 0, configurationClassNames.length);
        }
    }

    /* ------------------------------------------------------------ */
    public String[] getConfigurationClassNames ()
    {
        return _configurationClassNames;
    }
    
    /* ------------------------------------------------------------ */
    /** 
     * @param war Filename or URL of the web application directory or WAR file. 
     */
    public void setWAR(String war)
    {
        _war= war;
    }

    /* ------------------------------------------------------------ */
    public String getWAR()
    {
        return _war;
    }

    /* ------------------------------------------------------------ */
    public WebApplicationHandler getWebApplicationHandler()
    {
        if (_webAppHandler == null)
            getServletHandler();
        return _webAppHandler;
    }

    /* ------------------------------------------------------------ */
    private void resolveWebApp() throws IOException
    {
        if (_webApp == null && _war != null && _war.length() > 0)
        {
            // Set dir or WAR
            _webApp= Resource.newResource(_war);

            // Accept aliases for WAR files
            if (_webApp.getAlias() != null)
            {
                log.info(_webApp + " anti-aliased to " + _webApp.getAlias());
                _webApp= Resource.newResource(_webApp.getAlias());
            }

            if (log.isDebugEnabled())
                log.debug(
                    "Try webapp=" + _webApp + ", exists=" + _webApp.exists() + ", directory=" + _webApp.isDirectory());

            // Is the WAR usable directly?
            if (_webApp.exists() && !_webApp.isDirectory() && !_webApp.toString().startsWith("jar:"))
            {
                // No - then lets see if it can be turned into a jar URL.
                Resource jarWebApp= Resource.newResource("jar:" + _webApp + "!/");
                if (jarWebApp.exists() && jarWebApp.isDirectory())
                {
                    _webApp= jarWebApp;
                    _war= _webApp.toString();
                    if (log.isDebugEnabled())
                        log.debug(
                            "Try webapp="
                                + _webApp
                                + ", exists="
                                + _webApp.exists()
                                + ", directory="
                                + _webApp.isDirectory());
                }
            }

            // If we should extract or the URL is still not usable
            if (_webApp.exists()
                && (!_webApp.isDirectory()
                    || (_extract && _webApp.getFile() == null)
                    || (_extract && _webApp.getFile() != null && !_webApp.getFile().isDirectory())))
            {
                // Then extract it.
                File tempDir= new File(getTempDirectory(), "webapp");
                if (tempDir.exists())
                    tempDir.delete();
                tempDir.mkdir();
                tempDir.deleteOnExit();
                log.info("Extract " + _war + " to " + tempDir);
                JarResource.extract(_webApp, tempDir, true);
                _webApp= Resource.newResource(tempDir.getCanonicalPath());

                if (log.isDebugEnabled())
                    log.debug(
                        "Try webapp="
                            + _webApp
                            + ", exists="
                            + _webApp.exists()
                            + ", directory="
                            + _webApp.isDirectory());
            }

            // Now do we have something usable?
            if (!_webApp.exists() || !_webApp.isDirectory())
            {
                log.warn("Web application not found " + _war);
                throw new java.io.FileNotFoundException(_war);
            }

            if (log.isDebugEnabled())
                log.debug("webapp=" + _webApp);

            // Iw there a WEB-INF directory?
            _webInf= _webApp.addPath("WEB-INF/");
            if (!_webInf.exists() || !_webInf.isDirectory())
                _webInf= null;
            else
            {
                // Is there a WEB-INF work directory
                Resource work= _webInf.addPath("work");
                if (work.exists()
                    && work.isDirectory()
                    && work.getFile() != null
                    && work.getFile().canWrite()
                    && getAttribute("javax.servlet.context.tempdir") == null)
                    setAttribute("javax.servlet.context.tempdir", work.getFile());
            }

            // ResourcePath
            super.setBaseResource(_webApp);
        }
    }


    /* ------------------------------------------------------------ */
    public Resource getWebInf() throws IOException
    {
        if (_webInf==null)
            resolveWebApp();
        return _webInf;
    }
    
    /* ------------------------------------------------------------ */
    /** Get the context ServletHandler.
     * Conveniance method. If no ServletHandler exists, a new one is added to
     * the context.  This derivation of the method creates a
     * WebApplicationHandler extension of ServletHandler.
     * @return WebApplicationHandler
     */
    public synchronized ServletHandler getServletHandler()
    {
        if (_webAppHandler == null)
        {
            _webAppHandler= (WebApplicationHandler)getHandler(WebApplicationHandler.class);
            if (_webAppHandler == null)
            {
                if (getHandler(ServletHandler.class) != null)
                    throw new IllegalStateException("Cannot have ServletHandler in WebApplicationContext");
                _webAppHandler= new WebApplicationHandler();
                addHandler(_webAppHandler);
            }
        }
        return _webAppHandler;
    }

    /* ------------------------------------------------------------ */
    public void setPermissions(PermissionCollection permissions)
    {
        if (!_ignorewebjetty)
            log.warn("Permissions set with web-jetty.xml enabled");
        super.setPermissions(permissions);
    }

    /* ------------------------------------------------------------ */
    public boolean isIgnoreWebJetty()
    {
        return _ignorewebjetty;
    }

    /* ------------------------------------------------------------ */
    /** 
     * @param b If TRUE, web-jetty.xml and jetty-web.xml configuration
     * files are ignored. 
     */
    public void setIgnoreWebJetty(boolean b)
    {
        _ignorewebjetty= b;
        if (b && getPermissions() != null)
            log.warn("Permissions set with web-jetty.xml enabled");
    }

    /* ------------------------------------------------------------ */
    public boolean isDistributable()
    {
        return _distributable;
    }

    /* ------------------------------------------------------------ */
    public void setDistributable(boolean distributable)
    {
        _distributable=distributable;
    }

    /* ------------------------------------------------------------ */
    public Configuration[] getConfigurations ()
    {
        return _configurations;
    }

    /* ------------------------------------------------------------ */
    protected Configuration[] loadConfigurations() throws Exception
    {
        String[] names = _configurationClassNames;
        
        //if this webapp does not have its own set of configurators, use the defaults
        if (null==names)
            names = ((Server)getHttpServer()).getWebApplicationConfigurationClassNames();
        
        if (null!=names)
        {
            //instantiate instances for each
            Object[] nullArgs = new Object[0];
            Configuration[] configurations = new Configuration[names.length];
            for (int i=0; i< names.length; i++)
            {
                configurations[i] =
                    (Configuration)Loader.loadClass(WebApplicationContext.class, names[i]).getConstructors()[0].newInstance(nullArgs);
                if (log.isDebugEnabled()){log.debug("Loaded instance of "+names[i]);};
            }
            return configurations;
        }
        else
            return new Configuration[0];
    }

    /* ------------------------------------------------------------ */
    protected void configureClassPath() throws Exception
    {
        //call each of the instances
        // first, configure the classpaths
        for (int i=0; i<_configurations.length;i++)
        {
            _configurations[i].setWebApplicationContext(this);
            _configurations[i].configureClassPath();
        }
    }

    /* ------------------------------------------------------------ */
    protected void configureDefaults() throws Exception
    {
        //next, configure default settings
        for (int i=0;i<_configurations.length;i++)
        {
            _configurations[i].setWebApplicationContext(this);
            _configurations[i].configureDefaults();
        }
    }

    /* ------------------------------------------------------------ */
    protected void configureWebApp () throws Exception
    {
        //finally, finish configuring the webapp
        for (int i=0;i<_configurations.length;i++)
        {
            _configurations[i].setWebApplicationContext(this);
            _configurations[i].configureWebApp();
        }
        
    }
 
    /* ------------------------------------------------------------ */
    /** Start the Web Application.
     * @exception IOException 
     */
    protected void doStart() throws Exception
    {
        if (isStarted())
            return;

        // save context classloader
        Thread thread= Thread.currentThread();
        ClassLoader lastContextLoader= thread.getContextClassLoader();

        MultiException mex= null;
        try
        {
            // Find the webapp
            resolveWebApp();

            // Get the handler
            getServletHandler();
          
            _configurations=loadConfigurations();
            
            // initialize the classloader            
            configureClassPath();
            initClassLoader(true);
            thread.setContextClassLoader(getClassLoader());
            initialize();
            
            // Do the default configuration
            configureDefaults();

            // Set classpath for Jasper.
            Map.Entry entry= _webAppHandler.getHolderEntry("test.jsp");
            if (entry != null)
            {
                ServletHolder jspHolder= (ServletHolder)entry.getValue();
                if (jspHolder != null && jspHolder.getInitParameter("classpath") == null)
                {
                    String fileClassPath= getFileClassPath();
                    jspHolder.setInitParameter("classpath", fileClassPath);
                    if (log.isDebugEnabled())
                        log.debug("Set classpath=" + fileClassPath + " for " + jspHolder);
                }
            }
            
            // configure webapp
            configureWebApp();

            // If we have servlets, don't init them yet
            _webAppHandler.setAutoInitializeServlets(false);

            // Start handlers
            super.doStart();

            mex= new MultiException();
            // Context listeners
            if (_contextListeners != null && _webAppHandler != null)
            {
                ServletContextEvent event= new ServletContextEvent(getServletContext());
                for (int i= 0; i < LazyList.size(_contextListeners); i++)
                {
                    try
                    {
                        ((ServletContextListener)LazyList.get(_contextListeners, i)).contextInitialized(event);
                    }
                    catch (Exception ex)
                    {
                        mex.add(ex);
                    }
                }
            }

            // OK to Initialize servlets now
            if (_webAppHandler != null && _webAppHandler.isStarted())
            {
                try
                {
                    _webAppHandler.initializeServlets();
                }
                catch (Exception ex)
                {
                    mex.add(ex);
                }
            }
        }
        catch (Exception e)
        {
            log.warn("Configuration error on " + _war, e);
            throw e;
        }
        finally
        {
            thread.setContextClassLoader(lastContextLoader);
        }

        if (mex != null)
            mex.ifExceptionThrow();
    }

    /* ------------------------------------------------------------ */
    /** Stop the web application.
     * Handlers for resource, servlet, filter and security are removed
     * as they are recreated and configured by any subsequent call to start().
     * @exception InterruptedException 
     */
    protected void doStop() throws Exception
    {
        MultiException mex=new MultiException();
        
        
        Thread thread= Thread.currentThread();
        ClassLoader lastContextLoader= thread.getContextClassLoader();
        
        try
        {
            // Context listeners
            if (_contextListeners != null)
            {
                if (_webAppHandler != null)
                {
                    ServletContextEvent event= new ServletContextEvent(getServletContext());
                    
                    for (int i= LazyList.size(_contextListeners); i-- > 0;)
                    {
                        try 
                        {
                            ((ServletContextListener)LazyList.get(_contextListeners, i)).contextDestroyed(event);
                        }
                        catch (Exception e)
                        {
                            mex.add(e);
                        }
                    }
                }
            }
            _contextListeners= null;
            
            // Stop the context
            try
            {
                super.doStop();
            }
            catch (Exception e)
            {
                mex.add(e);
            }
            
            // clean up
            clearSecurityConstraints();
            
            if (_webAppHandler != null)
                removeHandler(_webAppHandler);
            _webAppHandler= null;
            
            if (_errorPages != null)
                _errorPages.clear();
            _errorPages= null;
            
            _webApp=null;
            _webInf=null;
            
            _configurations=null;
            
        }
        finally
        {
            thread.setContextClassLoader(lastContextLoader);
        }
        
        if (mex!=null)
            mex.ifExceptionThrow();
    }
    

    /* ------------------------------------------------------------ */
    public void destroy()
    {
        super.destroy();
        if (isStarted())
            throw new IllegalStateException();

        _defaultsDescriptor=null;
        _war=null;
        _configurationClassNames=null;
        if (_resourceAliases!=null)
            _resourceAliases.clear();
        _resourceAliases=null;
        _contextListeners=null;
        if (_errorPages!=null)
            _errorPages.clear();
        _errorPages=null;
    }

    /* ------------------------------------------------------------ */
    public void handle(String pathInContext, String pathParams, HttpRequest httpRequest, HttpResponse httpResponse)
        throws HttpException, IOException
    {
        if (!isStarted())
            return;
        try
        {
            super.handle(pathInContext, pathParams, httpRequest, httpResponse);
        }
        finally
        {
            if (!httpRequest.isHandled())
                httpResponse.sendError(HttpResponse.__404_Not_Found);
            httpRequest.setHandled(true);
            if (!httpResponse.isCommitted())
            {
                httpResponse.completing();
                httpResponse.commit();
            }
        }
    }

    /* ------------------------------------------------------------ */
    public synchronized void addEventListener(EventListener listener) throws IllegalArgumentException
    {
        if (listener instanceof ServletContextListener)
        {
            _contextListeners= LazyList.add(_contextListeners, listener);
        }
 
        super.addEventListener(listener);
    }

    /* ------------------------------------------------------------ */
    public synchronized void removeEventListener(EventListener listener)
    {
        _contextListeners= LazyList.remove(_contextListeners, listener);
        super.removeEventListener(listener);
    }

    /* ------------------------------------------------------------ */
    public String getDisplayName()
    {
        return getHttpContextName();
    }
    
    /* ------------------------------------------------------------ */
    public void setDisplayName(String name)
    {
        setHttpContextName(name);
    }

    /* ------------------------------------------------------------ */
    /** Set the defaults web.xml file.
     * The default web.xml is used to configure all webapplications
     * before the WEB-INF/web.xml file is applied.  By default the
     * org/mortbay/jetty/servlet/webdefault.xml resource from the
     * org.mortbay.jetty.jar is used.
     * @param defaults File, Resource, URL or null.
     */
    public void setDefaultsDescriptor(String defaults)
    {
        _defaultsDescriptor= defaults;
    }

    /* ------------------------------------------------------------ */
    public String getDefaultsDescriptor()
    {
        return _defaultsDescriptor;
    }

    /* ------------------------------------------------------------ */
    /** 
     * @param extract If true, a WAR is extracted to a temporary
     * directory before being deployed. 
     */
    public void setExtractWAR(boolean extract)
    {
        _extract= extract;
    }

    /* ------------------------------------------------------------ */
    public boolean getExtractWAR()
    {
        return _extract;
    }

    /* ------------------------------------------------------------ */
    /**
     * Initialize is called by the start method after the contexts classloader
     * has been initialied, but before the defaults descriptor has been applied.
     * The default implementation does nothing.
     *
     * @exception Exception if an error occurs
     */
    protected void initialize() throws Exception
    {
    }


    /* ------------------------------------------------------------ */
    protected UserRealm getUserRealm(String name)
    {
        return getHttpServer().getRealm(name);
    }

    /* ------------------------------------------------------------ */
    public String toString()
    {
        String name = getDisplayName();
        return "WebApplicationContext[" + getContextPath() + "," + (name == null ? _war : name) + "]";
    }

    /* ------------------------------------------------------------ */
    /** Set Resource Alias.
     * Resource aliases map resource uri's within a context.
     * They may optionally be used by a handler when looking for
     * a resource.  
     * @param alias 
     * @param uri 
     */
    public void setResourceAlias(String alias, String uri)
    {
        if (_resourceAliases == null)
            _resourceAliases= new HashMap(5);
        _resourceAliases.put(alias, uri);
    }

    /* ------------------------------------------------------------ */
    public Map getResourceAliases()
    {
        if (_resourceAliases == null)
            return null;
        return Collections.unmodifiableMap(_resourceAliases);
    }
    
    /* ------------------------------------------------------------ */
    public String getResourceAlias(String alias)
    {
        if (_resourceAliases == null)
            return null;
        return (String)_resourceAliases.get(alias);
    }

    /* ------------------------------------------------------------ */
    public String removeResourceAlias(String alias)
    {
        if (_resourceAliases == null)
            return null;
        return (String)_resourceAliases.remove(alias);
    }

    /* ------------------------------------------------------------ */
    public Resource getResource(String uriInContext) throws IOException
    {
        IOException ioe= null;
        Resource resource= null;
        try
        {
            resource= super.getResource(uriInContext);
            if (resource != null && resource.exists())
                return resource;
        }
        catch (IOException e)
        {
            ioe= e;
        }

        String aliasedUri= getResourceAlias(uriInContext);
        if (aliasedUri != null)
            return super.getResource(aliasedUri);

        if (ioe != null)
            throw ioe;

        return resource;
    }

    /* ------------------------------------------------------------ */
    /** set error page URI.
     * @param error A string representing an error code or a
     * exception classname
     * @param uriInContext
     */
    public void setErrorPage(String error, String uriInContext)
    {
        if (_errorPages == null)
            _errorPages= new HashMap();
        _errorPages.put(error, uriInContext);
    }

    /* ------------------------------------------------------------ */
    /** get error page URI.
     * @param error A string representing an error code or a
     * exception classname
     * @return URI within context
     */
    public String getErrorPage(String error)
    {
        if (_errorPages == null)
            return null;
        return (String)_errorPages.get(error);
    }

    /* ------------------------------------------------------------ */
    public String removeErrorPage(String error)
    {
        if (_errorPages == null)
            return null;
        return (String)_errorPages.remove(error);
    }
    
 
    
    /* ------------------------------------------------------------------------------- */
    /** Base Class for WebApplicationContext Configuration.
     * This class can be extended to customize or extend the configuration
     * of the WebApplicationContext.  If WebApplicationContext.setConfiguration is not
     * called, then an XMLConfiguration instance is created.
     * 
     * @version $Revision: 1.136 $
     * @author gregw
     */
    public static interface Configuration extends Serializable
    {
        /* ------------------------------------------------------------------------------- */
        /** Set up a context on which to perform the configuration.
         * @param context
         */
        public void setWebApplicationContext (WebApplicationContext context);

        /* ------------------------------------------------------------------------------- */
        /** Get the context on which the configuration is performed.
         * @return
         */
        public WebApplicationContext getWebApplicationContext ();
        
        /* ------------------------------------------------------------------------------- */
        /** Configure ClassPath.
         * This method is called before the context ClassLoader is created.  
         * Paths and libraries should be added to the context using the setClassPath,
         * addClassPath and addClassPaths methods.  The default implementation looks
         * for WEB-INF/classes, WEB-INF/lib/*.zip and WEB-INF/lib/*.jar
         * @throws Exception
         */
        public  void configureClassPath()
        throws Exception;

        /* ------------------------------------------------------------------------------- */
        /** Configure Defaults.
         * This method is called to intialize the context to the containers default configuration.
         * Typically this would mean application of the webdefault.xml file.  The default 
         * implementation does nothing.
         * @throws Exception
         */
        public  void configureDefaults()
        throws Exception;
        

        /* ------------------------------------------------------------------------------- */
        /** Configure WebApp.
         * This method is called to apply the standard and vendor deployment descriptors.
         * Typically this is web.xml and jetty-web.xml.  The default implementation does nothing.
         * @throws Exception
         */
        public  void configureWebApp()
        throws Exception;
        
    }

}
