/*
 * Created on 2004-6-9
 *
 * 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.io.Serializable;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.HashMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.huihoo.willow.Container;
import org.huihoo.willow.Context;
import org.huihoo.willow.ContextDeployer;
import org.huihoo.willow.Engine;
import org.huihoo.willow.Globals;
import org.huihoo.willow.Lifecycle;
import org.huihoo.willow.LifecycleException;
import org.huihoo.willow.Loader;
import org.huihoo.willow.loader.WorkflowLoader;

import workflow.WorkflowException;
import workflow.script.core.ScriptContext;
import workflow.services.WorkflowContext;
import workflow.wfmc.xpdl.WorkflowPackage;

import com.zosatapo.commons.store.Store;
import com.zosatapo.workflow.threads.LitterCleaner;

/**
 * @author reic
 *
 * To change the template for this generated type comment go to
 * Window - Preferences - Java - Code Generation - Code and Comments
 */
public class StandardContext extends ContextBase implements ContextDeployer, Context, Serializable
{
	private transient Log log = LogFactory.getLog(StandardContext.class);

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

	/**
	 * Create a new StandardContext component with the default basic Valve.
	 */
	public StandardContext()
	{
		super();
	}

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

	/**
	 * The ScriptContext implementation associated with this Context.
	 */
	private transient ApplicationContext scriptContext = null;

	/**
	 * Compiler classpath to use.
	 */
	private String compilerClasspath = null;

	/**
	 * The "follow standard delegation model" flag that will be used to
	 * configure our ClassLoader.
	 */
	private boolean delegate = false;

	/**
		* The auto deploy flag for this Context.
		*/
	private boolean autoDeploy = true;

	/**
	* The display name of this workflow application.
	*/
	private String displayName = null;

	/**
	 * The document root for this workflow application.
	 */
	private String appBase = null;

	/**
	 * The descriptive information string for this implementation.
	 */
	private static final String info = "org.huohoo.core.StandardContext/1.0";

	/**
	 * The reloadable flag for this workflow application.
	 */
	private boolean reloadable = false;

	/**
	 * The pathname to the work directory for this context (relative to
	 * the server's home if not absolute).
	 */
	private String workDir = null;

	/**
	 * The <code>EngineDeployer</code> to whom we delegate application
	 * deployment requests.
	 */
	private ContextDeployer deployer = null;

	/**
	 * The context initialization parameters for this workflow application,
	 * keyed by name.
	 */
	private HashMap parameters = new HashMap();

	/**
	 * The context initialization stores for this workflow application,
	 * keyed by store-key.
	 */
	private HashMap stores = new HashMap();

	/**
	 * The context initialization factory for this workflow application,
	 * keyed by factory-key.
	 */
	private HashMap factories = new HashMap();

	/**
	 * The application available flag for this workflow context.
	 */
	private boolean available = false;

	/**
	 * The request processing pause flag (while reloading occurs)
	 */
	private boolean paused = false;

	// ----------------------------------------------------- workflow context Properties

	/**
	 * Return the "follow standard delegation model" flag used to configure
	 * our ClassLoader.
	 */
	public boolean getDelegate()
	{

		return (this.delegate);

	}

	/**
	 * Set the "follow standard delegation model" flag used to configure
	 * our ClassLoader.
	 *
	 * @param delegate The new flag
	 */
	public void setDelegate(boolean delegate)
	{

		boolean oldDelegate = this.delegate;
		this.delegate = delegate;
		support.firePropertyChange("delegate", new Boolean(oldDelegate), new Boolean(this.delegate));

	}

	/**
	 * Return the display name of this workflow application.
	 */
	public String getDisplayName()
	{

		return (this.displayName);

	}

	/**
	 * Return the compiler classpath.
	 */
	public String getCompilerClasspath()
	{
		return compilerClasspath;
	}

	/**
	 * Set the compiler classpath.
	 */
	public void setCompilerClasspath(String compilerClasspath)
	{
		this.compilerClasspath = compilerClasspath;
	}

	/**
	 * Set the display name of this workflow application.
	 *
	 * @param displayName The new display name
	 */
	public void setDisplayName(String displayName)
	{

		String oldDisplayName = this.displayName;
		this.displayName = displayName;
		support.firePropertyChange("displayName", oldDisplayName, this.displayName);
	}

	/**
	 * Return the document root for this Context.  This can be an absolute
	 * pathname, a relative pathname
	 */
	public String getAppBase()
	{
		return (this.appBase);
	}

	/**
	 * Return the document root for this Context. 
	 */
	public File getAbsoluteAppBase()
	{
		String absolute_appBase = getAppBase();
		File absolute_appBaseFile = new File(absolute_appBase);

		if (!absolute_appBaseFile.isAbsolute())
		{
			Engine parent = (Engine) this.getParent();
			absolute_appBaseFile = new File(parent.getAbsoluteEngineBase(), absolute_appBase);
		}

		return (absolute_appBaseFile);
	}

	/**
	 * Set the document root for this workflow context.  This can be an absolute
	 * pathname, a relative pathname
	 *
	 * @param docBase The new document root
	 */
	public void setAppBase(String appBase)
	{

		this.appBase = appBase;

	}

	/**
	 * Return descriptive information about this Container implementation and
	 * the corresponding version number, in the format
	 * <code>&lt;description&gt;/&lt;version&gt;</code>.
	 */
	public String getInfo()
	{

		return (info);

	}

	/**
	 * Set the Loader with which this workflow context is associated.
	 *
	 * @param loader The newly associated loader
	 */
	public synchronized void setLoader(Loader loader)
	{

		super.setLoader(loader);

	}

	/**
	 * Return the reloadable flag for this workflow application.
	 */
	public boolean getReloadable()
	{

		return (this.reloadable);

	}

	/**
	 * Set the reloadable flag for this workflow application.
	 *
	 * @param reloadable The new reloadable flag
	 */
	public void setReloadable(boolean reloadable)
	{

		boolean oldReloadable = this.reloadable;
		this.reloadable = reloadable;
		support.firePropertyChange(
			"reloadable",
			new Boolean(oldReloadable),
			new Boolean(this.reloadable));

	}

	/**
	 * Return the value of the auto deploy flag.  If true, it indicates that 
	 * this xpdl will be dynamically deployed.
	 */
	public boolean getAutoDeploy()
	{

		return (this.autoDeploy);

	}

	/**
	 * Set the auto deploy flag value for this Context.
	 * 
	 * @param autoDeploy The new auto deploy flag
	 */
	public void setAutoDeploy(boolean autoDeploy)
	{

		boolean oldAutoDeploy = this.autoDeploy;
		this.autoDeploy = autoDeploy;
		support.firePropertyChange("autoDeploy", oldAutoDeploy, this.autoDeploy);

	}

	/**
	 * Return the servlet context for which this Context is a facade.
	 */
	public ScriptContext getScriptContext()
	{

		if (scriptContext == null)
		{
			scriptContext = new ApplicationContext(getBasePath(), this);
		}
		return (scriptContext);

	}

	// ------------------------------------------------------ Public Properties

	/** Get the absolute path to the work dir.
	 *  To avoid duplication.
	 * 
	 * @return
	 */
	public String getWorkPath()
	{
		File workDir = new File(getWorkDir());
		if (!workDir.isAbsolute())
		{
			File willowHome = new File(System.getProperty(Globals.PROPS_WILLOW_HOME));
			String willowHomePath = null;
			try
			{
				willowHomePath = willowHome.getCanonicalPath();
				workDir = new File(willowHomePath, getWorkDir());
			}
			catch (IOException e)
			{
			}
		}
		return workDir.getAbsolutePath();
	}

	/**
	 * Return the work directory for this workflow context.
	 */
	public String getWorkDir()
	{

		return (this.workDir);

	}

	/**
	 * Set the work directory for this workflow context.
	 *
	 * @param workDir The new work directory
	 */
	public void setWorkDir(String workDir)
	{

		this.workDir = workDir;

		if (started)
		{
			postWorkDirectory();
		}

	}

	/**
	 * Return the application available flag for this workflow context.
	 */
	public boolean getAvailable()
	{

		return (this.available);

	}

	/**
	 * Set the application available flag for this workflow context.
	 *
	 * @param available The new application available flag
	 */
	public void setAvailable(boolean available)
	{

		boolean oldAvailable = this.available;
		this.available = available;
		support.firePropertyChange("available", new Boolean(oldAvailable), new Boolean(this.available));

	}

	/**
		 * Add a new context initialization parameter.
		 *
		 * @param name Name of the new parameter
		 * @param value Value of the new  parameter
		 *
		 * @exception IllegalArgumentException if the name or value is missing,
		 *  or if this context initialization parameter has already been
		 *  registered
		 */
	public void addInitParameter(String name, String value)
	{
		// Validate the proposed context initialization parameter
		if ((name == null) || (value == null))
			throw new IllegalArgumentException(sm.getString("standardContext.parameter.required"));
		if (parameters.get(name) != null)
			throw new IllegalArgumentException(sm.getString("standardContext.parameter.duplicate", name));

		// Add this parameter to our defined set
		synchronized (parameters)
		{
			parameters.put(name, value);
		}
		fireContainerEvent("addInitParameter", name);

	}

	/**
	 * Return the value for the specified context initialization
	 * parameter name, if any; otherwise return <code>null</code>.
	 *
	 * @param name Name of the parameter to return
	 */
	public String findInitParameter(String name)
	{

		synchronized (parameters)
		{
			return ((String) parameters.get(name));
		}

	}

	/**
	 * Return the names of all defined context initialization parameters
	 * for this Context.  If no parameters are defined, a zero-length
	 * array is returned.
	 */
	public String[] findInitParameters()
	{

		synchronized (parameters)
		{
			String results[] = new String[parameters.size()];
			return ((String[]) parameters.keySet().toArray(results));
		}

	}
	public void removeInitParameter(String name)
	{

		synchronized (parameters)
		{
			parameters.remove(name);
		}
		fireContainerEvent("removeInitParameter", name);

	}

	public void addFactoryClass(String name, String value)
	{
		// Validate the proposed context initialization parameter
		if ((name == null) || (value == null))
			throw new IllegalArgumentException(sm.getString("standardContext.factory.required"));
		if (factories.get(name) != null)
			throw new IllegalArgumentException(sm.getString("standardContext.factory.duplicate", name));

		// Add this parameter to our defined set
		synchronized (factories)
		{
			factories.put(name, value);
		}
		fireContainerEvent("addIFactoryClass", name);

	}

	public String findFactoryClass(String name)
	{

		synchronized (factories)
		{
			return ((String) factories.get(name));
		}

	}

	public String[] findFactoryClasses()
	{

		synchronized (factories)
		{
			String results[] = new String[factories.size()];
			return ((String[]) factories.keySet().toArray(results));
		}

	}
	public void removeFactoryClass(String name)
	{

		synchronized (factories)
		{
			factories.remove(name);
		}
		fireContainerEvent("removeFactoryClass", name);

	}

	public void addStore(String name, Store value)
	{
		// Validate the proposed context initialization parameter
		if ((name == null) || (value == null))
			throw new IllegalArgumentException(sm.getString("standardContext.store.required"));
		if (stores.get(name) != null)
			throw new IllegalArgumentException(sm.getString("standardContext.store.duplicate", name));

		// Add this parameter to our defined set
		synchronized (stores)
		{
			stores.put(name, value);
		}
		fireContainerEvent("addStore", name);

	}

	public Store findStore(String name)
	{

		synchronized (stores)
		{
			return ((Store) stores.get(name));
		}

	}

	public String[] findStores()
	{

		synchronized (stores)
		{
			String results[] = new String[stores.size()];
			return ((String[]) stores.keySet().toArray(results));
		}

	}

	public void removeStore(String name)
	{

		synchronized (stores)
		{
			stores.remove(name);
		}
		fireContainerEvent("removeStore", name);

	}

	// -------------------------------------------------------- workflow context Methods
	/**
	 * Add a child Container, only if the proposed child is an implementation
	 * of Engine.
	 *
	 * @param child Child container to be added
	 */
	public void addChild(Container child)
	{
		throw new IllegalArgumentException(sm.getString("standardContext.notChild"));
	}

	/**
	 * Reload this workflow application, if reloading is supported.
	 * <p>
	 * <b>IMPLEMENTATION NOTE</b>:  This method is designed to deal with
	 * reloads required by changes to classes in the underlying repositories
	 * of our class loader.  It does not handle changes to the workflow application
	 * deployment descriptor.  If that has occurred, you should stop this
	 * workflow context and create (and start) a new workflow context instance instead.
	 *
	 * @exception IllegalStateException if the <code>reloadable</code>
	 *  property is set to <code>false</code>.
	 */
	public synchronized void reload()
	{

		// Validate our current component state
		if (!started)
		{
			throw new IllegalStateException(sm.getString("containerBase.notStarted", logName()));
		}

		// Make sure reloading is enabled
		if (!reloadable)
		{
			throw new IllegalStateException(sm.getString("standardContext.notReloadable"));
		}

		log.info(sm.getString("standardContext.reloadingStarted"));

		// Stop accepting requests temporarily
		setPaused(true);

		try
		{
			stop();
		}
		catch (LifecycleException e)
		{
			log.error(sm.getString("standardContext.stoppingContext"), e);
		}

		try
		{
			start();
		}
		catch (LifecycleException e)
		{
			log.error(sm.getString("standardContext.startingContext"), e);
		}

		setPaused(false);

	}

	// --------------------------------------------------------- Public Methods

	/**
	 * Start this workflow context component.
	 *
	 * @exception LifecycleException if a startup error occurs
	 */
	public synchronized void start() throws LifecycleException
	{
		if (started)
		{
			log.info(sm.getString("standardContext.alreadyStarted", logName()));
			return;
		}

		String logName = "willow." + getParent().getName() + "." + getName() + ".Context";
		log = LogFactory.getLog(logName);

		log.debug("Starting " + logName);

		// Notify our interested LifecycleListeners
		lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null);

		setAvailable(false);

		boolean ok = true;

		if (getLoader() == null)
		{
			ClassLoader parent = null;
			log.debug("Configuring non-privileged default Loader");
			parent = getParentClassLoader();

			WorkflowLoader webappLoader = new WorkflowLoader(parent);
			webappLoader.setDelegate(getDelegate());
			setLoader(webappLoader);
		}

		// Post work directory
		postWorkDirectory();

		// Standard container startup
		log.debug("Processing standard container startup");

		if (ok)
		{
			started = true;

			// Start our subordinate components, if any
			if ((loader != null) && (loader instanceof Lifecycle))
			{
				((Lifecycle) loader).start();
			}
			if ((logger != null) && (logger instanceof Lifecycle))
			{
				((Lifecycle) logger).start();
			}
			
			// Notify our interested LifecycleListeners
			lifecycle.fireLifecycleEvent(START_EVENT, null);

			// Start ContainerBackgroundProcessor thread
			super.threadStart();
		}

		// Set available status depending upon startup success
		if (ok)
		{
			log.debug("Starting completed");
			setAvailable(true);
		}
		else
		{
			log.error(sm.getString("standardContext.startFailed"));
			try
			{
				stop();
			}
			catch (Throwable t)
			{
				log.error(sm.getString("standardContext.startCleanup"), t);
			}
			setAvailable(false);
		}

		if (ok)
		{
			// Notify our interested LifecycleListeners
			lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null);
			scriptContext.setAttribute(Globals.CLASS_LOADER_ATTR, getLoader().getClassLoader());
		}

		// Close all JARs right away to avoid always opening a peak number 
		// of files on startup
		if (getLoader() instanceof WorkflowLoader)
		{
			((WorkflowLoader) getLoader()).closeJARs(true);
		}
		
		// Reinitializing if something went wrong
		if (!ok && started)
		{
			stop();
		}

	}

	/**
	 * Stop this workflow context component.
	 *
	 * @exception LifecycleException if a shutdown error occurs
	 */
	public synchronized void stop() throws LifecycleException
	{

		// Validate and update our current component state
		if (!started)
		{
			throw new LifecycleException(sm.getString("containerBase.notStarted", logName()));
		}

		log.debug("Stopping");

		// Notify our interested LifecycleListeners
		lifecycle.fireLifecycleEvent(BEFORE_STOP_EVENT, null);

		// Mark this application as unavailable while we shut down
		setAvailable(false);

		// Stop ContainerBackgroundProcessor thread
		super.threadStop();

		// Normal container shutdown processing
		log.debug("Processing standard container shutdown");

		// Notify our interested LifecycleListeners
		lifecycle.fireLifecycleEvent(STOP_EVENT, null);
		started = false;

		try
		{
			// Stop our child containers, if any
			Container[] children = findChildren();
			for (int i = 0; i < children.length; i++)
			{
				if (children[i] instanceof Lifecycle)
				{
					((Lifecycle) children[i]).stop();
				}
			}

			if ((logger != null) && (logger instanceof Lifecycle))
			{
				((Lifecycle) logger).stop();
			}
			if ((loader != null) && (loader instanceof Lifecycle))
			{
				((Lifecycle) loader).stop();
			}
		}
		finally
		{
		}

		// Notify our interested LifecycleListeners
		lifecycle.fireLifecycleEvent(AFTER_STOP_EVENT, null);
		log.debug("Stopping complete");
	}

	/**
	 * Return a String representation of this component.
	 */
	public String toString()
	{

		StringBuffer sb = new StringBuffer();
		if (getParent() != null)
		{
			sb.append(getParent().toString());
			sb.append(".");
		}
		sb.append("StandardContext[");
		sb.append(getName());
		sb.append("]");
		return (sb.toString());

	}

	/**
	 * Execute a periodic task, such as reloading, etc. This method will be
	 * invoked inside the classloading context of this container. Unexpected
	 * throwables will be caught and logged.
	 */
	public void backgroundProcess()
	{

		if (!started)
		{
			return;
		}

		if (getLoader() != null)
		{
			if (reloadable && (getLoader().modified()))
			{
				try
				{
					Thread.currentThread().setContextClassLoader(StandardContext.class.getClassLoader());
					reload();
				}
				finally
				{
					if (getLoader() != null)
					{
						Thread.currentThread().setContextClassLoader(getLoader().getClassLoader());
					}
				}
			}
			if (getLoader() instanceof WorkflowLoader)
			{
				((WorkflowLoader) getLoader()).closeJARs(false);
			}
		}
		
		LitterCleaner.clear(this);
		
		lifecycle.fireLifecycleEvent("check", null);
	}

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

	/**
	 * Return a File object representing the base directory for the
	 * entire servlet container (i.e. the Engine container if present).
	 */
	protected File engineBaseFile()
	{
		StandardEngine eng = (StandardEngine) this.getParent();
		String engineBase = eng.getEngineBase();

		File baseFile = new File(engineBase);

		if (!baseFile.isAbsolute())
		{
			baseFile = new File(System.getProperty(Globals.PROPS_WILLOW_HOME), engineBase);
		}

		return (baseFile);
	}

	/**
	 * Return the request processing paused flag for this workflow context.
	 */
	public boolean getPaused()
	{

		return (this.paused);

	}

	/**
	 * Set the appropriate context attribute for our work directory.
	 */
	private void postWorkDirectory()
	{

		// Acquire (or calculate) the work directory path
		String workDir = getWorkDir();
		if (workDir == null)
		{
			// Retrieve our parent (normally a engine) name
			String engineName = null;
			String engineWorkDir = null;

			Container parentEngine = getParent();
			if (parentEngine != null)
			{
				engineName = parentEngine.getName();
				engineWorkDir = ((StandardEngine) parentEngine).getWorkDir();
			}

			if ((engineName == null) || (engineName.length() < 1))
			{
				engineName = "_";
			}

			String temp = getName();
			if (temp.startsWith("/"))
			{
				temp = temp.substring(1);
			}

			temp = temp.replace('/', '_');
			temp = temp.replace('\\', '_');
			if (temp.length() < 1)
			{
				temp = "_";
			}

			if (engineWorkDir != null)
			{
				workDir = engineWorkDir + File.separator + temp;
			}
			else
			{
				workDir =
					org.huihoo.willow.startup.Constants.WORK_DIRECTORY
						+ File.separator
						+ engineName
						+ File.separator
						+ temp;
			}
			setWorkDir(workDir);
		}

		// Create this directory if necessary
		File dir = new File(workDir);
		if (!dir.isAbsolute())
		{
			File willowHome = new File(System.getProperty(Globals.PROPS_WILLOW_HOME));
			String willowHomePath = null;
			try
			{
				willowHomePath = willowHome.getCanonicalPath();
				dir = new File(willowHomePath, workDir);
			}
			catch (IOException e)
			{
			}
		}
		dir.mkdirs();

		getScriptContext().setAttribute(Globals.WORK_DIR_ATTR, dir);
		if (getScriptContext() instanceof ApplicationContext)
		{
			((ApplicationContext) getScriptContext()).setAttributeReadOnly(Globals.WORK_DIR_ATTR);
		}
	}

	/**
	 * Set the request processing paused flag for this workflow context.
	 *
	 * @param paused The new request processing paused flag
	 */
	private void setPaused(boolean paused)
	{

		this.paused = paused;

	}

	/**
	 * Get base path (full path name).
	 */
	private String getBasePath()
	{
		String appBase = null;

		File file = new File(getAppBase());
		if (!file.isAbsolute())
		{
			appBase = new File(engineBaseFile(), getAppBase()).getPath();
		}
		else
		{
			appBase = file.getPath();
		}
		return appBase;
	}

	/**
	 * Validate the syntax of a proposed <code>&lt;url-pattern&gt;</code>
	 * for conformance with specification requirements.
	 *
	 * @param urlPattern URL pattern to be validated
	 */
	private boolean validateURLPattern(String urlPattern)
	{

		if (urlPattern == null)
			return (false);
		if (urlPattern.startsWith("*."))
		{
			if (urlPattern.indexOf('/') < 0)
				return (true);
			else
				return (false);
		}
		if ((urlPattern.startsWith("/")) && (urlPattern.indexOf("*.") < 0))
			return (true);
		else
			return (false);

	}

	// ------------------------------------------------------- ContextDeployer Methods

	public void install(String xpdlFileName, URL xpdlFileURL) throws WorkflowException
	{
		getDeployer().install(xpdlFileName, xpdlFileURL);
	}

	public WorkflowPackage findDeployedPackage(String xpdlFileName)
	{

		return (getDeployer().findDeployedPackage(xpdlFileName));

	}

	public String[] findDeployedPackages()
	{

		return (getDeployer().findDeployedPackages());

	}

	public void remove(String xpdlFileName) throws WorkflowException
	{

		getDeployer().remove(xpdlFileName);

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

	static String STANDARD_CONTEXT_DEPLOYER = "org.huihoo.willow.core.StandardContextDeployer";

	public ContextDeployer getDeployer()
	{
		if (deployer != null)
		{
			return deployer;
		}

		log.info("Create Context deployer for direct deployment ");

		try
		{
			Class c = Class.forName(STANDARD_CONTEXT_DEPLOYER);
			deployer = (ContextDeployer) c.newInstance();
			Method m = c.getMethod("setContext", new Class[] { Context.class });
			m.invoke(deployer, new Object[] { this });
		}
		catch (Throwable t)
		{
			log.error("Error creating deployer ", t);
		}
		return deployer;
	}

	public void setDeployer(ContextDeployer d)
	{
		this.deployer = d;
	}

	//-------------------------------------------------------------
	// WorkflowService  Implementation
	//-------------------------------------------------------------
	public WorkflowContext getWorkflowContext()
	{
		if (scriptContext == null)
		{
			scriptContext = new ApplicationContext(getBasePath(), this);
		}
		return (scriptContext);
	}
}