package org.marketcetera.util.ws.stateful; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import org.marketcetera.util.misc.ClassVersion; import org.marketcetera.util.ws.tags.NodeId; import org.marketcetera.util.ws.tags.SessionId; /** * A session manager. It maps session IDs ({@link SessionId}) to * session holders ({@link SessionHolder}), and (optionally) * automatically expires sessions (map entries) which remain unused * for a time interval that exceeds the session lifespan. This * expiration is performed at regular intervals by a reaper thread, * initiated during construction as part of the caller's thread group; * the interval between scans is normally 5% of the session lifespan. * * @author tlerios@marketcetera.com * @since 1.0.0 * @version $Id: SessionManager.java 16873 2014-04-11 16:55:47Z colin $ */ /* $License$ */ @ClassVersion("$Id: SessionManager.java 16873 2014-04-11 16:55:47Z colin $") public class SessionManager<T> { // CLASS DATA. /** * The maximum sleep interval between reaper scans, in ms. */ public static final long MAX_SCAN_INTERVAL= 60*1000; /** * A sentinel value for an infinite session lifespan (non-expiring * sessions). */ public static final long INFINITE_SESSION_LIFESPAN= -1; // INSTANCE DATA. private NodeId mServerId; private final long mSessionLife; private final SessionFactory<T> mSessionFactory; private final HashMap<SessionId,SessionHolder<T>> mMap= new HashMap<SessionId,SessionHolder<T>>(); /** * The reaper. */ @ClassVersion("$Id: SessionManager.java 16873 2014-04-11 16:55:47Z colin $") final class Reaper extends Thread { // INSTANCE DATA. private long mScanInterval; // CONSTRUCTORS. /** * Creates a new reaper. */ public Reaper() { super(Thread.currentThread().getThreadGroup(), Messages.REAPER_THREAD_NAME.getText()); setDaemon(true); mScanInterval=Math.min (MAX_SCAN_INTERVAL,(long)(getLifespan()*0.05)); } /** * Returns the time interval between scans, in ms. * * @return The interval. */ private long getScanInterval() { return mScanInterval; } // Runnable. @Override public void run() { while (true) { long cutoff=System.currentTimeMillis()-getLifespan(); synchronized (getMap()) { for (Iterator<Map.Entry<SessionId,SessionHolder<T>>> i= getMap().entrySet().iterator();i.hasNext();) { Map.Entry<SessionId,SessionHolder<T>> entry=i.next(); if (entry.getValue().getLastAccess()<=cutoff) { Messages.REAPER_EXPIRED_SESSION.info (this,entry.getKey(), entry.getValue().getCreationContext()); if (getSessionFactory()!=null) { getSessionFactory().removedSession (entry.getValue().getSession()); } i.remove(); } } } try { Thread.sleep(getScanInterval()); } catch (InterruptedException ex) { Messages.REAPER_TERMINATED.info(this,ex,getServerId()); return; } } } } // CONSTRUCTORS. /** * Creates a new session manager whose sessions are created by the * given factory, and which have the given lifespan, in ms. * * @param sessionFactory The session factory. It may be null. * @param sessionLife The lifespan. Use {@link * #INFINITE_SESSION_LIFESPAN} for an infinite lifespan. */ public SessionManager (SessionFactory<T> sessionFactory, long sessionLife) { mSessionFactory=sessionFactory; mSessionLife=sessionLife; if (getLifespan()!=INFINITE_SESSION_LIFESPAN) { (new Reaper()).start(); } } /** * Creates a new session manager whose sessions have the given * lifespan, in ms. * * @param sessionLife The lifespan. Use {@link * #INFINITE_SESSION_LIFESPAN} for an infinite lifespan. */ public SessionManager (long sessionLife) { this(null,sessionLife); } /** * Creates a new session manager whose sessions are created by the * given factory, and which never expire. * * @param sessionFactory The session factory. It may be null. */ public SessionManager (SessionFactory<T> sessionFactory) { this(sessionFactory,INFINITE_SESSION_LIFESPAN); } /** * Creates a new session manager whose sessions never expire. */ public SessionManager() { this(INFINITE_SESSION_LIFESPAN); } // INSTANCE METHODS. /** * Sets the receiver's server ID to the given one. * * @param serverId The server ID, which may be null. */ void setServerId (NodeId serverId) { mServerId=serverId; } /** * Returns the receiver's server ID. * * @return The server ID, which may be null. */ public NodeId getServerId() { return mServerId; } /** * Returns the receiver's session factory. * * @return The factory. It may be null. */ public SessionFactory<T> getSessionFactory() { return mSessionFactory; } /** * Returns the lifespan of the sessions managed by the receiver. * * @return The lifespan, in ms. */ public long getLifespan() { return mSessionLife; } /** * Returns the receiver's map. * * @return The map. */ private HashMap<SessionId,SessionHolder<T>> getMap() { return mMap; } /** * Adds the given holder, associated with the given session ID, to * the receiver. This addition counts as an access that renews the * session's expiration counter. * * @param id The session ID. * @param holder The holder. */ public void put(SessionId id, SessionHolder<T> holder) { synchronized (getMap()) { holder.markAccess(); if(getSessionFactory()!=null) { holder.setSession(getSessionFactory().createSession(holder.getCreationContext(), holder.getUser(), id)); } getMap().put(id, holder); } } /** * Returns the holder that the receiver associates with the given * session ID. This access renews the session's expiration * counter. * * @param id The session ID. * * @return The holder. It is null if there is no holder for the * given ID. */ public SessionHolder<T> get (SessionId id) { synchronized (getMap()) { SessionHolder<T> holder=getMap().get(id); if (holder==null) { return null; } holder.markAccess(); return holder; } } /** * Removes the holder that the receiver associates with the given * session ID. This method is a no-op if no such association * exists. * * @param id The session ID. */ public void remove (SessionId id) { synchronized (getMap()) { SessionHolder<T> holder=getMap().remove(id); if ((holder!=null) && (getSessionFactory()!=null)) { getSessionFactory().removedSession(holder.getSession()); } } } }