/*
 * Created on 2004-6-12
 *
 * To change the template for this generated file go to
 * Window - Preferences - Java - Code Generation - Code and Comments
 */
package org.huihoo.willow.core;

import java.io.File;
import java.io.IOException;
import java.net.URL;

import org.apache.commons.digester.Digester;
import org.huihoo.willow.Container;
import org.huihoo.willow.Context;
import org.huihoo.willow.EngineDeployer;
import org.huihoo.willow.Engine;
import org.huihoo.willow.Globals;
import org.huihoo.willow.Lifecycle;
import org.huihoo.willow.LifecycleException;
import org.huihoo.willow.LifecycleListener;
import org.huihoo.willow.startup.ContextRuleSet;
import org.huihoo.willow.util.StringManager;
import org.huihoo.willow.util.WillowDigester;


/**
 * @author reic
 *
 * To change the template for this generated type comment go to
 * Window - Preferences - Java - Code Generation - Code and Comments
 */
public class StandardEngineDeployer extends ContainerBase implements EngineDeployer
{

	private static org.apache.commons.logging.Log log =
		org.apache.commons.logging.LogFactory.getLog(StandardEngineDeployer.class);

	// ----------------------------------------------------------- Constructors

	public StandardEngineDeployer()
	{
	}

	/**
	 * Create a new StandardEngineDeployer associated with the specified
	 * StandardEngine.
	 *
	 * @param engine The StandardEngine we are associated with
	 */
	public StandardEngineDeployer(StandardEngine engine)
	{

		super();
		this.engine = engine;

	}

	// ----------------------------------------------------- Instance Variables

	/**
	 * The <code>ContextRuleSet</code> associated with our
	 * <code>digester</code> instance.
	 */
	private ContextRuleSet contextRuleSet = null;

	/**
	* The <code>Digester</code> instance to use for deploying workflow applications
	* to this <code>Host</code>.  <strong>WARNING</strong> - Usage of this
	* instance must be appropriately synchronized to prevent simultaneous
	* access by multiple threads.
	*/
	private Digester digester = null;

	/**
	 * The <code>StandardEngine</code> instance we are associated with.
	 */
	protected StandardEngine engine = null;

	/**
	 * The string manager for this package.
	 */
	protected static StringManager sm = StringManager.getManager(Constants.PACKAGE);

	// -------------------------------------------------------- Depoyer Methods

	public Engine getEngine()
	{
		return engine;
	}

	public void setEngine(Engine engine)
	{
		this.engine = (StandardEngine) engine;
	}

	/**
	 * Return the name of the Container with which this EngineDeployer is associated.
	 */
	public String getName()
	{

		return (engine.getName());

	}

	/**
	 * Install a new  application, whose workflow application archive is at the
	 * specified URL, into this container with the specified app name.
	 * <p>
	 * If this application is successfully installed, a ContainerEvent of type
	 * <code>PRE_INSTALL_EVENT</code> will be sent to registered listeners
	 * before the associated Context is started, and a ContainerEvent of type
	 * <code>INSTALL_EVENT</code> will be sent to all registered listeners
	 * after the associated Context is started, with the newly created
	 * <code>Context</code> as an argument.
	 *
	 * @param contextName The app name to which this application should
	 *  be installed (must be unique)
	 * @param dirURL A URL of  type "file:" that points to an unpacked 
	 *  directory structure containing  the workflow application to be installed
	 *
	 * @exception IllegalArgumentException if the specified app name  is malformed 
	 * @exception IllegalStateException if the specified app name
	 *  is already attached to an existing workflow application
	 * @exception IOException if an input/output error was encountered
	 *  during installation
	 */
	public synchronized void install(String contextName, URL dirURL) throws IOException
	{
		// Validate the format and state of our arguments
		if (contextName == null)
		{
			throw new IllegalArgumentException(sm.getString("standardEngine.nameRequired"));
		}

		if (findDeployedApp(contextName) != null)
		{
			throw new IllegalStateException(sm.getString("standardEngine.nameUsed", contextName));
		}
		if (dirURL == null)
		{
			throw new IllegalArgumentException(sm.getString("standardEngine.dirRequired"));
		}

		// Calculate the document base for the new workflow application
		log.info(sm.getString("standardEngine.installing", contextName, dirURL.toString()));
		String url = dirURL.toString();
		String appBase = null;

		if (url.startsWith("file://"))
		{
			appBase = url.substring(7);
		}
		else if (url.startsWith("file:"))
		{
			appBase = url.substring(5);
		}
		else
		{
			throw new IllegalArgumentException(sm.getString("standardEngine.dirURL", url));
		}

		// Determine if directory to install is in the engine engineBase
		boolean isEngineBase = false;
		
		File engineBase = new File(engine.getEngineBase());
		if (!engineBase.isAbsolute())
		{
			engineBase = new File(System.getProperty(Globals.PROPS_WILLOW_HOME), engine.getEngineBase());
		}
		
		File contextFile = new File(appBase);
		File baseDir = contextFile.getParentFile();
		if (engineBase.getCanonicalPath().equals(baseDir.getCanonicalPath()))
		{
			isEngineBase = true;
		}

		// Make sure contextName and directory names match when
		// installing from the engine engineBase
		if (isEngineBase && engine.getAutoDeploy())
		{
			String filename = contextFile.getName();

			if(!filename.equals(contextName))
			{
				throw new IllegalArgumentException(sm.getString("standardEngine.nameMatch", contextName, filename));
			}
		}



		// Install the new workflow application
		try
		{
			Class clazz = Class.forName(engine.getContextClass());
			Context context = (Context) clazz.newInstance();
			context.setName(contextName);
			context.setAppBase(appBase);
			if (context instanceof Lifecycle)
			{
				clazz = Class.forName(engine.getConfigClass());
				LifecycleListener listener = (LifecycleListener) clazz.newInstance();
				((Lifecycle) context).addLifecycleListener(listener);
			}
			engine.fireContainerEvent(PRE_INSTALL_EVENT, context);
			engine.addChild(context);
			engine.fireContainerEvent(INSTALL_EVENT, context);
		}
		catch (ClassNotFoundException e)
		{
			log.info("", e);
		}
		catch (Exception e)
		{
			log.info("Error installing", e);
			throw new IOException(e.toString());
		}

	}

	/**
	 * Return the Context for the deployed application that is associated
	 * with the specified app name  (if any); otherwise return
	 * <code>null</code>.
	 *
	 * @param contextName The app name  of the requested workflow application
	 */
	public Context findDeployedApp(String contextName)
	{

		return ((Context) engine.findChild(contextName));

	}

	/**
	 * Return the app name s of all deployed workflow applications in this
	 * Container.  If there are no deployed applications, a zero-length
	 * array is returned.
	 */
	public String[] findDeployedApps()
	{

		Container children[] = engine.findChildren();
		String results[] = new String[children.length];
		for (int i = 0; i < children.length; i++)
		{	
			results[i] = children[i].getName();
		}
		return (results);

	}

	/**
	 * Remove an existing workflow application, attached to the specified app
	 * name.  If this application is successfully removed, a
	 * ContainerEvent of type <code>REMOVE_EVENT</code> will be sent to all
	 * registered listeners, with the removed <code>Context</code> as
	 * an argument.
	 *
	 * @param contextName The app name of the application to be removed
	 *
	 * @exception IllegalArgumentException if the specified app name is malformed 
	 * @exception IllegalArgumentException if the specified app name does
	 *  not identify a currently installed workflow application
	 * @exception IOException if an input/output error occurs during
	 *  removal
	 */
	public void remove(String contextName) throws IOException
	{

		// Validate the format and state of our arguments
		if (contextName == null)
		{	
			throw new IllegalArgumentException(sm.getString("standardEngine.nameRequired"));			
		}
		// Locate the context and associated work directory
		Context context = findDeployedApp(contextName);
		if (context == null)
			throw new IllegalArgumentException(sm.getString("standardEngine.nameMissing", contextName));

		// Remove this workflow application
		log.info(sm.getString("standardEngine.removing", contextName));
		try
		{
			engine.removeChild(context);
			engine.fireContainerEvent(REMOVE_EVENT, context);
		}
		catch (Exception e)
		{
			log.error(sm.getString("standardEngine.removeError", contextName), e);
			throw new IOException(e.toString());
		}

	}

	/**
	 * Remove an existing workflow application, attached to the specified app
	 * name.  If this application is successfully removed, a
	 * ContainerEvent of type <code>REMOVE_EVENT</code> will be sent to all
	 * registered listeners, with the removed <code>Context</code> as
	 * an argument. Deletes the workflow application war file and/or directory
	 * if they exist in the Host's appBase.
	 *
	 * @param contextName The application name of the application to be removed
	 * @param undeploy boolean flag to remove workflow application from server
	 *
	 * @exception IllegalArgumentException if the specified app name
	 * @exception IllegalArgumentException if the specified app name does
	 *  not identify a currently installed workflow application
	 * @exception IOException if an input/output error occurs during
	 *  removal
	 */
	public void remove(String contextName, boolean undeploy) throws IOException
	{

		// Validate the format and state of our arguments
		if (contextName == null)
		{
			throw new IllegalArgumentException(sm.getString("standardEngine.nameRequired"));
		}

		// Locate the context and associated work directory
		Context context = findDeployedApp(contextName);
		if (context == null)
		{
			throw new IllegalArgumentException(sm.getString("standardEngine.nameMissing", contextName));
		}

		// Remove this workflow application
		engine.log(sm.getString("standardEngine.removing", contextName));
		try
		{
			// Get the work directory for the Context
			File workDir = new File(((StandardContext)context).getWorkPath());
			engine.removeChild(context);

			if (undeploy)
			{
				// Remove the workflow application directory  file if it
				// exists in the Engine's engineBase directory.

				// Determine if directoryr to remove is in the engine engineBase
				boolean isAppBase = false;
				File appBase = new File(engine.getEngineBase());
				if (!appBase.isAbsolute())
				{
					appBase = new File(System.getProperty(Globals.PROPS_WILLOW_HOME), engine.getEngineBase());
				}
				File contextFile = new File(context.getAppBase());
				File baseDir = contextFile.getParentFile();
				if ((baseDir == null) || (appBase.getCanonicalPath().equals(baseDir.getCanonicalPath())))
				{
					isAppBase = true;
				}

				// Only remove directory and/or war if they are located in the
				// Host's appBase autoDeploy is true
				if (isAppBase && engine.getAutoDeploy())
				{
					String filename = contextFile.getName();

					if (filename.equals(contextName))
					{
						if (contextFile.isDirectory())
						{
							deleteDir(contextFile);
						}	
					}
				}

				// Remove the work directory for the Context
				if (workDir == null
					&& context instanceof StandardContext
					&& ((StandardContext) context).getWorkDir() != null)
				{
					workDir = new File(((StandardContext) context).getWorkPath());
				}
				if (workDir != null && workDir.exists())
				{
					deleteDir(workDir);
				}
			}

			engine.fireContainerEvent(REMOVE_EVENT, context);
		}
		catch (Exception e)
		{
			engine.log(sm.getString("standardEngine.removeError", contextName), e);
			throw new IOException(e.toString());
		}

	}

	/**
	 * Start an existing workflow application, attached to the specified app
	 * name.  Only starts a workflow application if it is not running.
	 *
	 * @param contextName The app name of the application to be started
	 *
	 * @exception IllegalArgumentException if the specified app name
	 * @exception IllegalArgumentException if the specified app name does
	 *  not identify a currently installed workflow application
	 * @exception IOException if an input/output error occurs during
	 *  startup
	 */
	public void start(String contextName) throws IOException
	{

		// Validate the format and state of our arguments
		if (contextName == null)
		{
			throw new IllegalArgumentException(sm.getString("standardEngine.nameRequired"));
		}

		Context context = findDeployedApp(contextName);
		if (context == null)
		{
			throw new IllegalArgumentException(sm.getString("standardEngine.nameMissing", contextName));
		}
		log.info("standardEngine.start " + contextName);
		try
		{
			((Lifecycle) context).start();
		}
		catch (LifecycleException e)
		{
			log.info("standardEngine.start " + contextName + ": ", e);
			throw new IllegalStateException("standardEngine.start " + contextName + ": " + e);
		}
	}

	/**
	 * Stop an existing workflow application, attached to the specified context
	 * path.  Only stops a workflow application if it is running.
	 *
	 * @param contextName The app name of the application to be stopped
	 *
	 * @exception IllegalArgumentException if the specified app name 
	 *  is malformed (it must be "" or start with a slash)
	 * @exception IllegalArgumentException if the specified app name  does
	 *  not identify a currently installed workflow application
	 * @exception IOException if an input/output error occurs while stopping
	 *  the workflow application
	 */
	public void stop(String contextName) throws IOException
	{

		// Validate the format and state of our arguments
		if (contextName == null)
		{
			throw new IllegalArgumentException(sm.getString("standardEngine.nameRequired"));
		}

		Context context = findDeployedApp(contextName);
		if (context == null)
		{
			throw new IllegalArgumentException(sm.getString("standardEngine.nameMissing", contextName));
		}
		log.info("standardEngine.stop " + contextName);
		try
		{
			((Lifecycle) context).stop();
		}
		catch (LifecycleException e)
		{
			log.error("standardEngine.stop " + contextName + ": ", e);
			throw new IllegalStateException("standardEngine.stop " + contextName + ": " + e);
		}

	}

	// ------------------------------------------------------ Delegated Methods

	/**
	 * Delegate a request to add a child Context to our associated Host.
	 *
	 * @param child The child Context to be added
	 */
	public void addChild(Container child)
	{

		Context context = (Context) child;
		String contextName = context.getName();
		if (contextName == null)
		{
			throw new IllegalArgumentException(sm.getString("standardEngine.nameRequired"));
		}

		if (engine.findChild(contextName) != null)
		{
			throw new IllegalStateException(sm.getString("standardEngine.nameUsed", contextName));
		}
			
		engine.fireContainerEvent(PRE_INSTALL_EVENT, context);
		engine.addChild(child);
		engine.fireContainerEvent(INSTALL_EVENT, context);

	}

	/**
	 * Delegate a request for the parent class loader to our associated Host.
	 */
	public ClassLoader getParentClassLoader()
	{

		return (engine.getParentClassLoader());

	}

	// ------------------------------------------------------ Protected Methods

	/**
	 * Create (if necessary) and return a Digester configured to process the
	 * context configuration descriptor for an application.
	 */
	protected Digester createDigester()
	{
		if (digester == null)
		{
			digester = new WillowDigester();
			if (engine.getDebug() > 0)
			{
				digester.setDebug(3);
			}
			digester.setValidating(false);
			contextRuleSet = new ContextRuleSet("");
			digester.addRuleSet(contextRuleSet);
		}
		return (digester);

	}

	/**
	 * Delete the specified directory, including all of its contents and
	 * subdirectories recursively.
	 *
	 * @param dir File object representing the directory to be deleted
	 */
	protected void deleteDir(File dir)
	{

		String files[] = dir.list();
		if (files == null)
		{
			files = new String[0];
		}
		for (int i = 0; i < files.length; i++)
		{
			File file = new File(dir, files[i]);
			if (file.isDirectory())
			{
				deleteDir(file);
			}
			else
			{
				file.delete();
			}
		}
		dir.delete();

	}

}
