//----------------------------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.PropertyChangeSupport;
import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;

import org.huihoo.commons.util.Enumerator;
import org.huihoo.willow.Manager;
import org.huihoo.willow.util.StringManager;
import org.huihoo.workflow.server.WorkflowSession;
import org.huihoo.workflow.usermodel.WorkflowParticipant;


public class StandardSession extends AbstractSession
{

	// ----------------------------------------------------------- Constructors

	/**
	 * Construct a new Session associated with the specified Manager.
	 *
	 * @param manager The manager with which this Session is associated
	 */
	public StandardSession(Manager manager)
	{
		super();
		
		this.manager = manager;
		if (manager instanceof ManagerBase)
		{
			this.debug = ((ManagerBase) manager).getDebug();
		}
	}

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

	/**
	 * The dummy attribute value serialized when a NotSerializableException is
	 * encountered in <code>writeObject()</code>.
	 */
	protected static final String NOT_SERIALIZED = "___NOT_SERIALIZABLE_EXCEPTION___";

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

	/**
	 * The collection of user data attributes associated with this Session.
	 */
	protected HashMap attributes = new HashMap();

	/**
	 * The time this session was created, in milliseconds since midnight,
	 * January 1, 1970 GMT.
	 */
	protected long creationTime = 0L;

	/**
	 * The debugging detail level for this component.  NOTE:  This value
	 * is not included in the serialized version of this object.
	 */
	protected transient int debug = 0;

	/**
	 * We are currently processing a session expiration, so bypass
	 * certain IllegalStateException tests.  NOTE:  This value is not
	 * included in the serialized version of this object.
	 */
	protected transient boolean expiring = false;

	/**
	 * The session identifier of this Session.
	 */
	protected String id = null;

	/**
	 * Descriptive information describing this Session implementation.
	 */
	protected static final String info = "StandardSession/1.0";

	/**
	 * The last accessed time for this Session.
	 */
	protected long lastAccessedTime = creationTime;

	/**
	 * The maximum time interval (in seconds) between client requests before
	 * the servlet container may invalidate this session.  A negative time
	 * indicates that the session should never time out.
	 */
	protected int maxInactiveInterval = -1;

	/**
	 * Flag indicating whether this session is new or not.
	 */
	protected boolean isNew = false;

	/**
	 * Flag indicating whether this session is valid or not.
	 */
	protected boolean isValid = false;

	/**
	 * The authenticated WorkflowParticipant associated with this session, if any.
	 */
	protected   WorkflowParticipant participant = null;

	/**
	 * The property change support for this component.  NOTE:  This value
	 * is not included in the serialized version of this object.
	 */
	protected transient PropertyChangeSupport support = new PropertyChangeSupport(this);

	/**
	 * The session event listeners for this Session.
	 */
	protected transient ArrayList listeners = new ArrayList();

	/**
	 * The Manager with which this Session is associated.
	 */
	protected transient Manager manager = null;
	
	/**
	 * The current accessed time for this session.
	 */
	protected long thisAccessedTime = creationTime;


	/**
	 * Set the creation time for this session.  This method is called by the
	 * Manager when an existing Session instance is reused.
	 *
	 * @param time The new creation time
	 */
	public void setCreationTime(long time)
	{

		this.creationTime = time;
		this.lastAccessedTime = time;
		this.thisAccessedTime = time;

	}

	/**
	 * Return the session identifier for this session.
	 */
	public String getId()
	{

		return (this.id);

	}

	/**
	 * Set the session identifier for this session.
	 *
	 * @param id The new session identifier
	 */
	public void setId(String id)
	{

		if ((this.id != null) && (manager != null))
		{
			manager.remove(this);
		}

		this.id = id;

		if (manager != null)
		{
			manager.add(this);
		}
	}

	/**
	 * Return descriptive information about this Session 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 last time the client sent a request associated with this
	 * session, as the number of milliseconds since midnight, January 1, 1970
	 * GMT.  Actions that your application takes, such as getting or setting
	 * a value associated with the session, do not affect the access time.
	 */
	public long getLastAccessedTime()
	{
		if (!isValid())
		{
			throw new IllegalStateException(sm.getString("standardSession.getLastAccessedTime.ise"));
		}
		return (this.lastAccessedTime);

	}

	/**
	 * Return the Manager within which this Session is valid.
	 */
	public Manager getManager()
	{

		return (this.manager);

	}

	/**
	 * Set the Manager within which this Session is valid.
	 *
	 * @param manager The new Manager
	 */
	public void setManager(Manager manager)
	{

		this.manager = manager;

	}

	/**
	 * Return the maximum time interval, in seconds, between client requests
	 * before the servlet container will invalidate the session.  A negative
	 * time indicates that the session should never time out.
	 */
	public int getMaxInactiveInterval()
	{

		return (this.maxInactiveInterval);

	}

	/**
	 * Set the maximum time interval, in seconds, between client requests
	 * before the servlet container will invalidate the session.  A negative
	 * time indicates that the session should never time out.
	 *
	 * @param interval The new maximum interval
	 */
	public void setMaxInactiveInterval(int interval)
	{

		this.maxInactiveInterval = interval;
		if (isValid && interval == 0)
		{
			expire();
		}

	}

	/**
	 * Set the <code>isNew</code> flag for this session.
	 *
	 * @param isNew The new value for the <code>isNew</code> flag
	 */
	public void setNew(boolean isNew)
	{

		this.isNew = isNew;

	}

	/**
	 * Return the authenticated WorkflowParticipant that is associated with this Session.
	 * This provides an <code>Authenticator</code> with a means to cache a
	 * previously authenticated WorkflowParticipant, and avoid potentially expensive
	 * <code>Realm.authenticate()</code> calls on every request.  If there
	 * is no current associated WorkflowParticipant, return <code>null</code>.
	 */
	public WorkflowParticipant getParticipant()
	{

		return (this.participant);

	}

	/**
	 * Set the authenticated WorkflowParticipant that is associated with this Session.
	 *
	 * @param workflowUser The new WorkflowParticipant, or <code>null</code> if none
	 */
	public void setParticipant(WorkflowParticipant workflowUser)
	{

		WorkflowParticipant oldWorkflowUser = this.participant;
		this.participant = workflowUser;
		support.firePropertyChange("participant", oldWorkflowUser, this.participant);

	}

	/**
	 * Return the <code>WiilowSession</code> for which this object
	 * is the facade.
	 */
	public WorkflowSession getSession()
	{

		return this;

	}

	/**
	 * Return the <code>isValid</code> flag for this session.
	 */
	public boolean isValid()
	{

		if (this.expiring)
		{
			return true;
		}

		if (!this.isValid)
		{
			return false;
		}

		if (maxInactiveInterval >= 0)
		{
			long timeNow = System.currentTimeMillis();
			int timeIdle = (int) ((timeNow - lastAccessedTime) / 1000L);
			if (timeIdle >= maxInactiveInterval)
			{
				expire();
			}
		}

		return (this.isValid);
	}

	/**
	 * Set the <code>isValid</code> flag for this session.
	 *
	 * @param isValid The new value for the <code>isValid</code> flag
	 */
	public void setValid(boolean isValid)
	{

		this.isValid = isValid;
	}

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

	/**
	 * Update the accessed time information for this session.  This method
	 * should be called by the context when a request comes in for a particular
	 * session, even if the application does not reference it.
	 */
	public void access()
	{

		this.isNew = false;
		this.lastAccessedTime = this.thisAccessedTime;
		this.thisAccessedTime = System.currentTimeMillis();

		evaluateIfValid();
	}

	/**
	 * Perform the internal processing required to invalidate this session,
	 * without triggering an exception if the session has already expired.
	 */
	public void expire()
	{

		// Mark this session as "being expired" if needed
		if (expiring)
			return;

		synchronized (this)
		{

			if (manager == null)
				return;

			expiring = true;

			setValid(false);

			// Remove this session from our manager's active sessions
			if (manager != null)
			{
				manager.remove(this);
			}

			// We have completed expire of this session
			expiring = false;

		}

	}

	/**
	 * Release all object references, and initialize instance variables, in
	 * preparation for reuse of this object.
	 */
	public void recycle()
	{

		// Reset the instance variables associated with this Session
		attributes.clear();
		creationTime = 0L;
		expiring = false;
		id = null;
		lastAccessedTime = 0L;
		maxInactiveInterval = -1;
		setParticipant(null);
		isNew = false;
		isValid = false;
		manager = null;

	}

	/**
	 * Return a string representation of this object.
	 */
	public String toString()
	{

		StringBuffer sb = new StringBuffer();
		sb.append("StandardSession[");
		sb.append(id);
		sb.append("]");
		return (sb.toString());

	}

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

	/**
	 * Read a serialized version of the contents of this session object from
	 * the specified object input stream, without requiring that the
	 * StandardSession itself have been serialized.
	 *
	 * @param stream The object input stream to read from
	 *
	 * @exception ClassNotFoundException if an unknown class is specified
	 * @exception IOException if an input/output error occurs
	 */
	public void readObjectData(ObjectInputStream stream) throws ClassNotFoundException, IOException
	{

		readObject(stream);

	}

	/**
	 * Write a serialized version of the contents of this session object to
	 * the specified object output stream, without requiring that the
	 * StandardSession itself have been serialized.
	 *
	 * @param stream The object output stream to write to
	 *
	 * @exception IOException if an input/output error occurs
	 */
	public void writeObjectData(ObjectOutputStream stream) throws IOException
	{

		writeObject(stream);

	}

	// ------------------------------------------------- WorkflowSession Properties

	/**
	 * Return the time when this session was created, in milliseconds since
	 * midnight, January 1, 1970 GMT.
	 *
	 * @exception IllegalStateException if this method is called on an
	 *  invalidated session
	 */
	public long getCreationTime()
	{

		if (!isValid())
			throw new IllegalStateException(sm.getString("standardSession.getCreationTime.ise"));

		return (this.creationTime);

	}

	/**
	 * Invalidates this session and unbinds any objects bound to it.
	 *
	 * @exception IllegalStateException if this method is called on
	 *  an invalidated session
	 */
	public void invalidate()
	{

		if (!isValid())
			throw new IllegalStateException(sm.getString("standardSession.invalidate.ise"));

		// Cause this session to expire
		expire();

	}

	/**
	 * Return <code>true</code> if the client does not yet know about the
	 * session, or if the client chooses not to join the session.  For
	 * example, if the server used only cookie-based sessions, and the client
	 * has disabled the use of cookies, then a session would be new on each
	 * request.
	 *
	 * @exception IllegalStateException if this method is called on an
	 *  invalidated session
	 */
	public boolean isNew()
	{

		if (!isValid())
			throw new IllegalStateException(sm.getString("standardSession.isNew.ise"));

		return (this.isNew);

	}

	//----------------------------------------------WorkflowSession Public Methods
	/**
	 * Return the object bound with the specified name in this session, or
	 * <code>null</code> if no object is bound with that name.
	 *
	 * @param name Name of the attribute to be returned
	 *
	 * @exception IllegalStateException if this method is called on an
	 *  invalidated session
	 */
	public Object getAttribute(String name)
	{

		if (!isValid())
			throw new IllegalStateException(sm.getString("standardSession.getAttribute.ise"));

		synchronized (attributes)
		{
			return (attributes.get(name));
		}

	}

	/**
	 * Return an <code>Enumeration</code> of <code>String</code> objects
	 * containing the names of the objects bound to this session.
	 *
	 * @exception IllegalStateException if this method is called on an
	 *  invalidated session
	 */
	public Enumeration getAttributeNames()
	{

		if (!isValid())
			throw new IllegalStateException(sm.getString("standardSession.getAttributeNames.ise"));

		synchronized (attributes)
		{
			return (new Enumerator(attributes.keySet(), true));
		}

	}
	/**
	 * Remove the object bound with the specified name from this session.  If
	 * the session does not have an object bound with this name, this method
	 * does nothing.
	 * <p>
	 * After this method executes, and if the object implements
	 * <code>HttpSessionBindingListener</code>, the container calls
	 * <code>valueUnbound()</code> on the object.
	 *
	 * @param name Name of the object to remove from this session.
	 *
	 * @exception IllegalStateException if this method is called on an
	 *  invalidated session
	 */
	public void removeAttribute(String name)
	{

		// Validate our current state
		if (!isValid())
			throw new IllegalStateException(sm.getString("standardSession.removeAttribute.ise"));

		// Remove this attribute from our collection
		Object value = null;
		boolean found = false;
		synchronized (attributes)
		{
			found = attributes.containsKey(name);
			if (found)
			{
				value = attributes.get(name);
				attributes.remove(name);
			}
			else
			{
				return;
			}
		}

	}

	/**
	 * Bind an object to this session, using the specified name.  If an object
	 * of the same name is already bound to this session, the object is
	 * replaced.
	 *
	 * @param name Name to which the object is bound, cannot be null
	 * @param value Object to be bound, cannot be null
	 *
	 * @exception IllegalStateException if this method is called on an
	 *  invalidated session
	 */
	public void setAttribute(String name, Object value)
	{

		// Name cannot be null
		if (name == null)
			throw new IllegalArgumentException(sm.getString("standardSession.setAttribute.namenull"));

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

		// Validate our current state
		if (!isValid())
			throw new IllegalStateException(sm.getString("standardSession.setAttribute.ise"));
		if ((manager != null) && !(value instanceof Serializable))
			throw new IllegalArgumentException(sm.getString("standardSession.setAttribute.iae"));

		// Replace or add this attribute
		Object unbound = null;
		synchronized (attributes)
		{
			unbound = attributes.get(name);
			attributes.put(name, value);
		}

	}

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

	/**
	 * Read a serialized version of this session object from the specified
	 * object input stream.
	 * <p>
	 * <b>IMPLEMENTATION NOTE</b>:  The reference to the owning Manager
	 * is not restored by this method, and must be set explicitly.
	 *
	 * @param stream The input stream to read from
	 *
	 * @exception ClassNotFoundException if an unknown class is specified
	 * @exception IOException if an input/output error occurs
	 */
	protected void readObject(ObjectInputStream stream) throws ClassNotFoundException, IOException
	{
		// Deserialize the scalar instance variables (except Manager)    
		creationTime = ((Long) stream.readObject()).longValue();
		lastAccessedTime = ((Long) stream.readObject()).longValue();
		maxInactiveInterval = ((Integer) stream.readObject()).intValue();
		isNew = ((Boolean) stream.readObject()).booleanValue();
		isValid = ((Boolean) stream.readObject()).booleanValue();
		thisAccessedTime = ((Long) stream.readObject()).longValue();

		id = (String) stream.readObject();
		if (debug >= 2)
		{
			log("readObject() loading session " + id);
		}

		// Deserialize the attribute count and attribute values
		if (attributes == null)
		{
			attributes = new HashMap();
		}

		int n = ((Integer) stream.readObject()).intValue();
		boolean isValidSave = isValid;
		isValid = true;
		for (int i = 0; i < n; i++)
		{
			String name = (String) stream.readObject();
			Object value = (Object) stream.readObject();
			if ((value instanceof String) && (value.equals(NOT_SERIALIZED)))
			{
				continue;
			}
			if (debug >= 2)
			{
				log("  loading attribute '" + name + "' with value '" + value + "'");
			}
			synchronized (attributes)
			{
				attributes.put(name, value);
			}
		}
		isValid = isValidSave;

	}

	/**
	 * Write a serialized version of this session object to the specified
	 * object output stream.
	 * <p>
	 * <b>IMPLEMENTATION NOTE</b>:  The owning Manager will not be stored
	 * in the serialized representation of this Session.  After calling
	 * <code>readObject()</code>, you must set the associated Manager
	 * explicitly.
	 *
	 * @param stream The output stream to write to
	 *
	 * @exception IOException if an input/output error occurs
	 */
	protected void writeObject(ObjectOutputStream stream) throws IOException
	{
		// Write the scalar instance variables (except Manager)
		stream.writeObject(new Long(creationTime));
		stream.writeObject(new Long(lastAccessedTime));
		stream.writeObject(new Integer(maxInactiveInterval));
		stream.writeObject(new Boolean(isNew));
		stream.writeObject(new Boolean(isValid));
		stream.writeObject(new Long(thisAccessedTime));
		stream.writeObject(id);
		if (debug >= 2)
		{
			log("writeObject() storing session " + id);
		}

		// Accumulate the names of serializable and non-serializable attributes
		String keys[] = keys();
		ArrayList saveNames = new ArrayList();
		ArrayList saveValues = new ArrayList();
		for (int i = 0; i < keys.length; i++)
		{
			Object value = null;
			synchronized (attributes)
			{
				value = attributes.get(keys[i]);
			}
			if (value == null)
			{
				continue;
			}
			else if (value instanceof Serializable)
			{
				saveNames.add(keys[i]);
				saveValues.add(value);
			}
		}

		// Serialize the attribute count and the Serializable attributes
		int n = saveNames.size();
		stream.writeObject(new Integer(n));
		for (int i = 0; i < n; i++)
		{
			stream.writeObject((String) saveNames.get(i));
			try
			{
				stream.writeObject(saveValues.get(i));
				if (debug >= 2)
					log("  storing attribute '" + saveNames.get(i) + "' with value '" + saveValues.get(i) + "'");
			}
			catch (NotSerializableException e)
			{
				log(sm.getString("standardSession.notSerializable", saveNames.get(i), id), e);
				stream.writeObject(NOT_SERIALIZED);
				if (debug >= 2)
					log("  storing attribute '" + saveNames.get(i) + "' with value NOT_SERIALIZED");
			}
		}

	}

	protected void evaluateIfValid()
	{
		/*
		* If this session has expired or is in the process of expiring or
		* will never expire, return
		*/
		if (!this.isValid || expiring || maxInactiveInterval < 0)
		{
			return;
		}

		if (!isValid())
		{
			try
			{
				expire();
			}
			catch (Throwable t)
			{
				log(sm.getString("standardSession.expireException"), t);
			}
		}

	}

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

	/**
	 * Return the names of all currently defined session attributes
	 * as an array of Strings.  If there are no defined attributes, a
	 * zero-length array is returned.
	 */
	protected String[] keys()
	{

		String results[] = new String[0];
		synchronized (attributes)
		{
			return ((String[]) attributes.keySet().toArray(results));
		}

	}

	/**
	 * Return the value of an attribute without a check for validity.
	 */
	protected Object getAttributeInternal(String name)
	{

		synchronized (attributes)
		{
			return (attributes.get(name));
		}

	}

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

		if ((manager != null) && (manager instanceof ManagerBase))
		{
			((ManagerBase) manager).log(message);
		}
		else
		{
			System.out.println("StandardSession: " + message);
		}

	}

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

		if ((manager != null) && (manager instanceof ManagerBase))
		{
			((ManagerBase) manager).log(message, throwable);
		}
		else
		{
			System.out.println("StandardSession: " + message);
			throwable.printStackTrace(System.out);
		}

	}

}