/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2005 Bull S.A.
 * Contact: jonas-team@objectweb.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * Initial developer(s): Miroslav Halas based on Catalina service
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.objectweb.jonas.web.jetty50;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.Vector;

import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.naming.Context;
import javax.naming.NamingException;

import org.objectweb.util.monolog.api.BasicLevel;

import org.objectweb.jonas.jmx.JmxService;
import org.objectweb.jonas.jmx.JonasObjectName;
import org.objectweb.jonas.service.ServiceException;
import org.objectweb.jonas.service.ServiceManager;
import org.objectweb.jonas.web.AbsJWebContainerServiceImpl;
import org.objectweb.jonas.web.JWebContainerServiceException;

import org.mortbay.http.HttpContext;
import org.mortbay.http.HttpListener;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.servlet.WebApplicationContext;

/**
 * This class provides an implementation of the Jetty service (as web container
 * service).
 * @author Miroslav Halas (initial developer)
 * @author Florent Benoit
 */
public class JettyJWebContainerServiceImpl extends AbsJWebContainerServiceImpl {

    /**
     * Name of the default web.xml file
     */
    private static final String JETTY_DEFAULT_WEB_XML_FILE = AbsJWebContainerServiceImpl.JONAS_BASE + File.separator + "conf" + File.separator + "jetty5-webdefault.xml";

    /**
     * Configuration used to configure Jetty.
     */
    private static String config = null;

    /**
     * Name of the variable specifying configuration file for Jetty
     */
    private static final String JETTY_CONFIG = "config";

    /**
     * Our own instance of Jetty server.
     */
    private Server jettyServer = null;

    /**
     * Initialize the Jetty service.
     * @param ctx the configuration context of the service.
     * @throws ServiceException if the initialization failed.
     */
    protected void doInit(javax.naming.Context ctx) throws ServiceException {

        super.doInit(ctx);

        String strJettyHome = System.getProperty("jetty.home");
        if (strJettyHome != null) {
            if (getLogger().isLoggable(BasicLevel.DEBUG)) {
                getLogger().log(BasicLevel.DEBUG, "");
            }

            try {
                config = (String) ctx.lookup(JETTY_CONFIG);
            } catch (NamingException neExc) {
                // by default we take jetty5.xml under JONAS_BASE/conf
                String jonasbase = System.getProperties().getProperty("jonas.base");
                config = jonasbase + "/conf/jetty5.xml";
            }
            getLogger().log(BasicLevel.LEVEL_DEBUG, "using configuration file " + config);

            jettyServer = new Server();
        }

    }

    /**
     * Start the Jetty service in a new thread
     * @throws ServiceException if the startup failed.
     */
    public void doStart() throws ServiceException {
        if (getLogger().isLoggable(BasicLevel.DEBUG)) {
            getLogger().log(BasicLevel.DEBUG, "");
        }

        if (jettyServer != null) {
            JmxService srvcJMX = null;

            // Start Jetty directly and just pass it to JMX if available
            if (config != null) {
                try {
                    jettyServer.configure(config);
                    jettyServer.start();
                } catch (Exception eExc) {
                    getLogger().log(BasicLevel.LEVEL_ERROR,
                            "error has occured while starting Jetty server using configuration file " + config, eExc);
                }
            }

            ServiceManager sm = null;
            try {
                sm = ServiceManager.getInstance();
            } catch (Exception e) {
                String err = "Cannot get ServiceManager instance.";
                getLogger().log(BasicLevel.ERROR, err);
                throw new ServiceException(err, e);
            }

            try {
                srvcJMX = (JmxService) sm.getJmxService();
                if (srvcJMX != null) {
                    // Register JettyService MBean : JettyService
                    Object obj;
                    ObjectName objname;
                    // Once we register this bean, it will start Jetty using
                    // given configuration
                    // file. That's why we have to do it in run() instead in
                    // doStart()
                    obj = new JettyJonasServerMBean(jettyServer);
                    objname = JonasObjectName.wwwService();
                    srvcJMX.getJmxServer().registerMBean(obj, objname);
                }
            } catch (ServiceException seExc) {
                // Jmx Service not available, do nothing
                getLogger().log(
                        BasicLevel.LEVEL_DEBUG,
                        "cannot start Jetty server using configuration file " + config
                                + " using JMX. Will start without JMX.", seExc);
            } catch (Exception eExc) {
                getLogger().log(BasicLevel.LEVEL_ERROR, "cannot start Jetty server using configuration file " + config,
                        eExc);
                throw new ServiceException("Cannot start Jetty server using configuration file " + config, eExc);
            }

            // ... and deploy wars of the jonas.properties
            super.doStart();
        } else {
            throw new ServiceException("Cannot start Jetty server.");
        }
    }

    /**
     * Stop the Jetty service.
     * @throws ServiceException if the stop failed.
     */
    protected void doStop() throws ServiceException {
        // Undeploy the wars ...
        super.doStop();

        // ... and shut down embedded jetty
        if (getLogger().isLoggable(BasicLevel.DEBUG)) {
            getLogger().log(BasicLevel.DEBUG, "");
        }
        if (isStarted()) {
            JmxService srvcJMX = null;

            ServiceManager sm = null;
            try {
                sm = ServiceManager.getInstance();
            } catch (Exception e) {
                String err = "Cannot get ServiceManager instance.";
                getLogger().log(BasicLevel.ERROR, err);
                throw new ServiceException(err, e);
            }

            try {
                srvcJMX = (JmxService) sm.getJmxService();
                if (srvcJMX != null) {
                    ObjectName objname;

                    objname = new ObjectName("jonas:type=service,name=jetty");
                    srvcJMX.getJmxServer().unregisterMBean(objname);
                    jettyServer = null;
                    objname = new ObjectName("jonas:type=jetty,name=jettylog");
                    srvcJMX.getJmxServer().unregisterMBean(objname);
                    objname = new ObjectName("jonas:type=jetty,name=jettycode");
                    srvcJMX.getJmxServer().unregisterMBean(objname);
                }
            } catch (ServiceException seExc) {
                getLogger().log(BasicLevel.LEVEL_ERROR, "JMX Service not available", seExc);
            } catch (Exception eExc) {
                getLogger().log(BasicLevel.LEVEL_ERROR, "Cannot stop Jetty server", eExc);
                throw new ServiceException("Cannot stop Jetty server", eExc);
            }

            if (jettyServer != null) {
                try {
                    jettyServer.stop();
                    jettyServer.destroy();
                    jettyServer = null;
                } catch (Exception eExc) {
                    getLogger().log(BasicLevel.LEVEL_ERROR,
                            "error has occured while stopping Jetty server using configuration file " + config, eExc);
                }
            }
        }
    }

    /**
     * Create the environment and delegate the operation to the implementation
     * of the web container.
     * @param ctx the context which contains the configuration in order to
     *        deploy a WAR.
     * @throws JWebContainerServiceException if the registration of the WAR
     *         failed.
     */
    protected void doRegisterWar(Context ctx) throws JWebContainerServiceException {
        // Get the 5 parameters :
        //   - warURL is the URL of the war to register (required param).
        //   - contextRoot is the context root to which this application
        // should be installed (must be unique) (required param).
        //   - hostName is the name of the host on which deploy the war
        // (optional param taken into account only if no <context> element
        //  was declared in server.xml for this web application) .
        //   - java2DelegationModel the compliance to java2 delegation model
        //   - parentCL the war classloader of this war.
        //URL warURL = null;
        URL unpackedWarURL = null;
        String contextRoot = null;
        boolean java2DelegationModel = true;
        try {
            unpackedWarURL = (URL) ctx.lookup("unpackedWarURL");
            contextRoot = (String) ctx.lookup("contextRoot");
            Boolean bool = (Boolean) ctx.lookup("java2DelegationModel");
            java2DelegationModel = bool.booleanValue();
        } catch (NamingException e) {
            String err = "Error while getting parameter from context param ";
            getLogger().log(BasicLevel.ERROR, err + e.getMessage());
            throw new JWebContainerServiceException(err, e);
        }

        ClassLoader webClassLoader = null;
        try {
            webClassLoader = (ClassLoader) ctx.lookup("parentCL");
        } catch (NamingException e) {
            String err = "error while getting parameter from context param ";
            getLogger().log(BasicLevel.ERROR, err + e.getMessage());
            throw new JWebContainerServiceException(err, e);
        }

        String hostName = null;
        try {
            hostName = (String) ctx.lookup("hostName");
        } catch (NamingException e) {
            hostName = "";
        }

        String earAppName = null;
        try {
            earAppName = (String) ctx.lookup("earAppName");
        } catch (NamingException e) {
            // no ear case, so no ear application name
            earAppName = null;
        }

        // Install a new web application, whose web application archive is
        // at the specified URL, into this container with the specified
        // context root.
        // A context root of "" (the empty string) should be used for the root
        // application for this container. Otherwise, the context root must
        // start with a slash.

        if (contextRoot.equals("/")) {
            contextRoot = "";
        } else if (contextRoot.equalsIgnoreCase("ROOT")) {
            // Jetty uses ROOT.war and ROOT directory to as root context
            contextRoot = "";
        }

        // install the war.
        File fWar = new File(unpackedWarURL.getFile());
        String fileName = fWar.getAbsolutePath();

        if (jettyServer != null) {
            try {
                WebApplicationContext contextWebApp;

                if ((hostName == null) || (hostName.length() == 0)) {
                    // There is no host
                    if (getLogger().isLoggable(BasicLevel.DEBUG)) {
                        getLogger().log(BasicLevel.DEBUG,
                                "Jetty server installing web app " + fileName + " and context " + contextRoot);
                    }
                    contextWebApp = jettyServer.addWebApplication(contextRoot, fileName);
                } else {
                    // Host was specified
                    if (getLogger().isLoggable(BasicLevel.DEBUG)) {
                        getLogger().log(
                                BasicLevel.DEBUG,
                                "Jetty server installing web app " + fileName + " on host " + hostName + " and context "
                                + contextRoot);
                    }
                    contextWebApp = jettyServer.addWebApplication(hostName, contextRoot, fileName);

                }

                contextWebApp.setAttribute("J2EEDomainName", getDomainName());
                contextWebApp.setAttribute("J2EEServerName", getJonasServerName());
                contextWebApp.setAttribute("J2EEApplicationName", earAppName);

                // Add default xml descriptor
                File webDefaults = new File(JETTY_DEFAULT_WEB_XML_FILE);
                if (webDefaults.exists()) {
                    contextWebApp.setDefaultsDescriptor(webDefaults.toURL().toExternalForm());
                } else {
                    getLogger().log(BasicLevel.WARN, "The file '" + JETTY_DEFAULT_WEB_XML_FILE + "' is not present. Check that your JONAS_BASE is up-to-date.");
                }

                // Specifying the jsp class path used by jasper
                contextWebApp.setAttribute("org.apache.catalina.jsp_classpath", getJOnASClassPath(webClassLoader));

                // Set the parent class loader
                contextWebApp.setParentClassLoader(webClassLoader);

                // Set this classloader to the Java2 compliant mode ?
                contextWebApp.setClassLoaderJava2Compliant(java2DelegationModel);

                if (getLogger().isLoggable(BasicLevel.DEBUG)) {
                    getLogger().log(BasicLevel.DEBUG,
                        "Webapp class loader java 2 delegation model set to " + java2DelegationModel);

                    getLogger().log(BasicLevel.DEBUG, "Jetty server starting web app " + fileName);
                }
                contextWebApp.start();
                if (getLogger().isLoggable(BasicLevel.DEBUG)) {
                    getLogger().log(BasicLevel.DEBUG, "Jetty server is running web app " + fileName);
                }

            } catch (IOException ioeExc) {
                String err = "Cannot install this web application " + ioeExc;
                getLogger().log(BasicLevel.ERROR, err);
                throw new JWebContainerServiceException(err, ioeExc);
            } catch (Exception eExc) {
                String err = "Cannot start this web application " + eExc;
                getLogger().log(BasicLevel.ERROR, err);
                throw new JWebContainerServiceException(err, eExc);
            }
        } else {
            if (getLogger().isLoggable(BasicLevel.DEBUG)) {
                getLogger().log(BasicLevel.DEBUG, "No Jetty server to install web app " + fileName);
            }
        }

        // TODO We need to have the J2EE WebModule MBean here, should add it to the Context
        // Store WebModule ObjectName in Context
        try {
            ctx.rebind("WebModule", getDummyJSR77ObjectName(hostName, contextRoot, earAppName));
        } catch (Exception e) {
            // NamingException or Mbean related Exception
            // TODO i18n
            String err = "Cannot rebind WebModule ObjectName in Context";
            getLogger().log(BasicLevel.ERROR, err, e);
            throw new JWebContainerServiceException(err, e);
        }

    }

    /**
     * Create a fake, JSR77 MBean ObjectName
     * @param hostName host name
     * @param contextRoot context root name
     * @param earAppName application name
     * @return a fake JSR77 WebModule ObjectName
     * @throws MalformedObjectNameException if ObjectName incorrect
     */
    private ObjectName getDummyJSR77ObjectName(String hostName, String contextRoot, String earAppName) throws MalformedObjectNameException {
        // jonas:j2eeType=WebModule,name=//localhost/,J2EEApplication=none,J2EEServer=jonas
        return ObjectName.getInstance(getDomainName() + ":j2eeType=WebModule,name=" + "/" + contextRoot + ",J2EEApplication=" + earAppName + ",J2EEServer=" + getJonasServerName());
    }

    /**
     * Return the classpath which can be used for jsp compiling by Jasper. This
     * classpath is extracted from the web classloader.
     * @param webClassLoader the ClassLoader used for extract URLs.
     * @return the jonas classpath which is useful for JSP compiling.
     */
    public String getJOnASClassPath(ClassLoader webClassLoader) {

        StringBuffer classpath = new StringBuffer();
        int n = 0;
        while (webClassLoader != null) {
            if (!(webClassLoader instanceof URLClassLoader)) {
                break;
            }
            URL[] repositories = ((URLClassLoader) webClassLoader).getURLs();
            for (int i = 0; i < repositories.length; i++) {
                String repository = repositories[i].toString();
                if (repository.startsWith("file://")) {
                    repository = repository.substring("file://".length());
                } else if (repository.startsWith("file:")) {
                    repository = repository.substring("file:".length());
                } else {
                    continue;
                }
                if (repository == null) {
                    continue;
                }
                if (n > 0) {
                    classpath.append(File.pathSeparator);
                }
                classpath.append(repository);
                n++;
            }
            webClassLoader = webClassLoader.getParent();
        }

        return classpath.toString();
    }

    /**
     * Delegate the unregistration to the implementation of the web container.
     * @param ctx the context which contains the configuration in order to
     *        undeploy a WAR.
     * @throws JWebContainerServiceException if the unregistration failed.
     */
    protected void doUnRegisterWar(Context ctx) throws JWebContainerServiceException {
        // Get the 2 parameters :
        //   - contextRoot is the context root to be removed (required param).
        //   - hostName is the name of the host to remove the war (optional).
        String contextRoot = null;
        try {
            contextRoot = (String) ctx.lookup("contextRoot");
        } catch (NamingException e) {
            String err = "Error while getting parameter from context param ";
            getLogger().log(BasicLevel.ERROR, err + e.getMessage());
            throw new JWebContainerServiceException(err, e);
        }

        String hostName = null;
        try {
            hostName = (String) ctx.lookup("hostName");
        } catch (NamingException e) {
            hostName = "";
        }
        // A context root of "" (the empty string) should be used for the root
        // application for this container. Otherwise, the context root must
        // start with a slash.

        if (contextRoot.equals("/")) {
            contextRoot = "";
        } else if (contextRoot.equalsIgnoreCase("ROOT")) {
            // Jetty uses ROOT.war and ROOT directory to as root context
            contextRoot = "";
        }

        if (jettyServer != null) {
            HttpContext contextWebApp;

            if ((hostName == null) || (hostName.length() == 0)) {
                // There is no host
                if (getLogger().isLoggable(BasicLevel.DEBUG)) {
                    getLogger().log(BasicLevel.DEBUG, "Jetty server looking upweb app " + " from context " + contextRoot);
                }
                contextWebApp = jettyServer.getContext(contextRoot);
            } else {
                // Host was specified
                if (getLogger().isLoggable(BasicLevel.DEBUG)) {
                getLogger().log(BasicLevel.DEBUG,
                        "Jetty server looking up web app " + " on host " + hostName + " and context " + contextRoot);
                }
                contextWebApp = jettyServer.getContext(hostName, contextRoot);
            }

            if (contextWebApp != null) {
                if (getLogger().isLoggable(BasicLevel.DEBUG)) {
                    getLogger().log(BasicLevel.DEBUG,
                            "Jetty server found and is stopping web app at context " + contextRoot);
                }
                // Stop it gracefully
                try {
                    contextWebApp.stop(true);
                } catch (InterruptedException ieExc) {
                    getLogger().log(BasicLevel.LEVEL_DEBUG,
                            "Jetty server encoutered exception while stopping web application ", ieExc);
                }
                if (getLogger().isLoggable(BasicLevel.DEBUG)) {
                    getLogger().log(BasicLevel.DEBUG,
                            "Jetty server stopped and is removing web app at context " + contextRoot);
                }

                jettyServer.removeContext(contextWebApp);
                if (getLogger().isLoggable(BasicLevel.DEBUG)) {
                    getLogger().log(BasicLevel.DEBUG,
                            "Jetty server removed and is destroying web app at context " + contextRoot);
                }
                contextWebApp.destroy();

                if (getLogger().isLoggable(BasicLevel.DEBUG)) {
                    getLogger().log(BasicLevel.DEBUG, "Jetty server unloaded web app at context " + contextRoot);
                }

            } else {
                if (getLogger().isLoggable(BasicLevel.DEBUG)) {
                    getLogger().log(BasicLevel.DEBUG, "Jetty server didn't find web app at context " + contextRoot);
                }
            }

        } else {
            if (getLogger().isLoggable(BasicLevel.DEBUG)) {
                getLogger().log(BasicLevel.DEBUG, "No Jetty server to install web app at context " + contextRoot);
            }
        }
    }

    /**
     * Update info of the serverName and serverVersion
     */
    protected void updateServerInfos() {
        String infos = org.mortbay.http.Version.getImplVersion();

        StringTokenizer st = new StringTokenizer(infos, "/");
        if (st.countTokens() != 2) {
            setServerName(infos);
            setServerVersion("");
        } else {
            setServerName(st.nextToken());
            setServerVersion(st.nextToken());
        }
    }

    /**
     * Return the Default host name of the web container.
     * @return the Default host name of the web container.
     * @throws JWebContainerServiceException when default host cannot be
     *         resolved (multiple services).
     */
    public String getDefaultHost() throws JWebContainerServiceException {
        Map hosts = jettyServer.getHostMap();
        // If we have more than 1 host, we cannot determine default host!
        if (hosts.isEmpty()) {
            String err = "Cannot determine default host : Jetty server has no host!";
            throw new JWebContainerServiceException(err);
        }

        String vHost = (String) hosts.keySet().iterator().next();
        if (vHost == null) {
            vHost =  "localhost";
        }

        if (hosts.size() > 1) {
            if (getLogger().isLoggable(BasicLevel.WARN)) {
                getLogger().log(BasicLevel.WARN, "More than 1 host found, using the first one : " + vHost);
            }
        }
        return vHost;
    }

    /**
     * Return the Default HTTP port number of the web container (can be null if
     * multiple HTTP connector has been set).
     * @return the Default HTTP port number of the web container.
     * @throws JWebContainerServiceException when default HTTP port cannot be
     *         resolved (multiple occurences).
     */
    public String getDefaultHttpPort() throws JWebContainerServiceException {
        return String.valueOf(getFirstListenerFromScheme("http").getPort());
    }

    /**
     * Return the Default HTTPS port number of the web container (can be null if
     * multiple HTTPS connector has been set).
     * @return the Default HTTPS port number of the web container.
     * @throws JWebContainerServiceException when default HTTPS port cannot be
     *         resolved (multiple occurences).
     */
    public String getDefaultHttpsPort() throws JWebContainerServiceException {
        return String.valueOf(getFirstListenerFromScheme("https").getPort());
    }

    /**
     * @param myScheme matching URL scheme (http, https, ...)
     * @return Returns the first HttpListener found
     */
    private HttpListener getFirstListenerFromScheme(String myScheme) {
        HttpListener[] listeners = jettyServer.getListeners();
        Vector http = new Vector();
        for (int i = 0; i < listeners.length; i++) {
            String scheme = listeners[i].getDefaultScheme();
            if (scheme.equalsIgnoreCase(myScheme)) {
                http.add(listeners[i]);
            }
        }
        if (http.isEmpty()) {
            String err = "Cannot determine default '" + myScheme + "' port :" + " Jetty server has 0 '" + myScheme + "' Listener";
            throw new JWebContainerServiceException(err);
        }

        HttpListener hl = (HttpListener) http.get(0);
        // Check if there are more than one HTTP connectors specified, if so, warn the administrator.
        if (http.size() > 1) {
            if (getLogger().isLoggable(BasicLevel.WARN)) {
                getLogger().log(BasicLevel.WARN, "Found multiple Listener for scheme '" + myScheme + "'"
                        + ", using first by default! (port:" + hl.getPort() + ")");
            }
        }

        return hl;
    }

}

