//----------------------------BEGIN LICENSE----------------------------
/* 
 * Willow : the Open Source WorkFlow Project
 * Distributable under GNU LGPL license by gun.org
 *
 * Copyright (C) 2004-2010 huihoo.org 
 * Copyright (C) 2004-2010  ZosaTapo <dertyang@hotmail.com> 
 * 
 * ====================================================================
 * Project Homepage : http://www.huihoo.org/willow
 * Source Forge     : http://sourceforge.net/projects/huihoo
 * Mailing list     : willow@lists.sourceforge.net 
 */
//----------------------------END  LICENSE----------------------------

package org.huihoo.willow.core;

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;

import org.huihoo.commons.logging.SystemLogHandler;
import org.huihoo.commons.util.Enumerator;
import org.huihoo.willow.Container;
import org.huihoo.willow.Context;
import org.huihoo.willow.LifecycleException;
import org.huihoo.willow.Loader;
import org.huihoo.willow.Wrapper;
import org.huihoo.workflow.selector.Selector;
import org.huihoo.workflow.selector.SelectorException;
import org.huihoo.workflow.selector.UnavailableException;
import org.huihoo.workflow.server.WorkflowConfig;
import org.huihoo.workflow.server.WorkflowContext;
import org.huihoo.workflow.xpdl.WorkflowCondition;

/**
 * Standard implementation of the <b>Wrapper</b> interface that represents
 * an individual script definition.  No child Containers are allowed, and
 * the parent Container must be a Context.
 */
public class StandardWrapper extends ContainerBase implements WorkflowConfig, Wrapper
{

	private static org.apache.commons.logging.Log log =
		org.apache.commons.logging.LogFactory.getLog(StandardWrapper.class);
	// ----------------------------------------------------------- Constructors

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

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

	/**
	 * The date and time at which this script will become available (in
	 * milliseconds since the epoch), or zero if the script is available.
	 * If this value equals Long.MAX_VALUE, the unavailability of this
	 * script is considered permanent.
	 */
	private long available = 0L;

	/**
	 * The count of allocations that are currently active (even if they
	 * are for the same instance, as will be true on a non-STM script).
	 */
	private int countAllocated = 0;

	/**
	 * The debugging detail level for this component.
	 */
	private int debug = 0;

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

	/**
	 * The (single) initialized instance of this script.
	 */
	private Selector instance = null;

	/**
	 * The WorkflowCondition for this script.
	 */
	private WorkflowCondition workflowCondition = null;

	/**
	 * The load-on-startup order value (negative value means load on
	 * first call) for this script.
	 */
	private int loadOnStartup = -1;

	/**
	 * Mappings associated with the wrapper.
	 */
	private ArrayList mappings = new ArrayList();

	/**
	 * The initialization parameters for this script, keyed by
	 * parameter name.
	 */
	private HashMap parameters = new HashMap();

	/**
	 * The notification sequence number.
	 */
	private long sequenceNumber = 0;

	/**
	 * The fully qualified script class name for this script.
	 */
	private String selectorClass = null;

	/**
	 * Are we unloading our script instance at the moment?
	 */
	private boolean unloading = false;

	/**
	 * Should we swallow System.out
	 */
	private boolean swallowOutput = false;

	private long loadTime = 0;
	private int classLoadTime = 0;

	// ------------------------------------------------------------- Properties

	/**
	 * Return the available date/time for this script, in milliseconds since
	 * the epoch.  If this date/time is Long.MAX_VALUE, it is considered to mean
	 * that unavailability is permanent and any request for this script will return
	 * an SC_NOT_FOUND error.  If this date/time is in the future, any request for
	 * this script will return an SC_SERVICE_UNAVAILABLE error.  If it is zero,
	 * the script is currently available.
	 */
	public long getAvailable()
	{

		return (this.available);

	}

	/**
	 * Set the available date/time for this script, in milliseconds since the
	 * epoch.  If this date/time is Long.MAX_VALUE, it is considered to mean
	 * that unavailability is permanent and any request for this script will return
	 * an SC_NOT_FOUND error. If this date/time is in the future, any request for
	 * this script will return an SC_SERVICE_UNAVAILABLE error.
	 *
	 * @param available The new available date/time
	 */
	public void setAvailable(long available)
	{

		long oldAvailable = this.available;
		if (available > System.currentTimeMillis())
			this.available = available;
		else
			this.available = 0L;
		support.firePropertyChange("available", new Long(oldAvailable), new Long(this.available));

	}

	/**
	 * Return the number of active allocations of this script, even if they
	 * are all for the same instance (as will be true for servlets that do
	 * not implement <code>SingleThreadModel</code>.
	 */
	public int getCountAllocated()
	{

		return (this.countAllocated);

	}

	/**
	 * Return the debugging detail level for this component.
	 */
	public int getDebug()
	{

		return (this.debug);

	}

	/**
	 * Set the debugging detail level for this component.
	 *
	 * @param debug The new debugging detail level
	 */
	public void setDebug(int debug)
	{

		int oldDebug = this.debug;
		this.debug = debug;
		support.firePropertyChange("debug", new Integer(oldDebug), new Long(this.debug));

	}

	/**
	 * 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);

	}

	/**
	 * Return the load-on-startup order value (negative value means
	 * load on first call).
	 */
	public int getLoadOnStartup()
	{

		return (this.loadOnStartup);

	}

	/**
	 * Set the load-on-startup order value (negative value means
	 * load on first call).
	 *
	 * @param value New load-on-startup value
	 */
	public void setLoadOnStartup(int value)
	{

		int oldLoadOnStartup = this.loadOnStartup;
		this.loadOnStartup = value;
		support.firePropertyChange("loadOnStartup", new Integer(oldLoadOnStartup), new Integer(this.loadOnStartup));

	}

	/**
	 * Set the load-on-startup order value from a (possibly null) string.
	 * Per the specification, any missing or non-numeric value is converted
	 * to a zero, so that this script will still be loaded at startup
	 * time, but in an arbitrary order.
	 *
	 * @param value New load-on-startup value
	 */
	public void setLoadOnStartupString(String value)
	{

		try
		{
			setLoadOnStartup(Integer.parseInt(value));
		}
		catch (NumberFormatException e)
		{
			setLoadOnStartup(0);
		}
	}

	public String getLoadOnStartupString()
	{
		return Integer.toString(getLoadOnStartup());
	}

	/**
	 * Set the parent Container of this Wrapper, but only if it is a Context.
	 *
	 * @param container Proposed parent Container
	 */
	public void setParent(Container container)
	{

		if ((container != null) && !(container instanceof Context))
			throw new IllegalArgumentException(sm.getString("standardWrapper.notContext"));
		if (container instanceof StandardContext)
		{
			swallowOutput = ((StandardContext) container).getSwallowOutput();
		}
		super.setParent(container);

	}

	/**
	 * Return the fully qualified script class name for this script.
	 */
	public String getSelectorClass()
	{

		return (this.selectorClass);

	}

	/**
	 * Set the fully qualified script class name for this script.
	 *
	 * @param scriptClass Script class name
	 */
	public void setSelectorClass(String scriptClass)
	{

		String oldScriptClass = this.selectorClass;
		this.selectorClass = scriptClass;
		support.firePropertyChange("scriptClass", oldScriptClass, this.selectorClass);

	}

	/**
	 * Set the name of this script.  This is an alias for the normal
	 * <code>Container.setName()</code> method, and complements the
	 * <code>getScriptName()</code> method required by the
	 * <code>ScriptConfig</code> interface.
	 *
	 * @param name The new name of this script
	 */
	public void setSelectorName(String name)
	{

		setName(name);

	}

	/**
	 * Is this script currently unavailable?
	 */
	public boolean isUnavailable()
	{

		if (available == 0L)
			return (false);
		else if (available <= System.currentTimeMillis())
		{
			available = 0L;
			return (false);
		}
		else
			return (true);

	}

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

	/**
	 * Refuse to add a child Container, because Wrappers are the lowest level
	 * of the Container hierarchy.
	 *
	 * @param child Child container to be added
	 */
	public void addChild(Container child)
	{

		throw new IllegalStateException(sm.getString("standardWrapper.notChild"));

	}

	/**
	 * Add a new script initialization parameter for this script.
	 *
	 * @param name Name of this initialization parameter to add
	 * @param value Value of this initialization parameter to add
	 */
	public void addInitParameter(String name, String value)
	{

		synchronized (parameters)
		{
			parameters.put(name, value);
		}
		fireContainerEvent("addInitParameter", name);

	}

	/**
	 * Add a mapping associated with the Wrapper.
	 *
	 * @param pattern The new wrapper mapping
	 */
	public void addMapping(String mapping)
	{

		synchronized (mappings)
		{
			mappings.add(mapping);
		}
		fireContainerEvent("addMapping", mapping);

	}

	/**
	 * Allocate an initialized instance of this Script that is ready to have
	 * its <code>service()</code> method called.  If the script class does
	 * not implement <code>SingleThreadModel</code>, the (only) initialized
	 * instance may be returned immediately.  If the script class implements
	 * <code>SingleThreadModel</code>, the Wrapper implementation must ensure
	 * that this instance is not allocated again until it is deallocated by a
	 * call to <code>deallocate()</code>.
	 *
	 * @exception ScriptException if the script init() method threw
	 *  an exception
	 * @exception ScriptException if a loading error occurs
	 */
	public Selector allocate() throws SelectorException
	{

		// If we are currently unloading this script, throw an exception
		if (unloading)
			throw new SelectorException(sm.getString("standardWrapper.unloading", getName()));

		// If not SingleThreadedModel, return the same instance every time

		// Load and initialize our instance if necessary
		if (instance == null)
		{
			synchronized (this)
			{
				if (instance == null)
				{
					try
					{

						instance = loadSelecor();
					}
					catch (SelectorException e)
					{
						throw e;
					}
					catch (Throwable e)
					{
						e.printStackTrace();
						throw new SelectorException(sm.getString("standardWrapper.allocate"), e);
					}
				}
			}

			if (log.isTraceEnabled())
				log.trace("  Returning non-STM instance");
			countAllocated++;

		}

		return (instance);
	}

	/**
	 * Return this previously allocated script to the pool of available
	 * instances.  If this script class does not implement SingleThreadModel,
	 * no action is actually required.
	 *
	 * @param script The script to be returned
	 *
	 * @exception ScriptException if a deallocation error occurs
	 */
	public void deallocate(Selector script) throws SelectorException
	{
		countAllocated--;
		return;

	}

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

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

	}

	/**
	 * Return the names of all defined initialization parameters for this
	 * script.
	 */
	public String[] findInitParameters()
	{

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

	}

	/**
	 * Return the mappings associated with this wrapper.
	 */
	public String[] findMappings()
	{

		synchronized (mappings)
		{
			return (String[]) mappings.toArray(new String[mappings.size()]);
		}

	}

	/**
	 * Load and initialize an instance of this script, if there is not already
	 * at least one initialized instance.  This can be used, for example, to
	 * load servlets that are marked in the deployment descriptor to be loaded
	 * at server startup time.
	 * <p>
	 * <b>IMPLEMENTATION NOTE</b>:  Scripts whose classnames begin with
	 * <code>org.apache.catalina.</code> (so-called "container" servlets)
	 * are loaded by the same classloader that loaded this class, rather than
	 * the classloader for the current web application.
	 * This gives such classes access to Catalina internals, which are
	 * prevented for classes loaded for web applications.
	 *
	 * @exception ScriptException if the script init() method threw
	 *  an exception
	 * @exception ScriptException if some other loading problem occurs
	 */
	public synchronized void load() throws SelectorException
	{
		instance = loadSelecor();
	}

	/**
	 * Load and initialize an instance of this script, if there is not already
	 * at least one initialized instance.  This can be used, for example, to
	 * load servlets that are marked in the deployment descriptor to be loaded
	 * at server startup time.
	 */
	public synchronized Selector loadSelecor() throws SelectorException
	{

		// Nothing to do if we already have an instance or an instance pool
		if ((instance != null))
			return instance;

		PrintStream out = System.out;
		if (swallowOutput)
		{
			SystemLogHandler.startCapture();
		}

		Selector script;
		try
		{
			long t1 = System.currentTimeMillis();
			// If this "script" is really a JSP file, get the right class.
			// HOLD YOUR NOSE - this is a kludge that avoids having to do special
			// case Catalina-specific code in Jasper - it also requires that the
			// script path be replaced by the <script-file> element content in
			// order to be completely effective
			String actualClass = selectorClass;
			if ((actualClass == null) && (workflowCondition != null))
			{
				Wrapper scriptWrapper = (Wrapper) ((Context) getParent()).findChild(Constants.SCRIPT_SELECTOR_NAME);
				if (scriptWrapper != null)
					actualClass = scriptWrapper.getSelectorClass();
			}

			// Complain if no script class has been specified
			if (actualClass == null)
			{
				unavailable(null);
				throw new SelectorException(sm.getString("standardWrapper.notClass", getName()));
			}

			// Acquire an instance of the class loader to be used
			Loader loader = getLoader();
			if (loader == null)
			{
				unavailable(null);
				throw new SelectorException(sm.getString("standardWrapper.missingLoader", getName()));
			}

			ClassLoader classLoader = loader.getClassLoader();

			// Load the specified script class from the appropriate class loader
			Class classClass = null;
			try
			{
				if (classLoader != null)
				{
					classClass = classLoader.loadClass(actualClass);
				}
				else
				{
					classClass = Class.forName(actualClass);
				}
			}
			catch (ClassNotFoundException e)
			{
				e.printStackTrace();
				unavailable(null);

				getWorkflowContext().log("Error loading " + classLoader + " " + actualClass, e);
				throw new SelectorException(sm.getString("standardWrapper.missingClass", actualClass), e);
			}

			if (classClass == null)
			{
				unavailable(null);
				throw new SelectorException(sm.getString("standardWrapper.missingClass", actualClass));
			}

			// Instantiate and initialize an instance of the script class itself
			try
			{
				script = (Selector) classClass.newInstance();
			}
			catch (ClassCastException e)
			{
				unavailable(null);
				// Restore the context ClassLoader
				throw new SelectorException(sm.getString("standardWrapper.notSelector", actualClass), e);
			}
			catch (Throwable e)
			{
				unavailable(null);
				// Restore the context ClassLoader
				throw new SelectorException(sm.getString("standardWrapper.instantiate", actualClass), e);
			}

			classLoadTime = (int) (System.currentTimeMillis() - t1);
			// Call the initialization method of this script
			try
			{
				script.init(this);
			}
			catch (UnavailableException f)
			{
				unavailable(f);
				throw f;
			}
			catch (SelectorException f)
			{
				// If the script wanted to be unavailable it would have
				// said so, so do not call unavailable(null).
				throw f;
			}
			catch (Throwable f)
			{
				getWorkflowContext().log("StandardWrapper.Throwable", f);
				// If the script wanted to be unavailable it would have
				// said so, so do not call unavailable(null).
				throw new SelectorException(sm.getString("standardWrapper.initException", getName()), f);
			}

			fireContainerEvent("load", this);

			loadTime = System.currentTimeMillis() - t1;
		}
		finally
		{
			if (swallowOutput)
			{
				String log = SystemLogHandler.stopCapture();
				if (log != null && log.length() > 0)
				{
					if (getWorkflowContext() != null)
					{
						getWorkflowContext().log(log);
					}
					else
					{
						out.println(log);
					}
				}
			}
		}
		return script;

	}

	/**
	 * Remove the specified initialization parameter from this script.
	 *
	 * @param name Name of the initialization parameter to remove
	 */
	public void removeInitParameter(String name)
	{

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

	}

	/**
	 * Remove a mapping associated with the wrapper.
	 *
	 * @param mapping The pattern to remove
	 */
	public void removeMapping(String mapping)
	{

		synchronized (mappings)
		{
			mappings.remove(mapping);
		}
		fireContainerEvent("removeMapping", mapping);

	}

	/**
	 * 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("StandardWrapper[");
		sb.append(getName());
		sb.append("]");
		return (sb.toString());

	}

	/**
	 * Process an UnavailableException, marking this script as unavailable
	 * for the specified amount of time.
	 *
	 * @param unavailable The exception that occurred, or <code>null</code>
	 *  to mark this script as permanently unavailable
	 */
	public void unavailable(UnavailableException unavailable)
	{
		getWorkflowContext().log(sm.getString("standardWrapper.unavailable", getName()));
		if (unavailable == null)
			setAvailable(Long.MAX_VALUE);
		else
		{
			int unavailableSeconds = unavailable.getUnavailableSeconds();
			if (unavailableSeconds <= 0)
				unavailableSeconds = 60; // Arbitrary default
			setAvailable(System.currentTimeMillis() + (unavailableSeconds * 1000L));
		}

	}

	/**
	 * Unload all initialized instances of this script, after calling the
	 * <code>destroy()</code> method for each instance.  This can be used,
	 * for example, prior to shutting down the entire script engine, or
	 * prior to reloading all of the classes from the Loader associated with
	 * our Loader's repository.
	 *
	 * @exception ScriptException if an exception is thrown by the
	 *  destroy() method
	 */
	public synchronized void unload() throws SelectorException
	{

		// Nothing to do if we have never loaded the instance
		if ((instance == null))
			return;
		unloading = true;

		// Loaf a while if the current instance is allocated
		// (possibly more than once if non-STM)
		if (countAllocated > 0)
		{
			int nRetries = 0;
			while ((nRetries < 21) && (countAllocated > 0))
			{
				if ((nRetries % 10) == 0)
				{
					log.info(sm.getString("standardWrapper.waiting", new Integer(countAllocated)));
				}
				try
				{
					Thread.sleep(100);
				}
				catch (InterruptedException e)
				{
					;
				}
				nRetries++;
			}
		}

		ClassLoader oldCtxClassLoader = Thread.currentThread().getContextClassLoader();
		ClassLoader classLoader = instance.getClass().getClassLoader();

		PrintStream out = System.out;
		if (swallowOutput)
		{
			SystemLogHandler.startCapture();
		}

		// Call the script destroy() method
		try
		{

			Thread.currentThread().setContextClassLoader(classLoader);
			instance.destroy();
		}
		catch (Throwable t)
		{

			instance = null;
			fireContainerEvent("unload", this);
			unloading = false;
			throw new SelectorException(sm.getString("standardWrapper.destroyException", getName()), t);
		}
		finally
		{
			// restore the context ClassLoader
			Thread.currentThread().setContextClassLoader(oldCtxClassLoader);
			// Write captured output
			if (swallowOutput)
			{
				String log = SystemLogHandler.stopCapture();
				if (log != null && log.length() > 0)
				{
					if (getWorkflowContext() != null)
					{
						getWorkflowContext().log(log);
					}
					else
					{
						out.println(log);
					}
				}
			}
		}

		// Deregister the destroyed instance
		instance = null;

		unloading = false;
		fireContainerEvent("unload", this);

	}

	// -------------------------------------------------- ScriptConfig Methods

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

		return (findInitParameter(name));

	}

	/**
	 * Return the set of initialization parameter names defined for this
	 * script.  If none are defined, an empty Enumeration is returned.
	 */
	public Enumeration getInitParameterNames()
	{

		synchronized (parameters)
		{
			return (new Enumerator(parameters.keySet()));
		}

	}

	/**
	 * Return the script context with which this script is associated.
	 */
	public WorkflowContext getWorkflowContext()
	{

		if (parent == null)
			return (null);
		else if (!(parent instanceof Context))
			return (null);
		else
			return (((Context) parent).getWorkflowContext());

	}

	/**
	 * Return the name of this script.
	 */
	public String getSelectortName()
	{

		return (getName());

	}

	public long getLoadTime()
	{
		return loadTime;
	}

	public void setLoadTime(long loadTime)
	{
		this.loadTime = loadTime;
	}

	public int getClassLoadTime()
	{
		return classLoadTime;
	}

	// -------------------------------------------------------- Package Methods

	// -------------------------------------------------------- Private Methods

	/**
	 * Add a default Mapper implementation if none have been configured
	 * explicitly.
	 *
	 * @param mapperClass Java class name of the default Mapper
	 */
	protected void addDefaultMapper(String mapperClass)
	{

		; // No need for a default Mapper on a Wrapper

	}

	// ------------------------------------------------------ Lifecycle Methods

	/**
	 * Start this component, pre-loading the script if the load-on-startup
	 * value is set appropriately.
	 *
	 * @exception LifecycleException if a fatal error occurs during startup
	 */
	public void start() throws LifecycleException
	{

		// Start up this component
		super.start();

		// Load and initialize an instance of this script if requested
		// MOVED TO StandardContext START() METHOD

		setAvailable(0L);

	}

	/**
	 * Stop this component, gracefully shutting down the script if it has
	 * been initialized.
	 *
	 * @exception LifecycleException if a fatal error occurs during shutdown
	 */
	public void stop() throws LifecycleException
	{

		setAvailable(Long.MAX_VALUE);

		// Shut down our script instance (if it has been initialized)
		try
		{
			unload();
		}
		catch (SelectorException e)
		{
			getWorkflowContext().log(sm.getString("standardWrapper.unloadException", getName()), e);
		}

		// Shut down this component
		super.stop();
	}

	public String getWorkflowName()
	{
		return (getName());
	}
}