//----------------------------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.workflow.script.compiler;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilePermission;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.CodeSource;
import java.security.PermissionCollection;
import java.security.Policy;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.huihoo.commons.logging.SystemLogHandler;
import org.huihoo.workflow.script.Constants;
import org.huihoo.workflow.script.Options;
import org.huihoo.workflow.script.ScriptCompilationContext;
import org.huihoo.workflow.script.selector.ScriptSelectorWrapper;
import org.huihoo.workflow.server.WorkflowContext;
import org.huihoo.workflow.util.StringManager;
import org.huihoo.workflow.xpdl.WorkflowCondition;

public class ScriptRuntimeContext implements Runnable
{
	// Logger
	private static Log log = LogFactory.getLog(ScriptRuntimeContext.class);

	protected static StringManager sm = StringManager.getManager(Constants.RESOURCE_BUNDLE_BASE);

	/**
	 * Preload classes required at runtime by a Rule Script so that
	 * we don't get a defineClassInPackage security exception.
	 */
	static {

		SecurityClassLoad.securityClassLoad(ScriptRuntimeContext.class.getClassLoader());
	}

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

	/**
	 * Create a ScriptRuntimeContext for a org.huihoo.workflow application servletContext.
	 *
	 * Loads in any previously generated dependencies from file.
	 *
	 * @param WorkflowContext for org.huihoo.workflow application
	 */
	public ScriptRuntimeContext(WorkflowContext servletContext, Options options)
	{

		System.setErr(new SystemLogHandler(System.err));
		System.setOut(new SystemLogHandler(System.out));

		this.servletContext = servletContext;
		this.options = options;

		// Get the parent class loader
		parentClassLoader = (URLClassLoader) servletContext.getAttribute(Constants.SCRIPT_CLASS_LOADER);

		if (parentClassLoader == null)
		{
			parentClassLoader = (URLClassLoader) this.getClass().getClassLoader();
		}

		if (log.isDebugEnabled())
		{
			if (parentClassLoader != null)
			{
				log.debug(sm.getString("script.message.parent_class_loader_is", parentClassLoader.toString()));
			}
			else
			{
				log.debug(sm.getString("script.message.parent_class_loader_is", "<none>"));
			}
		}

		initClassPath();

		if (System.getSecurityManager() != null)
		{
			initSecurity();
		}

		// If workflow application servletContext is running from a
		// directory, start the background compilation thread
		String appBase = servletContext.getRealPath("/");
		if (!options.getDevelopment() && appBase != null && options.getReloading())
		{
			if (appBase.endsWith(File.separator))
			{
				appBase = appBase.substring(0, appBase.length() - 1);
			}
			String directory = appBase.substring(appBase.lastIndexOf(File.separator));
			threadName = threadName + "[" + directory + "]";
			threadStart();
		}
	}

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

	/**
	 * This workflow applications WorkflowContext
	 */
	private WorkflowContext servletContext;
	private Options options;
	private URLClassLoader parentClassLoader;
	private PermissionCollection permissionCollection;
	private CodeSource codeSource;
	private String classpath;

	/**
	 * Maps WFS pages to their ScriptBase's
	 */
	private Map wfses = Collections.synchronizedMap(new HashMap());

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

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

	/**
		* Name to register for the background thread.
		*/
	private String threadName = "ScriptRuntimeContext";
	// ------------------------------------------------------ Protected Methods

	/**
	 * Add a new ScriptWrapper.
	 *
	 * @param String uri of WFS
	 * @param Script className for WFS
	 */
	public void addWrapper(WorkflowCondition workflowCondition, ScriptSelectorWrapper scriptWrapper)
	{
		wfses.remove(workflowCondition);
		wfses.put(workflowCondition, scriptWrapper);
	}

	/**
	 * Get an already existing ScriptWrapper.
	 *
	 */
	public ScriptSelectorWrapper getWrapper(WorkflowCondition workflowCondition)
	{
		return (ScriptSelectorWrapper) wfses.get(workflowCondition);
	}

	/**
	 * Remove a  ScriptWrapper.
	 *
	 */
	public void removeWrapper(WorkflowCondition workflowCondition)
	{
		wfses.remove(workflowCondition);
	}

	/**
	 * Get the SecurityManager Policy CodeSource for this workflow
	 * applicaiton context.
	 *
	 * @return CodeSource for Rule
	 */
	public CodeSource getCodeSource()
	{
		return codeSource;
	}

	/**
	 * Get the parent URLClassLoader.
	 *
	 * @return URLClassLoader parent
	 */
	public URLClassLoader getParentClassLoader()
	{
		return parentClassLoader;
	}

	/**
	 * Get the SecurityManager PermissionCollection for this
	 * workflow application context.
	 *
	 * @return PermissionCollection permissions
	 */
	public PermissionCollection getPermissionCollection()
	{
		return permissionCollection;
	}

	/**
	 * Process a "destory" event for this org.huihoo.workflow application servletContext.
	 */
	public void destroy()
	{
		threadStop();

		if (System.err instanceof SystemLogHandler)
		{
			System.setErr(((SystemLogHandler) System.err).getWrapped());
		}

		Iterator servlets = wfses.values().iterator();
		while (servlets.hasNext())
		{
			((ScriptSelectorWrapper) servlets.next()).destroy();
		}

	}

	/**
	 * The classpath that is passed off to the Java compiler.
	 */
	public String getClassPath()
	{
		return classpath;
	}

	/**
	 * Method used to initialize classpath for compiles.
	 */
	private void initClassPath()
	{

		URL[] urls = parentClassLoader.getURLs();
		StringBuffer cpath = new StringBuffer();
		String sep = System.getProperty("path.separator");

		for (int i = 0; i < urls.length; i++)
		{
			// Willow can use URL's other than file URL's,
			// a protocol other than file: will generate a
			// bad file system path, so only add file:
			// protocol URL's to the classpath.

			if (urls[i].getProtocol().equals("file"))
			{
				cpath.append((String) urls[i].getFile() + sep);
			}
		}

		cpath.append(options.getScratchDir() + sep);
		classpath = cpath.toString();

		String cp = (String) servletContext.getAttribute(Constants.SCRIPT_CLASS_PATH);

		if (cp == null || cp.equals(""))
		{
			cp = options.getClassPath();
		}

		if (cp != null)
		{
			classpath += cp;
		}
	}

	/**
	 * Method used to initialize SecurityManager data.
	 */
	private void initSecurity()
	{

		// Setup the PermissionCollection for this web app context
		// based on the permissions configured for the root of the
		// workflow app context directory, then add a file read permission
		// for that directory.
		Policy policy = Policy.getPolicy();
		if (policy != null)
		{
			try
			{
				// Get the permissions for the web app context
				String docBase = servletContext.getRealPath("/");
				if (docBase == null)
				{
					docBase = options.getScratchDir().toString();
				}
				String codeBase = docBase;
				if (!codeBase.endsWith(File.separator))
				{
					codeBase = codeBase + File.separator;
				}
				File contextDir = new File(codeBase);
				URL url = contextDir.getCanonicalFile().toURL();
				codeSource = new CodeSource(url, null);
				permissionCollection = policy.getPermissions(codeSource);

				// Create a file read permission for web app context directory
				if (!docBase.endsWith(File.separator))
				{
					permissionCollection.add(new FilePermission(docBase, "read"));
					docBase = docBase + File.separator;
				}
				else
				{
					permissionCollection.add(new FilePermission(docBase.substring(0, docBase.length() - 1), "read"));
				}
				docBase = docBase + "-";
				permissionCollection.add(new FilePermission(docBase, "read"));

				// Create a file read permission for web app tempdir (work)
				// directory
				String workDir = options.getScratchDir().toString();
				if (!workDir.endsWith(File.separator))
				{
					permissionCollection.add(new FilePermission(workDir, "read"));
					workDir = workDir + File.separator;
				}
				workDir = workDir + "-";
				permissionCollection.add(new FilePermission(workDir, "read"));

				// Allow the JSP to access org.apache.jasper.runtime.HttpJspBase
				permissionCollection.add(new RuntimePermission("accessClassInPackage.org.apache.jasper.runtime"));

				if (parentClassLoader instanceof URLClassLoader)
				{
					URL[] urls = parentClassLoader.getURLs();
					String jarUrl = null;
					String jndiUrl = null;
					for (int i = 0; i < urls.length; i++)
					{
						if (jndiUrl == null && urls[i].toString().startsWith("jndi:"))
						{
							jndiUrl = urls[i].toString() + "-";
						}
						if (jarUrl == null && urls[i].toString().startsWith("jar:jndi:"))
						{
							jarUrl = urls[i].toString();
							jarUrl = jarUrl.substring(0, jarUrl.length() - 2);
							jarUrl = jarUrl.substring(0, jarUrl.lastIndexOf('/')) + "/-";
						}
					}
					if (jarUrl != null)
					{
						permissionCollection.add(new FilePermission(jarUrl, "read"));
						permissionCollection.add(new FilePermission(jarUrl.substring(4), "read"));
					}
					if (jndiUrl != null)
						permissionCollection.add(new FilePermission(jndiUrl, "read"));
				}
			}
			catch (Exception e)
			{
				servletContext.log("Security Init for context failed", e);
			}
		}
	}

	// -------------------------------------------------------- Thread Support

	/**
	 * Start the background thread that will periodically check for
	 * changes to compile time script files in a WFS.
	 *
	 * @exception IllegalStateException if we should not be starting
	 *  a background thread now
	 */
	protected void threadStart()
	{

		// Has the background thread already been started?
		if (thread != null)
		{
			return;
		}

		// Start the background thread
		threadDone = false;
		thread = new Thread(this, threadName);
		thread.setDaemon(true);
		thread.start();

	}

	/**
	 * Stop the background thread that is periodically checking for
	 * changes to compile time included files in a RULE.
	 */
	protected void threadStop()
	{

		if (thread == null)
		{
			return;
		}

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

	}

	/**
	 * Sleep for the duration specified by the <code>checkInterval</code>
	 * property.
	 */
	protected void threadSleep()
	{

		try
		{
			Thread.sleep(options.getCheckInterval() * 1000L);
		}
		catch (InterruptedException e)
		{
			;
		}

	}

	// ------------------------------------------------------ Background Thread

	/**
	 * Method used by background thread to check the RULE dependencies
	 * registered with this class for RULE's.
	 */
	private void checkCompile()
	{
		Object[] scriptWrappers = wfses.values().toArray();
		for (int i = 0; i < scriptWrappers.length; i++)
		{
			ScriptSelectorWrapper scriptWrapper = (ScriptSelectorWrapper) scriptWrappers[i];
			ScriptCompilationContext ctxt = scriptWrapper.getCompilationContext();

			// scriptWrapper also synchronizes on this when
			// it detects it has to do a reload
			synchronized (scriptWrapper)
			{
				try
				{
					ctxt.compile();
				}
				catch (FileNotFoundException ex)
				{
					ctxt.incrementRemoved();
				}
				catch (Throwable t)
				{
					scriptWrapper.getWorkflowContext().log("Background compile failed", t);
				}
			}
		}
	}

	/**
	 * The background thread that checks for changes to files
	 * included by a RULE and flags that a recompile is required.
	 */
	public void run()
	{

		// Loop until the termination semaphore is set
		while (!threadDone)
		{

			// Wait for our check interval
			threadSleep();

			// Check for script files which are newer than the
			// WFS which uses them.
			try
			{
				checkCompile();
			}
			catch (Throwable t)
			{
				t.printStackTrace();
			}
		}
	}
}
