/*
 * Created on 2004-6-8
 *
 * 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.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.Serializable;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
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.ContainerEvent;
import org.huihoo.willow.ContainerListener;
import org.huihoo.willow.Lifecycle;
import org.huihoo.willow.LifecycleException;
import org.huihoo.willow.LifecycleListener;
import org.huihoo.willow.Loader;
import org.huihoo.willow.Logger;
import org.huihoo.willow.util.LifecycleSupport;
import org.huihoo.willow.util.StringManager;


/**
 * @author reic
 *
 * To change the template for this generated type comment go to
 * Window - Preferences - Java - Code Generation - Code and Comments
 */
public abstract class ContainerBase implements Container, Lifecycle, Serializable
{

	private static Log log =LogFactory.getLog(ContainerBase.class);

	/**
	 * Perform addChild with the permissions of this class.
	 * addChild can be called with the XML parser on the stack,
	 * this allows the XML parser to have fewer privileges than
	 * Tomcat.
	 */
	protected class PrivilegedAddChild implements PrivilegedAction
	{

		private Container child;

		PrivilegedAddChild(Container child)
		{
			this.child = child;
		}

		public Object run()
		{
			addChildInternal(child);
			return null;
		}

	}

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

	/**
	 * The child Containers belonging to this Container, keyed by name.
	 */
	protected HashMap children = new HashMap();

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

	/**
	 * The processor delay for this component.
	 */
	protected int backgroundProcessorDelay = 30000;

	/**
	 * The lifecycle event support for this component.
	 */
	protected LifecycleSupport lifecycle = new LifecycleSupport(this);

	/**
	 * The container event listeners for this Container.
	 */
	protected ArrayList listeners = new ArrayList();

	/**
	 * The Loader implementation with which this Container is associated.
	 */
	protected Loader loader = null;

	/**
	 * The Logger implementation with which this Container is associated.
	 */
	protected Logger logger = null;

	/**
	 * The human-readable name of this Container.
	 */
	protected String name = null;

	/**
	 * The parent Container to which this Container is a child.
	 */
	protected Container parent = null;

	/**
	 * The parent class loader to be configured when we install a Loader.
	 */
	protected ClassLoader parentClassLoader = null;
	
	/**
	 * The string manager for this package.
	 */
	protected static StringManager sm = StringManager.getManager(Constants.PACKAGE);

	/**
	 * Has this component been started?
	 */
	protected boolean started = false;

	/**
	 * The property change support for this component.
	 */
	protected PropertyChangeSupport support = new PropertyChangeSupport(this);

	/**
	 * The background thread.
	 */
	protected Thread thread = null;

	/**
	 * The background thread completion semaphore.
	 */
	protected boolean threadDone = false;

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

	/**
	 * 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 Integer(this.debug));

	}

	/**
	 * Get the delay between the invocation of the backgroundProcess method on
	 * this container and its children. Child containers will not be invoked
	 * if their delay value is not negative (which would mean they are using 
	 * their own thread). Setting this to a positive value will cause 
	 * a thread to be spawn. After waiting the specified amount of time, 
	 * the thread will invoke the executePeriodic method on this container 
	 * and all its children.
	 */
	public int getBackgroundProcessorDelay()
	{
		return backgroundProcessorDelay;
	}

	/**
	 * Set the delay between the invocation of the execute method on this
	 * container and its children.
	 * 
	 * @param delay The delay in seconds between the invocation of 
	 *              backgroundProcess methods
	 */
	public void setBackgroundProcessorDelay(int delay)
	{
		backgroundProcessorDelay = delay;
	}

	/**
	 * 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 this.getClass().getName();
	}

	/**
	 * Return the Loader with which this Container is associated.  If there is
	 * no associated Loader, return the Loader associated with our parent
	 * Container (if any); otherwise, return <code>null</code>.
	 */
	public Loader getLoader()
	{

		if (loader != null)
			return (loader);
		if (parent != null)
			return (parent.getLoader());
		return (null);

	}

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

		// Change components if necessary
		Loader oldLoader = this.loader;
		if (oldLoader == loader)
			return;
		this.loader = loader;

		// Stop the old component if necessary
		if (started && (oldLoader != null) && (oldLoader instanceof Lifecycle))
		{
			try
			{
				((Lifecycle) oldLoader).stop();
			}
			catch (LifecycleException e)
			{
				log.error("ContainerBase.setLoader: stop: ", e);
			}
		}

		// Start the new component if necessary
		if (loader != null)
		{
			loader.setContainer(this);
		}
		
		if (started && (loader != null) && (loader instanceof Lifecycle))
		{
			try
			{
				((Lifecycle) loader).start();
			}
			catch (LifecycleException e)
			{
				log.error("ContainerBase.setLoader: start: ", e);
			}
		}

		// Report this property change to interested listeners
		support.firePropertyChange("loader", oldLoader, this.loader);

	}

	/**
	 * Return the Logger with which this Container is associated.  If there is
	 * no associated Logger, return the Logger associated with our parent
	 * Container (if any); otherwise return <code>null</code>.
	 */
	public Logger getLogger()
	{

		if (logger != null)
			return (logger);
		if (parent != null)
			return (parent.getLogger());
		return (null);

	}

	/**
	 * Set the Logger with which this Container is associated.
	 *
	 * @param logger The newly associated Logger
	 */
	public synchronized void setLogger(Logger logger)
	{
		// Change components if necessary
		Logger oldLogger = this.logger;
		if (oldLogger == logger)
		{
			return;
		}
		
		this.logger = logger;

		// Stop the old component if necessary
		if (started && (oldLogger != null) && (oldLogger instanceof Lifecycle))
		{
			try
			{
				((Lifecycle) oldLogger).stop();
			}
			catch (LifecycleException e)
			{
				log.error("ContainerBase.setLogger: stop: ", e);
			}
		}

		// Start the new component if necessary
		if (logger != null)
		{
			logger.setContainer(this);
		}
		if (started && (logger != null) && (logger instanceof Lifecycle))
		{
			try
			{
				((Lifecycle) logger).start();
			}
			catch (LifecycleException e)
			{
				log.error("ContainerBase.setLogger: start: ", e);
			}
		}

		// Report this property change to interested listeners
		support.firePropertyChange("logger", oldLogger, this.logger);

	}

	/**
	 * Return an object which may be utilized for mapping to this component.
	 */
	public Object getMappingObject()
	{
		return this;
	}

	/**
	 * Return a name string (suitable for use by humans) that describes this
	 * Container.  Within the set of child containers belonging to a particular
	 * parent, Container names must be unique.
	 */
	public String getName()
	{

		return (name);

	}

	/**
	 * Set a name string (suitable for use by humans) that describes this
	 * Container.  Within the set of child containers belonging to a particular
	 * parent, Container names must be unique.
	 *
	 * @param name New name of this container
	 *
	 * @exception IllegalStateException if this Container has already been
	 *  added to the children of a parent Container (after which the name
	 *  may not be changed)
	 */
	public void setName(String name)
	{

		String oldName = this.name;
		this.name = name;
		support.firePropertyChange("name", oldName, this.name);
	}

	/**
	 * Return the Container for which this Container is a child, if there is
	 * one.  If there is no defined parent, return <code>null</code>.
	 */
	public Container getParent()
	{

		return (parent);

	}

	/**
	 * Set the parent Container to which this Container is being added as a
	 * child.  This Container may refuse to become attached to the specified
	 * Container by throwing an exception.
	 *
	 * @param container Container to which this Container is being added
	 *  as a child
	 *
	 * @exception IllegalArgumentException if this Container refuses to become
	 *  attached to the specified Container
	 */
	public void setParent(Container container)
	{

		Container oldParent = this.parent;
		this.parent = container;
		support.firePropertyChange("parent", oldParent, this.parent);

	}

	/**
	 * Return the parent class loader (if any) for this web application.
	 * This call is meaningful only <strong>after</strong> a Loader has
	 * been configured.
	 */
	public ClassLoader getParentClassLoader()
	{
		if (parentClassLoader != null)
			return (parentClassLoader);
		if (parent != null)
		{
			return (parent.getParentClassLoader());
		}
		return (ClassLoader.getSystemClassLoader());

	}

	/**
	 * Set the parent class loader (if any) for this web application.
	 * This call is meaningful only <strong>before</strong> a Loader has
	 * been configured, and the specified value (if non-null) should be
	 * passed as an argument to the class loader constructor.
	 *
	 *
	 * @param parent The new parent class loader
	 */
	public void setParentClassLoader(ClassLoader parent)
	{
		ClassLoader oldParentClassLoader = this.parentClassLoader;
		this.parentClassLoader = parent;
		support.firePropertyChange("parentClassLoader", oldParentClassLoader, this.parentClassLoader);

	}
	// ------------------------------------------------------ Container Methods

	/**
	 * Add a new child Container to those associated with this Container,
	 * if supported.  Prior to adding this Container to the set of children,
	 * the child's <code>setParent()</code> method must be called, with this
	 * Container as an argument.  This method may thrown an
	 * <code>IllegalArgumentException</code> if this Container chooses not
	 * to be attached to the specified Container, in which case it is not added
	 *
	 * @param child New child Container to be added
	 *
	 * @exception IllegalArgumentException if this exception is thrown by
	 *  the <code>setParent()</code> method of the child Container
	 * @exception IllegalArgumentException if the new child does not have
	 *  a name unique from that of existing children of this Container
	 * @exception IllegalStateException if this Container does not support
	 *  child Containers
	 */
	public void addChild(Container child)
	{
		if (System.getSecurityManager() != null)
		{
			PrivilegedAction dp = new PrivilegedAddChild(child);
			AccessController.doPrivileged(dp);
		}
		else
		{
			addChildInternal(child);
		}
	}

	private void addChildInternal(Container child)
	{

		log.debug("Add child " + child + " " + this);
		
		synchronized (children)
		{
			if (children.get(child.getName()) != null)
			{
				throw new IllegalArgumentException(
					"addChild:  Child name '" + child.getName() + "' is not unique");
			}
			
			child.setParent(this); // May throw IAE
			
			if (started && (child instanceof Lifecycle))
			{
				try
				{
					((Lifecycle) child).start();
				}
				catch (LifecycleException e)
				{
					log.error("ContainerBase.addChild: start: ", e);
					throw new IllegalStateException("ContainerBase.addChild: start: " + e);
				}
			}
			children.put(child.getName(), child);

			fireContainerEvent(ADD_CHILD_EVENT, child);
		}

	}

	/**
	 * Add a container event listener to this component.
	 *
	 * @param listener The listener to add
	 */
	public void addContainerListener(ContainerListener listener)
	{

		synchronized (listeners)
		{
			listeners.add(listener);
		}

	}

	/**
	 * Add a property change listener to this component.
	 *
	 * @param listener The listener to add
	 */
	public void addPropertyChangeListener(PropertyChangeListener listener)
	{

		support.addPropertyChangeListener(listener);

	}

	/**
	 * Return the child Container, associated with this Container, with
	 * the specified name (if any); otherwise, return <code>null</code>
	 *
	 * @param name Name of the child Container to be retrieved
	 */
	public Container findChild(String name)
	{

		if (name == null)
			return (null);
		synchronized (children)
		{ // Required by post-start changes
			return ((Container) children.get(name));
		}

	}

	/**
	 * Return the set of children Containers associated with this Container.
	 * If this Container has no children, a zero-length array is returned.
	 */
	public Container[] findChildren()
	{

		synchronized (children)
		{
			Container results[] = new Container[children.size()];
			return ((Container[]) children.values().toArray(results));
		}

	}

	/**
	 * Return the set of container listeners associated with this Container.
	 * If this Container has no registered container listeners, a zero-length
	 * array is returned.
	 */
	public ContainerListener[] findContainerListeners()
	{

		synchronized (listeners)
		{
			ContainerListener[] results = new ContainerListener[listeners.size()];
			return ((ContainerListener[]) listeners.toArray(results));
		}

	}

	/**
	 * Remove an existing child Container from association with this parent
	 * Container.
	 *
	 * @param child Existing child Container to be removed
	 */
	public void removeChild(Container child)
	{

		synchronized (children)
		{
			if (children.get(child.getName()) == null)
				return;
			children.remove(child.getName());
		}

		if (started && (child instanceof Lifecycle))
		{
			try
			{
				if (child instanceof ContainerBase)
				{
					if (((ContainerBase) child).started)
					{
						((Lifecycle) child).stop();
					}
				}
				else
				{
					((Lifecycle) child).stop();
				}
			}
			catch (LifecycleException e)
			{
				log.error("ContainerBase.removeChild: stop: ", e);
			}
		}

		fireContainerEvent(REMOVE_CHILD_EVENT, child);

		child.setParent(null);

	}

	/**
	 * Remove a container event listener from this component.
	 *
	 * @param listener The listener to remove
	 */
	public void removeContainerListener(ContainerListener listener)
	{

		synchronized (listeners)
		{
			listeners.remove(listener);
		}

	}

	/**
	 * Remove a property change listener from this component.
	 *
	 * @param listener The listener to remove
	 */
	public void removePropertyChangeListener(PropertyChangeListener listener)
	{

		support.removePropertyChangeListener(listener);

	}

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

	/**
	 * Add a lifecycle event listener to this component.
	 *
	 * @param listener The listener to add
	 */
	public void addLifecycleListener(LifecycleListener listener)
	{

		lifecycle.addLifecycleListener(listener);

	}

	/**
	 * Get the lifecycle listeners associated with this lifecycle. If this 
	 * Lifecycle has no listeners registered, a zero-length array is returned.
	 */
	public LifecycleListener[] findLifecycleListeners()
	{

		return lifecycle.findLifecycleListeners();

	}

	/**
	 * Remove a lifecycle event listener from this component.
	 *
	 * @param listener The listener to remove
	 */
	public void removeLifecycleListener(LifecycleListener listener)
	{

		lifecycle.removeLifecycleListener(listener);

	}

	/**
	 * Prepare for active use of the public methods of this Component.
	 *
	 * @exception LifecycleException if this component detects a fatal error
	 *  that prevents it from being started
	 */
	public synchronized void start() throws LifecycleException
	{		
		// Validate and update our current component state
		if (started)
		{
			log.info(sm.getString("containerBase.alreadyStarted", logName()));
			return;
		}

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

		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();
		}
		
		
		// Start our child containers, if any
		Container children[] = findChildren();
		for (int i = 0; i < children.length; i++)
		{			
			if (children[i] instanceof Lifecycle)
			{
				((Lifecycle) children[i]).start();
			}
		}

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

		// Start our thread
		threadStart();

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

	}

	/**
	 * Gracefully shut down active use of the public methods of this Component.
	 *
	 * @exception LifecycleException if this component detects a fatal error
	 *  that needs to be reported
	 */
	public synchronized void stop() throws LifecycleException
	{

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

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

		// Stop our thread
		threadStop();

		// Notify our interested LifecycleListeners
		lifecycle.fireLifecycleEvent(STOP_EVENT, null);
		started = false;
		
		// 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();
			}
		}

		// Stop our subordinate components, if any
		if ((logger != null) && (logger instanceof Lifecycle))
		{
			((Lifecycle) logger).stop();
		}
		
		if ((loader != null) && (loader instanceof Lifecycle))
		{
			((Lifecycle) loader).stop();
		}

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

	}

	/**
	 * 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()
	{
	}

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

	/**
	 * Notify all container event listeners that a particular event has
	 * occurred for this Container.  The default implementation performs
	 * this notification synchronously using the calling thread.
	 *
	 * @param type Event type
	 * @param data Event data
	 */
	public void fireContainerEvent(String type, Object data)
	{

		if (listeners.size() < 1)
		{
			return;
		}
		ContainerEvent event = new ContainerEvent(this, type, data);
		ContainerListener list[] = new ContainerListener[0];
		synchronized (listeners)
		{
			list = (ContainerListener[]) listeners.toArray(list);
		}
		for (int i = 0; i < list.length; i++)
		{	 
			((ContainerListener) list[i]).containerEvent(event);			
		}

	}

	/**
	 * Log the specified message to our current Logger (if any).
	 *
	 * @param message Message to be logged
	 */
	protected void log(String message)
	{

		 Logger logger = getLogger();
		 if (logger != null)
		{
			logger.log(logName() + ": " + message);
		}
		 else
			{
				log.info(message);
			}
	}

	/**
	 * Log the specified message and exception to our current Logger
	 * (if any).
	 *
	 * @param message Message to be logged
	 * @param throwable Related exception
	 */
	protected void log(String message, Throwable throwable)
	{

		Logger logger = getLogger();
		if (logger != null)
		{
			logger.log(logName() + ": " + message, throwable);
		}
		else
		{
			log.error(message, throwable);
		}

	}

	/**
	 * Return the abbreviated name of this container for logging messsages
	 */
	protected String logName()
	{

		String className = this.getClass().getName();
		int period = className.lastIndexOf(".");
		if (period >= 0)
			className = className.substring(period + 1);
		return (className + "[" + getName() + "]");

	}

	/**
	 * Start the background thread that will periodically check for
	 * session timeouts.
	 */
	protected void threadStart()
	{
		if (thread != null)
			return;
		if (backgroundProcessorDelay <= 0)
			return;

		threadDone = false;
		String threadName = "ContainerBackgroundProcessor[" + toString() + "]";
		thread = new Thread(new ContainerBackgroundProcessor(), threadName);
		thread.setDaemon(true);
		thread.start();
	}

	/**
	 * Stop the background thread that is periodically checking for
	 * session timeouts.
	 */
	protected void threadStop()
	{
		if (thread == null)
			return;

		threadDone = true;
		thread.interrupt();
		thread = null;
	}

	// -------------------------------------- ContainerExecuteDelay Inner Class

	/**
	 * Private thread class to invoke the backgroundProcess method 
	 * of this container and its children after a fixed delay.
	 */
	protected class ContainerBackgroundProcessor implements Runnable
	{
		public void run()
		{
			while (!threadDone)
			{
				try
				{
					Thread.sleep(backgroundProcessorDelay * 1000L);
				}
				catch (InterruptedException e)
				{
					;
				}
				if (!threadDone)
				{
					Container parent = (Container) getMappingObject();
					ClassLoader cl = Thread.currentThread().getContextClassLoader();
					if (parent.getLoader() != null)
					{
						cl = parent.getLoader().getClassLoader();
					}
					processChildren(parent, cl);
				}
			}
			
		}

		protected void processChildren(Container container, ClassLoader cl)
		{
			try
			{
				if (container.getLoader() != null)
				{
					Thread.currentThread().setContextClassLoader(container.getLoader().getClassLoader());
				}
				container.backgroundProcess();
			}
			catch (Throwable t)
			{
				log.error("Exception invoking periodic operation: ", t);
			}
			finally
			{
				Thread.currentThread().setContextClassLoader(cl);
			}
			Container[] children = container.findChildren();
			for (int i = 0; i < children.length; i++)
			{
				if (children[i].getBackgroundProcessorDelay() <= 0)
				{
					processChildren(children[i], cl);
				}
			}
		}

	}

}