/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, 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.deployment.services;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.enterprise.deploy.shared.ModuleType;
import javax.enterprise.deploy.spi.exceptions.TargetException;
import javax.management.MBeanServer;
import javax.management.ObjectName;

import org.jboss.dependency.spi.Controller;
import org.jboss.deployers.client.spi.Deployment;
import org.jboss.deployers.plugins.main.MainDeployerImpl;
import org.jboss.deployers.spi.DeploymentState;
import org.jboss.deployers.structure.spi.DeploymentContext;
import org.jboss.deployers.structure.spi.DeploymentUnit;
import org.jboss.deployers.vfs.spi.client.VFSDeploymentFactory;
import org.jboss.deployment.spi.SerializableTargetModuleID;
import org.jboss.logging.Logger;
import org.jboss.mx.util.MBeanServerLocator;
import org.jboss.system.metadata.ServiceMetaData;
import org.jboss.util.file.Files;
import org.jboss.vfs.VFS;
import org.jboss.vfs.VirtualFile;

/**
 * A service that supports the JSR-88 DeploymentManager operations.
 * 
 * @author Scott.Stark@jboss.org
 * @author adrian@jboss.org
 * @version $Revision: 102463 $
 */
public class DeploymentManagerService
   implements DeploymentManagerServiceMBean
{
   private static Logger log = Logger.getLogger(DeploymentManagerService.class);

   /** The deployment factory */
   VFSDeploymentFactory deploymentFactory = VFSDeploymentFactory.getInstance();
   
   /**
    * Map<deployURL, TargetID> of the deployment modules.
    */
   private Map<String, SerializableTargetModuleID> moduleMap =
      Collections.synchronizedMap(new HashMap<String, SerializableTargetModuleID>());

   /**
    * The type of attachment used to identify a CAR
    */
   private Class carDeployerType;
   /**
    * The type of attachment used to identify a EAR
    */
   private Class earDeployerType;
   /**
    * The type of attachment used to identify a EJB
    */
   private Class ejbDeployerType;
   private Class ejb3DeployerType;
   /**
    * The type of attachment used to identify a RAR
    */
   private Class rarDeployerType;
   /**
    * The type of attachment used to identify a WAR
    */
   private Class warDeployerType;
   /**
    * The MainDeployer
    */
   private MainDeployerImpl mainDeployer;

   /**
    * The controller
    */
   private Controller controller;

   /**
    * The local directory for uploaded content.
    */
   private File uploadDir;
   private boolean failOnCollision;
   private boolean deleteOnUndeploy;
   
   /**
    * Get the deployment name
    * 
    * @param file the file
    * @return the name;
    */
   public static String getDeploymentName(VirtualFile file)
   {
      if (file == null)
         throw new IllegalArgumentException("Null file");
      try
      {
         URI uri = file.asFileURI();   // gets the file path without trailing slash
         return uri.toString();
      }
      catch (Exception e)
      {
         throw new IllegalArgumentException("File does not have a valid uri: " + file, e);
      }
   }

   public MainDeployerImpl getMainDeployer()
   {
      return mainDeployer;
   }
   public void setMainDeployer(MainDeployerImpl mainDeployer)
   {
      this.mainDeployer = mainDeployer;
   }

   public Controller getController()
   {
      return controller;
   }

   public void setController(Controller controller)
   {
      this.controller = controller;
   }

   public Class getCarDeployerType()
   {
      return carDeployerType;
   }
   public void setCarDeployerType(Class carDeployerType)
   {
      this.carDeployerType = carDeployerType;
   }
   public Class getEarDeployerType()
   {
      return earDeployerType;
   }
   public void setEarDeployerType(Class earDeployerType)
   {
      this.earDeployerType = earDeployerType;
   }
   public Class getEjbDeployerType()
   {
      return ejbDeployerType;
   }
   public void setEjbDeployerType(Class ejbDeployerType)
   {
      this.ejbDeployerType = ejbDeployerType;
   }

   public Class getEjb3DeployerType()
   {
      return ejb3DeployerType;
   }
   public void setEjb3DeployerType(Class ejb3DeployerType)
   {
      this.ejb3DeployerType = ejb3DeployerType;
   }

   public Class getRarDeployerType()
   {
      return rarDeployerType;
   }
   public void setRarDeployerType(Class rarDeployerType)
   {
      this.rarDeployerType = rarDeployerType;
   }
   public Class getWarDeployerType()
   {
      return warDeployerType;
   }
   public void setWarDeployerType(Class warDeployerType)
   {
      this.warDeployerType = warDeployerType;
   }

   public void setModuleMap(Map<String, SerializableTargetModuleID> moduleMap)
   {
      this.moduleMap = moduleMap;
   }
   public File getUploadDir()
   {
      return this.uploadDir;
   }

   public void setUploadDir(File uploadDir)
   {
      this.uploadDir = uploadDir;
   }

   public boolean isDeleteOnUndeploy()
   {
      return deleteOnUndeploy;
   }

   public void setDeleteOnUndeploy(boolean deleteOnUndeploy)
   {
      this.deleteOnUndeploy = deleteOnUndeploy;
   }

   public boolean isFailOnCollision()
   {
      return failOnCollision;
   }

   public void setFailOnCollision(boolean failOnCollision)
   {
      this.failOnCollision = failOnCollision;
   }

   public Map getModuleMap()
   {
      return Collections.unmodifiableMap(moduleMap);
   }

   public void deploy(SerializableTargetModuleID moduleID) throws Exception
   {
      String url = moduleID.getModuleID();

      // Create a status object
      URL deployURL = new URL(url);
      int contentLength = deployURL.openConnection().getContentLength();
      log.debug("Begin deploy, url: " + deployURL + ", contentLength: " + contentLength);

      // Create the local path
      File path = new File(deployURL.getFile());
      String archive = path.getName();
      File deployFile = new File(uploadDir, archive);
      if (failOnCollision && deployFile.exists())
         throw new IOException("deployURL(" + deployURL + ") collides with: " + deployFile.getPath());

      if (deployFile.exists())
         Files.delete(deployFile);

      File parentFile = deployFile.getParentFile();
      if (parentFile.exists() == false)
      {
         if (parentFile.mkdirs() == false)
            throw new IOException("Failed to create local path: " + parentFile);
      }

      InputStream is = moduleID.getContentIS();
      if( is == null )
      {
         is = deployURL.openStream();
      }
      BufferedInputStream bis = new BufferedInputStream(is);
      byte[] buffer = new byte[4096];
      FileOutputStream fos = new FileOutputStream(deployFile);
      BufferedOutputStream bos = new BufferedOutputStream(fos);
      int count = 0;
      while ((count = bis.read(buffer)) > 0)
      {
         bos.write(buffer, 0, count);
      }
      bis.close();
      bos.close();

      moduleMap.put(url, moduleID);

      // TODO: arrange deploy_phase2 (and TC glue code).
      deploy_phase2(url);
   }

   /* Invoke the method on the contexts */
   public void hack(SerializableTargetModuleID moduleID,
         DeploymentContext info, String method)
   {
      try {
         MBeanServer server = MBeanServerLocator.locateJBoss();
         String objNames = getComponentName(info);

         String contexts = null;
         if (objNames != null)
         {
           String token = ":war=";
           int i = objNames.indexOf(token);
           if (i == -1)
             return;
           i = i + token.length();
           contexts = objNames.substring(i);
         }

         String onStr = "*:j2eeType=WebModule,*,J2EEApplication=none,J2EEServer=none";
         ObjectName objectName = new ObjectName(onStr);
         Set mbeans = server.queryNames(objectName, null);
         if (mbeans != null && mbeans.size () > 0 && contexts != null)
         {
            for (Iterator iter = mbeans.iterator(); iter.hasNext();)
            {
               ObjectName inst = (ObjectName) iter.next();
               String name = inst.toString();
               String token = ".*" + contexts + ".*";
               if (name.matches(token))
               {
                   server.invoke(inst, method, new Object[] {}, new String[] {});
               }
            }
         }


      } catch (Throwable e)
      {
         log.info("hack: failed" + e);
      }
   }
   public void start(String url) throws Exception
   {
      SerializableTargetModuleID moduleID = (SerializableTargetModuleID)moduleMap.get(url);
      if (moduleID == null)
         throw new IOException("deployURL(" + url + ") has not been distributed");

      if (moduleID.isRunning()) {
         log.error("start, url: " + url + " Already started");
         return;
      }
      URL deployURL = new URL(url);
      // Build the local path
      File path = new File(deployURL.getFile());
      String archive = path.getName();
      File deployFile = new File(uploadDir, archive);

      VirtualFile root = VFS.getChild(deployFile.toURI());
      Deployment deployment = mainDeployer.getDeployment(getDeploymentName(root));

      /* TODO: that is a hack */
      if (deployment == null)
      {
         deployment = deploymentFactory.createVFSDeployment(getDeploymentName(root), root);
         mainDeployer.addDeployment(deployment);
         mainDeployer.process();
         moduleID.setRunning(true);
         moduleID.clearChildModuleIDs();
         // Repopulate the child modules
         DeploymentContext context = mainDeployer.getDeploymentContext(deployment.getName()); 
         fillChildrenTargetModuleID(moduleID, context);
      }

      // hack(moduleID, context, "start");
   }
   /*
    * Get the component name.
    */
   private String getComponentName(DeploymentContext context)
   {
      if (context == null)
         return null;

      DeploymentUnit u = context.getDeploymentUnit();
      ServiceMetaData data = u.getAttachment(ServiceMetaData.class);
      if (data == null)
         return null;
      ObjectName objectName = data.getObjectName();
      return objectName.toString();
   }

   public void deploy_phase2(String url) throws Exception
   {
      SerializableTargetModuleID moduleID = moduleMap.get(url);
      if (moduleID == null)
         throw new IOException("deployURL(" + url + ") has not been distributed");
      if (moduleID.isRunning())
      {
         return;
      }

      URL deployURL = new URL(url);
      // Build the local path
      File path = new File(deployURL.getFile());
      String archive = path.getName();
      File deployFile = new File(uploadDir, archive);
      if (deployFile.exists() == false)
         throw new IOException("deployURL(" + url + ") has no local archive");

      VirtualFile root = VFS.getChild(deployFile.toURI());
      Deployment deployment = deploymentFactory.createVFSDeployment(getDeploymentName(root), root);
      mainDeployer.addDeployment(deployment);
      DeploymentContext context = null;
      try
      {
         mainDeployer.process();
         context = mainDeployer.getDeploymentContext(deployment.getName());
         mainDeployer.checkComplete(deployment);
      }
      catch (Exception e)
      {
         /* destroy the context */
         hack(moduleID, context, "destroy");
         /* clear the child */
         moduleID.clearChildModuleIDs();
         /* remove the deployment */
         mainDeployer.removeDeployment(getDeploymentName(root));
         mainDeployer.process();

         /* remove the map */
         if (deleteOnUndeploy)
           Files.delete(deployFile);
 
         moduleMap.remove(url);
         throw e;
      }

      moduleID.setRunning(true);
      moduleID.clearChildModuleIDs();
      // Repopulate the child modules
      fillChildrenTargetModuleID(moduleID, context);

      // TODO: invoke the start.
      // hack(moduleID, context, "start");
         
   }

   public void stop(String url) throws Exception
   {
      SerializableTargetModuleID moduleID = (SerializableTargetModuleID)moduleMap.get(url);
      if (moduleID == null)
         throw new IOException("deployURL(" + url + ") has not been distributed");

      URL deployURL = new URL(url);
      // Build the local path
      File path = new File(deployURL.getFile());
      String archive = path.getName();
      File deployFile = new File(uploadDir, archive);
      if (deployFile.exists() == false)
         throw new IOException("deployURL(" + url + ") has no local archive");

      VirtualFile root = VFS.getChild(deployFile.toURI());
      mainDeployer.removeDeployment(getDeploymentName(root));
      mainDeployer.process();
      moduleID.setRunning(false);
   }

   public void undeploy(String url) throws Exception
   {
      SerializableTargetModuleID moduleID = (SerializableTargetModuleID)moduleMap.get(url);
      if (moduleID == null)
         return;

      // If the module is still running, stop it
      if (moduleID.isRunning())
         stop(url);

      URL deployURL = new URL(url);
      // Build the local path
      File path = new File(deployURL.getFile());
      String archive = path.getName();
      File deployFile = new File(uploadDir, archive);
      if (deployFile.exists() == false)
         throw new IOException("deployURL(" + url + ") has not been distributed");

      if (deleteOnUndeploy)
         Files.delete(deployFile);

      moduleMap.remove(url);
   }

   public SerializableTargetModuleID[] getAvailableModules(int moduleType)
      throws TargetException
   {
      ArrayList<SerializableTargetModuleID> matches = new ArrayList<SerializableTargetModuleID>();
      Iterator<SerializableTargetModuleID> modules = moduleMap.values().iterator();
      while (modules.hasNext())
      {
         SerializableTargetModuleID module = modules.next();
         if (module.getModuleType() == moduleType)
            matches.add(module);
      }

      SerializableTargetModuleID[] ids = new SerializableTargetModuleID[matches.size()];
      matches.toArray(ids);
      return ids;
   }

   /**
    * Travers the deployment children to build up the jsr88 module children 
    * @param moduleID
    * @param info
    */
   private void fillChildrenTargetModuleID(SerializableTargetModuleID moduleID,
         DeploymentContext info)
   {
      List<DeploymentContext> children = info.getChildren();
      for(DeploymentContext ctx : children)
      {
         try
         {
            ModuleType type = getModuleType(ctx);
            // Discard unknown ModuleTypes
            if (type != null)
            {
               String module = ctx.getName();
               // TODO, DEPLOYED may not be the same as running?
               boolean isRunning = ctx.getState() == DeploymentState.DEPLOYED;
               SerializableTargetModuleID child = new SerializableTargetModuleID(moduleID, module, type.getValue(), isRunning);
               moduleID.addChildTargetModuleID(child);
               fillChildrenTargetModuleID(child, ctx);
            }
         }
         catch (UnsupportedOperationException e)
         {
            if (log.isTraceEnabled())
               log.trace("Ignoring", e);
         }
      }
   }

   /**
    * Determine the module type by looking for an attachment that identifies
    * the deployer type.
    * 
    * @param info
    * @return
    */
   private ModuleType getModuleType(DeploymentContext info)
   {
      ModuleType type = null;
      DeploymentUnit unit = info.getDeploymentUnit();

      if (unit.getAttachment(carDeployerType) != null)
      {
         type = ModuleType.CAR;
      }
      else if (unit.getAttachment(ejbDeployerType) != null
            || unit.getAttachment(ejb3DeployerType) != null)
      {
         type = ModuleType.EJB;
      }
      else if (unit.getAttachment(earDeployerType) != null)
      {
         type = ModuleType.EAR;
      }
      else if (unit.getAttachment(rarDeployerType) != null)
      {
         type = ModuleType.RAR;
      }
      else if (unit.getAttachment(warDeployerType) != null)
      {
         type = ModuleType.WAR;
      }

      return type;
   }

}
