/*
 * JBoss, the OpenSource EJB server
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.deployment;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.StringTokenizer;

import javax.management.MBeanException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.management.RuntimeErrorException;
import javax.management.RuntimeMBeanException;

import org.jboss.util.ServiceMBeanSupport;


/**
 *   The AutoDeployer is used to automatically deploy applications or
 *   components thereof.
 *
 *   <p> It can be used on either .jar or .xml files. The AutoDeployer
 *   can be configured to "watch" one or more files. If they are
 *   updated they will be redeployed.
 *
 *   <p> If it is set to watch a directory instead of a single file,
 *   all files within that directory will be watched separately.
 *
 *   <p> When a file is to be deployed, the AutoDeployer will use the
 *   configured deployer to deploy it.
 *
 *   @see org.jboss.deployment.J2eeDeployer
 *   @author Rickard Öberg (rickard.oberg@telkel.com)
 *   @author Toby Allsopp (toby.allsopp@peace.com)
 *   @author Scott.Stark@jboss.org
 *   @version $Revision: 1.7.2.2 $
 */
public class AutoDeployer
   extends ServiceMBeanSupport
   implements AutoDeployerMBean, Runnable
{
   // Constants -----------------------------------------------------

   // Attributes ----------------------------------------------------

   // Callback to the JMX agent
   MBeanServer server;

   // in case more then one J2eeDeployers are available
   String deployerList = "";

   /** JMX names of the configured deployers */
   ObjectName[] deployerNames;
   /** DeploymentInfo check interval */
   int timeout = 3000;

   // The watch thread
   boolean running = false;

   // Watch these directories for new files
   ArrayList watchedDirectories = new ArrayList();

   // These URL's are being watched
   HashMap watchedURLs = new HashMap();
   
   // URL list
   String urlList = "";

   /** Filters, one per configured deployer, to decide which files are
       deployable and which should be ignored */
   FilenameFilter[] deployableFilters = null;

   // Static --------------------------------------------------------

   // Constructors --------------------------------------------------
   public AutoDeployer()
   {
      this("");
   }

   public AutoDeployer(String urlList)
   {
      this ("J2EE:service=J2eeDeployer", urlList);
   }

   public AutoDeployer(String _namedDeployer, String urlList)
   {
      setDeployers(_namedDeployer);
      setURLs(urlList);
   }

   public void setURLs(String urlList)
   {
      this.urlList = urlList;
   }

   public String getURLs()
   {
      return urlList;
   }

   /**
    * Gets the Timeout attribute of the AutoDeployer object
    *
    * @return The Timeout value
    */
   public int getTimeout()
   {
      return timeout;
   }

   /**
    * Sets the Timeout attribute of the AutoDeployer object
    *
    * @param to The new Timeout value
    */
   public void setTimeout(int to)
   {
      this.timeout = to;
   }


   public void setDeployers(String deployers)
   {
      this.deployerList = deployers;
   }

   public String getDeployers()
   {
      return deployerList;
   }

   // Public --------------------------------------------------------
   public void run()
   {
      do
      {
         // Sleep
         if (running)
         {
            try { Thread.sleep(timeout); } catch (InterruptedException e) {}
         }

         try
         {
            // Check directories - add new entries to list of files
            for (int i = 0; i < watchedDirectories.size(); i++)
            {
               File dir = (File)watchedDirectories.get(i);
               File[] files = dir.listFiles();
               for (int idx = 0; idx < files.length; idx++)
               {
                  URL fileURL = files[idx].toURL();

                  // Check if it's a deployable file
                  for (int j=0; j<deployerNames.length; ++j)
                  {
                     if (!deployableFilters[j].accept(null, fileURL.getFile()))
                        continue; // Was not deployable - skip it...
                     if( watchedURLs.containsKey(fileURL) == false )
                        watchedURLs.put(fileURL, new DeploymentInfo(fileURL));
                  }
               }
            }


            // undeploy removed jars
            Iterator iterator = watchedURLs.values().iterator();

            while (iterator.hasNext())
            {
               DeploymentInfo deployment = (DeploymentInfo) iterator.next();
               URL url = deployment.url;

               // if the url is a file that doesn't exist
               // TODO: real urls
               if (url.getProtocol().startsWith("file") && ! new File(url.getFile()).exists())
               {
                  // the file does not exist anymore. undeploy
                  log.info("Auto undeploy of "+url);
                  try
                  {
                     undeploy(url.toString(), deployment.deployerName);
                  }
                  catch (Exception e)
                  {
                     log.error("Undeployment failed", e);
                  }

                  // this should be the safe way to call watchedURLS.remove
                  iterator.remove();
               }
            }

            // Check watched URLs
            iterator = watchedURLs.values().iterator();
            while (iterator.hasNext())
            {
               DeploymentInfo deployment = (DeploymentInfo) iterator.next();

               // Get last modified timestamp
               long lm;
               if (deployment.watch.getProtocol().startsWith("file"))
               {
                  // Get timestamp of file from file system
                  lm = new File(deployment.watch.getFile()).lastModified();
               } else
               {
                  // Use URL connection to get timestamp
                  lm = deployment.watch.openConnection().getLastModified();
               }

               // Check old timestamp -- always deploy if first check
               if ((deployment.lastModified == 0) || (deployment.lastModified < lm))
               {
                  log.info("Auto deploy of "+deployment.url);
                  deployment.lastModified = lm;
                  try
                  {
                     deploy(deployment.url.toString(), deployment.deployerName);
                  } catch (Throwable e)
                  {
                     log.error("DeploymentInfo failed:"+deployment.url, e);
                     // DeploymentInfo failed - won't retry until updated
                  }
               }
            }
         } catch (Exception e)
         {
            e.printStackTrace(System.err);

				// Stop auto deployer
            running = false;
         }
      } while(running);
   }

   // ServiceMBeanSupport overrides ---------------------------------
   public String getName()
   {
      return "Auto deploy";
   }

   protected ObjectName getObjectName(MBeanServer server, ObjectName name)
      throws javax.management.MalformedObjectNameException
   {
      this.server = server;
      return name==null ? new ObjectName(OBJECT_NAME) : name;
   }

   protected void startService()
      throws Exception
   {
      // Save JMX names of configured deployers
      StringTokenizer deployers = new StringTokenizer(deployerList, ";");
      deployerNames = new ObjectName[deployers.countTokens()];
      deployableFilters = new FilenameFilter[deployerNames.length];
      for (int i=0; i<deployerNames.length && deployers.hasMoreTokens(); ++i)
      {
         String deployerName = deployers.nextToken().trim();
         try
         {
            deployerNames[i] = new ObjectName(deployerName);
         }
         catch (MalformedObjectNameException mfone)
         {
            log.warn("The string '" + deployerName + "'is not a valid " +
                        "object name - ignoring it.");
            continue;
         }

         // Ask the deployer for a filter to detect deployable files
         try
         {
            deployableFilters[i] = (FilenameFilter) server.invoke(
               deployerNames[i], "getDeployableFilter", new Object[0],
               new String[0]);
         }
         catch (ReflectionException re)
         {
            log.info("Deployer '" + deployerNames[i] + "' doesn't provide a " +
                    "filter - will try to deploy all files");
            deployableFilters[i] = new FilenameFilter()
               {
                  public boolean accept(File dir, String filename)
                  {
                     return true;
                  }
               };
         }
      }

      StringTokenizer urls = new StringTokenizer(urlList, ",");
      // Add URLs to list
      while (urls.hasMoreTokens())
      {
         String url = urls.nextToken().trim();

         // Check for deployment directories
         File urlFile = new File(url.startsWith ("file:") ? url.substring (5) : url);
         if( urlFile.isDirectory() )
         {
            try
            {
               File modFile = urlFile.getCanonicalFile();
               if( Util.hasDeploymentDescriptor(modFile) >= 0 )
               {
                  // This is an unpackaged J2EE module
                  URL modURL = modFile.toURL();
                  DeploymentInfo info = new DeploymentInfo(modURL);
                  watchedURLs.put(modURL, info);
                  log.info("Watching module directory: "+modFile);
               }
               else
               {
                  // This is a directory whose contents shall be checked
                  // for deployments
                  watchedDirectories.add(modFile);
                  log.info("Watching directory: "+modFile);
               }
            }
            catch (Exception e)
            {
               log.info("Cannot auto-deploy module: "+urlFile, e);
            }
         }
         else if (urlFile.exists()) // It's a file
         {
            try
            {
               File modFile = urlFile.getCanonicalFile();
               URL modURL = modFile.toURL();
               DeploymentInfo info = new DeploymentInfo(modURL);
               watchedURLs.put(modURL, info);
               log.info("Auto-deploying module archive: "+modFile);
            }
            catch (Exception e)
            {
               log.info("Cannot auto-deploy module: "+urlFile, e);
            }
         }
         else // It's a real URL (probably http:)
         {
            try
            {
               URL infoURL = new URL(url);
               watchedURLs.put(infoURL, new DeploymentInfo(infoURL));
            } catch (MalformedURLException e)
            {
               // Didn't work
               log.warn("Cannot auto-deploy "+url);
            }
         }
      }

      // Pre-deploy. This is done so that deployments available
      // on start of container is deployed ASAP
      run(); 

      // Start auto deploy thread
      running = true;
      new Thread(this, "AutoDeployer").start();
   }

   protected void stopService()
   {
   	// Stop auto deploy thread
      running = false;
      
      // Clear lists
      watchedDirectories.clear();
      watchedURLs.clear();
   }

   // Protected -----------------------------------------------------
   protected void deploy(String url, ObjectName deployerName)
      throws Exception
   {
      try
      {
         // Call the appropriate deployer through the JMX server
         server.invoke(deployerName, "deploy", new Object[] { url },
                       new String[] { "java.lang.String" });
      } catch (RuntimeMBeanException e)
      {
          throw e.getTargetException();
      } catch (MBeanException e)
      {
         throw e.getTargetException();
      } catch (RuntimeErrorException e)
      {
         throw e.getTargetError();
      }
   }

   protected void undeploy(String url, ObjectName deployerName)
      throws Exception
   {
      try
      {
         // Call the appropriate deployer through the JMX server
         server.invoke(deployerName, "undeploy", new Object[] { url },
                       new String[] { "java.lang.String" });
      } catch (MBeanException e)
      {
         throw e.getTargetException();
      } catch (RuntimeErrorException e)
      {
         throw e.getTargetError();
      }
   }

   // Inner classes -------------------------------------------------

	// This class holds info about a deployement, such as the URL and the last timestamp
   class DeploymentInfo
   {
      long lastModified;
      URL url;
      URL watch;
      ObjectName deployerName;

      DeploymentInfo(URL url)
         throws MalformedURLException
      {
         this.url = url;
         for (int i=0; i<deployableFilters.length; ++i)
         {
            if (deployableFilters[i].accept(null, url.getFile()))
            {
               watch = url;
               deployerName = deployerNames[i];
            }
         }
         if( watch == null )
            throw new MalformedURLException("Failed to find deployer for: "+url);

         // If this is a directory, get the descriptor to watch
         String protocol = watch.getProtocol();
         File watchDir = new File(watch.getFile());
         if( protocol.equals("file") && watchDir.isDirectory() )
         {
            String path = Util.getDeploymentDescriptor(watchDir);
            watch = new URL(watch, path);
         }
      }
   }
}
