/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2006, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file 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.tc5;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.CodeSource;
import java.security.cert.Certificate;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import javax.management.Attribute;
import javax.management.ObjectInstance;
import javax.management.ObjectName;

import org.apache.catalina.Loader;
import org.jboss.deployment.DeploymentException;
import org.jboss.deployment.DeploymentInfo;
import org.jboss.metadata.WebMetaData;
import org.jboss.web.AbstractWebContainer;
import org.jboss.web.AbstractWebDeployer;
import org.jboss.web.WebApplication;
import org.jboss.web.tomcat.security.CustomPrincipalValve;
import org.jboss.web.tomcat.security.JaccContextValve;
import org.jboss.web.tomcat.security.RunAsListener;
import org.jboss.web.tomcat.security.SecurityAssociationValve;
import org.jboss.web.tomcat.tc5.session.AbstractJBossManager;
import org.jboss.web.tomcat.tc5.session.ClusteringNotSupportedException;
import org.jboss.web.tomcat.tc5.session.JBossCacheManager;

/**
 * The tomcat web application deployer
 *
 * @author Scott.Stark@jboss.org
 * @author Costin Manolache
 * @version $Revision: 57206 $
 */
public class TomcatDeployer extends AbstractWebDeployer
{
   /**
    * The name of the war level context configuration descriptor
    */
   private static final String CONTEXT_CONFIG_FILE = "WEB-INF/context.xml";

   private DeployerConfig config;
   private String[] javaVMs =
      {" jboss.management.local:J2EEServer=Local,j2eeType=JVM,name=localhost"};
   private String serverName = "jboss";
   private HashMap vhostToHostNames = new HashMap();

   public void init(Object containerConfig) throws Exception
   {
      this.config = (DeployerConfig) containerConfig;
      super.setJava2ClassLoadingCompliance(config.isJava2ClassLoadingCompliance());
      super.setUnpackWars(config.isUnpackWars());
      super.setLenientEjbLink(config.isLenientEjbLink());
      super.setDefaultSecurityDomain(config.getDefaultSecurityDomain());
   }

   /**
    * Perform the tomcat specific deployment steps.
    */
   protected void performDeploy(WebApplication appInfo, String warUrl,
      AbstractWebContainer.WebDescriptorParser webAppParser)
      throws Exception
   {
      WebMetaData metaData = appInfo.getMetaData();
      String hostName = null;
      // Get any jboss-web/virtual-hosts
      Iterator vhostNames = metaData.getVirtualHosts();
      // Map the virtual hosts onto the configured hosts
      Iterator hostNames = mapVirtualHosts(vhostNames);
      if (hostNames.hasNext())
      {
         hostName = hostNames.next().toString();
      }
      performDeployInternal(hostName, appInfo, warUrl, webAppParser);
      while (hostNames.hasNext())
      {
         String additionalHostName = hostNames.next().toString();
         performDeployInternal(additionalHostName, appInfo, warUrl, webAppParser);
      }
   }

   protected void performDeployInternal(String hostName,
      WebApplication appInfo, String warUrl,
      AbstractWebContainer.WebDescriptorParser webAppParser)
      throws Exception
   {

      WebMetaData metaData = appInfo.getMetaData();
      String ctxPath = metaData.getContextRoot();
      if (ctxPath.equals("/") || ctxPath.equals("/ROOT") || ctxPath.equals(""))
      {
         log.debug("deploy root context=" + ctxPath);
         ctxPath = "/";
         metaData.setContextRoot(ctxPath);
      }

      log.info("deploy, ctxPath=" + ctxPath + ", warUrl=" + shortWarUrlFromServerHome(warUrl));

      URL url = new URL(warUrl);

      ClassLoader loader = Thread.currentThread().getContextClassLoader();
      /* If we are using the jboss class loader we need to augment its path
      to include the WEB-INF/{lib,classes} dirs or else scoped class loading
      does not see the war level overrides. The call to setWarURL adds these
      paths to the deployment UCL.
      */
      Loader webLoader = null;
      if (config.isUseJBossWebLoader())
      {
         WebCtxLoader jbossLoader = new WebCtxLoader(loader);
         jbossLoader.setWarURL(url);
         webLoader = jbossLoader;
      }
      else
      {
         String[] pkgs = config.getFilteredPackages();
         WebAppLoader jbossLoader = new WebAppLoader(loader, pkgs);
         jbossLoader.setDelegate(getJava2ClassLoadingCompliance());
         webLoader = jbossLoader;
      }

      // 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.
      if (appInfo.getAppData() == null)
         webAppParser.parseWebAppDescriptors(loader, appInfo.getMetaData());

      appInfo.setName(url.getPath());
      appInfo.setClassLoader(loader);
      appInfo.setURL(url);

      String objectNameS = config.getCatalinaDomain()
         + ":j2eeType=WebModule,name=//" +
         ((hostName == null) ? "localhost" : hostName)
         + ctxPath + ",J2EEApplication=none,J2EEServer=none";

      ObjectName objectName = new ObjectName(objectNameS);

      if (server.isRegistered(objectName))
      {
         log.debug("Already exists, destroying " + objectName);
         server.invoke(objectName, "destroy", new Object[]{},
            new String[]{});
      }

      server.createMBean("org.apache.commons.modeler.BaseModelMBean",
         objectName, new Object[]{config.getContextClassName()},
         new String[]{"java.lang.String"});

      // Find and set config file on the context
      // If WAR is packed, expand config file to temp folder
      String ctxConfig = null;
      try
      {
         ctxConfig = findConfig(url);
      }
      catch (IOException e)
      {
         log.debug("No " + CONTEXT_CONFIG_FILE + " in " + url, e);
      }

      server.setAttribute(objectName, new Attribute("docBase", url.getFile()));

      server.setAttribute(objectName, new Attribute("configFile", ctxConfig));

      server.setAttribute(objectName, new Attribute
         ("defaultContextXml", "context.xml"));
      server.setAttribute(objectName, new Attribute
         ("defaultWebXml", "conf/web.xml"));

      server.setAttribute(objectName, new Attribute("javaVMs", javaVMs));

      server.setAttribute(objectName, new Attribute("server", serverName));

      server.setAttribute(objectName, new Attribute
         ("saveConfig", Boolean.FALSE));

      if (webLoader != null)
      {
         server.setAttribute(objectName, new Attribute
            ("loader", webLoader));
      }
      else
      {
         server.setAttribute(objectName, new Attribute
            ("parentClassLoader", loader));
      }

      server.setAttribute(objectName, new Attribute
         ("delegate", new Boolean(getJava2ClassLoadingCompliance())));

      String[] jspCP = getCompileClasspath(loader);
      StringBuffer classpath = new StringBuffer();
      for (int u = 0; u < jspCP.length; u++)
      {
         String repository = jspCP[u];
         if (repository == null)
            continue;
         if (repository.startsWith("file://"))
            repository = repository.substring(7);
         else if (repository.startsWith("file:"))
            repository = repository.substring(5);
         else
            continue;
         if (repository == null)
            continue;
         // ok it is a file.  Make sure that is is a directory or jar file
         File fp = new File(repository);
         if (!fp.isDirectory())
         {
            // if it is not a directory, try to open it as a zipfile.
            try
            {
               // avoid opening .xml files
               if (fp.getName().toLowerCase().endsWith(".xml"))
                  continue;
               
               ZipFile zip = new ZipFile(fp);
               zip.close();
            }
            catch (IOException e)
            {
               continue;
            }

         }
         if (u > 0)
            classpath.append(File.pathSeparator);
         classpath.append(repository);
      }

      server.setAttribute(objectName, new Attribute
         ("compilerClasspath", classpath.toString()));

      // Set the session cookies flag according to metadata
      switch (metaData.getSessionCookies())
      {
         case WebMetaData.SESSION_COOKIES_ENABLED:
            server.setAttribute(objectName, new Attribute
               ("cookies", new Boolean(true)));
            log.debug("Enabling session cookies");
            break;
         case WebMetaData.SESSION_COOKIES_DISABLED:
            server.setAttribute(objectName, new Attribute
               ("cookies", new Boolean(false)));
            log.debug("Disabling session cookies");
            break;
         default:
            log.debug("Using session cookies default setting");
      }

      // Add a valve to estalish the JACC context before authorization valves
      Certificate[] certs = null;
      CodeSource cs = new CodeSource(url, certs);
      JaccContextValve jaccValve = new JaccContextValve(metaData.getJaccContextID(), cs);
      server.invoke(objectName, "addValve",
         new Object[]{jaccValve},
         new String[]{"org.apache.catalina.Valve"});

      // Pass the metadata to the RunAsListener via a thread local
      RunAsListener.metaDataLocal.set(metaData);

      try
      {
         // Init the container; this will also start it
         server.invoke(objectName, "init", new Object[]{}, new String[]{});
      }
      finally
      {
         RunAsListener.metaDataLocal.set(null);
      }

      // make the context class loader known to the WebMetaData, ws4ee needs it
      // to instanciate service endpoint pojos that live in this webapp
      Loader ctxLoader = (Loader)server.getAttribute(objectName, "loader");
      metaData.setContextLoader(ctxLoader.getClassLoader());

      // Clustering
      if (metaData.getDistributable())
      {
         // Try to initate clustering, fallback to standard if no clustering is available
         try
         {
            AbstractJBossManager manager = null;
            String managerClassName = config.getManagerClass();
            Class managerClass = Thread.currentThread().getContextClassLoader().loadClass(managerClassName);
            manager = (AbstractJBossManager) managerClass.newInstance();

            if (manager instanceof JBossCacheManager)
            {        
               // TODO either deprecate snapshot mode or move its config
               // into jboss-web.xml.
               String snapshotMode = config.getSnapshotMode();
               int snapshotInterval = config.getSnapshotInterval();
               JBossCacheManager jbcm = (JBossCacheManager) manager;
               jbcm.setSnapshotMode(snapshotMode);
               jbcm.setSnapshotInterval(snapshotInterval);
            }
            
            String name = "//" + ((hostName == null) ? "localhost" : hostName) + ctxPath;
            manager.init(name, metaData, config.isUseJK(), config.isUseLocalCache());
            
            // Don't assign the manager to the context until all config
            // is done, or else the manager will be started without the config
            server.setAttribute(objectName, new Attribute("manager", manager));
            
            log.debug("Enabled clustering support for ctxPath=" + ctxPath);
         }
         catch (ClusteringNotSupportedException e)
         {
            // JBAS-3513 Just log a WARN, not an ERROR
            log.warn("Failed to setup clustering, clustering disabled. ClusteringNotSupportedException: " + 
                  e.getMessage());
         }
         catch (NoClassDefFoundError ncdf)
         {
            // JBAS-3513 Just log a WARN, not an ERROR
            log.debug("Classes needed for clustered webapp unavailable", ncdf);
            log.warn("Failed to setup clustering, clustering disabled. NoClassDefFoundError: " + 
                  ncdf.getMessage());            
         }
         catch(Throwable t)
         {
            // TODO consider letting this through and fail the deployment
            log.error("Failed to setup clustering, clustering disabled. Exception: ", t);
         }
      }

      /* Add security association valve after the authorization
      valves so that the authenticated user may be associated with the
      request thread/session.
      */
      SecurityAssociationValve valve = new SecurityAssociationValve(metaData,
         config.getSecurityManagerService());
      valve.setSubjectAttributeName(config.getSubjectAttributeName());
      server.invoke(objectName, "addValve",
         new Object[]{valve},
         new String[]{"org.apache.catalina.Valve"});

      // Retrieve the state, and throw an exception in case of a failure
      Integer state = (Integer) server.getAttribute(objectName, "state");
      if (state.intValue() != 1)
      {
         throw new DeploymentException("URL " + warUrl + " deployment failed");
      }

      appInfo.setAppData(objectName);

      // Create mbeans for the servlets
      DeploymentInfo di = webAppParser.getDeploymentInfo();
      di.deployedObject = objectName;
      ObjectName servletQuery = new ObjectName
         (config.getCatalinaDomain() + ":j2eeType=Servlet,WebModule="
         + objectName.getKeyProperty("name") + ",*");
      Iterator iterator = server.queryMBeans(servletQuery, null).iterator();
      while (iterator.hasNext())
      {
         di.mbeans.add(((ObjectInstance) iterator.next()).getObjectName());
      }

      log.debug("Initialized: " + appInfo + " " + objectName);

   }


   /**
    * Called as part of the undeploy() method template to ask the
    * subclass for perform the web container specific undeployment steps.
    */
   protected 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=" + shortWarUrlFromServerHome(warUrl));

      WebMetaData metaData = appInfo.getMetaData();
      String hostName = null;
      Iterator hostNames = metaData.getVirtualHosts();
      if (hostNames.hasNext())
      {
         hostName = hostNames.next().toString();
      }
      performUndeployInternal(hostName, warUrl, appInfo);
      while (hostNames.hasNext())
      {
         String additionalHostName = hostNames.next().toString();
         performUndeployInternal(additionalHostName, warUrl, appInfo);
      }

   }

   protected void performUndeployInternal(String hostName, String warUrl,
      WebApplication appInfo)
      throws Exception
   {

      WebMetaData metaData = appInfo.getMetaData();
      String ctxPath = metaData.getContextRoot();

      // If the server is gone, all apps were stopped already
      if (server == null)
         return;

      ObjectName objectName = new ObjectName(config.getCatalinaDomain()
         + ":j2eeType=WebModule,name=//" +
         ((hostName == null) ? "localhost" : hostName)
         + ctxPath + ",J2EEApplication=none,J2EEServer=none");

      if (server.isRegistered(objectName))
      {
         // Contexts should be stopped by the host already
         server.invoke(objectName, "destroy", new Object[]{},
            new String[]{});
      }

   }

   /**
    * Resolve the input virtual host names to the names of the configured Hosts
    * @param vhostNames Iterator<String> for the jboss-web/virtual-host elements 
    * @return Iterator<String> of the unique Host names
    * @throws Exception
    */ 
   protected synchronized Iterator mapVirtualHosts(Iterator vhostNames)
      throws Exception
   {
      if( vhostToHostNames.size() == 0 )
      {
         // Query the configured Host mbeans
         String hostQuery = config.getCatalinaDomain() + ":type=Host,*";
         ObjectName query = new ObjectName(hostQuery);
         Set hosts = server.queryNames(query, null);
         Iterator iter = hosts.iterator();
         while( iter.hasNext() )
         {
            ObjectName host = (ObjectName) iter.next();
            String name = host.getKeyProperty("host");
            if( name != null )
            {
               vhostToHostNames.put(name, name);
                String[] aliases = (String[])
                    server.invoke(host, "findAliases", null, null);
               int count = aliases != null ? aliases.length : 0;
               for(int n = 0;n < count; n ++)
               {
                  vhostToHostNames.put(aliases[n], name);
               }
            }
         }
      }

      // Map the virtual host names to the hosts
      HashSet hosts = new HashSet();
      while( vhostNames.hasNext() )
      {
         String vhost = (String) vhostNames.next();
         String host = (String) vhostToHostNames.get(vhost);
         if( host == null )
         {
            log.warn("Failed to map vhost: "+vhost);
            // This will cause a new host to be created
            host = vhost;
         }
         hosts.add(host);
      }
      return hosts.iterator();
   }

   private String findConfig(URL warURL) throws IOException
   {
      String result = null;
      // See if the warUrl is a dir or a file
      File warFile = new File(warURL.getFile());
      if (warURL.getProtocol().equals("file") && warFile.isDirectory() == true)
      {
         File webDD = new File(warFile, CONTEXT_CONFIG_FILE);
         if (webDD.exists() == true) result = webDD.getAbsolutePath();
      }
      else
      {
         ZipFile zipFile = new ZipFile(warFile);
         ZipEntry entry = zipFile.getEntry(CONTEXT_CONFIG_FILE);
         if (entry != null)
         {
            InputStream zipIS = zipFile.getInputStream(entry);
            byte[] buffer = new byte[512];
            int bytes;
            result = warFile.getAbsolutePath() + "-context.xml";
            FileOutputStream fos = new FileOutputStream(result);
            while ((bytes = zipIS.read(buffer)) > 0)
            {
               fos.write(buffer, 0, bytes);
            }
            zipIS.close();
            fos.close();
         }
         zipFile.close();
      }
      return result;
   }

    private String shortWarUrlFromServerHome(String warUrl)
    {
        String serverHomeUrl =  System.getProperty(org.jboss.system.server.ServerConfig.SERVER_HOME_URL);

        if (warUrl == null || serverHomeUrl == null)
            return warUrl;

        if (warUrl.startsWith(serverHomeUrl))
          return ".../" + warUrl.substring(serverHomeUrl.length());
        else
          return warUrl;
   }

}
