/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2005-2006 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
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.objectweb.jonas.web.catalina55;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.net.URL;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Vector;

import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
import javax.naming.LinkRef;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.StringRefAddr;

import org.xml.sax.InputSource;

import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.Engine;
import org.apache.catalina.Host;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.Realm;
import org.apache.catalina.Server;
import org.apache.catalina.Service;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardEngine;
import org.apache.catalina.core.StandardServer;
import org.apache.catalina.deploy.ContextResource;
import org.apache.catalina.deploy.NamingResources;
import org.apache.catalina.startup.ContextConfig;
import org.apache.commons.modeler.Registry;
import org.apache.tomcat.util.digester.Digester;

import org.objectweb.jonas_lib.I18n;

import org.objectweb.jonas.jmx.J2eeObjectName;
import org.objectweb.jonas.jmx.JonasObjectName;
import org.objectweb.jonas.jndi.checker.IResourceCheckerManager;
import org.objectweb.jonas.jndi.checker.impl.ResourceCheckerManager;
import org.objectweb.jonas.jndi.interceptors.impl.JNDIInterceptorsComponent;
import org.objectweb.jonas.security.realm.web.catalina55.JACC;
import org.objectweb.jonas.server.LoaderManager;
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.objectweb.jonas.web.lib.PermissionManager;
import org.objectweb.jonas.web.wrapper.CatalinaJWebContainerService;

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

/**
 * This class provides an implementation of the Catalina service
 * (as web container service).
 * @author Florent Benoit
 * @author Ludovic Bert (Tomcat 4.0)
 */
public class CatalinaJWebContainerServiceImpl
    extends AbsJWebContainerServiceImpl
    implements CatalinaJWebContainerService {

    /**
     * Relative path of the configuration file
     */
    protected static final String CONFIG_FILE = "conf/server.xml";

    /**
     * Internationalization
     */
    private static I18n i18n = null;

    /**
     * The reference on the catalina server.
     */
    private Server server = null;

    /**
     * The shared extensions class loader for this server.
     */
    private ClassLoader parentClassLoader = null;

    /**
     * Catalina loader
     */
    private ClassLoader catalinaLoader = null;

    /**
     * Web container started ?
     */
    private boolean tomcatStarted = false;

    /**
     * Resource checker.
     */
    private IResourceCheckerManager resourceCheckerManager;


    /**
     * Initialize the Catalina 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);

        // Set the environment
        initCatalinaEnvironment();

        // parent ClassLoader
        LoaderManager lm = LoaderManager.getInstance();
        try {
            parentClassLoader = lm.getAppsLoader();
            catalinaLoader = lm.getCatalinaLoader();
        } catch (Exception e1) {
            throw new ServiceException("Cannot get Application/Catalina ClassLoader", e1);
        }

        // Internationalization
        i18n = I18n.getInstance(CatalinaJWebContainerServiceImpl.class, catalinaLoader);

        // Add JOnAS-Tomcat mbean
        if (getLogger().isLoggable(BasicLevel.DEBUG)) {
            getLogger().log(BasicLevel.DEBUG, "Add mbeans descriptions to the JMX Registry");
        }
        InputStream stream = this.getClass().getResourceAsStream("/org/objectweb/jonas/web/catalina55/mbeans-descriptors.xml");
        try {
            Registry.loadRegistry(stream);
            stream.close();
        } catch (Exception e) {
            getLogger().log(BasicLevel.ERROR, "Can't add the mbeans description into Tomcat" + e.getMessage());
        }

        // Add MBean for Catalina Connector creation
        CatalinaConnectorFactory cf = new CatalinaConnectorFactory();
        MBeanServer mbeanServer = getMbeanServer();
        cf.setMyServer(mbeanServer);
        if (mbeanServer != null) {
            ObjectName on = null;
            try {
                on = JonasObjectName.catalinaConnectorFactory(getDomainName());
                mbeanServer.registerMBean(cf, on);
            } catch (MalformedObjectNameException e1) {
                getLogger().log(BasicLevel.WARN, "Can't create Objectname for CatalinaConnectorFactory MBean");
            } catch (InstanceAlreadyExistsException e2) {
                getLogger().log(BasicLevel.WARN, "MBean named " + on.toString() + "already registered in the MBean server");
            } catch (MBeanRegistrationException e2) {
                getLogger().log(BasicLevel.WARN, "Can't register MBean " + on.toString());
                e2.printStackTrace();
            } catch (NotCompliantMBeanException e2) {
                getLogger().log(BasicLevel.WARN, "Can't register MBean " + on.toString());
                e2.printStackTrace();
            }
        }

        // Add MBean for WebModule Proxy creation
        WebModuleProxy px = new WebModuleProxy();
        px.setMyServer(mbeanServer);
        if (mbeanServer != null) {
            ObjectName on = null;
            try {
                on = JonasObjectName.webModuleProxy(getDomainName());
                mbeanServer.registerMBean(px, on);
            } catch (MalformedObjectNameException e1) {
                getLogger().log(BasicLevel.WARN, "Can't create Objectname for WebModuleProxy MBean");
            } catch (InstanceAlreadyExistsException e2) {
                getLogger().log(BasicLevel.WARN, "MBean named " + on.toString() + "already registered in the MBean server");
            } catch (MBeanRegistrationException e2) {
                getLogger().log(BasicLevel.WARN, "Can't register MBean " + on.toString());
                e2.printStackTrace();
            } catch (NotCompliantMBeanException e2) {
                getLogger().log(BasicLevel.WARN, "Can't register MBean " + on.toString());
                e2.printStackTrace();
            }
        }

        // Get the resource checker manager (if any)
        try {
            ServiceManager manager = ServiceManager.getInstance();
            manager.getService("jndi-intercept");

            // At this point, if the jndi-intercept service is here, we could use the resource manager "singleton"
            resourceCheckerManager = new ResourceCheckerManager();
        } catch (Exception e) {
            // Ignored, if we pass here, this is because there is no jndi-intercept service available
        }

    }


    /**
     * Init the environment of catalina
     * set catalina.home, catalina.base and unset the tomcat naming
     * @throws ServiceException if catalina home is not set
     */
    protected void initCatalinaEnvironment() throws ServiceException {

        // if the user do not specify catalina.base,
        // set catalina.base to catalina.home.
        String catalinaHome = System.getProperty("catalina.home");
        if (catalinaHome != null) {
            if (System.getProperty("catalina.base") == null) {
                System.setProperty("catalina.base", catalinaHome);
            }
        } else {
            throw new ServiceException("catalina.home property is not set");
        }

        // use the JOnAS naming instead of Tomcat naming
        System.setProperty("catalina.useNaming", "false");
    }


    /**
     * Start the Catalina service in a new thread
     * @throws ServiceException if the startup failed.
     */
    public void doStart() throws ServiceException {

        // Create the digester for the parsing of the server.xml.
        Digester digester = createServerDigester();

        // Execute the digester for the parsing of the server.xml.
        // And configure the catalina server.
        File configFile = null;

        try {
            configFile = getConfigFile();
        } catch (FileNotFoundException e) {
            String err = i18n.getMessage("CatJWebServ.doStart.fileNotFound", configFile, e.getMessage());
            getLogger().log(BasicLevel.ERROR, err);
            throw new ServiceException(err, e);
        }

        try {
            InputSource is = new InputSource("file://" + configFile.getAbsolutePath());
            FileInputStream fis = new FileInputStream(configFile);
            is.setByteStream(fis);
            digester.push(this);
            digester.parse(is);
            fis.close();
        } catch (Exception e) {
            String err = i18n.getMessage("CatJWebServ.doStart.readfileError", configFile, e.getMessage());
            getLogger().log(BasicLevel.ERROR, err);
            throw new ServiceException(err, e);
        }

        javax.naming.Context ctxOld = setGlobalNamingResources();

        // Set the Domain and the name for each known Engine
        Iterator it = getEngines().iterator();
        while (it.hasNext()) {
            StandardEngine oSE = (StandardEngine) it.next();
            // WARNING : the order of th two next lines is very important.
            // The domain must be set in first and the name after.
            // In the others cases, Tomcat 5 doesn't set correctly these two properties
            // because there are somes controls that forbid to have a difference between
            // the name and the domain. Certainly a bug !
            oSE.setDomain(getDomainName());
            oSE.setName(getDomainName());
        }

        // Finaly start catalina ...
        if (server instanceof Lifecycle) {
            try {
                server.initialize();
                ((Lifecycle) server).start();
            } catch (Exception e) {
                String err = i18n.getMessage("CatJWebServ.doStart.startCatalinaError");
                getLogger().log(BasicLevel.ERROR, err);
                throw new ServiceException(e.getMessage(), e);
            }
        }
        // Reset server context
        getNaming().setComponentContext(ctxOld);

        // Tomcat is started
        tomcatStarted = true;

        // ... and deploy wars of the jonas.properties
        super.doStart();
    }

    /**
     * Defines the naming resources for tomcat
     * only use for avoid null pointer when using a server.xml of
     * the original distribution
     * @return the previous naming context
     * @throws ServiceException if a naming exception occurs
     */
    protected javax.naming.Context setGlobalNamingResources() throws ServiceException {
        // Set the global context of the server
        NamingResources namingResources = server.getGlobalNamingResources();
        javax.naming.Context globalCtx = null;
        javax.naming.Context ctxold = null;
        try {
            globalCtx = getNaming().createEnvironmentContext("catalina_global");
            ctxold = getNaming().setComponentContext(globalCtx);

            int i;
            // Resources
            ContextResource[] resources = namingResources.findResources();
            for (i = 0; i < resources.length; i++) {
                addResource(globalCtx, resources[i]);
            }
        } catch (NamingException e) {
            String err = i18n.getMessage("CatJWebServ.doStart.populateEnvError", e.getMessage());
            getLogger().log(BasicLevel.ERROR, err);
            throw new ServiceException(err, e);
        }
        // Set the global naming context of the server
        ((StandardServer) server).setGlobalNamingContext(globalCtx);
        return ctxold;
    }


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

        // ... and shut down embedded catalina
        if (server instanceof Lifecycle) {
            try {
                ((Lifecycle) server).stop();
            } catch (Exception e) {
                String err = i18n.getMessage("CatJWebServ.doStop.stopCatalinaError");
                getLogger().log(BasicLevel.ERROR, err);
                throw new ServiceException(e.getMessage(), e);
            }
        }

    }


    /**
     * Deploy a specific WAR file specified in the context.
     * @param ctx the context which contains the configuration in order
     * to deploy the WAR.
     * @throws JWebContainerServiceException if the registration of the WAR
     * failed.
     */
    protected void doRegisterWar(javax.naming.Context ctx)
        throws JWebContainerServiceException {
        // Get the 7 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) .
        //   - Name of the Ear application of this war if any
        //   - java2DelegationModel the compliance to java2 delegation model
        //   - parentCL the war classloader of this war.
        //   - jonasDD JOnAS Deployment Desc content
        //   - permissionManager JACC permission manager

        URL warURL = null;
        URL earURL = null;
        URL unpackedWarURL = null;
        String contextRoot = null;
        boolean java2DelegationModel = true;
        PermissionManager permissionManager = null;
        boolean inEarCase = true;
        String earAppName = null;
        String jonasDD = null;
        try {
            warURL = (URL) ctx.lookup("warURL");
            unpackedWarURL = (URL) ctx.lookup("unpackedWarURL");
            contextRoot = (String) ctx.lookup("contextRoot");
            Boolean bool = (Boolean) ctx.lookup("java2DelegationModel");
            java2DelegationModel = bool.booleanValue();
            jonasDD = (String) ctx.lookup("jonasDD");
            permissionManager = (PermissionManager) ctx.lookup("permissionManager");
        } catch (NamingException e) {
            String err = i18n.getMessage("CatJWebServ.doRegisterWar.gettingParamError");
            getLogger().log(BasicLevel.ERROR, err + e.getMessage());
            throw new JWebContainerServiceException(err, e);
        }

        try {
            earAppName = (String) ctx.lookup("earAppName");
            earURL = (URL) ctx.lookup("earURL");
        } catch (NamingException e) {
            // no ear case, so no ear application name
            inEarCase = false;
            // set earURL to something only to avoid null value
            earURL = warURL;
        }

        ClassLoader webClassLoader = null;

        try {
            webClassLoader = (ClassLoader) ctx.lookup("parentCL");
        } catch (NamingException e) {
            String err = i18n.getMessage("CatJWebServ.doRegisterWar.gettingParamError");
            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 = "";
        }

        // 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 {
            contextRoot = "/" + contextRoot;
        }

        // Install the war.
        File fWar = new File(warURL.getFile());
        //File fpackedWar = new File(packedWarURL.getFile());

        // Unpacked path of the war
        String destDir = null;
        if (fWar.isDirectory()) {
            destDir = warURL.getPath();
        } else {
            destDir = unpackedWarURL.getPath();
        }

        // META-INF/context.xml file
        File contextXmlFile = new File(destDir + File.separator + "META-INF" + File.separator + "context.xml");


        // Check if some contexts were configured in server.xml
        List jonasContexts = getConfiguredMatchingJonasContexts(contextRoot, fWar, destDir);

        Host deployer = null;
        // The context was not found in server.xml, a new context will be created.
        if (jonasContexts.isEmpty()) {
            // Find host on which deploy the context
            deployer = findHost(hostName);

            // Create context (false because not in server.xml)
            JOnASStandardContext context = new JOnASStandardContext(false, java2DelegationModel, inEarCase);
            context.setDocBase(destDir);
            context.setPath(contextRoot);
            ContextConfig config = new ContextConfig();

            // add the config
            ((Lifecycle) context).addLifecycleListener(config);
            jonasContexts.add(context);
        }

        // Configure these contexts and start them
        Iterator it = jonasContexts.iterator();
        while (it.hasNext()) {
            JOnASStandardContext jStdCtx = (JOnASStandardContext) it.next();

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

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

            // For checking resources
            if (resourceCheckerManager != null) {
                jStdCtx.addValve(new CheckOpenResourcesValve(resourceCheckerManager));
            }

            // Add valve for setting unauthenticated user
            jStdCtx.addValve(new ResetAuthenticationValve());

            // META-INF/context.xml file support
            if (contextXmlFile.exists()) {
                jStdCtx.setConfigFile(contextXmlFile.getAbsolutePath());
            }

            // JSR 77
            jStdCtx.setJ2EEServer(getJonasServerName());
            jStdCtx.setServer(J2eeObjectName.J2EEServer(getDomainName(), getJonasServerName()).toString());
            if (getMbeanServer() != null) {
                try {
                    String[] as = (String[]) getMbeanServer().getAttribute(J2eeObjectName.J2EEServer(getDomainName()
                        , getJonasServerName()), "javaVMs");
                    jStdCtx.setJavaVMs(as);
                } catch (Exception e) {
                    //String err = i18n.getMessage("CatJWebServ.doRegisterWar.startContextError", jStdCtx);
                    //getLogger().log(BasicLevel.WARN, err + e.getMessage());
                    getLogger().log(BasicLevel.WARN, "Set MBean JVM error : " + e.getClass().getName() + " " + e.getMessage());
                }
            }
            if (earAppName != null) {
                jStdCtx.setJ2EEApplication(earAppName);
            }
            jStdCtx.setJonasDeploymentDescriptor(jonasDD);

            // Set the realm
            Realm ctxRealm = jStdCtx.getRealm();

            // Take realm of parent
            if (ctxRealm == null) {
                ctxRealm = deployer.getRealm();
            }


            if (ctxRealm != null && ctxRealm instanceof JACC) {
                JACC jaccRealm = null;
                try {
                    jaccRealm = (JACC) ((JACC) ctxRealm).clone();
                } catch (CloneNotSupportedException cnse) {
                    String err = "Cannot clone the realm used by the existing context or its parent realm";
                    getLogger().log(BasicLevel.ERROR, err + cnse.getMessage());
                    throw new JWebContainerServiceException(err, cnse);
                }
                if (getLogger().isLoggable(BasicLevel.DEBUG)) {
                    getLogger().log(BasicLevel.DEBUG, "Setting permission manager to " + permissionManager);
                }
                jaccRealm.setPermissionManager(permissionManager);
                jaccRealm.setContext(jStdCtx);
                // Set the new realm
                jStdCtx.setRealm(jaccRealm);
            }

            // Set the attributes transfered from the JOnAS War MBean.
            // Note that 'java2DelegationModel' and 'inEarCase' attributes
            // are passed asarguments to the JOnASStandardContext's constructor
            jStdCtx.setWarURL(warURL);
            if (inEarCase) {
                jStdCtx.setEarURL(earURL);
            }

            // ... and start it or add it
            if (jStdCtx.isInServerXml()) {
                try {
                    jStdCtx.setLoader(null);
                    jStdCtx.start();
                } catch (LifecycleException lce) {
                    String err = i18n.getMessage("CatJWebServ.doRegisterWar.startContextError", jStdCtx);
                    getLogger().log(BasicLevel.ERROR, err + lce.getMessage());
                    throw new JWebContainerServiceException(err, lce);

                }
            } else {
                // add it
                if (deployer == null) {
                    String err = i18n.getMessage("CatJWebServ.doRegisterWar.deployerIsNull", jStdCtx);
                    getLogger().log(BasicLevel.ERROR, err);
                    throw new JWebContainerServiceException(err);
                }
                deployer.addChild(jStdCtx);
            }

            // ...and check if it is now configured
            checkStartedContext(jStdCtx, permissionManager);

            // Store WebModule ObjectName in Context
            try {
                ctx.rebind("WebModule", jStdCtx.createObjectName(getDomainName(), jStdCtx.getParentName()));
            } 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);
            }
        }
    }


    /**
     * Check if there is a previous JOnASStandardContext which match the current context
     * If true, deploy our context into the configured context
     * @param contextRoot name of the context that we want to find preconfigured contexts
     * @param fpackedWar file of the original war file
     * @param destDir name of the unpacked directory of the war file
     * @return true if a context was found and used
     */
    protected List getConfiguredMatchingJonasContexts(String contextRoot, File fpackedWar, String destDir) {
        ArrayList jonasContexts = new ArrayList();
        // Check contexts of all services to see if there is a configured context.
        // that we must use instead of creating a new one.
        // If a context is matching, we set its docbase and start it.
        Iterator it = getContexts().iterator();
        while (it.hasNext()) {
            Context ctx = (Context) it.next();
            // Continue if it is not a JOnAS context
            if (!(ctx instanceof JOnASStandardContext)) {
                continue;
            }
            JOnASStandardContext jStdCtx = (JOnASStandardContext) ctx;
            if (jStdCtx != null) {
                // Not created in server.xml file
                if (!jStdCtx.isInServerXml()) {
                    continue;
                }

                // The context was configured ?
                String serverCtxRoot = jStdCtx.getPath();
                if (!serverCtxRoot.equals(contextRoot)) {
                    continue;
                }
                if (jStdCtx.getPrivileged()) {
                    // Can deploy a privileged context only if it's a .war file
                    if (fpackedWar.isDirectory()) {
                        String err = i18n.getMessage("CatJWebServ.getConfiguredMatchingJonasContexts.privilegedContextError");
                        getLogger().log(BasicLevel.ERROR, err);
                    }
                    jStdCtx.setDocBase(fpackedWar.getPath());
                } else {
                    jStdCtx.setDocBase(destDir);
                }
                jonasContexts.add(jStdCtx);
            }
        }
        return jonasContexts;
    }


    /**
     * Check that the context that was started was right configured
     * @param context context to check
     * @param permissionManager the permission manager used for JACC
     * @throws JWebContainerServiceException if the context was not right configured
     */
    protected void checkStartedContext(Context context, PermissionManager permissionManager) throws JWebContainerServiceException {
        // Check if the context was configured
        if (!context.getConfigured()) {
            String err = i18n.getMessage("CatJWebServ.checkStartedContext.configuredFail", context.getName());
            getLogger().log(BasicLevel.ERROR, err);
            throw new JWebContainerServiceException(err);
        }

        // Check that JACC realm is correctly set
        Realm ctxRealm = context.getRealm();
        if (ctxRealm != null && ctxRealm instanceof JACC) {
            JACC jaccRealm = (JACC) ctxRealm;
            PermissionManager ctxPerm = jaccRealm.getPermissionManager();
            if (ctxPerm == null && permissionManager != null) {
                jaccRealm.setPermissionManager(permissionManager);
                // set context on the realm.
                jaccRealm.setContext(context);
            }
        }
        if (getLogger().isLoggable(BasicLevel.DEBUG)) {
            getLogger().log(BasicLevel.DEBUG, "context " + context + " started");
        }

    }

    /**
     * Gets all the engines of the current Tomcat server
     * @return all the engines of the current Tomcat server
     * @throws JWebContainerServiceException if engines can not be retrieved
     */
    protected synchronized List getEngines() throws JWebContainerServiceException {
        List engines = new ArrayList();
        Service[] services = server.findServices();

        // Get contexts of each engine of each service
        for (int s = 0; s < services.length; s++) {
            Container cont = services[s].getContainer();
            if (!(cont instanceof Engine)) {
                String err = "The container of the service must be an engine (server.xml)";
                throw new JWebContainerServiceException(err);
            }
            engines.add(cont);
        }
        return engines;
    }

    /**
     * Gets all the contexts of the current Tomcat server
     * @return all the contexts of the current Tomcat server
     * @throws JWebContainerServiceException if contexts can not be retrieved
     */
    protected synchronized List getContexts() throws JWebContainerServiceException {
        List contexts = new ArrayList();

        Iterator itEngines = getEngines().iterator();
        while (itEngines.hasNext()) {
            Engine engine = (Engine) itEngines.next();
            Container[] hosts = engine.findChildren();

            for (int j = 0; j < hosts.length; j++) {
                Container[] containers = hosts[j].findChildren();
                for (int k = 0; k < containers.length; k++) {
                    Container container = containers[k];
                    if (container instanceof Context) {
                        contexts.add(container);
                    }
                }
            }
        }
        return contexts;
    }


    /**
     * Undeploy a specific WAR file specified in the context.
     * @param ctx the context which contains the configuration in order
     * to undeploy a WAR.
     * @throws JWebContainerServiceException if the unregistration failed.
     */
    protected void doUnRegisterWar(javax.naming.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 = i18n.getMessage("CatJWebServ.doUnRegisterWar.gettingParamError");
            getLogger().log(BasicLevel.ERROR, err + e.getMessage());
            throw new JWebContainerServiceException(err, e);
        }

        if (contextRoot.equals("/")) {
            contextRoot = "";
        } else {
            contextRoot = "/" + contextRoot;
        }

        String hostName = null;
        try {
            hostName = (String) ctx.lookup("hostName");
        } catch (NamingException e) {
            if (getLogger().isLoggable(BasicLevel.DEBUG)) {
                getLogger().log(BasicLevel.DEBUG, "No define hostname for this context");
            }
        }

        // Undeploy all matching context if there is no hostname provided
        if (hostName == null) {
            boolean found = false;
            Iterator it = getContexts().iterator();
            while (it.hasNext()) {
                Context context = (Context) it.next();
                String serverCtxRoot = context.getPath();
                if (serverCtxRoot.equals(contextRoot)) {
                    removeContext(context);
                    found = true;
                    break;
                }
            }
            if (!found) {
                String err = i18n.getMessage("CatJWebServ.doUnRegisterWar.undeployError", contextRoot);
                getLogger().log(BasicLevel.ERROR, err);
                throw new JWebContainerServiceException(err);
            }
        } else {
            // Remove the specified Context from the set of defined Contexts
            // for its associated Host.
            Host host = findHost(hostName);
            org.apache.catalina.Context context = host.map(contextRoot);
            if (context != null) {
                removeContext(context);
            } else {
                String err = i18n.getMessage("CatJWebServ.doUnRegisterWar.undeployError", contextRoot);
                getLogger().log(BasicLevel.ERROR, err);
                throw new JWebContainerServiceException(err);
            }
        }
    }


    /**
     * Remove the specified Context from the set of defined Contexts for its
     * associated Host.  If this is the last Context for this Host, the Host
     * will also be removed.
     *
     * @param context The Context to be removed
     * @throws JWebContainerServiceException if the context can not be removed
     */
    public synchronized void removeContext(Context context) throws JWebContainerServiceException {

        // Is this Context actually among those that are deployed ?
        boolean found = false;
        Iterator it = getContexts().iterator();
        while (it.hasNext() && !found) {
            if (context == (Context) it.next()) {
                found = true;
            }
        }

        if (!found) {
            return;
        }
        // Remove this Context from the associated Host
        // Or stop it if it is a JOnAS standard context (to keep the configuration)
        if (context instanceof JOnASStandardContext) {
            JOnASStandardContext jctx = (JOnASStandardContext) context;
            if (jctx.isInServerXml()) {
                try {
                    ((JOnASStandardContext) context).stop();
                } catch (LifecycleException le) {
                    throw new JWebContainerServiceException("Cannot stop context (" + le.getMessage() + ")");
                }
            } else {
                // must not persist
                context.getParent().removeChild(context);
                // Bug Tomcat 5 doesn't remove MBean WebModule
                // Delete the next line when will be fixed
                // Bug Tomcat 5 : begin
                if (getMbeanServer() != null) {
                    try {
                        // unregister WebModule MBean
                        getMbeanServer().unregisterMBean(jctx.getJmxName());
                    } catch (Exception e) {
                        getLogger().log(BasicLevel.ERROR, "Cannot remove the MBean for the WebModule " + jctx.getObjectName() + " : " + e.getMessage());
                    }
                }
            }
        } else {
            context.getParent().removeChild(context);
            // Bug Tomcat 5 doesn't remove MBean WebModule
            // Delete the next line when will be fixed
            // Bug Tomcat 5 : begin
            if (context instanceof StandardContext) {
                StandardContext ctx = (StandardContext) context;
                if (getMbeanServer() != null) {
                    try {
                        // unregister WebModule MBean
                        getMbeanServer().unregisterMBean(ctx.getJmxName());
                    } catch (Exception e) {
                        getLogger().log(BasicLevel.ERROR
                            , "Cannot remove the MBean for the WebModule " + ctx.getObjectName()
                            + " : " + e.getMessage());
                    }
                }
            }
        }

    }


    /**
     * The server is started ?
     * @return boolean true if the catalina container is running.
     */
    public boolean isTomcatStarted() {
        return tomcatStarted;
    }


    /**
     * Set the server instance we are configuring.
     *
     * @param server The new server
     */
    public void setServer(Server server) {
        this.server = server;
    }


    /**
     * Return a File object representing the server.xml configuration file.
     * @return a File object representing the server.xml configuration file.
     * @throws FileNotFoundException if the configuration file is not found.
     */
    protected File getConfigFile() throws FileNotFoundException {
        String fileName = System.getProperty("catalina.base");
        fileName = fileName + File.separator + CONFIG_FILE;
        File file = new File(fileName);
        if (!file.exists()) {
            String err = i18n.getMessage("CatJWebServ.getConfigFile.findfileError", fileName);
            throw new FileNotFoundException(err);
        }
        return (file);
    }


    /**
     * Find the specified host.
     * @param hostName the name of the host to find.
     * @return the host found.
     * @throws JWebContainerServiceException if the specified host cannot be
     * found.
     */
    public Host findHost(String hostName)
        throws JWebContainerServiceException {

        Service[] services = server.findServices();
        // Check number of services
        if (services.length < 1) {
            String err = "At least one service must be define in the server.xml of Tomcat";
            throw new JWebContainerServiceException(err);
        }

        // Two cases :
        //   1/ No host specified
        //      -> defaulthost of the engine of the first service.
        //   2) A host is specified in the jonas-web.xml
        //      -> find the host and deploy on this host


        if (hostName == null || hostName.equals("")) {
            // First case

            //Take first service
            Service service = services[0];

            Container cont = service.getContainer();
            if (!(cont instanceof Engine)) {
                String err = "The container of the service must be an engine";
                throw new JWebContainerServiceException(err);
            }

            Engine engine = (Engine) cont;
            String defaultHost = engine.getDefaultHost();
            if (defaultHost == null) {
                String err = "Default host must be specified in server.xml or host must be specified in jonas-web.xml";
                throw new JWebContainerServiceException(err);
            }
            Container child = engine.findChild(defaultHost);
            // found, return it
            if (child instanceof Host) {
                return (Host) child;
            }
            // else error
            String err = "Default host " + defaultHost + " not found";
            throw new JWebContainerServiceException(err);
        }
        // Get all hosts.
        Vector hosts = new Vector();

        for (int s = 0; s < services.length; s++) {
            Container cont = services[s].getContainer();
            if (!(cont instanceof Engine)) {
                String err = "The container of a service must be an engine";
                throw new JWebContainerServiceException(err);
            }
            Engine engine = (Engine) cont;
            Container child = engine.findChild(hostName);
            if (child instanceof Host) {
                hosts.addElement(child);
            }
        }

        // error
        if (hosts.size() == 0) {
            // No host found.
            String err = "Host " + hostName + " not found in all services/Engine of server.xml";
            throw new JWebContainerServiceException(err);
        }

        // first element
        return (Host) hosts.elementAt(0);
    }


    /**
     * Set the specified resources in the naming context.
     * @param ctx the context where the bindings are set.
     * @param resource the resource to bind.
     * @throws JWebContainerServiceException if the bind failed.
     */
    private void addResource(javax.naming.Context ctx, ContextResource resource) throws JWebContainerServiceException {

        String name = resource.getName();
        String type = resource.getType();
        String registryName = name;

        // Add only UserDatabase resources
        if (!type.equals("org.apache.catalina.UserDatabase")) {
            return;
        }

        // Create a reference to the resource.
        Reference ref = new Reference("org.apache.catalina.UserDatabase", "org.apache.catalina.users.MemoryUserDatabaseFactory", null);

        // Add extra parameters
        Iterator params = resource.listProperties();
        while (params.hasNext()) {
            String paramName = (String) params.next();
            String paramValue = (String) resource.getProperty(paramName);
            StringRefAddr refAddr = new StringRefAddr(paramName, paramValue);
            ref.add(refAddr);
        }
        try {
            javax.naming.Context ictx = getNaming().getInitialContext();

            // Bind into registry
            ictx.rebind(registryName, ref);

            // Bind LinkRef
            LinkRef lref = new LinkRef(registryName);
            ctx.rebind(name, lref);

        } catch (Exception e) {
            e.printStackTrace();
            String err = "Can't add the resource " + name;
            throw new JWebContainerServiceException(err, e);
        }
    }


    /**
     * Create and configure the Digester that will be used for the xml
     * parsing of the configuration file.
     * @return Digester the digester containing the rules for the xml parsing
     * of the server.xml.
     */
    protected Digester createServerDigester() {

        // Initialize the digester
        Digester digester = new Digester();
        digester.setValidating(false);
        digester.addRuleSet(new JCatalinaRuleSet(parentClassLoader));

        // Use context class loader.
        // Could avoid problem for users putting digester in JONAS_ROOT/lib/ext folder
        digester.setUseContextClassLoader(true);
        return (digester);

    }


    /**
     * Update info of the serverName and serverVersion
     */
    protected void updateServerInfos() {
        String infos = org.apache.catalina.util.ServerInfo.getServerInfo();
        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 {
        Engine engine = (Engine) getFirstService().getContainer();
        return engine.getDefaultHost();
    }


    /**
     * Return the Default HTTP port number of the web container. Returns the first
     * connector port if more than one are defined.
     * @return the Default HTTP port number of the web container.
     * @throws JWebContainerServiceException when default HTTP port cannot be
     *         resolved or none are defined.
     */
    public String getDefaultHttpPort() throws JWebContainerServiceException {
        return String.valueOf(getFirstConnectorFromScheme("http").getPort());
    }


    /**
     * @param scheme URL scheme (http, ...)
     * @return Returns a List of Connectors for the given scheme (http, https, ...)
     */
    private Connector getFirstConnectorFromScheme(String scheme) {
        Service svc = getFirstService();
        // take the first service
        Connector[] conn = svc.findConnectors();
        Vector httpConn = new Vector();
        for (int i = 0; i < conn.length; i++) {
            if (conn[i].getScheme().equalsIgnoreCase(scheme)) {
                httpConn.add(conn[i]);
            }
        }

        if (httpConn.isEmpty()) {
            // No connectors defined
            throw new JWebContainerServiceException("No Connectors found for scheme '" + scheme + "' in " + CONFIG_FILE);
        }

        Connector c = (Connector) httpConn.get(0);

        // Check if there are more than one HTTP connectors specified, if so, warn the administrator.
        if (httpConn.size() > 1) {
            if (getLogger().isLoggable(BasicLevel.WARN)) {
                getLogger().log(BasicLevel.WARN, "Found multiple Connectors for scheme '" + scheme + "' in " + CONFIG_FILE
                        + ", using first by default! (port:" + c.getPort() + ")");
            }
        }

        return c;
    }


    /**
     * @return Returns the first found Service
     */
    private Service getFirstService() {
        Service[] svc = server.findServices();
        // throw exception if no service is defined
        if ((svc == null) || (svc.length == 0)) {
            // No Services specified.
            throw new JWebContainerServiceException("No Services found in " + CONFIG_FILE);
        }
        // Check if there are more than one services specified and if so, warn the administrator.
        if (svc.length > 1) {
            if (getLogger().isLoggable(BasicLevel.WARN)) {
                getLogger().log(BasicLevel.WARN, "Found multiple Services in " + CONFIG_FILE + ", using first by default!");
            }
        }
        return svc[0];
    }


    /**
     * 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 (0 occurences).
     */
    public String getDefaultHttpsPort() throws JWebContainerServiceException {
        return String.valueOf(getFirstConnectorFromScheme("https").getPort());
    }

    /**
     * Register a WAR by delegating the operation to the registerWar()
     * method. This is used for JMX management.
     * @param fileName the name of the war to deploy.
     * @throws RemoteException if rmi call failed.
     * @throws JWebContainerServiceException if the registration failed.
     */
    public void registerWarMBean(String fileName) throws RemoteException, JWebContainerServiceException {
        ClassLoader old = null;
        try {
            old = Thread.currentThread().getContextClassLoader();
            Thread.currentThread().setContextClassLoader(catalinaLoader);
            super.registerWarMBean(fileName);
        } catch (Exception e) {
            throw new ServiceException("Exception during registering war", e);
        } finally {
            if (old != null) {
                Thread.currentThread().setContextClassLoader(old);
            }
        }
    }

    /**
     * Unregister a WAR by delegating the operation to the unRegisterWar()
     * method. This is used for JMX management.
     * @param fileName the name of the war to undeploy.
     * @throws RemoteException if rmi call failed.
     * @throws JWebContainerServiceException if the unregistration failed.
     */
    public void unRegisterWarMBean(String fileName) throws RemoteException, JWebContainerServiceException {
        ClassLoader old = null;
        try {
            old = Thread.currentThread().getContextClassLoader();
            Thread.currentThread().setContextClassLoader(catalinaLoader);
            super.unRegisterWarMBean(fileName);
        } catch (Exception e) {
            throw new ServiceException("Exception during unregistering war", e);
        } finally {
            if (old != null) {
                Thread.currentThread().setContextClassLoader(old);
            }
        }
    }
}
