//----------------------------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.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.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.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 org.huihoo.workflow.runtime.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 Engine with which this Manager is associated.
*/
protected Engine engine;
/**
* 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);
// ------------------------------------------------------------- Properties
/**
* Return the Engine with which this Manager is associated.
*/
public Engine getEngine()
{
return (this.engine);
}
/**
* 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 RULE )
* - so use it if available.
*/
public void setRandomFile(String s)
{
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());
}
// -------------------------------------------------------- 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 Engine with which this Manager has been associated. If
* it is a Engine (the usual case), listen for changes to the session
* timeout property.
*
* @param engine The associated Engine
*/
public void setEngine(Engine engine)
{
// Default processing provided by our superclass
Engine oldEngine = this.engine;
this.engine = engine;
support.firePropertyChange("engine", oldEngine, this.engine);
// Register with the new Engine (if any)
if ((this.engine != null))
{
setMaxInactiveInterval(this.engine.getSessionTimeout() * 60);
this.engine.addPropertyChangeListener(this);
}
}
/**
* Return descriptive information about this Manager implementation and
* the corresponding version number, in the format
* <code><description>/<version></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);
}
}