//----------------------------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.File;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;

import javax.naming.Binding;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;

import org.huihoo.commons.util.Enumerator;
import org.huihoo.commons.util.FileSystem;
import org.huihoo.willow.Logger;
import org.huihoo.willow.deploy.ApplicationParameter;
import org.huihoo.willow.naming.resources.DirContextURLStreamHandler;
import org.huihoo.willow.naming.resources.Resource;
import org.huihoo.willow.util.ResourceSet;
import org.huihoo.willow.util.StringManager;
import org.huihoo.workflow.server.WorkflowContext;

public class ApplicationContext implements WorkflowContext
{
	// ----------------------------------------------------------- Constructors
	/**
	 * Construct a new instance of this class, associated with the specified
	 * Context instance.
	 *
	 * @param context The associated Context instance
	 */
	public ApplicationContext(String basePath, StandardContext context)
	{
		super();
		this.context = context;
		this.basePath = basePath;
		this.workflowConfig = new ApplicationConfig(this);
	}

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

	/**
	 * The context attributes for this context.
	 */
	private HashMap attributes = new HashMap();

	/**
	 * List of read only attributes for this context.
	 */
	private HashMap readOnlyAttributes = new HashMap();

	/**
	 * The Context instance with which we are associated.
	 */
	private StandardContext context = null;

	/**
	 * The TemplateContext implementation associated with this Context.
	 */
	private transient ApplicationConfig workflowConfig = null;

	/**
	 * Empty collection to serve as the basis for empty enumerations.
	 * <strong>DO NOT ADD ANY ELEMENTS TO THIS COLLECTION!</strong>
	 */
	private static final ArrayList empty = new ArrayList();

	/**
	 * The merged context initialization parameters for this Context.
	 */
	private HashMap parameters = null;

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

	/**
	 * Base path.
	 */
	private String basePath = null;

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

	/**
	 * Return the resources object that is mapped to a specified path.
	 * The path must begin with a "/" and is interpreted as relative to the
	 * current context root.
	 */
	public DirContext getResources()
	{

		return context.getResources();

	}

	// ------------------------------------------------- TemplateContext Methods

	/**
	 * Return the value of the specified context attribute, if any;
	 * otherwise return <code>null</code>.
	 *
	 * @param name Name of the context attribute to return
	 */
	public Object getAttribute(String name)
	{
		synchronized (attributes)
		{
			return (attributes.get(name));
		}

	}

	/**
	 * Return an enumeration of the names of the context attributes
	 * associated with this context.
	 */
	public Enumeration getAttributeNames()
	{
		synchronized (attributes)
		{
			return new Enumerator(attributes.keySet(), true);
		}

	}
	/**
	 * Return the value of the specified initialization parameter, or
	 * <code>null</code> if this parameter does not exist.
	 *
	 * @param name Name of the initialization parameter to retrieve
	 */
	public String getInitParameter(final String name)
	{
		mergeParameters();
		synchronized (parameters)
		{
			return ((String) parameters.get(name));
		}
	}

	/**
	 * Return the names of the context's initialization parameters, or an
	 * empty enumeration if the context has no initialization parameters.
	 */
	public Enumeration getInitParameterNames()
	{
		mergeParameters();
		synchronized (parameters)
		{
			return (new Enumerator(parameters.keySet()));
		}

	}

	/**
	 * Return the real path for a given virtual path, if possible; otherwise
	 * return <code>null</code>.
	 *
	 * @param path The path to the desired resource
	 */
	public String getRealPath(String path)
	{
		if (!context.isFilesystemBased())
		{
			return null;
		}

		File file = new File(basePath, path);
		return (file.getAbsolutePath());
	}

	/**
	 * Return the URL to the resource that is mapped to a specified path.
	 * The path must begin with a "/" and is interpreted as relative to the
	 * current context root.
	 *
	 * @param path The path to the desired resource
	 *
	 * @exception MalformedURLException if the path is not given
	 *  in the correct form
	 */
	public URL getResource(String path) throws MalformedURLException
	{

		if (path == null || !path.startsWith("/"))
		{
			throw new MalformedURLException(sm.getString("applicationContext.resourcePath.error", path));
		}

		path = FileSystem.normalize(path);
		if (path == null)
		{
			return (null);
		}

		String libPath = "/WEB-INF/lib/";
		if ((path.startsWith(libPath)) && (path.endsWith(".jar")))
		{
			File jarFile = null;
			if (context.isFilesystemBased())
			{
				jarFile = new File(basePath, path);
			}
			else
			{
				jarFile = new File(context.getWorkPath(), path);
			}
			if (jarFile.exists())
			{
				return jarFile.toURL();
			}
			else
			{
				return null;
			}
		}
		else
		{

			DirContext resources = context.getResources();
			if (resources != null)
			{
				String fullPath = context.getName() + path;
				String hostName = context.getParent().getName();
				try
				{
					return new URL("jndi", null, 0, getJNDIUri(hostName, fullPath), new DirContextURLStreamHandler(resources));
				}
				catch (Exception e)
				{
					// Ignore
				}
			}
		}

		return (null);
	}

	/**
	 * Return the requested resource as an <code>InputStream</code>.  The
	 * path must be specified according to the rules described under
	 * <code>getResource</code>.  If no such resource can be identified,
	 * return <code>null</code>.
	 *
	 * @param path The path to the desired resource.
	 */
	public InputStream getResourceAsStream(String path)
	{
		path = FileSystem.normalize(path);
		if (path == null)
			return (null);

		DirContext resources = context.getResources();
		if (resources != null)
		{
			try
			{
				Object resource = resources.lookup(path);
				if (resource instanceof Resource)
				{
					return (((Resource) resource).streamContent());
				}
			}
			catch (Exception e)
			{
			}
		}
		return (null);
	}

	/**
	 * Return a Set containing the resource paths of resources member of the
	 * specified collection. Each path will be a String starting with
	 * a "/" character. The returned set is immutable.
	 *
	 * @param path Collection path
	 */
	public Set getResourcePaths(String path)
	{

		path = FileSystem.normalize(path);
		if (path == null)
		{
			return (null);
		}

		DirContext resources = context.getResources();
		if (resources != null)
		{
			return (getResourcePathsInternal(resources, path));
		}
		return (null);

	}

	/**
	 * Internal implementation of getResourcesPath() logic.
	 *
	 * @param resources Directory context to search
	 * @param path Collection path
	 */
	private Set getResourcePathsInternal(DirContext resources, String path)
	{

		ResourceSet set = new ResourceSet();
		try
		{
			listCollectionPaths(set, resources, path);
		}
		catch (NamingException e)
		{
			return (null);
		}
		set.setLocked(true);
		return (set);

	}

	/**
	 * Return the display name of this workflow application.
	 */
	public String getWorkflowContextName()
	{
		return (context.getDisplayName());
	}

	/**
	 * Writes the specified message to a script log file.
	 *
	 * @param message Message to be written
	 */
	public void log(String message)
	{

		Logger logger = context.getLogger();
		if (logger != null)
		{
			logger.log(context.logName() + message);
		}

	}

	/**
	 * Writes the specified message and exception to a script log file.
	 *
	 * @param message Message to be written
	 * @param throwable Exception to be reported
	 */
	public void log(String message, Throwable throwable)
	{

		Logger logger = context.getLogger();
		if (logger != null)
		{
			logger.log(context.logName() + message, throwable);
		}

	}

	/**
	 * Remove the context attribute with the specified name, if any.
	 *
	 * @param name Name of the context attribute to be removed
	 */
	public void removeAttribute(String name)
	{

		boolean found = false;

		// Remove the specified attribute
		synchronized (attributes)
		{
			// Check for read only attribute
			if (readOnlyAttributes.containsKey(name))
			{
				return;
			}
			found = attributes.containsKey(name);
			if (found)
			{
				attributes.remove(name);
			}
			else
			{
				return;
			}
		}

	}

	/**
	 * Bind the specified value with the specified context attribute name,
	 * replacing any existing value for that name.
	 *
	 * @param name Attribute name to be bound
	 * @param value New attribute value to be bound
	 */
	public void setAttribute(String name, Object value)
	{
		// Name cannot be null
		if (name == null)
		{
			throw new IllegalArgumentException(sm.getString("applicationContext.setAttribute.namenull"));
		}

		// Null value is the same as removeAttribute()
		if (value == null)
		{
			removeAttribute(name);
			return;
		}

		// Add or replace the specified attribute
		synchronized (attributes)
		{
			// Check for read only attribute
			if (readOnlyAttributes.containsKey(name))
			{
				return;
			}
			attributes.put(name, value);
		}
	}

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

	/**
	 * Clear all application-created attributes.
	 */
	void clearAttributes()
	{

		// Create list of attributes to be removed
		ArrayList list = new ArrayList();
		synchronized (attributes)
		{
			Iterator iter = attributes.keySet().iterator();
			while (iter.hasNext())
			{
				list.add(iter.next());
			}
		}

		// Remove application originated attributes
		// (read only attributes will be left in place)
		Iterator keys = list.iterator();
		while (keys.hasNext())
		{
			String key = (String) keys.next();
			removeAttribute(key);
		}

	}

	/**
	 * Set an attribute as read only.
	 */
	void setAttributeReadOnly(String name)
	{

		synchronized (attributes)
		{
			if (attributes.containsKey(name))
			{
				readOnlyAttributes.put(name, name);
			}
		}

	}

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

	/**
	 * Merge the context initialization parameters specified in the application
	 * deployment descriptor with the application parameters described in the
	 * server configuration, respecting the <code>override</code> property of
	 * the application parameters appropriately.
	 */
	private void mergeParameters()
	{
		if (parameters != null)
		{
			return;
		}
		HashMap results = new HashMap();
		String names[] = context.findInitParameters();
		for (int i = 0; i < names.length; i++)
		{
			results.put(names[i], context.findInitParameter(names[i]));
		}

		ApplicationParameter params[] = context.findApplicationParameters();
		for (int i = 0; i < params.length; i++)
		{
			if (params[i].getOverride())
			{
				if (results.get(params[i].getName()) == null)
					results.put(params[i].getName(), params[i].getValue());
			}
			else
			{
				results.put(params[i].getName(), params[i].getValue());
			}
		}

		parameters = results;

	}

	/**
	 * List resource paths (recursively), and store all of them in the given
	 * Set.
	 */
	private static void listPaths(Set set, DirContext resources, String path) throws NamingException
	{

		Enumeration childPaths = resources.listBindings(path);
		while (childPaths.hasMoreElements())
		{
			Binding binding = (Binding) childPaths.nextElement();
			String name = binding.getName();
			String childPath = path + "/" + name;
			set.add(childPath);
			Object object = binding.getObject();
			if (object instanceof DirContext)
			{
				listPaths(set, resources, childPath);
			}
		}

	}

	/**
	 * List resource paths (recursively), and store all of them in the given
	 * Set.
	 */
	private static void listCollectionPaths(Set set, DirContext resources, String path) throws NamingException
	{

		Enumeration childPaths = resources.listBindings(path);
		while (childPaths.hasMoreElements())
		{
			Binding binding = (Binding) childPaths.nextElement();
			String name = binding.getName();
			StringBuffer childPath = new StringBuffer(path);
			if (!"/".equals(path) && !path.endsWith("/"))
			{
				childPath.append("/");
			}
			childPath.append(name);
			Object object = binding.getObject();
			if (object instanceof DirContext)
			{
				childPath.append("/");
			}
			set.add(childPath.toString());
		}

	}

	/**
	 * Get full path, based on the host name and the context path.
	 */
	private static String getJNDIUri(String hostName, String path)
	{
		if (!path.startsWith("/"))
		{
			return "/" + hostName + "/" + path;
		}
		else
		{
			return "/" + hostName + path;
		}
	}
}
