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

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Random;

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.Engine;
import org.huihoo.willow.Lifecycle;
import org.huihoo.willow.LifecycleException;
import org.huihoo.willow.LifecycleListener;
import org.huihoo.willow.util.LifecycleSupport;
import org.huihoo.willow.util.StringManager;

import workflow.services.WorkflowSession;

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

	protected DataInputStream randomIS = null;
	protected String devRandomSource = "/dev/urandom";

	/**
	 * The number of random bytes to include when generating a
	 * session identifier.
	 */
	protected static final int SESSION_ID_BYTES = 16;

	/**
	 * The Container with which this Manager is associated.
	 */
	protected Container container;

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

	/**
	 * A String initialization parameter used to increase the entropy of
	 * the initialization of our random number generator.
	 */
	protected String entropy = null;

	/**
	 * The descriptive information string for this implementation.
	 */
	private static final String info = "SessionManager/1.0";

	/**
	 * The default maximum inactive interval (in seconds) for WillowSessions created by
	 * this Manager.
	 */
	protected int maxInactiveInterval = 24*60*60; //one day

	/**
	 * The descriptive name of this SessionManager(for logging).
	 */
	protected static String name = "SessionManager";

	/**
	 * A random number generator to use when generating session identifiers.
	 */
	protected Random random = null;

	/**
	 * The Java class name of the random number generator class to be used
	 * when generating session identifiers.
	 */
	protected String randomClass = "java.security.SecureRandom";

	/**
	 * The set of currently active WillowSessions for this Manager, keyed by
	 * session identifier.
	 */
	protected HashMap sessions = new HashMap();

	// Number of sessions created by this manager
	protected int sessionCounter = 0;

	protected int maxActive = 0;

	// number of duplicated session ids - anything >0 means we have problems
	protected int duplicates = 0;

	protected boolean initialized = false;

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

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

	// ------------------------------------------------------------- Security classes
	private class PrivilegedSetRandomFile implements PrivilegedAction
	{

		public Object run()
		{
			try
			{
				File f = new File(devRandomSource);
				if (!f.exists())
					return null;
				randomIS = new DataInputStream(new FileInputStream(f));
				randomIS.readLong();
				if (log.isDebugEnabled())
					log.debug("Opening " + devRandomSource);
				return randomIS;
			}
			catch (IOException ex)
			{
				return null;
			}
		}
	}

	// ------------------------------------------------------------- Properties
	/**
	 * Return the Container with which this Manager is associated.
	 */
	public Container getContainer()
	{

		return (this.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)
	{

		this.debug = debug;

	}

	/**
	 * Return the default maximum inactive interval (in seconds)
	 * for WillowSessions created by this Manager.
	 */
	public int getMaxInactiveInterval()
	{

		return (this.maxInactiveInterval);

	}

	/**
	 * Set the default maximum inactive interval (in seconds)
	 * for WillowSessions created by this Manager.
	 *
	 * @param interval The new default value
	 */
	public void setMaxInactiveInterval(int interval)
	{

		int oldMaxInactiveInterval = this.maxInactiveInterval;
		this.maxInactiveInterval = interval;
		support.firePropertyChange(
			"maxInactiveInterval",
			new Integer(oldMaxInactiveInterval),
			new Integer(this.maxInactiveInterval));

	}

	/** Use /dev/random-type special device. This is new code, but may reduce the
	*  big delay in generating the random.
	*
	*  You must specify a path to a random generator file. Use /dev/urandom
	*  for linux ( or similar ) systems. Use /dev/random for maximum security
	*  ( it may block if not enough "random" exist ). You can also use
	*  a pipe that generates random.
	*
	*  The code will check if the file exists, and default to java Random
	*  if not found. There is a significant performance difference, very
	*  visible on the first call to getWillowSession ( like in the first JSP )
	*  - so use it if available.
	*/
	public void setRandomFile(String s)
	{
		// as a hack, you can use a static file - and genarate the same
		// session ids ( good for strange debugging )
		if (System.getSecurityManager() != null)
		{
			randomIS = (DataInputStream) AccessController.doPrivileged(new PrivilegedSetRandomFile());
		}
		else
		{
			try
			{
				devRandomSource = s;
				File f = new File(devRandomSource);
				if (!f.exists())
					return;
				randomIS = new DataInputStream(new FileInputStream(f));
				randomIS.readLong();
				if (log.isDebugEnabled())
					log.debug("Opening " + devRandomSource);
			}
			catch (IOException ex)
			{
				randomIS = null;
			}
		}
	}

	public String getRandomFile()
	{
		return devRandomSource;
	}

	/**
	 * Return the random number generator instance we should use for
	 * generating session identifiers.  If there is no such generator
	 * currently defined, construct and seed a new one.
	 */
	public synchronized Random getRandom()
	{
		if (this.random == null)
		{
			synchronized (this)
			{
				if (this.random == null)
				{
					// Calculate the new random number generator seed
					long seed = System.currentTimeMillis();
					long t1 = seed;
					try
					{
						// Construct and seed a new random number generator
						Class clazz = Class.forName(randomClass);
						this.random = (Random) clazz.newInstance();
						this.random.setSeed(seed);
					}
					catch (Exception e)
					{
						// Fall back to the simple case
						log.error(sm.getString("sessionManager.random", randomClass), e);
						this.random = new java.util.Random();
						this.random.setSeed(seed);
					}
					long t2 = System.currentTimeMillis();
					if ((t2 - t1) > 100)
						log.debug(sm.getString("sessionManager.seeding", randomClass) + " " + (t2 - t1));
				}
			}
		}

		return (this.random);

	}

	/**
	 * Return the random number generator class name.
	 */
	public String getRandomClass()
	{

		return (this.randomClass);

	}

	/**
	 * Set the random number generator class name.
	 *
	 * @param randomClass The new random number generator class name
	 */
	public void setRandomClass(String randomClass)
	{

		String oldRandomClass = this.randomClass;
		this.randomClass = randomClass;
		support.firePropertyChange("randomClass", oldRandomClass, this.randomClass);

	}

	/**
	 * Add this WillowSession to the set of active WillowSessions for this Manager.
	 *
	 * @param session WillowSession to be added
	 */
	public void add(WorkflowSession session)
	{

		synchronized (sessions)
		{
			sessions.put(session.getId(), session);
			if (sessions.size() > maxActive)
			{
				maxActive = sessions.size();
			}
		}
	}

	/**
	 * Return the active WillowSession, associated with this Manager, with the
	 * specified session id (if any); otherwise return <code>null</code>.
	 *
	 * @param id The session id for the session to be returned
	 *
	 * @exception IllegalStateException if a new session cannot be
	 *  instantiated for any reason
	 * @exception IOException if an input/output error occurs while
	 *  processing this request
	 */
	public WillowSession findSession(String id)
	{

		if (id == null)
		{
			return (null);
		}
		synchronized (sessions)
		{
			WillowSession session = (WillowSession) sessions.get(id);
			return (session);
		}

	}

	/**
	 * Return the set of active WillowSessions associated with this Manager.
	 * If this Manager has no active WillowSessions, a zero-length array is returned.
	 */
	public WillowSession[] findSessions()
	{

		WillowSession results[] = null;
		synchronized (sessions)
		{
			results = new WillowSession[sessions.size()];
			results = (WillowSession[]) sessions.values().toArray(results);
		}
		return (results);

	}

	/**
	 * Remove this WillowSession from the active WillowSessions for this Manager.
	 *
	 * @param session WillowSession to be removed
	 */
	public void remove(WorkflowSession session)
	{

		synchronized (sessions)
		{
			sessions.remove(session.getId());
		}

	}

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

		support.removePropertyChangeListener(listener);

	}

	// ------------------------------------------------------ Protected Methods

	protected void getRandomBytes(byte bytes[])
	{
		// Generate a byte array containing a session identifier
		if (devRandomSource != null && randomIS == null)
		{
			setRandomFile(devRandomSource);
		}
		if (randomIS != null)
		{
			try
			{
				int len = randomIS.read(bytes);
				if (len == bytes.length)
				{
					return;
				}
				log.debug("Got " + len + " " + bytes.length);
			}
			catch (Exception ex)
			{
			}
			devRandomSource = null;
			randomIS = null;
		}
		getRandom().nextBytes(bytes);
	}

	/**
	 * Generate and return a new session identifier.
	 */
	protected synchronized String generateSessionId()
	{
		byte bytes[] = new byte[SESSION_ID_BYTES];
		getRandomBytes(bytes);

		// Render the result as a String of hexadecimal digits
		StringBuffer result = new StringBuffer();
		for (int i = 0; i < bytes.length; i++)
		{
			byte b1 = (byte) ((bytes[i] & 0xf0) >> 4);
			byte b2 = (byte) (bytes[i] & 0x0f);
			if (b1 < 10)
				result.append((char) ('0' + b1));
			else
				result.append((char) ('A' + (b1 - 10)));
			if (b2 < 10)
				result.append((char) ('0' + b2));
			else
				result.append((char) ('A' + (b2 - 10)));
		}
		return (result.toString());

	}

	// ------------------------------------------------------ Protected Methods

	/**
	 * Retrieve the enclosing Engine for this Manager.
	 *
	 * @return an Engine object (or null).
	 */
	public Engine getEngine()
	{
		Engine e = null;
		for (Container c = getContainer(); e == null && c != null; c = c.getParent())
		{
			if (c != null && c instanceof Engine)
			{
				e = (Engine) c;
			}
		}
		return e;
	}

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

	/**
	 * Log a message on the Logger associated with our Container (if any).
	 *
	 * @param message Message to be logged
	 * @deprecated
	 */
	protected void log(String message)
	{
		log.info(message);
	}

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

	public void setSessionCounter(int sessionCounter)
	{
		this.sessionCounter = sessionCounter;
	}

	/** 
	 * Total sessions created by this manager.
	 *
	 * @return sessions created
	 */
	public int getSessionCounter()
	{
		return sessionCounter;
	}

	/** 
	 * Number of duplicated session IDs generated by the random source.
	 * Anything bigger than 0 means problems.
	 *
	 * @return
	 */
	public int getDuplicates()
	{
		return duplicates;
	}

	public void setDuplicates(int duplicates)
	{
		this.duplicates = duplicates;
	}

	/** 
	 * Returns the number of active sessions
	 *
	 * @return number of sessions active
	 */
	public int getActiveSessions()
	{
		return sessions.size();
	}

	/**
	 * Max number of concurent active sessions
	 *
	 * @return
	 */
	public int getMaxActive()
	{
		return maxActive;
	}

	public void setMaxActive(int maxActive)
	{
		this.maxActive = maxActive;
	}

	/** 
	 * For debugging: return a list of all session ids currently active
	 *
	 */
	public String listSessionIds()
	{
		StringBuffer sb = new StringBuffer();
		Iterator keys = sessions.keySet().iterator();
		while (keys.hasNext())
		{
			sb.append(keys.next()).append(" ");
		}
		return sb.toString();
	}

	public void expireSession(String sessionId)
	{
		WillowSession s = (WillowSession) sessions.get(sessionId);
		if (s == null)
		{
			log.info("WillowSession not found " + sessionId);
			return;
		}
		s.expire();
	}

	public String getLastAccessedTime(String sessionId)
	{
		WillowSession s = (WillowSession) sessions.get(sessionId);
		if (s == null)
		{
			log.info("WillowSession not found " + sessionId);
			return "";
		}
		return new Date(s.getLastAccessedTime()).toString();
	}

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

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

	/**
	 * The maximum number of active WillowSessions allowed, or -1 for no limit.
	 */
	protected int maxActiveWillowSessions = -1;

	/**
	 * Path name of the disk file in which active sessions are saved
	 * when we stop, and from which these sessions are loaded when we start.
	 * A <code>null</code> value indicates that no persistence is desired.
	 * If this pathname is relative, it will be resolved against the
	 * temporary working directory provided by our context, available via
	 * the <code>javax.servlet.context.tempdir</code> context attribute.
	 */
	protected String pathname = "SESSIONS.ser";

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

	int rejectedWillowSessions = 0;
	int expiredWillowSessions = 0;
	long processingTime = 0;

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

	/**
	 * Set the Container with which this Manager has been associated.  If
	 * it is a Engine (the usual case), listen for changes to the session
	 * timeout property.
	 *
	 * @param container The associated Container
	 */
	public void setContainer(Container container)
	{

		// De-register from the old Container (if any)
		if ((this.container != null) && (this.container instanceof Context))
			 ((Context) this.container).removePropertyChangeListener(this);

		// Default processing provided by our superclass
		Container oldContainer = this.container;
		this.container = container;
		support.firePropertyChange("container", oldContainer, this.container);

		// Register with the new Container (if any)
		if ((this.container != null) && (this.container instanceof Engine))
		{
			 setMaxInactiveInterval(((Engine) this.container).getSessionTimeout() * 60);
			 ((Engine) this.container).addPropertyChangeListener(this);
		}

	}

	/**
	 * Return descriptive information about this Manager implementation and
	 * the corresponding version number, in the format
	 * <code>&lt;description&gt;/&lt;version&gt;</code>.
	 */
	public String getInfo()
	{

		return (this.info);

	}

	/**
	 * Return the maximum number of active WillowSessions allowed, or -1 for
	 * no limit.
	 */
	public int getMaxActiveSessions()
	{

		return (this.maxActiveWillowSessions);

	}

	/** Number of session creations that failed due to maxActiveWillowSessions
	 *
	 * @return
	 */
	public int getRejectedSessions()
	{
		return rejectedWillowSessions;
	}

	public void setRejectedSessions(int rejectedWillowSessions)
	{
		this.rejectedWillowSessions = rejectedWillowSessions;
	}

	/** Number of sessions that expired.
	 *
	 * @return
	 */
	public int getExpiredSessions()
	{
		return expiredWillowSessions;
	}

	public void setExpiredSessions(int expiredWillowSessions)
	{
		this.expiredWillowSessions = expiredWillowSessions;
	}

	public long getProcessingTime()
	{
		return processingTime;
	}

	public void setProcessingTime(long processingTime)
	{
		this.processingTime = processingTime;
	}

	/**
	 * Set the maximum number of actives WillowSessions allowed, or -1 for
	 * no limit.
	 *
	 * @param max The new maximum number of sessions
	 */
	public void setMaxActiveSessions(int max)
	{

		int oldMaxActiveWillowSessions = this.maxActiveWillowSessions;
		this.maxActiveWillowSessions = max;
		support.firePropertyChange(
			"maxActiveWillowSessions",
			new Integer(oldMaxActiveWillowSessions),
			new Integer(this.maxActiveWillowSessions));

	}

	/**
	 * Return the descriptive short name of this Manager implementation.
	 */
	public String getName()
	{

		return (name);

	}

	/**
	 * Return the session persistence pathname, if any.
	 */
	public String getPathname()
	{

		return (this.pathname);

	}

	/**
	 * Set the session persistence pathname to the specified value.  If no
	 * persistence support is desired, set the pathname to <code>null</code>.
	 *
	 * @param pathname New session persistence pathname
	 */
	public void setPathname(String pathname)
	{

		String oldPathname = this.pathname;
		this.pathname = pathname;
		support.firePropertyChange("pathname", oldPathname, this.pathname);

	}

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

	/**
	 * Construct and return a new session object, based on the default
	 * settings specified by this Manager's properties.  The session
	 * id will be assigned by this method, and available via the getId()
	 * method of the returned session.  If a new session cannot be created
	 * for any reason, return <code>null</code>.
	 *
	 * @exception IllegalStateException if a new session cannot be
	 *  instantiated for any reason
	 */
	public WillowSession createSession()
	{

		if ((maxActiveWillowSessions >= 0) && (sessions.size() >= maxActiveWillowSessions))
		{
			rejectedWillowSessions++;
			throw new IllegalStateException(sm.getString("sessionManager.createWillowSession.ise"));
		}

		WillowSession session = new WillowSession(this);

		// Initialize the properties of the new session and return it
		session.setNew(true);
		session.setValid(true);
		session.setCreationTime(System.currentTimeMillis());
		session.setMaxInactiveInterval(this.maxInactiveInterval);
		String sessionId = generateSessionId();

		synchronized (sessions)
		{
			while (sessions.get(sessionId) != null)
			{ // Guarantee uniqueness
				duplicates++;
				sessionId = generateSessionId();
			}
		}

		session.setId(sessionId);
		sessionCounter++;

		return (session);

	}

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

	}

	/**
	 * Prepare for the beginning of active use of the public methods of this
	 * component.  This method should be called after <code>configure()</code>,
	 * and before any of the public methods of the component are utilized.
	 *
	 * @exception LifecycleException if this component detects a fatal error
	 *  that prevents this component from being used
	 */
	public void start() throws LifecycleException
	{
		// Validate and update our current component state
		if (started)
		{
			return;
		}
		lifecycle.fireLifecycleEvent(START_EVENT, null);
		started = true;

		// Force initialization of the random number generator
		log.debug("Force random number initialization starting");
		 generateSessionId();
		log.debug("Force random number initialization completed");

	}

	/**
	 * Gracefully terminate the active use of the public methods of this
	 * component.  This method should be the last one called on a given
	 * instance of this component.
	 *
	 * @exception LifecycleException if this component detects a fatal error
	 *  that needs to be reported
	 */
	public void stop() throws LifecycleException
	{

		log.debug("Stopping");

		// Validate and update our current component state
		if (!started)
		{
			throw new LifecycleException(sm.getString("sessionManager.notStarted"));
		}
		lifecycle.fireLifecycleEvent(STOP_EVENT, null);
		started = false;

		// Expire all active sessions
		WillowSession sessions[] = findSessions();
		for (int i = 0; i < sessions.length; i++)
		{
			WillowSession session = (WillowSession) sessions[i];
			if (!session.isValid())
				continue;
			try
			{
				session.expire();
			}
			catch (Throwable t)
			{
				;
			}
		}

		// Require a new random number generator if we are restarted
		this.random = null;

		if (initialized)
		{
			//destroy();
		}
	}

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

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

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

		// Process a relevant property change
		if (event.getPropertyName().equals("sessionTimeout"))
		{
			try
			{
				setMaxInactiveInterval(((Integer) event.getNewValue()).intValue() * 60);
			}
			catch (NumberFormatException e)
			{
				log.error(sm.getString("sessionManager.sessionTimeout", event.getNewValue().toString()));
			}
		}

	}

	// ------------------------------------------------------ Protected Methods

	/**
	 * Invalidate all sessions that have expired.
	 */
	public void processExpires()
	{
		long timeNow = System.currentTimeMillis();
		WillowSession sessions[] = findSessions();

		for (int i = 0; i < sessions.length; i++)
		{
			WillowSession session = (WillowSession) sessions[i];		
			if (!session.isValid())
			{
				try
				{
					expiredWillowSessions++;
					session.expire();
				}
				catch (Throwable t)
				{
					log.error(sm.getString("sessionManager.expireException"), t);
				}
			}
		}
		long timeEnd = System.currentTimeMillis();
		processingTime += (timeEnd - timeNow);

	}

}