/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This 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 (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.web.tomcat.tc4;

import java.net.URL;
import java.util.Iterator;
import java.io.File;
import java.lang.reflect.Method;
import javax.management.ObjectName;
import javax.naming.InitialContext;
import javax.servlet.ServletContext;

import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardWrapper;
import org.apache.catalina.Globals;
import org.apache.catalina.Deployer;
import org.apache.catalina.Container;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Host;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.Context;
import org.apache.catalina.Valve;
import org.apache.catalina.Realm;
import org.apache.catalina.Loader;
import org.apache.catalina.authenticator.AuthenticatorBase;
import org.apache.catalina.valves.ValveBase;

import org.jboss.deployment.DeploymentInfo;
import org.jboss.deployment.DeploymentException;
import org.jboss.metadata.WebMetaData;
import org.jboss.web.AbstractWebDeployer;
import org.jboss.web.WebApplication;
import org.jboss.web.AbstractWebContainer;
import org.jboss.web.tomcat.mbean.ServletInfo;
import org.jboss.web.tomcat.tc4.statistics.ContainerStatsValve;
import org.jboss.web.tomcat.session.ClusterManager;
import org.jboss.web.tomcat.session.SnapshotManager;
import org.jboss.web.tomcat.session.InstantSnapshotManager;
import org.jboss.web.tomcat.session.IntervalSnapshotManager;
import org.jboss.web.tomcat.session.ClusteredSessionValve;
import org.jboss.web.tomcat.session.ClusteringNotSupportedException;
import org.jboss.web.tomcat.security.JBossSecurityMgrRealm;
import org.jboss.util.file.Files;

/** The tomcat war deployer.
 *
 * @author Scott.Stark@jboss.org
 * @version $Revision: 1.1.2.5 $
 */
public class TomcatDeployer extends AbstractWebDeployer
{
   private DeployerConfig config;

   public void init(Object containerConfig) throws Exception
   {
      this.config = (DeployerConfig) containerConfig;
      super.setJava2ClassLoadingCompliance(config.java2ClassLoadingCompliance);
      super.setUnpackWars(config.unpackWars);
      super.setLenientEjbLink(config.lenientEjbLink);
      // Configure the servlet info MBean metadata
      ServletInfo.init();
   }

   /** Perform the tomcat specific deployment steps.
    */
   protected void performDeploy(WebApplication appInfo, String warUrl,
      AbstractWebContainer.WebDescriptorParser webAppParser) throws Exception
   {
      WebMetaData metaData = appInfo.getMetaData();
      String ctxPath = metaData.getContextRoot();
      if (ctxPath.equals("/"))
      {
         ctxPath = "";
         metaData.setContextRoot(ctxPath);
      }
      log.info("deploy, ctxPath=" + ctxPath + ", warUrl=" + warUrl);

      URL url = new URL(warUrl);
      createWebContext(appInfo, url, webAppParser);
      log.debug("Initialized: " + appInfo);
   }

   /** Perform the tomcat specific deployment steps.
    */
   public void performUndeploy(String warUrl, WebApplication appInfo)
      throws Exception
   {
      if (appInfo == null)
      {
         log.debug("performUndeploy, no WebApplication found for URL " + warUrl);
         return;
      }
      log.info("undeploy, ctxPath=" + appInfo.getMetaData().getContextRoot() + ", warUrl=" + warUrl);

      // Unreqister the servlet mbeans
      DeploymentInfo di = appInfo.getDeploymentInfo();
      Iterator iter = di.mbeans.iterator();
      while (iter.hasNext())
      {
         ObjectName name = (ObjectName) iter.next();
         try
         {
            server.unregisterMBean(name);
         }
         catch (Exception ignore)
         {
         }
      }

      StandardContext context = (StandardContext) appInfo.getAppData();
      if (context == null)
         throw new DeploymentException("URL " + warUrl + " is not deployed");

      File workDir = (File) context.getServletContext().getAttribute(Globals.WORK_DIR_ATTR);
      String ctxPath = context.getPath();
      Deployer deployer = (Deployer) context.getParent();
      deployer.remove(ctxPath);

      // Cleanup the webapp resources
      Container[] children = context.findChildren();
      for(int i = 0; i < children.length; i ++)
         context.removeChild(children[i]);
      LifecycleListener[] listeners = context.findLifecycleListeners();
      for(int i = 0; i < listeners.length; i ++)
         context.removeLifecycleListener(listeners[i]);

      // Unbind the ENC now that the web app is shutdown
      ClassLoader currentLoader = Thread.currentThread().getContextClassLoader();
      ClassLoader encLoader = appInfo.getMetaData().getENCLoader();
      Thread.currentThread().setContextClassLoader(encLoader);
      InitialContext ctx = new InitialContext();
      ctx.unbind("java:comp/env");
      Thread.currentThread().setContextClassLoader(currentLoader);

      context.setLoader(null);
      context.setLogger(null);
      context.setManager(null);
      context.setParent(null);
      context.setParentClassLoader(null);
      context.setRealm(null);
      context.setResources(null);

      if (workDir != null && config.isDeleteWorkDirs() == true)
      {
         log.debug("Deleting catalina work dir: " + workDir);
         Files.delete(workDir);
      }
   }


   private void createWebContext(final WebApplication appInfo, URL warUrl,
      final AbstractWebContainer.WebDescriptorParser webAppParser) throws Exception
   {
      ClassLoader loader = appInfo.getClassLoader();
      WebMetaData metaData = appInfo.getMetaData();
      String ctxPath = metaData.getContextRoot();
      appInfo.setName(warUrl.getPath());
      appInfo.setURL(warUrl);
      final StandardContext context = (StandardContext) config.getCatalina().createContext(ctxPath, warUrl.getFile());
      JBossSecurityMgrRealm jbossRealm = new JBossSecurityMgrRealm();
      context.setRealm(jbossRealm);
      if (config.isUseJBossWebLoader() == true)
      {
         WebCtxLoader webLoader = new WebCtxLoader(loader);
         webLoader.setWarURL(warUrl);
         context.setLoader(webLoader);
      }
      else
      {
         context.setParentClassLoader(loader);
      }

      // Create a web application info object
      appInfo.setAppData(context);

      String hostName = null;
      Iterator hosts = metaData.getVirtualHosts();
      if( hosts.hasNext() )
         hostName = (String) hosts.next();
      Host virtualHost = config.getCatalina().findHost(hostName);

      if (metaData.getDistributable())
      {
         // Try to initate clustering, fallback to standard if no clustering is available
         try
         {
            ClusterManager manager =
               new ClusterManager(virtualHost,
                                  context,
                                  this.log,
                                  metaData,
                                  config.isUseLocalCache());
            manager.setContainer(context);
            context.setManager(manager);

            // choose the snapshot manager
            SnapshotManager snap = null;
            String snapshotMode = config.getSnapshotMode();
            int snapshotInterval = config.getSnapshotInterval();
            if (snapshotMode.equals("instant"))
            {
               snap = new InstantSnapshotManager(manager, context);
            }
            else if (snapshotMode.equals("interval"))
            {
               snap = new IntervalSnapshotManager(manager, context, snapshotInterval);
            }
            else
            {
               log.error("Snapshot mode must be 'instant' or 'interval' - using 'instant'");
               snap = new InstantSnapshotManager(manager, context);
            }
            // Adding session snapshot valve
            ValveBase valve = new ClusteredSessionValve(snap);
            valve.setContainer(context);
            context.addValve(valve);

            log.info("Enabled clustering support for ctxPath=" + ctxPath);
         }
         catch (ClusteringNotSupportedException e)
         {
            log.error("Failed to setup clustering, clustering disabled");
         }
      }

      // Add the statistics valve to the context
      ContainerStatsValve valve = new ContainerStatsValve(config.getStats());
      valve.setContainer(context);
      context.addValve(valve);

      // Set the session cookies flag according to metadata
      switch(metaData.getSessionCookies())
      {
         case WebMetaData.SESSION_COOKIES_ENABLED:
            context.setCookies(true);
            log.debug("Enabling session cookies");
            break;
         case WebMetaData.SESSION_COOKIES_DISABLED:
            context.setCookies(false);
            log.debug("Disabling session cookies");
            break;
         default:
            log.debug("Using session cookies default setting");
      }

      /* We need to establish the JNDI ENC prior to the start of the web container
       so that init on startup servlets are able to interact with their ENC. We
       hook into the context lifecycle events to be notified of the start of the
       context as this occurs before the servlets are started. */
      final org.jboss.logging.Logger theLog = super.log;
      context.addLifecycleListener(new LifecycleListener()
      {
         public void lifecycleEvent(LifecycleEvent event)
         {
            Object source = event.getSource();
            if (source == context && event.getType().equals(Lifecycle.START_EVENT))
            {
               theLog.debug("Context.lifecycleEvent, event=" + event);
               contextInit(context, appInfo, webAppParser);
            }
         }
      }
      );
      initENC(appInfo, webAppParser);

      // A debug level of 1 is rather verbose so only enable debugging if trace priority is active
      int debugLevel = config.getDebugLevel();
      if ( debugLevel <= 1 )
         debugLevel = 0;
      context.setDebug(debugLevel);
      virtualHost.addChild(context);

      // Create mbeans for the servlets
      DeploymentInfo di = webAppParser.getDeploymentInfo();
      Container[] children = context.findChildren();
      for (int n = 0; n < children.length; n++)
      {
         if (children[n] instanceof StandardWrapper)
         {
            StandardWrapper servlet = (StandardWrapper) children[n];
            String name = servlet.getName();
            try
            {
               ObjectName oname = ServletInfo.createServletMBean(servlet, ctxPath,
                  virtualHost.getName(), config.getServiceName(), server);
               di.mbeans.add(oname);
            }
            catch (Exception e)
            {
               log.debug("Failed to create mbean for servlet: " + name, e);
            }
         }
      }
   }

   private void initENC(WebApplication appInfo, AbstractWebContainer.WebDescriptorParser webAppParser)
      throws Exception
   {
      ClassLoader tcl = Thread.currentThread().getContextClassLoader();
      WebMetaData metaData = appInfo.getMetaData();
      webAppParser.parseWebAppDescriptors(tcl, metaData);
   }

   /** Build the web application ENC.
    */
   private void contextInit(Context context, WebApplication appInfo,
      AbstractWebContainer.WebDescriptorParser webAppParser)
   {
      try
      {
         ServletContext servletCtx = context.getServletContext();
         if (servletCtx == null)
            return;

         /* We need to go through the context valves and set the cache flag
          on any AuthenticatorBase to false or else the JBossSecurityMgrRealm
          is not asked to authenticate every request. This can result in
          an authenticated user thread not receiving its authenticated
          Subject and this results in an authorization failure.
          */
         StandardContext stdctx = (StandardContext) context;
         Valve[] valves = stdctx.getValves();
         for (int v = 0; v < valves.length; v++)
         {
            Valve valve = valves[v];
            if (valve instanceof AuthenticatorBase)
            {
               AuthenticatorBase auth = (AuthenticatorBase) valve;
               auth.setCache(false);
            }
         }
         // Install the JBossSecurityMgrRealm as valve to clear the SecurityAssociation
         Realm realm = stdctx.getRealm();
         if (realm instanceof Valve)
            stdctx.addValve((Valve) realm);

         // Add all of the classpth elements
         ClassLoader rsrcLoader = Thread.currentThread().getContextClassLoader();
         String[] jspCP = getCompileClasspath(rsrcLoader);
         Loader ctxLoader = context.getLoader();
         for (int u = 0; u < jspCP.length; u++)
         {
            ctxLoader.addRepository(jspCP[u]);
         }

         // Enable parent delegation class loading
         ClassLoader scl = context.getLoader().getClassLoader();
         try
         {
            Class[] signature = {boolean.class};
            Method setDelegate = scl.getClass().getMethod("setDelegate", signature);
            Boolean parentDelegation = new Boolean(appInfo.getMetaData().getJava2ClassLoadingCompliance());
            Object[] args = {parentDelegation};
            setDelegate.invoke(scl, args);
            log.info("Using Java2 parent classloader delegation: " + parentDelegation);
         }
         catch (Exception e)
         {
            log.debug("Unable to invoke setDelegate on class loader:" + scl);
         }
      }
      catch (Exception e)
      {
         log.error("Failed to setup web application ENC", e);
      }
   }

}
