/*
 * @(#)ResourceManager.java	1.12 03/01/23
 *
 * Copyright 2003 Sun Microsystems, Inc. All rights reserved.
 * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

package workflow.client.factory;

import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
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 javax.naming.ConfigurationException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;

import workflow.client.MatrixClient;


/**
  * The ResourceManager class facilitates the reading of JNDI resource files.
  * 
  * @author Rosanna Lee
  * @author Scott Seligman
  * @version 1.12 03/01/23
  */

public final class ResourceManager
{

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

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

	private static final VersionHelper helper = VersionHelper.getVersionHelper();

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

	/*
	 * A cache of URL factory objects (ObjectFactory).
	 *
	 * A two-level cache keyed first on context class loader and then
	 * on classSuffix+propValue.  Value is the factory itself (weakly
	 * referenced so as not to prevent GC of the class loader) or
	 * NO_FACTORY if a previous search revealed no factory.  Used in
	 * getFactory().
	 */
	private static final WeakHashMap urlFactoryCache = new WeakHashMap(11);
	private static final WeakReference NO_FACTORY = new WeakReference(null);

	// 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 NamingException if an error occurs while reading a
	 *		resource file
	 */
	public static Hashtable getInitialEnvironment(Hashtable env) throws NamingException
	{
		String[] props = VersionHelper.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.getWorkflowNamingProperties();
		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.getWorkflowNamingProperty(i);
				if (val != null)
				{
					env.put(props[i], val);
				}
			}
		}

		// Merge the above with the values read from all application
		// resource files.  Colon-separated lists are concatenated.
		mergeTables(env, getApplicationResources());
		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 NamingException if an error occurs while reading the provider
	  * resource file.
	  */
	public static String getProperty(
		String propName,
		Hashtable env,
		MatrixClient client,
		boolean concat)
		throws NamingException
	{

		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 NamingException If encounter problem while reading the provider
	 * property file.
	 * @see javax.naming.spi.WorkflowNamingManager#getObjectInstance
	 * @see javax.naming.spi.WorkflowNamingManager#getStateToBind
	 * @see javax.naming.spi.DirectoryManager#getObjectInstance
	 * @see javax.naming.spi.DirectoryManager#getStateToBind
	 * @see javax.naming.ldap.ControlFactory#getControlInstance
	 */
	public static FactoryEnumeration getFactories(
		String propName,
		Hashtable env,
		MatrixClient client)
		throws NamingException
	{

		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 = helper.getContextClassLoader();

		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 = helper.loadClass(className, loader);
						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);
			}
		}
	}

	/**
	 * Retrieves a factory from a list of packages specified in a
	 * property.
	 * 
	 * The property is gotten from the environment and the provider
	 * resource file associated with the given context and concatenated.
	 * classSuffix is added to the end of this list.
	 * See getProperty(). The resulting property value is a list of package 
	 * prefixes.
	 *<p>
	 * This method then constructs a list of class names by concatenating
	 * each package prefix with classSuffix and attempts to load and 
	 * instantiate the class until one succeeds.
	 * Any class that cannot be loaded is ignored. 
	 * The resulting object is then cached in a two-level hash table,
	 * keyed first by the context class loader and then by the property's
	 * value and classSuffix.
	 * The next time threads of the same context class loader call this 
	 * method, they use the cached factory.
	 * If no factory can be loaded, NO_FACTORY is recorded in the table
	 * so that next time it'll return quickly.
	 *
	 * @param propName	The non-null property name
	 * @param env	The possibly null environment properties
	 * @param client	The possibly null context
	 * @param classSuffix The non-null class name
	 *			(e.g. ".ldap.ldapURLConstantsFactory).
	 * @param defaultPkgPrefix The non-null default package prefix.
	 *        (e.g., "com.sun.jndi.url").
	 * @return An factory object; null if none.
	 * @exception NamingException If encounter problem while reading the provider
	 * property file, or problem instantiating the factory.
	 *
	 * @see javax.naming.spi.WorkflowNamingManager#getURLConstants
	 * @see javax.naming.spi.WorkflowNamingManager#getURLObject
	 */
	public static Object getFactory(
		String propName,
		Hashtable env,
		MatrixClient client,
		String classSuffix,
		String defaultPkgPrefix)
		throws NamingException
	{

		// Merge property with provider property and supplied default
		String facProp = getProperty(propName, env, client, true);
		if (facProp != null)
			facProp += (":" + defaultPkgPrefix);
		else
			facProp = defaultPkgPrefix;

		// Cache factory based on context class loader, class name, and
		// property val
		ClassLoader loader = helper.getContextClassLoader();
		String key = classSuffix + " " + facProp;

		Map perLoaderCache = null;
		synchronized (urlFactoryCache)
		{
			perLoaderCache = (Map) urlFactoryCache.get(loader);
			if (perLoaderCache == null)
			{
				perLoaderCache = new HashMap(11);
				urlFactoryCache.put(loader, perLoaderCache);
			}
		}

		synchronized (perLoaderCache)
		{
			Object factory = null;

			WeakReference factoryRef = (WeakReference) perLoaderCache.get(key);
			if (factoryRef == NO_FACTORY)
			{
				return null;
			}
			else if (factoryRef != null)
			{
				factory = factoryRef.get();
				if (factory != null)
				{ // check if weak ref has been cleared
					return factory;
				}
			}

			// Not cached; find first factory and cache
			StringTokenizer parser = new StringTokenizer(facProp, ":");
			String className;
			while (factory == null && parser.hasMoreTokens())
			{
				className = parser.nextToken() + classSuffix;
				try
				{
					// System.out.println("loading " + className);
					factory = helper.loadClass(className, loader).newInstance();
				}
				catch (InstantiationException e)
				{
					NamingException ne = new NamingException("Cannot instantiate " + className);
					ne.setRootCause(e);
					throw ne;
				}
				catch (IllegalAccessException e)
				{
					NamingException ne = new NamingException("Cannot access " + className);
					ne.setRootCause(e);
					throw ne;
				}
				catch (Exception e)
				{
					// ignore ClassNotFoundException, IllegalArgumentException,
					// etc.
				}
			}

			// Cache it.
			perLoaderCache.put(key, (factory != null) ? new WeakReference(factory) : NO_FACTORY);
			return factory;
		}
	}

	// ---------- 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 NamingException if an error occurs while reading the file.
	 */
	private static Hashtable getProviderResource(Object obj) throws NamingException
	{
		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)
				{
					NamingException ne =
						new ConfigurationException("Error reading provider resource file for " + c);
					ne.setRootCause(e);
					throw ne;
				}
			}
			propertiesCache.put(c, props);
			return props;
		}
	}

	/*
	 * Returns the Hashtable (never null) that results from merging
	 * all application resource files available to this thread's
	 * context class loader.  The properties file in <java.home>/lib
	 * is also merged in.  The results are cached.
	 *
	 * SECURITY NOTES:
	 * 1.  JNDI needs permission to read the application resource files.
	 * 2.  Any class will be able to use JNDI to view the contents of
	 * the application resource files in its own classpath.  Give
	 * careful consideration to this before storing sensitive
	 * information there.
	 *
	 * @throws NamingException if an error occurs while reading a resource
	 *	file.
	 */
	private static Hashtable getApplicationResources() throws NamingException
	{

		ClassLoader cl = helper.getContextClassLoader();

		synchronized (propertiesCache)
		{
			Hashtable result = (Hashtable) propertiesCache.get(cl);
			if (result != null)
			{
				return result;
			}

			try
			{
				NamingEnumeration resources = helper.getResources(cl, APP_RESOURCE_FILE_NAME);
				while (resources.hasMore())
				{
					Properties props = new Properties();
					props.load((InputStream) resources.next());

					if (result == null)
					{
						result = props;
					}
					else
					{
						mergeTables(result, props);
					}
				}

			}
			catch (IOException e)
			{
				NamingException ne = new ConfigurationException("Error reading application resource file");
				ne.setRootCause(e);
				throw ne;
			}
			if (result == null)
			{
				result = new Hashtable(11);
			}
			propertiesCache.put(cl, result);
			return result;
		}
	}

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