/*
 * Created on 2004-6-1
 *
 * To change the template for this generated file go to
 * Window - Preferences - Java - Code Generation - Code and Comments
 */
package org.huihoo.willow.loader;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.io.FilePermission;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

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.Globals;
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.core.StandardContext;
import org.huihoo.willow.util.JarFilenameFilter;
import org.huihoo.willow.util.LifecycleSupport;
import org.huihoo.willow.util.StringManager;
import workflow.script.core.ScriptContext;

/**
 * @author Administrator
 *
 * To change the template for this generated type comment go to
 * Window - Preferences - Java - Code Generation - Code and Comments
 */
public class WorkflowLoader implements Lifecycle, Loader, PropertyChangeListener
{
	private static Log log = LogFactory.getLog(WorkflowLoader.class);

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

	/**
	 * Construct a new workflowLoader with no defined parent class loader
	 * (so that the actual parent will be the system class loader).
	 */
	public WorkflowLoader()
	{

		this(null);

	}

	/**
	 * Construct a new workflowLoader with the specified class loader
	 * to be defined as the parent of the ClassLoader we ultimately create.
	 *
	 * @param parent The parent class loader
	 */
	public WorkflowLoader(ClassLoader parent)
	{
		super();
		this.parentClassLoader = parent;
	}

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

	/**
	 * First load of the class.
	 */
	private static boolean first = true;

	/**
	 * The class loader being managed by this Loader component.
	 */
	private WorkflowClassLoader classLoader = null;

	/**
	 * The Container with which this Loader has been associated.
	 */
	private Container container = null;

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

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

	/**
	 * The descriptive information about this Loader implementation.
	 */
	private static final String info = "org.huihoo.willow.loader.WorkflowLoader/1.0";

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

	/**
	 * The Java class name of the ClassLoader implementation to be used.
	 * This class should extend workflowClassLoader, otherwise, a different 
	 * loader implementation must be used.
	 */
	private String loaderClass = "org.huihoo.willow.loader.WorkflowClassLoader";

	/**
	 * The parent class loader of the class loader we will create.
	 */
	private ClassLoader parentClassLoader = null;

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

	/**
	 * The set of repositories associated with this class loader.
	 */
	private String repositories[] = new String[0];

	/**
	   * The string manager for this package.
	   */
	protected static final StringManager sm = StringManager.getManager(Constants.PACKAGE);

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

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

	/**
	 * Classpath set in the loader.
	 */
	private String classpath = null;

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

	/**
	 * Return the Java class loader to be used by this Container.
	 */
	public ClassLoader getClassLoader()
	{

		return ((ClassLoader) classLoader);

	}

	/**
	 * Return the Container with which this Logger has been associated.
	 */
	public Container getContainer()
	{

		return (container);
	}

	/**
	 * Set the Container with which this Logger has been associated.
	 *
	 * @param container The associated Container
	 */
	public void setContainer(Container container)
	{
		this.container = container;
	}
	/**
	    * 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));

	}

	/**
	 * 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 descriptive information about this Loader implementation and
	 * the corresponding version number, in the format
	 * <code>&lt;description&gt;/&lt;version&gt;</code>.
	 */
	public String getInfo()
	{

		return (info);

	}

	/**
	 * Return the ClassLoader class name.
	 */
	public String getLoaderClass()
	{

		return (this.loaderClass);

	}

	/**
	 * Set the ClassLoader class name.
	 *
	 * @param loaderClass The new ClassLoader class name
	 */
	public void setLoaderClass(String loaderClass)
	{

		this.loaderClass = loaderClass;

	}

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

		return (this.reloadable);

	}

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

		// Process this property change
		boolean oldReloadable = this.reloadable;
		this.reloadable = reloadable;
		support.firePropertyChange(
			"reloadable",
			new Boolean(oldReloadable),
			new Boolean(this.reloadable));

	}

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

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

		support.addPropertyChangeListener(listener);

	}

	/**
	 * Add a new repository to the set of repositories for this class loader.
	 *
	 * @param repository Repository to be added
	 */
	public void addRepository(String repository)
	{

		log.debug(sm.getString("workflowLoader.addRepository", repository));

		for (int i = 0; i < repositories.length; i++)
		{
			if (repository.equals(repositories[i]))
				return;
		}
		String results[] = new String[repositories.length + 1];
		for (int i = 0; i < repositories.length; i++)
			results[i] = repositories[i];
		results[repositories.length] = repository;
		repositories = results;

		if (started && (classLoader != null))
		{
			classLoader.addRepository(repository);
			setClassPath();
		}

	}

	/**
	 * Return the set of repositories defined for this class loader.
	 * If none are defined, a zero-length array is returned.
	 * For security reason, returns a clone of the Array (since 
	 * String are immutable).
	 */
	public String[] findRepositories()
	{

		return ((String[]) repositories.clone());

	}

	public String[] getRepositories()
	{
		return ((String[]) repositories.clone());
	}

	/** Extra repositories for this loader
	 */
	public String getRepositoriesString()
	{
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < repositories.length; i++)
		{
			sb.append(repositories[i]).append(":");
		}
		return sb.toString();
	}

	/** 
	 * Classpath, as set in wangll.willow.wfs_classpath context property
	 *
	 * @return The classpath
	 */
	public String getClasspath()
	{
		return classpath;
	}

	/**
	 * Has the internal repository associated with this Loader been modified,
	 * such that the loaded classes should be reloaded?
	 */
	public boolean modified()
	{

		return (classLoader.modified());

	}

	/**
	 * Used to periodically signal to the classloader to release JAR resources.
	 */
	public void closeJARs(boolean force)
	{
		if (classLoader != null)
		{
			classLoader.closeJARs(force);
		}
	}

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

		support.removePropertyChangeListener(listener);

	}

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

		StringBuffer sb = new StringBuffer("workflowLoader[");
		if (container != null)
			sb.append(container.getName());
		sb.append("]");
		return (sb.toString());

	}

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

	}

	private boolean initialized = false;

	/**
	 * Start this component, initializing our associated class loader.
	 *
	 * @exception LifecycleException if a lifecycle error occurs
	 */
	public void start() throws LifecycleException
	{
		if (started)
		{
			throw new LifecycleException(sm.getString("workflowLoader.alreadyStarted"));
		}

		log.debug(sm.getString("workflowLoader.starting"));

		lifecycle.fireLifecycleEvent(START_EVENT, null);
		started = true;

		// Construct a class loader based on our current repositories list
		try
		{

			classLoader = createClassLoader();
			classLoader.setDebug(this.debug);
			classLoader.setDelegate(this.delegate);

			for (int i = 0; i < repositories.length; i++)
			{
				classLoader.addRepository(repositories[i]);
			}

			// Configure our repositories
			setRepositories();
			setClassPath();

			setPermissions();

			if (classLoader instanceof Lifecycle)
			{
				((Lifecycle) classLoader).start();
			}

		}
		catch (Throwable t)
		{
			log.error("LifecycleException ", t);
			throw new LifecycleException("start: ", t);
		}

	}

	/**
	 * Stop this component, finalizing our associated class loader.
	 *
	 * @exception LifecycleException if a lifecycle error occurs
	 */
	public void stop() throws LifecycleException
	{

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

		log.debug(sm.getString("workflowLoader.stopping"));
		lifecycle.fireLifecycleEvent(STOP_EVENT, null);
		started = false;

		classLoader = null;

	}

	// ----------------------------------------- PropertyChangeListener Methods

	/**
	 * Process property change events from our associated Container.
	 *
	 * @param event The property change event that has occurred
	 */
	public void propertyChange(PropertyChangeEvent event)
	{

		// Validate the source of this event
		if (!(event.getSource() instanceof Container))
			return;

		// Process a relevant property change
		if (event.getPropertyName().equals("reloadable"))
		{
			try
			{
				setReloadable(((Boolean) event.getNewValue()).booleanValue());
			}
			catch (NumberFormatException e)
			{
				log.error(sm.getString("workflowLoader.reloadable", event.getNewValue().toString()));
			}
		}

	}

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

	/**
	 * Create associated classLoader.
	 */
	private WorkflowClassLoader createClassLoader() throws Exception
	{

		Class clazz = Class.forName(loaderClass);
		WorkflowClassLoader classLoader = null;

		if (parentClassLoader == null)
		{
			parentClassLoader = Thread.currentThread().getContextClassLoader();
		}
		Class[] argTypes = { ClassLoader.class };
		Object[] args = { parentClassLoader };
		Constructor constr = clazz.getConstructor(argTypes);
		classLoader = (WorkflowClassLoader) constr.newInstance(args);

		return classLoader;

	}

	/**
	 * Log a message on the Logger associated with our Container (if any)
	 *
	 * @param message Message to be logged
	 */
	private void log(String message)
	{

		Logger logger = null;
		if (container != null)
		{
			logger = container.getLogger();
		}
		if (logger != null)
		{
			logger.log("workflowLoader[" + container.getName() + "]: " + message);
		}
		else
		{
			String containerName = null;
			if (container != null)
			{
				containerName = container.getName();
			}
			System.out.println("workflowLoader[" + containerName + "]: " + message);
		}

	}

	/**
	 * Log a message on the Logger associated with our Container (if any)
	 *
	 * @param message Message to be logged
	 * @param throwable Associated exception
	 */
	private void log(String message, Throwable throwable)
	{

		Logger logger = null;
		if (container != null)
			logger = container.getLogger();
		if (logger != null)
		{
			logger.log("workflowLoader[" + container.getName() + "] " + message, throwable);
		}
		else
		{
			String containerName = null;
			if (container != null)
			{
				containerName = container.getName();
			}
			System.out.println("workflowLoader[" + containerName + "]: " + message);
			System.out.println("" + throwable);
			throwable.printStackTrace(System.out);
		}

	}

	/**
	 * Configure associated class loader permissions.
	 */
	private void setPermissions()
	{
		if (System.getSecurityManager() == null)
		{
			return;

		}
		if (!(container instanceof Context))
		{
			return;

		}

		// Tell the class loader the root of the context
		ScriptContext scriptContext = ((Context) container).getScriptContext();

		// Assigning permissions for the work directory
		File workDir = (File) scriptContext.getAttribute(Globals.WORK_DIR_ATTR);
		if (workDir != null)
		{
			try
			{
				String workDirPath = workDir.getCanonicalPath();
				classLoader.addPermission(new FilePermission(workDirPath, "read,write"));
				classLoader.addPermission(
					new FilePermission(workDirPath + File.separator + "-", "read,write,delete"));
			}
			catch (IOException e)
			{
				// Ignore
			}
		}

		try
		{

			URL rootURL = scriptContext.getResource("/");
			classLoader.addPermission(rootURL);

			String contextRoot = scriptContext.getRealPath("/");
			try
			{
				contextRoot = (new File(contextRoot)).getCanonicalPath();
				classLoader.addPermission(contextRoot);
			}
			catch (IOException e)
			{
				// Ignore
			}

			URL classesURL = scriptContext.getResource("/WEB-INF/classes/");
			classLoader.addPermission(classesURL);
			URL libURL = scriptContext.getResource("/WEB-INF/lib/");
			classLoader.addPermission(libURL);

			if (libURL != null)
			{
				File rootDir = new File(contextRoot);
				File libDir = new File(rootDir, "WEB-INF/lib/");
				try
				{
					String path = libDir.getCanonicalPath();
					classLoader.addPermission(path);
				}
				catch (IOException e)
				{
				}
			}
		}
		catch (MalformedURLException e)
		{
		}
	}
	/**
	 * Configure the repositories for our class loader, based on the
	 * associated Container.
	 */
	private void setRepositories()
	{

		if (!(container instanceof Context))
		{
			return;
		}

		ScriptContext scriptContext = ((Context) container).getScriptContext();
		if (scriptContext == null)
		{
			return;
		}

		// Loading the work directory
		File workDir = (File) scriptContext.getAttribute(Globals.WORK_DIR_ATTR);
		if (workDir == null)
		{
			log.info("No work dir for " + scriptContext);
		}
		else
		{
			log.debug(sm.getString("workflowLoader.deploy", workDir.getAbsolutePath()));
			classLoader.setWorkDir(workDir);
		}

		// Setting up the class repository (/WEB-INF/classes), if it exists
		String classesPath = "/WEB-INF/classes";

		String absoluteClassesPath = scriptContext.getRealPath(classesPath);
		if (absoluteClassesPath != null)
		{
			File classRepository = new File(absoluteClassesPath);
			// Adding the repository to the class loader
			log.debug(
				sm.getString("workflowLoader.classDeploy", classesPath, classRepository.getAbsolutePath()));

			classLoader.addRepository(classesPath + "/", classRepository);

		}

		// Setting up the JAR repository (/WEB-INF/lib), if it exists
		String libPath = "/WEB-INF/lib";
		String absoluteLibPath = scriptContext.getRealPath(libPath);

		if (absoluteLibPath != null)
		{
			classLoader.addJarPath(libPath, new File(absoluteLibPath));

			File destDir = new File(absoluteLibPath);
			File[] files = destDir.listFiles(new JarFilenameFilter());

			for (int i = 0; i < files.length; ++i)
			{
				String filename = libPath + "/" + files[i].getName();

				File destFile = files[i];

				try
				{
					classLoader.addJar(filename, destFile);
				}
				catch (Exception ex)
				{
					// Catch the exception if there is an empty jar file
					// Should ignore and continute loading other jar files 
					// in the dir
				}
			}
		}
	}

	/**
	 * Set the appropriate context attribute for our class path.  This
	 * is required only because Jasper depends on it.
	 */
	private void setClassPath()
	{
		// Validate our current state information
		if (!(container instanceof Context))
		{
			return;
		}

		ScriptContext ScriptContext = ((Context) container).getScriptContext();

		if (ScriptContext == null)
		{
			return;
		}

		if (container instanceof StandardContext)
		{
			String baseClasspath = ((StandardContext) container).getCompilerClasspath();
			if (baseClasspath != null)
			{
				ScriptContext.setAttribute(Globals.CLASS_PATH_ATTR, baseClasspath);
				return;
			}
		}

		StringBuffer classpath = new StringBuffer();

		// Assemble the class path information from our class loader chain
		ClassLoader loader = getClassLoader();
		int n = 0;
		while (loader != null)
		{
			if (!(loader instanceof URLClassLoader))
			{
				String cp = getClasspath(loader);
				if (cp == null)
				{
					log.info("Unknown loader " + loader + " " + loader.getClass());
					break;
				}
				else
				{
					if (n > 0)
					{
						classpath.append(File.pathSeparator);
					}
					classpath.append(cp);
					n++;
				}

				break;
				//continue;
			}

			URL repositories[] = ((URLClassLoader) loader).getURLs();
			for (int i = 0; i < repositories.length; i++)
			{
				String repository = repositories[i].toString();
				if (repository.startsWith("file://"))
				{
					repository = repository.substring(7);
				}
				else if (repository.startsWith("file:"))
				{
					repository = repository.substring(5);
				}
				else
				{
					continue;
				}

				if (repository == null)
				{
					continue;

				}
				if (n > 0)
				{
					classpath.append(File.pathSeparator);
				}

				classpath.append(repository);
				n++;
			}

			loader = loader.getParent();
		}

		this.classpath = classpath.toString();

		// Store the assembled class path as a servlet context attribute
		ScriptContext.setAttribute(Globals.CLASS_PATH_ATTR, classpath.toString());
	}

	// try to extract the classpath from a loader that is not URLClassLoader
	private String getClasspath(ClassLoader loader)
	{
		try
		{
			Method m = loader.getClass().getMethod("getClasspath", new Class[] {
			});
			if (log.isTraceEnabled())
			{

			}
			log.trace("getClasspath " + m);
			if (m == null)
			{
				return null;
			}
			Object o = m.invoke(loader, new Object[] {
			});
			if (log.isDebugEnabled())
			{
				log.debug("gotClasspath " + o);
			}
			if (o instanceof String)
			{
				return (String) o;
			}

			return null;
		}
		catch (Exception ex)
		{
			if (log.isDebugEnabled())
			{
				log.debug("getClasspath ", ex);
			}
		}
		return null;
	}
}
