// ========================================================================
// $Id: Server.java,v 1.40 2005/10/21 13:52:11 gregwilkins Exp $
// Copyright 2002-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;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

import org.apache.commons.logging.Log;
import org.mortbay.log.LogFactory;
import org.mortbay.http.HttpContext;
import org.mortbay.http.HttpServer;
import org.mortbay.jetty.servlet.ServletHttpContext;
import org.mortbay.jetty.servlet.WebApplicationContext;
import org.mortbay.util.LogSupport;
import org.mortbay.util.Resource;
import org.mortbay.xml.XmlConfiguration;


/* ------------------------------------------------------------ */
/** The Jetty HttpServer.
 *
 * This specialization of org.mortbay.http.HttpServer adds knowledge
 * about servlets and their specialized contexts.   It also included
 * support for initialization from xml configuration files
 * that follow the XmlConfiguration dtd.
 *
 * HttpContexts created by Server are of the type
 * org.mortbay.jetty.servlet.ServletHttpContext unless otherwise
 * specified.
 *
 * This class also provides a main() method which starts a server for
 * each config file passed on the command line.  If the system
 * property JETTY_NO_SHUTDOWN_HOOK is not set to true, then a shutdown
 * hook is thread is registered to stop these servers.   
 *
 * @see org.mortbay.xml.XmlConfiguration
 * @see org.mortbay.jetty.servlet.ServletHttpContext
 * @version $Revision: 1.40 $
 * @author Greg Wilkins (gregw)
 */
public class Server extends HttpServer 
{
    static Log log = LogFactory.getLog(Server.class);
    private String[] _webAppConfigurationClassNames = 
        new String[]{"org.mortbay.jetty.servlet.XMLConfiguration", "org.mortbay.jetty.servlet.JettyWebConfiguration"};
    private String _configuration;
    private String _rootWebApp;
    private static ShutdownHookThread hookThread = new ShutdownHookThread();
    
    /* ------------------------------------------------------------ */
    /** Constructor. 
     */
    public Server()
    {
    }
    
    /* ------------------------------------------------------------ */
    /** Constructor. 
     * @param configuration The filename or URL of the XML
     * configuration file.
     */
    public Server(String configuration)
        throws IOException
    {
        this(Resource.newResource(configuration).getURL());
    }
    
    /* ------------------------------------------------------------ */
    /** Constructor. 
     * @param configuration The filename or URL of the XML
     * configuration file.
     */
    public Server(Resource configuration)
        throws IOException
    {
        this(configuration.getURL());
    }
    
    /* ------------------------------------------------------------ */
    /** Constructor. 
     * @param configuration The filename or URL of the XML
     * configuration file.
     */
    public Server(URL configuration)
        throws IOException
    {
        _configuration=configuration.toString();
        Server.hookThread.add(this);
        try
        {
            XmlConfiguration config=new XmlConfiguration(configuration);
            config.configure(this);
        }
        catch(IOException e)
        {
            throw e;
        }
        catch(InvocationTargetException e)
        {
            log.warn(LogSupport.EXCEPTION,e.getTargetException());
            throw new IOException("Jetty configuration problem: "+e.getTargetException());
        }
        catch(Exception e)
        {
            log.warn(LogSupport.EXCEPTION,e);
            throw new IOException("Jetty configuration problem: "+e);
        }
    }

    /* ------------------------------------------------------------ */
    public boolean getStopAtShutdown()
    {
        return hookThread.contains(this);
    }
    
    /* ------------------------------------------------------------ */
    public void setStopAtShutdown(boolean stop)
    {
        if (stop)
            hookThread.add(this);
        else
            hookThread.remove(this);
    }

    /* ------------------------------------------------------------ */
    /** Get the root webapp name.
     * @return The name of the root webapp (eg. "root" for root.war). 
     */
    public String getRootWebApp()
    {
        return _rootWebApp;
    }
    
    /* ------------------------------------------------------------ */
    /** Set the root webapp name.
     * @param rootWebApp The name of the root webapp (eg. "root" for root.war). 
     */
    public void setRootWebApp(String rootWebApp)
    {
        _rootWebApp = rootWebApp;
    }
    
    /* ------------------------------------------------------------ */
    /**  Configure the server from an XML file.
     * @param configuration The filename or URL of the XML
     * configuration file.
     */
    public void configure(String configuration)
        throws IOException
    {

        URL url=Resource.newResource(configuration).getURL();
        if (_configuration!=null && _configuration.equals(url.toString()))
            return;
        if (_configuration!=null)
            throw new IllegalStateException("Already configured with "+_configuration);
        try
        {
            XmlConfiguration config=new XmlConfiguration(url);
            _configuration=url.toString();
            config.configure(this);
        }
        catch(IOException e)
        {
            throw e;
        }
        catch(Exception e)
        {
            log.warn(LogSupport.EXCEPTION,e);
            throw new IOException("Jetty configuration problem: "+e);
        }
    }
    
    /* ------------------------------------------------------------ */
    public String getConfiguration()
    {
        return _configuration;
    }
    
    /* ------------------------------------------------------------ */
    /** Create a new ServletHttpContext.
     * Ths method is called by HttpServer to creat new contexts.  Thus
     * calls to addContext or getContext that result in a new Context
     * being created will return an
     * org.mortbay.jetty.servlet.ServletHttpContext instance.
     * @return ServletHttpContext
     */
    protected HttpContext newHttpContext()
    {
        return new ServletHttpContext();
    }
    
    /* ------------------------------------------------------------ */
    /** Create a new WebApplicationContext.
     * Ths method is called by Server to creat new contexts for web 
     * applications.  Thus calls to addWebApplication that result in 
     * a new Context being created will return an correct class instance.
     * Derived class can override this method to create instance of its
     * own class derived from WebApplicationContext in case it needs more
     * functionality.
     * @param webApp The Web application directory or WAR file.
     * @return WebApplicationContext
     */
    protected WebApplicationContext newWebApplicationContext(
       String webApp
    )
    {
        return new WebApplicationContext(webApp);
    }

    /* ------------------------------------------------------------ */
    /** Add Web Application.
     * @param contextPathSpec The context path spec. Which must be of
     * the form / or /path/*
     * @param webApp The Web application directory or WAR file.
     * @return The WebApplicationContext
     * @exception IOException 
     */
    public WebApplicationContext addWebApplication(String contextPathSpec,
                                                   String webApp)
        throws IOException
    {
        return addWebApplication(null,contextPathSpec,webApp);
    }
    
    /* ------------------------------------------------------------ */
    /** Add Web Application.
     * @param virtualHost Virtual host name or null
     * @param contextPathSpec The context path spec. Which must be of
     * the form / or /path/*
     * @param webApp The Web application directory or WAR file.
     * @return The WebApplicationContext
     * @exception IOException 
     */
    public WebApplicationContext addWebApplication(String virtualHost,
                                                   String contextPathSpec,
                                                   String webApp)
        throws IOException
    {
        WebApplicationContext appContext =
            newWebApplicationContext(webApp);
        appContext.setContextPath(contextPathSpec);
        addContext(virtualHost,appContext);
        if(log.isDebugEnabled())log.debug("Web Application "+appContext+" added");
        return appContext;
    }

    
    /* ------------------------------------------------------------ */
    /**  Add Web Applications.
     * Add auto webapplications to the server.  The name of the
     * webapp directory or war is used as the context name. If a
     * webapp is called "root" it is added at "/".
     * @param webapps Directory file name or URL to look for auto webapplication.
     * @exception IOException 
     */
    public WebApplicationContext[] addWebApplications(String webapps)
        throws IOException
    {
        return addWebApplications(null,webapps,null,false);
    }
    
    /* ------------------------------------------------------------ */
    /**  Add Web Applications.
     * Add auto webapplications to the server.  The name of the
     * webapp directory or war is used as the context name. If the
     * webapp matches the rootWebApp it is added as the "/" context.
     * @param host Virtual host name or null
     * @param webapps Directory file name or URL to look for auto webapplication.
     * @exception IOException 
     */
    public WebApplicationContext[] addWebApplications(String host,
                                                      String webapps)
        throws IOException
    {
        return addWebApplications(host,webapps,null,false);
    }
        
    /* ------------------------------------------------------------ */
    /**  Add Web Applications.
     * Add auto webapplications to the server.  The name of the
     * webapp directory or war is used as the context name. If the
     * webapp matches the rootWebApp it is added as the "/" context.
     * @param host Virtual host name or null
     * @param webapps Directory file name or URL to look for auto
     * webapplication.
     * @param extract If true, extract war files
     * @exception IOException 
     */
    public WebApplicationContext[] addWebApplications(String host,
                                                      String webapps,
                                                      boolean extract)
        throws IOException
    {
        return addWebApplications(host,webapps,null,extract);
    }
    
    /* ------------------------------------------------------------ */
    /**  Add Web Applications.
     * Add auto webapplications to the server.  The name of the
     * webapp directory or war is used as the context name. If the
     * webapp matches the rootWebApp it is added as the "/" context.
     * @param host Virtual host name or null
     * @param webapps Directory file name or URL to look for auto
     * webapplication.
     * @param defaults The defaults xml filename or URL which is
     * loaded before any in the web app. Must respect the web.dtd.
     * If null the default defaults file is used. If the empty string, then
     * no defaults file is used.
     * @param extract If true, extract war files
     * @exception IOException 
     */
    public WebApplicationContext[] addWebApplications(String host,
                                                      String webapps,
                                                      String defaults,
                                                      boolean extract)
        throws IOException
    {
        return addWebApplications(host,webapps,defaults,extract,true,null);
    }

    /* ------------------------------------------------------------ */
    /**  Add Web Applications.
     * Add auto webapplications to the server.  The name of the
     * webapp directory or war is used as the context name. If the
     * webapp matches the rootWebApp it is added as the "/" context.
     * @param host Virtual host name or null
     * @param webapps Directory file name or URL to look for auto
     * webapplication.
     * @param defaults The defaults xml filename or URL which is
     * loaded before any in the web app. Must respect the web.dtd.
     * If null the default defaults file is used. If the empty string, then
     * no defaults file is used.
     * @param extract If true, extract war files
     * @param java2CompliantClassLoader True if java2 compliance is applied to all webapplications
     * @exception IOException
     */
    public WebApplicationContext[] addWebApplications(String host,
                                                      String webapps,
                                                      String defaults,
                                                      boolean extract,
						      boolean java2CompliantClassLoader)
                throws IOException
    {
        return addWebApplications(host,webapps,defaults,extract,java2CompliantClassLoader,null);

    }
    /* ------------------------------------------------------------ */
    /**  Add Web Applications.
     * Add auto webapplications to the server.  The name of the
     * webapp directory or war is used as the context name. If the
     * webapp matches the rootWebApp it is added as the "/" context.
     * @param host Virtual host name or null
     * @param webapps Directory file name or URL to look for auto
     * webapplication.
     * @param defaults The defaults xml filename or URL which is
     * loaded before any in the web app. Must respect the web.dtd.
     * If null the default defaults file is used. If the empty string, then
     * no defaults file is used.
     * @param extract If true, extract war files
     * @param java2CompliantClassLoader True if java2 compliance is applied to all webapplications
     * @param Attributes[] A set of attributes to pass to setAttribute, format is first item is the key, second item is the value.
     * @exception IOException 
     */
    public WebApplicationContext[] addWebApplications(String host,
                                                      String webapps,
                                                      String defaults,
                                                      boolean extract,
						      boolean java2CompliantClassLoader,
						      String Attributes[])
        throws IOException
    {
        ArrayList wacs = new ArrayList();
        Resource r=Resource.newResource(webapps);
        if (!r.exists())
            throw new IllegalArgumentException("No such webapps resource "+r);
        
        if (!r.isDirectory())
            throw new IllegalArgumentException("Not directory webapps resource "+r);
        if(Attributes != null) {
            if(((Attributes.length / 2) * 2) != Attributes.length) {
                throw new IllegalArgumentException("Attributes must be in pairs of key,value.");
            }
        }
        String[] files=r.list();
        
        for (int f=0;files!=null && f<files.length;f++)
        {
            String context=files[f];
            
            if (context.equalsIgnoreCase("CVS/") ||
                context.equalsIgnoreCase("CVS") ||
                context.startsWith("."))
                continue;

            
            String app = r.addPath(r.encode(files[f])).toString();
            if (context.toLowerCase().endsWith(".war") ||
                context.toLowerCase().endsWith(".jar"))
            {
                context=context.substring(0,context.length()-4);
                Resource unpacked=r.addPath(context);
                if (unpacked!=null && unpacked.exists() && unpacked.isDirectory())
                    continue;
            }
            
            if (_rootWebApp!=null && (context.equals(_rootWebApp)||context.equals(_rootWebApp+"/")))
                context="/";
            else
                context="/"+context;

            WebApplicationContext wac= addWebApplication(host,
                                                         context,
                                                         app);
            wac.setExtractWAR(extract);
	    wac.setClassLoaderJava2Compliant(java2CompliantClassLoader);
            if (defaults!=null)
            {
                if (defaults.length()==0)
                    wac.setDefaultsDescriptor(null);
                else
                    wac.setDefaultsDescriptor(defaults);
            }
            if(Attributes != null) {
                for(int i = 0; i < Attributes.length; i++, i++) {
                    wac.setAttribute(Attributes[i],Attributes[i + 1]);
                }
            }
            wacs.add(wac);
        }

        return (WebApplicationContext[])wacs.toArray(new WebApplicationContext[wacs.size()]);
    }

    
    /* ------------------------------------------------------------ */
    /** setWebApplicationConfigurationClasses
     * Set up the list of classnames of WebApplicationContext.Configuration
     * implementations that will be applied to configure every webapp.
     * The list can be overridden by individual WebApplicationContexts.
     * @param configurationClasses
     */
    public void setWebApplicationConfigurationClassNames (String[] configurationClassNames)
    {
        if (configurationClassNames != null)
        {
            _webAppConfigurationClassNames = new String[configurationClassNames.length];
            System.arraycopy(configurationClassNames, 0, _webAppConfigurationClassNames, 0, configurationClassNames.length);
        }
    }
    
    public String[] getWebApplicationConfigurationClassNames ()
    {
        return _webAppConfigurationClassNames;
    }
    
    /* ------------------------------------------------------------ */
    /* ------------------------------------------------------------ */
    /* ------------------------------------------------------------ */
    public static void main(String[] arg)
    {
        String[] dftConfig={"etc/jetty.xml"};
        
        if (arg.length==0)
        {
            log.info("Using default configuration: etc/jetty.xml");
            arg=dftConfig;
        }

        final Server[] servers=new Server[arg.length];

        // create and start the servers.
        for (int i=0;i<arg.length;i++)
        {
            try
            {
                servers[i] = new Server(arg[i]);
                servers[i].setStopAtShutdown(true);
                servers[i].start();

            }
            catch(Exception e)
            {
                log.warn(LogSupport.EXCEPTION,e);
            }
        }
        
        // create and start the servers.
        for (int i=0;i<arg.length;i++)
        {
            try{servers[i].join();}
            catch (Exception e){LogSupport.ignore(log,e);}
        }
    }
    
   /**
    * ShutdownHook thread for stopping all servers.
    * 
    * Thread is hooked first time list of servers is changed.
    */
  private static class ShutdownHookThread extends Thread {
    private boolean hooked = false;
    private ArrayList servers = new ArrayList();

    /**
     * Hooks this thread for shutdown.
     * @see java.lang.Runtime#addShutdownHook(java.lang.Thread)
     */
    private void createShutdownHook() {
      if (!Boolean.getBoolean("JETTY_NO_SHUTDOWN_HOOK") && !hooked) {
        try {
          Method shutdownHook = java.lang.Runtime.class.getMethod("addShutdownHook",
              new Class[] { java.lang.Thread.class });
          shutdownHook.invoke(Runtime.getRuntime(), new Object[] { this });
          this.hooked = true;
        } catch (Exception e) {
          if (log.isDebugEnabled()) log.debug("No shutdown hook in JVM ", e);
        }
      }
    }

    /**
     * Add Server to servers list.
     */
    public boolean add(Server server) {
      createShutdownHook();
      return this.servers.add(server);
    }
    
    /**
     * Contains Server in servers list?
     */
    public boolean contains(Server server) {
      return this.servers.contains(server);
    }

    /**
     * Append all Servers from Collection
     */
    public boolean addAll(Collection c) {
      createShutdownHook();
      return this.servers.addAll(c);
    }

    /**
     * Clear list of Servers.
     */
    public void clear() {
      createShutdownHook();
      this.servers.clear();
    }

    /**
     * Remove Server from list.
     */
    public boolean remove(Server server) {
      createShutdownHook();
      return this.servers.remove(server);
    }

    /**
     * Remove all Servers in Collection from list.
     */
    public boolean removeAll(Collection c) {
      createShutdownHook();
      return this.servers.removeAll(c);
    }

    /**
     * Stop all Servers in list.
     */
    public void run() {
      setName("Shutdown");
      log.info("Shutdown hook executing");
      Iterator it = servers.iterator();
      while (it.hasNext()) {
        Server svr = (Server) it.next();
        if (svr == null) continue;
        try {
          svr.stop();
        } catch (Exception e) {
          log.warn(LogSupport.EXCEPTION, e);
        }
        log.info("Shutdown hook complete");

        // Try to avoid JVM crash
        try {
          Thread.sleep(1000);
        } catch (Exception e) {
          log.warn(LogSupport.EXCEPTION, e);
        }
      }
    }
  }
}




