//----------------------------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.session;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
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.Manager;
import org.huihoo.willow.Service;
import org.huihoo.willow.util.StringManager;


public abstract class ManagerBase implements Manager
{
	protected static Log log = LogFactory.getLog(ManagerBase.class);
	// ----------------------------------------------------- Instance Variables

	/**
	 * The default message digest algorithm to use if we cannot use
	 * the requested one.
	 */
	protected static final String DEFAULT_ALGORITHM = "MD5";

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

	/**
	 * The message digest algorithm to be used when generating session
	 * identifiers.  This must be an algorithm supported by the
	 * <code>java.security.MessageDigest</code> class on your platform.
	 */
	protected String algorithm = DEFAULT_ALGORITHM;

	/**
	 * The Service with which this Manager is associated.
	 */
	protected Service service;

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

	/**
	 * Return the MessageDigest implementation to be used when
	 * creating session identifiers.
	 */
	protected MessageDigest digest = null;

	/**
	 * The distributable flag for Sessions created by this Manager.  If this
	 * flag is set to <code>true</code>, any user attributes added to a
	 * session controlled by this Manager must be Serializable.
	 */
	protected boolean distributable;

	/**
	 * 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 = "ManagerBase/1.0";

	/**
	 * The default maximum inactive interval for Sessions created by
	 * this Manager.
	 */
	protected int maxInactiveInterval = 24*60*60; //one day

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

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

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


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

	/**
	 * Return the message digest algorithm for this Manager.
	 */
	public String getAlgorithm()
	{

		return (this.algorithm);

	}

	/**
	 * Set the message digest algorithm for this Manager.
	 *
	 * @param algorithm The new message digest algorithm
	 */
	public void setAlgorithm(String algorithm)
	{

		String oldAlgorithm = this.algorithm;
		this.algorithm = algorithm;
		support.firePropertyChange("algorithm", oldAlgorithm, this.algorithm);

	}

	/**
	 * Return the Service with which this Manager is associated.
	 */
	public Service getService()
	{
		return (this.service);
	}

	/**
	 * Set the Service with which this Manager is associated.
	 *
	 * @param service The newly associated Service
	 */
	public void setService(Service service)
	{

		Service oldService = this.service;
		this.service = service;
		support.firePropertyChange("service", oldService, this.service);
	}

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

	}

	/** Returns the name of the implementation class.
	 */
	public String getClassName()
	{
		return this.getClass().getName();
	}

	/**
	 * Return the MessageDigest object to be used for calculating
	 * session identifiers.  If none has been created yet, initialize
	 * one the first time this method is called.
	 */
	public synchronized MessageDigest getDigest()
	{

		if (this.digest == null)
		{
			long t1 = System.currentTimeMillis();
			if (log.isDebugEnabled())
			{
				log.debug(sm.getString("managerBase.getting", algorithm));
			}
			try
			{
				this.digest = MessageDigest.getInstance(algorithm);
			}
			catch (NoSuchAlgorithmException e)
			{
				log.error(sm.getString("managerBase.digest", algorithm), e);
				try
				{
					this.digest = MessageDigest.getInstance(DEFAULT_ALGORITHM);
				}
				catch (NoSuchAlgorithmException f)
				{
					log.error(sm.getString("managerBase.digest", DEFAULT_ALGORITHM), e);
					this.digest = null;
				}
			}
			if (log.isDebugEnabled())
			{
				log.debug(sm.getString("managerBase.gotten"));
			}
			long t2 = System.currentTimeMillis();
			if (log.isDebugEnabled())
			{
				log.debug("getDigest() " + (t2 - t1));
			}
		}

		return (this.digest);

	}

	/**
	 * Return the distributable flag for the sessions supported by
	 * this Manager.
	 */
	public boolean getDistributable()
	{

		return (this.distributable);

	}

	/**
	 * Set the distributable flag for the sessions supported by this
	 * Manager.  If this flag is set, all user data objects added to
	 * sessions associated with this manager must implement Serializable.
	 *
	 * @param distributable The new distributable flag
	 */
	public void setDistributable(boolean distributable)
	{

		boolean oldDistributable = this.distributable;
		this.distributable = distributable;
		support.firePropertyChange("distributable", new Boolean(oldDistributable), new Boolean(this.distributable));

	}

	/**
	 * Return the entropy increaser value, or compute a semi-useful value
	 * if this String has not yet been set.
	 */
	public String getEntropy()
	{

		// Calculate a semi-useful value if this has not been set
		if (this.entropy == null)
		{
			setEntropy(this.toString());
		}

		return (this.entropy);

	}

	/**
	 * Set the entropy increaser value.
	 *
	 * @param entropy The new entropy increaser value
	 */
	public void setEntropy(String entropy)
	{

		String oldEntropy = entropy;
		this.entropy = entropy;
		support.firePropertyChange("entropy", oldEntropy, this.entropy);

	}

	/**
	 * 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 default maximum inactive interval (in seconds)
	 * for Sessions created by this Manager.
	 */
	public int getMaxInactiveInterval()
	{

		return (this.maxInactiveInterval);

	}

	/**
	 * Set the default maximum inactive interval (in seconds)
	 * for Sessions 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));

	}

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

		return (name);

	}

	/**
	 * 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;
					char entropy[] = getEntropy().toCharArray();
					for (int i = 0; i < entropy.length; i++)
					{
						long update = ((byte) entropy[i]) << ((i % 8) * 8);
						seed ^= update;
					}
					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("managerBase.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("managerBase.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);

	}

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

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

		synchronized (sessions)
		{
			try
			{
				sessions.put(session.getId(), session);
			}
			catch (Exception e)
			{
				throw new RuntimeException();
			}
			if (sessions.size() > maxActive)
			{
				maxActive = sessions.size();
			}
		}
	}

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

		support.addPropertyChangeListener(listener);

	}

	/**
	 * 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 AbstractSession createSession()
	{

		// Recycle or create a AbstractSession instance
		AbstractSession session = createEmptySession();

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

	}

	/**
	 * Get a session from the recycled ones or create a new empty one.
	 * The PersistentManager manager does not need to create session data
	 * because it reads it from the Store.
	 */
	public AbstractSession createEmptySession()
	{
		return (getNewSession());
	}

	/**
	 * Return the active AbstractSession, 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
	 */
	public AbstractSession findSession(String id)
	{

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

	}

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

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

	}

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

		synchronized (sessions)
		{
			try
			{
				sessions.remove(session.getId());
			}
			catch (Exception e)
			{
				throw new RuntimeException();
			}
		}

	}

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

		support.removePropertyChangeListener(listener);

	}

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

	/**
	 * Get new session class to be used in the doLoad() method.
	 */
	protected StandardSession getNewSession()
	{
		return new StandardSession(this);
	}

	protected void getRandomBytes(byte bytes[])
	{
		Random random = getRandom();
		getRandom().nextBytes(bytes);
	}

	/**
	 * Generate and return a new session identifier.
	 */
	protected synchronized String generateSessionId()
	{
		byte bytes[] = new byte[SESSION_ID_BYTES];
		getRandomBytes(bytes);
		bytes = getDigest().digest(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());

	}

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

	/**
	 * Log a message on the Logger associated with our Service (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 Service (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)
	{
		AbstractSession s = (AbstractSession) sessions.get(sessionId);
		if (s == null)
		{
			log.info("Session not found " + sessionId);
			return;
		}
		s.expire();
	}

	public String getLastAccessedTime(String sessionId)
	{
		AbstractSession s = (AbstractSession) sessions.get(sessionId);
		if (s == null)
		{
			log.info("Session not found " + sessionId);
			return "";
		}
		try
		{
			return new Date(s.getLastAccessedTime()).toString();
		}
		catch (Exception e)
		{
			throw new RuntimeException();
		}
	}
}
