/*
 * 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.varia.deployment;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import java.net.URL;
import java.net.MalformedURLException;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarFile;
import java.util.jar.JarEntry;

import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;

import org.jboss.deployment.DeploymentException;
import org.jboss.deployment.DeploymentInfo;
import org.jboss.deployment.MainDeployerMBean;
import org.jboss.deployment.SubDeployer;
import org.jboss.deployment.SubDeployerSupport;

import org.jboss.system.ServiceControllerMBean;
import org.jboss.system.server.ServerConfig;
import org.jboss.system.server.ServerConfigLocator;

import org.jboss.util.Counter;
import org.jboss.util.file.Files;
import org.jboss.util.file.JarUtils;
import org.jboss.mx.util.MBeanProxyExt;

import org.jboss.varia.deployment.convertor.Convertor;

/**
 * This is the deployer for other vendor's applications
 * with dynamic migration of vendor-specific DDs to
 * JBoss specific DDs.
 *
 * @see org.jboss.varia.deployment.convertor.Convertor
 *
 * @author <a href="mailto:andreas@jboss.org">Andreas Schaefer</a>
 * @version $Revision: 57210 $
 *
 * @jmx.mbean
 *    name="jboss.system:service=ServiceDeployer"
 *    extends="org.jboss.deployment.SubDeployerMBean"
 */
public class FoeDeployer
   extends SubDeployerSupport
   implements SubDeployer, FoeDeployerMBean
{
   // Attributes ----------------------------------------------------
   /** A proxy to the ServiceControllerDeployer. */
   private ServiceControllerMBean serviceController;

   /** The deployers scratch directory. */
   private File scratchDirectory;
   
   /** Contains the list of available converters */
   private List converterList = new ArrayList();
   
   /** an increment for tmp files */
   private final Counter id = Counter.makeSynchronized(new Counter(0));

   /** map of exploaded deployment destionation in scratch directoy by DeploymentInfo */
   private ThreadLocal destinationByDI = new ThreadLocal() {
      protected Object initialValue()
      {
         return new HashMap();
      }
   };

   // SubDeployerSupport overrides ----------------------------------
   /**
    * Returns true if the there is a converter available to convert
    * the deployment unit.
    *
    * @jmx.managed-operation
    */
   public boolean accepts(DeploymentInfo di)
   {
      // delegate accepts to convertors
      Iterator i = converterList.iterator();
      while(i.hasNext())
      {
         Convertor converter = (Convertor)i.next();
         if(converter.accepts(di.url))
         {
            return true;
         }
      }
      return false;
   }

   /**
    * Returns true if the there is a converter available to convert
    * the deployment unit.
    */
   public boolean accepts(URL url)
   {
      // delegate accepts to convertors
      Iterator i = converterList.iterator();
      while(i.hasNext())
      {
         Convertor converter = (Convertor)i.next();
         if(converter.accepts(url))
         {
            return true;
         }
      }
      return false;
   }

   /**
    * At the init phase the deployment unit and its subdeployment units are unpacked.
    * @jmx.managed-operation
    */
   public void init(DeploymentInfo di)
      throws DeploymentException
   {
      // Determine the destination for unpacking and save it in the ThreadLocal
      Map destinations = (Map)destinationByDI.get();
      File destination = (File)destinations.get(di.parent);
      if(destination == null)
      {
         // Loop until for new destination
         while(destination == null || destination.exists())
            destination = new File(scratchDirectory, id.increment() + "." + di.shortName);
      }
      else
      {
         destination = new File(destination, di.shortName);
      }
      destinations.put(di, destination);
      destinationByDI.set(destinations);

      try
      {
         log.debug("unpacking to " + destination);
         inflateJar(di.localUrl, destination);
      }
      catch(Exception e)
      {
         throw new DeploymentException("Unpacking failed: ", e);
      }

      // invoke super class' initialization
      super.init(di);
   }
   
   /**
    * At the create phase, the conversion and packing is done.
    * @jmx.managed-operation
    */
   public void create(DeploymentInfo di)
      throws DeploymentException
   {
      try
      {
         // fetch the destionation of unpacked deployment from ThreadLocal
         Map destinations = (Map)destinationByDI.get();
         File inflateDest = (File)destinations.get(di);

         // Look for the converter that accepts vendor specific deployment descriptors
         // and let it convert them
         Iterator i = converterList.iterator();
         while(i.hasNext())
         {
            Convertor converter = (Convertor)i.next();
            if(converter.accepts(di.url))
            {
               // Convert them to JBoss specific DDs
               converter.convert(di, inflateDest);
               // Now conversion is done and we can leave
               break;
            }
         }

         // deflate
         File deflateDest = (File)destinations.get(di.parent);
         if(deflateDest == null)
            deflateDest = scratchDirectory;
         String validName = null;
         if(di.shortName.endsWith(".wl"))
            validName = di.shortName.substring(0, di.shortName.length()-3);
         else
            validName = di.shortName.substring( 0, di.shortName.length() - 4 ) + "jar";
         File convertedUnit = new File(deflateDest, validName);
         log.debug("deflating to " + convertedUnit);
         deflateJar(convertedUnit, inflateDest);

         // remove unpacked deployment unit
         Files.delete(inflateDest);

         // copy the converted app back to the deployment directory
         if(di.parent == null)
            copyFile(convertedUnit, new File(di.url.getFile()).getParentFile());
      }
      catch(Exception e)
      {
         log.error("Conversion error: ", e);
      }
   }

   /**
    * This method stops this deployment because it is not of any
    * use anymore (conversion is done)
    * @jmx.managed-operation
    */
   public void start(DeploymentInfo di)
      throws DeploymentException
   {
      stop(di);
      destroy(di);
   }

   /**
    * @jmx.managed-operation
    */
   public void stop(DeploymentInfo di)
   {
      log.debug("undeploying application: " + di.url);
   }

   /**
    * @jmx.managed-operation
    */
   public void destroy(DeploymentInfo di)
   {
      List services = di.mbeans;
      int lastService = services.size();
      for(ListIterator i = services.listIterator(lastService); i.hasPrevious();)
      {
         ObjectName name = (ObjectName)i.previous();
         log.debug( "destroying mbean " + name );
         try
         {
            serviceController.destroy(name);
         }
         catch(Exception e)
         {
            log.error("Could not destroy mbean: " + name, e);
         }
      }

      for(ListIterator i = services.listIterator(lastService); i.hasPrevious();)
      {
         ObjectName name = (ObjectName)i.previous();
         log.debug("removing mbean " + name);
         try
         {
            serviceController.remove( name );
         }
         catch(Exception e)
         {
            log.error("Could not remove mbean: " + name, e);
         }
      }
   }

   /**
    * This method is called in SubDeployerSupport.processNestedDeployments()
    * The method is overriden to deploy the deployments acceptable by FoeDeployer only.
    */
   protected void addDeployableJar(DeploymentInfo di, JarFile jarFile)
      throws DeploymentException
   {
      String urlPrefix = "jar:" + di.localUrl.toString() + "!/";
      for(Enumeration e = jarFile.entries(); e.hasMoreElements();)
      {
         JarEntry entry = (JarEntry)e.nextElement();
         String name = entry.getName();
         try
         {
            URL url = new URL(urlPrefix + name);
            if(isDeployable(name, url))
            {
               // Obtain a jar url for the nested jar
               // Append the ".wl" suffix to prevent other than FoeDeployer deployers'
               // attempts to deploy the deployment unit
               URL nestedURL = JarUtils.extractNestedJar(url, this.tempDeployDir);
               File file = new File(nestedURL.getFile());
               File wlFile = new File(nestedURL.getFile() + ".wl");
               file.renameTo(wlFile);

               if(accepts(wlFile.toURL()))
               {
                  deployUrl(di, wlFile.toURL(), name + ".wl");
               }
               else
               {
                  // if the deployment isn't accepted rename it back
                  wlFile.renameTo(new File(nestedURL.getFile()));
               }
            }
         }
         catch(MalformedURLException mue)
         {
            log.warn("Jar entry invalid; ignoring: " + name, mue);
         }
         catch(IOException ex)
         {
            log.warn("Failed to extract nested jar; ignoring: " + name, ex);
         }
      }
   }

   /**
    * The startService method
    * - gets the mbeanProxies for MainDeployer and ServiceController;
    * - creates scratch directory for foe work.
    *
    * @exception Exception if an error occurs
    */
   protected void startService()
      throws Exception
   {
      mainDeployer = (MainDeployerMBean) MBeanProxyExt.create(
         MainDeployerMBean.class,
         MainDeployerMBean.OBJECT_NAME,
         server
      );

      // get the controller proxy
      serviceController = (ServiceControllerMBean) MBeanProxyExt.create(
         ServiceControllerMBean.class,
         ServiceControllerMBean.OBJECT_NAME,
         server
      );

      ServerConfig config = ServerConfigLocator.locate();

      // build the scratch directory
      File tempDirectory = config.getServerTempDir();
      scratchDirectory = new File(tempDirectory, "foe");
      if(!scratchDirectory.exists())
         scratchDirectory.mkdirs();

      // Note: this should go the last.
      // scratch directory must be created before this call
      super.startService();
   }

   /**
    * Returns the ObjectName
    */
   protected ObjectName getObjectName(MBeanServer server, ObjectName name)
      throws MalformedObjectNameException
   {
      return name == null ? OBJECT_NAME : name;
   }

   // FoeDeployerMBean implementation -------------------------------
   /**
    * Add a new conveter to the list. If the same converter is
    * added, this new one won't be added, meaning everything stays the same.
    * This method is normally called by a Converter to be
    * called by this deployer to convert.
    *
    * @param converter New Converter to be added
    *
    * @jmx.managed-operation
    */
   public void addConvertor(Convertor converter)
   {
      converterList.add(converter);

      // try to deploy waiting deployment units
      // note: there is no need to synchronize, because MainDeployer
      // returns a copy of waiting deployments
      Collection waitingDeployments = mainDeployer.listWaitingForDeployer();
      if((waitingDeployments != null) && (waitingDeployments.size() > 0))
      {
         for( Iterator iter = waitingDeployments.iterator(); iter.hasNext(); )
         {
            DeploymentInfo di = (DeploymentInfo)iter.next();

            // check whether the converter accepts the deployment
            if(!converter.accepts(di.url))
               continue;
            
            log.debug("trying to deploy with new converter: " + di.shortName);
            try 
            {
               mainDeployer.redeploy(di);
            }
            catch (DeploymentException e)
            {
               log.error("DeploymentException while trying to deploy a package with new converter", e);
            }
         }
      }
   }

   /**
    * Removes a conveter from the list of converters. If the
    * converter does not exist nothing happens.
    * This method is normally called by a Converter to be removed
    * from the list if not serving anymore.
    *
    * @param converter Conveter to be removed from the list
    *
    * @jmx.managed-operation
    */
   public void removeConvertor(Convertor converter)
   {
      converterList.remove(converter);
   }

   // Private --------------------------------------------------------
   /**
    * The <code>inflateJar</code> copies the jar entries
    * from the jar url jarUrl to the directory destDir.
    *
    * @param fileURL URL pointing to the file to be inflated
    * @param destinationDirectory Directory to which the content shall be inflated to
    *
    * @exception DeploymentException if an error occurs
    * @exception IOException if an error occurs
    */
   protected void inflateJar( URL fileURL, File destinationDirectory )
      throws DeploymentException, IOException
   {
      File destFile = new File(fileURL.getFile());
      InputStream input = new FileInputStream(fileURL.getFile());
      JarUtils.unjar(input, destinationDirectory);
      // input is closed in unjar();
   }

   /**
    * Deflate a given directory into a JAR file
    *
    * @param jarFile The JAR file to be created
    * @param root Root directory of the files to be included (this directory
    *             will not be included in the path of the JAR content)
    **/
   private void deflateJar( File jarFile, File root )
      throws Exception
   {
      OutputStream output = new FileOutputStream(jarFile);
      JarUtils.jar(output, root.listFiles(), null, null, null);
      output.close();
   }

   /**
    * Copies the given File to a new destination with the same name
    *
    * @param source The source file to be copied
    * @param destinationDirectory File pointing to the destination directory
    **/
   private void copyFile(File source, File destinationDirectory)
      throws Exception
   {
      File target = new File( destinationDirectory, source.getName() );
      // Move may fail if target is used (because it is deployed)
      // Use Files.copy instead
      Files.copy(source, target);
   }
}
