//----------------------------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.startup;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;

import org.apache.commons.digester.Digester;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.huihoo.willow.Container;
import org.huihoo.willow.ContextDeployer;
import org.huihoo.willow.Globals;
import org.huihoo.willow.Lifecycle;
import org.huihoo.willow.LifecycleEvent;
import org.huihoo.willow.LifecycleListener;
import org.huihoo.willow.Logger;
import org.huihoo.willow.Service;
import org.huihoo.willow.RouteEvalutor;
import org.huihoo.willow.core.StandardContext;
import org.huihoo.willow.core.StandardService;
import org.huihoo.willow.util.StringManager;
import org.huihoo.willow.xmlrules.WorkflowRuleSet;
import org.huihoo.workflow.server.WorkflowContext;
import org.xml.sax.InputSource;
import org.xml.sax.SAXParseException;

public final class ContextConfig implements LifecycleListener
{
	private static Log log = LogFactory.getLog(ContextConfig.class);
	/**
	 * The string resources for this package.
	 */
	private static final StringManager sm = StringManager.getManager(Constants.PACKAGE);

	// ----------------------------------------------------- Instance Variables
	/**
	 * The Context we are associated with.
	 */
	private StandardContext context = null;

	private RouteEvalutor transitionSelector = null;

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

	/**
	 * The default workflow application's deployment location.
	 */
	private String defaultWorkflowXml = null;

	/**
	 * Track any fatal errors during startup configuration processing.
	 */
	private boolean ok = false;

	/**
	 * The names of applications that we have auto-deployed (to avoid
	 * double deployment attempts).
	 */
	protected ArrayList deployed = new ArrayList();
	/**
	 * Last modified dates of the XPDL's xml files of the XPDLs, keyed by
	 * xpdl file name.
	 */
	private HashMap xpdlXmlLastModified = new HashMap();

	/**
	 * The <code>Digester</code> we will use to process .workflow application
	 * deployment descriptor files.
	 */
	private Digester workflowDigester = null;
	/**
	 * The <code>Rule</code> used to parse the workflow.xml
	 */
	private WorkflowRuleSet workflowRuleSet = new WorkflowRuleSet();
	/**
	 * Attribute value used to turn on/off XML validation
	 */
	private boolean xmlValidation = false;
	/**
	 * Attribute value used to turn on/off XML namespace awarenes.
	 */
	private boolean xmlNamespaceAware = 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)
	{
		this.debug = debug;
	}

	/**
	 * Return the location of the default deployment descriptor
	 */
	public String getDefaultWorkflowXml()
	{
		if (defaultWorkflowXml == null)
		{
			defaultWorkflowXml = Globals.DefaultWorkflowXml;
		}
		return (this.defaultWorkflowXml);

	}

	/**
	 * Set the location of the default deployment descriptor
	 *
	 * @param path Absolute/relative path to the default web.xml
	 */
	public void setDefaultWorkflowXml(String path)
	{

		this.defaultWorkflowXml = path;

	}

	// --------------------------------------------------------- Public Methods
	/**
	 * Process the START event for an associated Context.
	 *
	 * @param event The lifecycle event that has occurred
	 */
	public void lifecycleEvent(LifecycleEvent event)
	{
		if (event.getType().equals("check"))
		{
			check();
		}
		// Identify the context we are associated with
		try
		{
			context = (StandardContext) event.getLifecycle();
			if (context instanceof StandardContext)
			{
				int contextDebug = ((StandardContext) context).getDebug();
				if (contextDebug > this.debug)
				{
					this.debug = contextDebug;
				}
			}
		}
		catch (ClassCastException e)
		{
			log.error(sm.getString("contextConfig.cce", event.getLifecycle()), e);
			return;
		}
		// Called from ContainerBase.addChild() -> StandardContext.start()
		// Process the event that has occurred
		if (event.getType().equals(Lifecycle.START_EVENT))
		{
			start();
		}
		else if (event.getType().equals(Lifecycle.STOP_EVENT))
		{
			stop();
		}
	}
	// -------------------------------------------------------- Private Methods
	/**
	 * Process the application configuration file, if it exists.
	 */
	private void applicationConfig()
	{
		// Open the application workflow.xml file, if it exists
		WorkflowContext servletContext = context.getWorkflowContext();
		InputStream stream = null;
		if (servletContext != null)
		{
			stream = servletContext.getResourceAsStream(Globals.ApplicationWorkflowXml);
			if (stream == null)
			{
				log.info(sm.getString("contextConfig.applicationMissing") + " " + context);
			}
		}
		if (workflowDigester == null)
		{
			workflowDigester = createWorkflowDigester();
		}
		// Process the application workflow.xml file
		synchronized (workflowDigester)
		{
			try
			{
				URL workflowURL = servletContext.getResource(Globals.ApplicationWorkflowXml);
				if (workflowURL != null)
				{
					//--------------------entire workflow application-----------------
					InputSource is = new InputSource(workflowURL.toExternalForm());
					is.setByteStream(stream);

					workflowDigester.clear();
					workflowDigester.setDebug(getDebug());
					workflowDigester.setUseContextClassLoader(false);
					workflowDigester.push(context);
					workflowDigester.parse(is);
				}

				Class c = Class.forName(context.getRouteEvalutorClass());
				transitionSelector = (RouteEvalutor) c.newInstance();
				context.setRouteEvalutor(transitionSelector);
				transitionSelector.setContainer(context);
			}
			catch (SAXParseException e)
			{
				log(sm.getString("contextConfig.applicationParse"), e);
				log(sm.getString("contextConfig.applicationPosition", "" + e.getLineNumber(), "" + e.getColumnNumber()));
			}
			catch (Exception e)
			{
				log(sm.getString("contextConfig.applicationParse"), e);
			}
			finally
			{
				try
				{
					if (stream != null)
					{
						stream.close();
					}
				}
				catch (IOException e)
				{
					log.error(sm.getString("contextConfig.applicationClose"), e);
				}
				workflowDigester.push(null);
			}
		}
	}
	/**
	 * Create (if necessary) and return a Digester configured to process the
	 * workflow application deployment descriptor (workflow.xml).
	 */
	private Digester createWorkflowDigester()
	{
		Digester workflowDigester = DigesterFactory.newDigester(xmlValidation, xmlNamespaceAware, workflowRuleSet);
		return workflowDigester;
	}

	/**
	 * Process the default configuration file, if it exists.
	 * The default config must be read with the container loader - so
	 * container servlets can be loaded
	 */
	private void defaultConfig()
	{
		long t1 = System.currentTimeMillis();

		// Open the default workflow file, if it exists
		if (defaultWorkflowXml == null && context instanceof StandardContext)
		{
			defaultWorkflowXml = ((StandardContext) context).getDefaultWorkflowXml();
		}
		// set the default if we don't have any overrides
		if (defaultWorkflowXml == null)
		{
			getDefaultWorkflowXml();
		}

		File file = new File(this.defaultWorkflowXml);
		if (!file.isAbsolute())
		{
			file = new File(System.getProperty(Globals.PROPS_WILLOW_HOME), this.defaultWorkflowXml);
		}

		InputStream stream = null;
		InputSource source = null;

		try
		{
			if (!file.exists())
			{
				// Use getResource and getResourceAsStream
				stream = getClass().getClassLoader().getResourceAsStream(defaultWorkflowXml);
				if (stream != null)
				{
					source = new InputSource(getClass().getClassLoader().getResource(defaultWorkflowXml).toString());
				}
				else
				{
					log.info("No default workflow.xml");
					return;
				}
			}
			else
			{
				source = new InputSource("file://" + file.getAbsolutePath());
				stream = new FileInputStream(file);
			}
		}
		catch (Exception e)
		{
			log.error(sm.getString("contextConfig.defaultMissing") + " " + defaultWorkflowXml + " " + file, e);
			return;
		}

		if (workflowDigester == null)
		{
			workflowDigester = createWorkflowDigester();
		}

		// Process the default workflow.xml file
		synchronized (workflowDigester)
		{
			try
			{
				source.setByteStream(stream);
				workflowDigester.setDebug(getDebug());

				workflowDigester.clear();
				workflowDigester.setClassLoader(this.getClass().getClassLoader());
				//log.info( "Using cl: " + webDigester.getClassLoader());
				workflowDigester.setUseContextClassLoader(false);
				workflowDigester.push(context);
				workflowDigester.parse(source);
			}
			catch (SAXParseException e)
			{
				log.error(sm.getString("contextConfig.defaultParse"), e);
				log.error(sm.getString("contextConfig.defaultPosition", "" + e.getLineNumber(), "" + e.getColumnNumber()));
			}
			catch (Exception e)
			{
				log.error(sm.getString("contextConfig.defaultParse"), e);
			}
			finally
			{
				try
				{
					if (stream != null)
					{
						stream.close();
					}
				}
				catch (IOException e)
				{
					log.error(sm.getString("contextConfig.defaultClose"), e);
				}
			}
		}

		workflowRuleSet.recycle();

		long t2 = System.currentTimeMillis();
		if ((t2 - t1) > 200)
		{
			log.debug("Processed default web.xml " + file + " " + (t2 - t1));
		}
	}

	/**
	 * Log a message on the Logger associated with our WorkflowContext (if any)
	 *
	 * @param message Message to be logged
	 */
	private void log(String message)
	{
		Logger logger = null;
		if (context != null)
		{
			logger = context.getLogger();
		}
		if (logger != null)
		{
			logger.log("ContextConfig[" + context.getName() + "]: " + message);
		}
		else
		{
			log.info(message);
		}
	}
	/**
	 * Log a message on the Logger associated with our WorkflowContext (if any)
	 *
	 * @param message Message to be logged
	 * @param throwable Associated exception
	 */
	private void log(String message, Throwable throwable)
	{
		Logger logger = null;
		if (context != null)
		{
			logger = context.getLogger();
		}
		if (logger != null)
		{
			logger.log("ContextConfig[" + context.getName() + "] " + message, throwable);
		}
		else
		{
			log.error(message, throwable);
		}
	}

	/**
	 * Process a "start" event for this WorkflowContext - in background
	 */
	private synchronized void start()
	{
		// Called from StandardContext.start()
		log.debug(sm.getString("contextConfig.start"));
		context.setConfigured(false);
		ok = true;

		// Set properties based on DefaultContext
		Container container = context.getParent();
		if (!context.getOverride())
		{
			if (container instanceof Service)
			{
				((Service) container).importDefaultContext(context);
				xmlValidation = ((Service) container).getXmlValidation();
				xmlNamespaceAware = ((Service) container).getXmlNamespaceAware();
			}

		}

		// Process the application workflow.xml files
		defaultConfig();
		applicationConfig();

		deployXPDLs();

		//	Make our application available if no problems were encountered
		if (ok)
		{
			context.setConfigured(true);
		}
		else
		{
			log.error(sm.getString("contextConfig.unavailable"));
			context.setConfigured(false);
		}
	}
	/**
	 * Process a "stop" event for this Context.
	 */
	private synchronized void stop()
	{
		log.debug(sm.getString("contextConfig.stop"));

		undeployXPDLs();

		// Removing application listeners
		String[] applicationListeners = context.findApplicationListeners();
		for (int i = 0; i < applicationListeners.length; i++)
		{
			context.removeApplicationListener(applicationListeners[i]);
		}

		//Removing parameters
		String[] appParams = context.findInitParameters();
		for (int i = 0; i < appParams.length; i++)
		{
			context.removeInitParameter(appParams[i]);
		}
	}
	/**
	 * Undeploy all deployed applications.
	 */
	protected void undeployXPDLs()
	{
		if (!(context instanceof ContextDeployer))
		{
			return;
		}
		log.debug(sm.getString("contextConfig.undeploying"));
		ContextDeployer deployer = (ContextDeployer) context;
		String xpdlFiles[] = deployer.findWorkflowXPDLs();
		for (int i = 0; i < xpdlFiles.length; i++)
		{
			log.debug(sm.getString("contextConfig.undeploy", xpdlFiles[i]));
			try
			{
				((ContextDeployer) context).remove(xpdlFiles[i]);
			}
			catch (Throwable t)
			{
				log.error(sm.getString("contextConfig.undeploy.error", xpdlFiles[i]), t);
			}
		}
		xpdlXmlLastModified.clear();
		deployed.clear();
	}
	/**
	 * Deploy applications for any directories or WAR files that are found
	 * in our "application root" directory.
	 */
	protected void deployXPDLs()
	{
		if (!(context instanceof ContextDeployer))
		{
			return;
		}

		String docBase = context.getDocBase();
		if (docBase == null || docBase.trim().length() == 0)
		{
			//@see WillowServlet
			return;
		}

		if (context.isFilesystemBased())
		{
			deployDirectories();
		}
		else
		{
			deployWARs();
		}
	}

	protected void deployDirectories()
	{
		WorkflowContext templateContext = context.getWorkflowContext();
		ContextDeployer contextDeploy = (ContextDeployer) context;
		File docBase = new File(context.getDocBase());

		if (!docBase.isAbsolute())
		{
			Container parent = context.getParent();
			if (parent != null && (parent instanceof StandardService))
			{
				docBase = new File(((StandardService) parent).getAppBase(), context.getDocBase());
			}
		}

		if (!docBase.exists() || !docBase.isDirectory())
		{
			return;
		}

		File xpdlBase = new File(docBase, "WEB-INF/xpdl");
		String files[] = xpdlBase.list();

		for (int i = 0; i < files.length; i++)
		{
			if (deployed.contains(files[i]))
			{
				continue;
			}
			File xpdl = new File(xpdlBase, files[i]);
			if (xpdl.isFile() && xpdl.getName().endsWith(".xpdl"))
			{
				deployed.add(files[i]);
				// Deploy the application in this directory
				log.debug(sm.getString("contextConfig.deployXPDL", files[i]));
				long t1 = System.currentTimeMillis();
				try
				{
					URL url = new URL("file", null, xpdl.getCanonicalPath());
					contextDeploy.install(xpdl.getName(), url);
				}
				catch (Throwable t)
				{
					log(sm.getString("contextConfig.deployXPDL.error", files[i]), t);
				}
				long t2 = System.currentTimeMillis();
				if ((t2 - t1) > 200)
				{
					log.debug("Deployed " + files[i] + " " + (t2 - t1));
				}
			}
		}
	}

	protected void deployWARs()
	{
		WorkflowContext templateContext = context.getWorkflowContext();
		ContextDeployer contextDeploy = (ContextDeployer) context;

		Set xpSet = templateContext.getResourcePaths("/WEB-INF/xpdl");
		Iterator iterator = xpSet.iterator();

		URL xpURL = null;
		String xp_path = null;
		String xp_temp = null;
		while (iterator.hasNext())
		{
			xp_path = (String) iterator.next();
			try
			{
				xpURL = templateContext.getResource(xp_path);
				xp_path = xpURL.toString();
				if (deployed.contains(xp_path))
				{
					continue;
				}

				int period = xp_path.lastIndexOf("/");
				if (period == -1 || !xp_path.endsWith(".xpdl"))
				{
					log(sm.getString("contextConfig.deployXPDL.error", xp_path));
				}
				else
				{
					deployed.add(xp_path);
					log.debug(sm.getString("contextConfig.deployXPDL", xp_path));
					long t1 = System.currentTimeMillis();
					try
					{
						contextDeploy.install(xp_path.substring(period + 1), xpURL);
					}
					catch (Throwable t)
					{
						log(sm.getString("contextConfig.deployXPDL.error", xp_path), t);
					}
					long t2 = System.currentTimeMillis();
					if ((t2 - t1) > 200)
					{
						log.debug("Deployed " + xp_path + " " + (t2 - t1));
					}
				}

			}
			catch (Throwable t)
			{
				log(sm.getString("contextConfig.deployXPDL.error", xp_path), t);
			}
		}
	}

	/**
	 * Check deployment descriptors last modified date.
	 */
	protected void checkXPDLLastModified()
	{
		if (!(context instanceof ContextDeployer))
		{
			return;
		}

		File contextBase = new File(context.getDocBase());
		if (!contextBase.exists() || !contextBase.isDirectory())
		{
			return;
		}
		ContextDeployer deployer = (ContextDeployer) context;
		String files[] = deployer.findWorkflowXPDLs();
		for (int i = 0; i < files.length; i++)
		{
			String xpdlFileName = files[i];
			File xpdlBase = new File(contextBase, "WEB-INF/xpdl");
			File xpdlFile = new File(xpdlBase, xpdlFileName);
			long newLastModified = xpdlFile.lastModified();
			Long lastModified = (Long) xpdlXmlLastModified.get(xpdlFileName);
			if (lastModified == null)
			{
				xpdlXmlLastModified.put(xpdlFileName, new Long(newLastModified));
			}
			else
			{
				if (lastModified.longValue() != newLastModified)
				{
					if (newLastModified > (lastModified.longValue() + 5000))
					{
						xpdlXmlLastModified.remove(xpdlFileName);
						try
						{
							URL url = new URL("file", null, xpdlFile.getCanonicalPath());
							deployer.remove(xpdlFileName);
							deployer.install(xpdlFileName, url);
						}
						catch (Throwable t)
						{
							log.error(sm.getString("contextConfig.deployXPDL.error", xpdlFile.getName()), t);
						}
					}
					else
					{
						xpdlXmlLastModified.put(xpdlFileName, new Long(newLastModified));
					}
				}
			}
		}
	}
	/**
	 * Deploy Workflow.
	 */
	protected void check()
	{
		if (context.isFilesystemBased() && context.getAutoDeploy())
		{
			deployXPDLs();
			checkXPDLLastModified();
		}
	}
}