//----------------------------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.desktop.helpers;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.WeakHashMap;

import org.huihoo.workflow.WorkflowException;
import org.huihoo.workflow.desktop.WorkflowDesktop;

public final class ResourceManager
{

	/*
	 * Name of provider resource files (without the package-name prefix.)
	 */
	private static final String PROVIDER_RESOURCE_FILE_NAME = "workflowdesktopprovider.properties";

	/*
	 * Name of application resource files.
	 */
	private static final String APP_RESOURCE_FILE_NAME = "workflowdesktop.properties";

	private static final PropertyUtils helper = PropertyUtils.getUtils();

	/*
	 * A cache of the properties that have been constructed by
	 * the ResourceManager.  A Hashtable from a provider resource
	 * file is keyed on a class in the resource file's package.
	 * One from application resource files is keyed on the thread's
	 * context class loader.
	 */
	private static final WeakHashMap propertiesCache = new WeakHashMap(11);

	/*
	 * A cache of factory objects (ObjectFactory, StateFactory, ControlFactory).
	 *
	 * A two-level cache keyed first on context class loader and then
	 * on propValue.  Value is a list of class or factory objects,
	 * weakly referenced so as not to prevent GC of the class loader.
	 * Used in getFactories().
	 */
	private static final WeakHashMap factoryCache = new WeakHashMap(11);

	// There should be no instances of this class.
	private ResourceManager()
	{
	}

	// ---------- Public methods ----------

	/*
	 * Given the environment parameter passed to the initial context
	 * constructor, returns the full environment for that initial
	 * context (never null).  This is based on the environment
	 * parameter, the applet parameters (where appropriate), the
	 * system properties, and all application resource files.
	 *
	 * <p> This method will modify <tt>env</tt> and save
	 * a reference to it.  The caller may no longer modify it.
	 *
	 * @param env	environment passed to initial context constructor.
	 *			Null indicates an empty environment.
	 *
	 * @throws , WorkflowException if an error occurs while reading a
	 *		resource file
	 */
	public static Hashtable getInitialEnvironment(Hashtable env) throws  WorkflowException
	{
		String[] props = PropertyUtils.PROPS; // system/applet properties
		if (env == null)
		{
			env = new Hashtable(11);
		}
		// Merge property values from env param, applet params, and system
		// properties.  The first value wins:  there's no concatenation of
		// colon-separated lists.
		// Read system properties by first trying System.getProperties(),
		// and then trying System.getProperty() if that fails.  The former
		// is more efficient due to fewer permission checks.
		//
		String[] wfnSysProps = helper.getProperties();
		for (int i = 0; i < props.length; i++)
		{
			Object val = env.get(props[i]);
			if (val == null)
			{
				// Read system property.
				val = (wfnSysProps != null) ? wfnSysProps[i] : helper.getProperty(i);
				if (val != null)
				{
					env.put(props[i], val);
				}
			}
		}
		
		return env;
	}

	/**
	  * Retrieves the property from the environment, or from the provider
	  * resource file associated with the given context.  The environment
	  * may in turn contain values that come from applet parameters,
	  * system properties, or application resource files.
	  *
	  * If <tt>concat</tt> is true and both the environment and the provider
	  * resource file contain the property, the two values are concatenated
	  * (with a ':' separator).
	  *
	  * Returns null if no value is found.
	  *
	  * @param propName	The non-null property name
	  * @param env	The possibly null environment properties
	  * @param client	The possibly null context
	  * @param concat	True if multiple values should be concatenated
	  * @return the property value, or null is there is none.
	  * @throws , WorkflowException if an error occurs while reading the provider
	  * resource file.
	  */
	public static String getProperty(
		String propName,
		Hashtable env,
		WorkflowDesktop client,
		boolean concat)
		throws  WorkflowException
	{

		String val1 = (env != null) ? (String) env.get(propName) : null;
		if ((client == null) || ((val1 != null) && !concat))
		{
			return val1;
		}
		String val2 = (String) getProviderResource(client).get(propName);
		if (val1 == null)
		{
			return val2;
		}
		else if ((val2 == null) || !concat)
		{
			return val1;
		}
		else
		{
			return (val1 + ":" + val2);
		}
	}

	/**
	 * Retrieves an enumeration of factory classes/object specified by a
	 * property.
	 * 
	 * The property is gotten from the environment and the provider
	 * resource file associated with the given context and concantenated.
	 * See getProperty(). The resulting property value is a list of class names.
	 *<p>
	 * This method then loads each class using the current thread's context
	 * class loader and keeps them in a list. Any class that cannot be loaded
	 * is ignored. The resulting list is then cached in a two-level
	 * hash table, keyed first by the context class loader and then by
	 * the property's value.
	 * The next time threads of the same context class loader call this 
	 * method, they can use the cached list.
	 *<p>
	 * After obtaining the list either from the cache or by creating one from
	 * the property value, this method then creates and returns a 
	 * FactoryEnumeration using the list. As the FactoryEnumeration is 
	 * traversed, the cached Class object in the list is instantiated and 
	 * replaced by an instance of the factory object itself.  Both class
	 * objects and factories are wrapped in weak references so as not to
	 * prevent GC of the class loader.
	 *<p>
	 * Note that multiple threads can be accessing the same cached list
	 * via FactoryEnumeration, which locks the list during each next().
	 * The size of the list will not change,
	 * but a cached Class object might be replaced by an instantiated factory
	 * object.
	 *
	 * @param propName	The non-null property name
	 * @param env	The possibly null environment properties
	 * @param client	The possibly null context
	 * @return An enumeration of factory classes/objects; null if none.
	 * @exception , WorkflowException If encounter problem while reading the provider
	 * property file.
	 */
	public static FactoryEnumeration getFactoryEnumeration(
		String propName,
		Hashtable env,
		WorkflowDesktop client)
		throws  WorkflowException
	{

		String facProp = getProperty(propName, env, client, true);
		if (facProp == null)
		{
			return null; // no classes specified; return null
		}

		// Cache is based on context class loader and property val
		ClassLoader loader = Thread.currentThread().getContextClassLoader();
		if(client !=null)
		{
			loader = client.getClass().getClassLoader();
		}
		Map perLoaderCache = null;
		synchronized (factoryCache)
		{
			perLoaderCache = (Map) factoryCache.get(loader);
			if (perLoaderCache == null)
			{
				perLoaderCache = new HashMap(11);
				factoryCache.put(loader, perLoaderCache);
			}
		}

		synchronized (perLoaderCache)
		{
			List factories = (List) perLoaderCache.get(facProp);
			if (factories != null)
			{
				// Cached list
				return factories.size() == 0 ? null : new FactoryEnumeration(factories, loader);
			}
			else
			{
				// Populate list with classes named in facProp; skipping
				// those that we cannot load
				StringTokenizer parser = new StringTokenizer(facProp, ":");
				factories = new ArrayList(5);
				while (parser.hasMoreTokens())
				{
					try
					{
						// System.out.println("loading");
						String className = parser.nextToken();
						Class c = loader.loadClass(className);
						factories.add(new NamedWeakReference(c, className));
					}
					catch (Exception e)
					{
						// ignore ClassNotFoundException, IllegalArgumentException
					}
				}
				// System.out.println("adding to cache: " + factories);
				perLoaderCache.put(facProp, factories);
				return new FactoryEnumeration(factories, loader);
			}
		}
	}

	// ---------- Private methods ----------

	/*
	 * Returns the properties contained in the provider resource file
	 * of an object's package.  Returns an empty hash table if the
	 * object is null or the resource file cannot be found.  The
	 * results are cached.
	 *
	 * @throws , WorkflowException if an error occurs while reading the file.
	 */
	private static Hashtable getProviderResource(Object obj) throws  WorkflowException
	{
		if (obj == null)
		{
			return (new Hashtable(1));
		}
		synchronized (propertiesCache)
		{
			Class c = obj.getClass();

			Hashtable props = (Hashtable) propertiesCache.get(c);
			if (props != null)
			{
				return props;
			}
			props = new Properties();

			InputStream istream = helper.getResourceAsStream(c, PROVIDER_RESOURCE_FILE_NAME);

			if (istream != null)
			{
				try
				{
					((Properties) props).load(istream);
				}
				catch (IOException e)
				{
					 WorkflowException ne =
						new WorkflowException("Error reading provider resource file for " + c);
					ne.setRootCause(e);
					throw ne;
				}
			}
			propertiesCache.put(c, props);
			return props;
		}
	}

	/*
	 * Merge the properties from one hash table into another.  Each
	 * property in props2 that is not in props1 is added to props1.
	 * For each property in both hash tables that is one of the
	 * standard JNDI properties that specify colon-separated lists,
	 * the values are concatenated and stored in props1.
	 */
	private static void mergeTables(Hashtable props1, Hashtable props2)
	{
		Enumeration keys = props2.keys();

		while (keys.hasMoreElements())
		{
			String prop = (String) keys.nextElement();
			Object val1 = props1.get(prop);
			if (val1 == null)
			{
				props1.put(prop, props2.get(prop));
			}
		}
	}
}
