--- /dev/null
+//
+// ========================================================================
+// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+
+package org.eclipse.jetty.server.session;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.ObjectOutputStream;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSessionEvent;
+import javax.servlet.http.HttpSessionListener;
+
+import org.eclipse.jetty.server.SessionIdManager;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.session.JDBCSessionIdManager.SessionTableSchema;
+import org.eclipse.jetty.util.ClassLoadingObjectInputStream;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * JDBCSessionManager
+ *
+ * SessionManager that persists sessions to a database to enable clustering.
+ *
+ * Session data is persisted to the JettySessions table:
+ *
+ * rowId (unique in cluster: webapp name/path + virtualhost + sessionId)
+ * contextPath (of the context owning the session)
+ * sessionId (unique in a context)
+ * lastNode (name of node last handled session)
+ * accessTime (time in milliseconds session was accessed)
+ * lastAccessTime (previous time in milliseconds session was accessed)
+ * createTime (time in milliseconds session created)
+ * cookieTime (time in milliseconds session cookie created)
+ * lastSavedTime (last time in milliseconds session access times were saved)
+ * expiryTime (time in milliseconds that the session is due to expire)
+ * map (attribute map)
+ *
+ * As an optimization, to prevent thrashing the database, we do not persist
+ * the accessTime and lastAccessTime every time the session is accessed. Rather,
+ * we write it out every so often. The frequency is controlled by the saveIntervalSec
+ * field.
+ */
+public class JDBCSessionManager extends AbstractSessionManager
+{
+ private static final Logger LOG = Log.getLogger(JDBCSessionManager.class);
+
+ private ConcurrentHashMap<String, Session> _sessions;
+ protected JDBCSessionIdManager _jdbcSessionIdMgr = null;
+ protected long _saveIntervalSec = 60; //only persist changes to session access times every 60 secs
+ protected SessionTableSchema _sessionTableSchema;
+
+
+
+
+ /**
+ * Session
+ *
+ * Session instance.
+ */
+ public class Session extends MemSession
+ {
+ private static final long serialVersionUID = 5208464051134226143L;
+
+ /**
+ * If dirty, session needs to be (re)persisted
+ */
+ protected boolean _dirty=false;
+
+
+ /**
+ * Time in msec since the epoch that a session cookie was set for this session
+ */
+ protected long _cookieSet;
+
+
+ /**
+ * Time in msec since the epoch that the session will expire
+ */
+ protected long _expiryTime;
+
+
+ /**
+ * Time in msec since the epoch that the session was last persisted
+ */
+ protected long _lastSaved;
+
+
+ /**
+ * Unique identifier of the last node to host the session
+ */
+ protected String _lastNode;
+
+
+ /**
+ * Virtual host for context (used to help distinguish 2 sessions with same id on different contexts)
+ */
+ protected String _virtualHost;
+
+
+ /**
+ * Unique row in db for session
+ */
+ protected String _rowId;
+
+
+ /**
+ * Mangled context name (used to help distinguish 2 sessions with same id on different contexts)
+ */
+ protected String _canonicalContext;
+
+
+ /**
+ * Session from a request.
+ *
+ * @param request
+ */
+ protected Session (HttpServletRequest request)
+ {
+ super(JDBCSessionManager.this,request);
+ int maxInterval=getMaxInactiveInterval();
+ _expiryTime = (maxInterval <= 0 ? 0 : (System.currentTimeMillis() + maxInterval*1000L));
+ _virtualHost = JDBCSessionManager.getVirtualHost(_context);
+ _canonicalContext = canonicalize(_context.getContextPath());
+ _lastNode = getSessionIdManager().getWorkerName();
+ }
+
+
+ /**
+ * Session restored from database
+ * @param sessionId
+ * @param rowId
+ * @param created
+ * @param accessed
+ */
+ protected Session (String sessionId, String rowId, long created, long accessed, long maxInterval)
+ {
+ super(JDBCSessionManager.this, created, accessed, sessionId);
+ _rowId = rowId;
+ super.setMaxInactiveInterval((int)maxInterval); //restore the session's previous inactivity interval setting
+ _expiryTime = (maxInterval <= 0 ? 0 : (System.currentTimeMillis() + maxInterval*1000L));
+ }
+
+
+ protected synchronized String getRowId()
+ {
+ return _rowId;
+ }
+
+ protected synchronized void setRowId(String rowId)
+ {
+ _rowId = rowId;
+ }
+
+ public synchronized void setVirtualHost (String vhost)
+ {
+ _virtualHost=vhost;
+ }
+
+ public synchronized String getVirtualHost ()
+ {
+ return _virtualHost;
+ }
+
+ public synchronized long getLastSaved ()
+ {
+ return _lastSaved;
+ }
+
+ public synchronized void setLastSaved (long time)
+ {
+ _lastSaved=time;
+ }
+
+ public synchronized void setExpiryTime (long time)
+ {
+ _expiryTime=time;
+ }
+
+ public synchronized long getExpiryTime ()
+ {
+ return _expiryTime;
+ }
+
+
+ public synchronized void setCanonicalContext(String str)
+ {
+ _canonicalContext=str;
+ }
+
+ public synchronized String getCanonicalContext ()
+ {
+ return _canonicalContext;
+ }
+
+ public void setCookieSet (long ms)
+ {
+ _cookieSet = ms;
+ }
+
+ public synchronized long getCookieSet ()
+ {
+ return _cookieSet;
+ }
+
+ public synchronized void setLastNode (String node)
+ {
+ _lastNode=node;
+ }
+
+ public synchronized String getLastNode ()
+ {
+ return _lastNode;
+ }
+
+ @Override
+ public void setAttribute (String name, Object value)
+ {
+ Object old = changeAttribute(name, value);
+ if (value == null && old == null)
+ return; //if same as remove attribute but attribute was already removed, no change
+
+ _dirty = true;
+ }
+
+ @Override
+ public void removeAttribute (String name)
+ {
+ Object old = changeAttribute(name, null);
+ if (old != null) //only dirty if there was a previous value
+ _dirty=true;
+ }
+
+ @Override
+ protected void cookieSet()
+ {
+ _cookieSet = getAccessed();
+ }
+
+ /**
+ * Entry to session.
+ * Called by SessionHandler on inbound request and the session already exists in this node's memory.
+ *
+ * @see org.eclipse.jetty.server.session.AbstractSession#access(long)
+ */
+ @Override
+ protected boolean access(long time)
+ {
+ synchronized (this)
+ {
+ if (super.access(time))
+ {
+ int maxInterval=getMaxInactiveInterval();
+ _expiryTime = (maxInterval <= 0 ? 0 : (time + maxInterval*1000L));
+ return true;
+ }
+ return false;
+ }
+ }
+
+
+
+
+
+ /**
+ * Change the max idle time for this session. This recalculates the expiry time.
+ * @see org.eclipse.jetty.server.session.AbstractSession#setMaxInactiveInterval(int)
+ */
+ @Override
+ public void setMaxInactiveInterval(int secs)
+ {
+ synchronized (this)
+ {
+ super.setMaxInactiveInterval(secs);
+ int maxInterval=getMaxInactiveInterval();
+ _expiryTime = (maxInterval <= 0 ? 0 : (System.currentTimeMillis() + maxInterval*1000L));
+ //force the session to be written out right now
+ try
+ {
+ updateSessionAccessTime(this);
+ }
+ catch (Exception e)
+ {
+ LOG.warn("Problem saving changed max idle time for session "+ this, e);
+ }
+ }
+ }
+
+
+ /**
+ * Exit from session
+ * @see org.eclipse.jetty.server.session.AbstractSession#complete()
+ */
+ @Override
+ protected void complete()
+ {
+ synchronized (this)
+ {
+ super.complete();
+ try
+ {
+ if (isValid())
+ {
+ if (_dirty)
+ {
+ //The session attributes have changed, write to the db, ensuring
+ //http passivation/activation listeners called
+ save(true);
+ }
+ else if ((getAccessed() - _lastSaved) >= (getSaveInterval() * 1000L))
+ {
+ updateSessionAccessTime(this);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.warn("Problem persisting changed session data id="+getId(), e);
+ }
+ finally
+ {
+ _dirty=false;
+ }
+ }
+ }
+
+ protected void save() throws Exception
+ {
+ synchronized (this)
+ {
+ try
+ {
+ updateSession(this);
+ }
+ finally
+ {
+ _dirty = false;
+ }
+ }
+ }
+
+ protected void save (boolean reactivate) throws Exception
+ {
+ synchronized (this)
+ {
+ if (_dirty)
+ {
+ //The session attributes have changed, write to the db, ensuring
+ //http passivation/activation listeners called
+ willPassivate();
+ updateSession(this);
+ if (reactivate)
+ didActivate();
+ }
+ }
+ }
+
+
+ @Override
+ protected void timeout() throws IllegalStateException
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Timing out session id="+getClusterId());
+ super.timeout();
+ }
+
+
+ @Override
+ public String toString ()
+ {
+ return "Session rowId="+_rowId+",id="+getId()+",lastNode="+_lastNode+
+ ",created="+getCreationTime()+",accessed="+getAccessed()+
+ ",lastAccessed="+getLastAccessedTime()+",cookieSet="+_cookieSet+
+ ",maxInterval="+getMaxInactiveInterval()+",lastSaved="+_lastSaved+",expiry="+_expiryTime;
+ }
+ }
+
+
+
+
+ /**
+ * Set the time in seconds which is the interval between
+ * saving the session access time to the database.
+ *
+ * This is an optimization that prevents the database from
+ * being overloaded when a session is accessed very frequently.
+ *
+ * On session exit, if the session attributes have NOT changed,
+ * the time at which we last saved the accessed
+ * time is compared to the current accessed time. If the interval
+ * is at least saveIntervalSecs, then the access time will be
+ * persisted to the database.
+ *
+ * If any session attribute does change, then the attributes and
+ * the accessed time are persisted.
+ *
+ * @param sec
+ */
+ public void setSaveInterval (long sec)
+ {
+ _saveIntervalSec=sec;
+ }
+
+ public long getSaveInterval ()
+ {
+ return _saveIntervalSec;
+ }
+
+
+
+ /**
+ * A method that can be implemented in subclasses to support
+ * distributed caching of sessions. This method will be
+ * called whenever the session is written to the database
+ * because the session data has changed.
+ *
+ * This could be used eg with a JMS backplane to notify nodes
+ * that the session has changed and to delete the session from
+ * the node's cache, and re-read it from the database.
+ * @param session
+ */
+ public void cacheInvalidate (Session session)
+ {
+
+ }
+
+
+ /**
+ * A session has been requested by its id on this node.
+ *
+ * Load the session by id AND context path from the database.
+ * Multiple contexts may share the same session id (due to dispatching)
+ * but they CANNOT share the same contents.
+ *
+ * Check if last node id is my node id, if so, then the session we have
+ * in memory cannot be stale. If another node used the session last, then
+ * we need to refresh from the db.
+ *
+ * NOTE: this method will go to the database, so if you only want to check
+ * for the existence of a Session in memory, use _sessions.get(id) instead.
+ *
+ * @see org.eclipse.jetty.server.session.AbstractSessionManager#getSession(java.lang.String)
+ */
+ @Override
+ public Session getSession(String idInCluster)
+ {
+ Session session = null;
+
+ synchronized (this)
+ {
+ Session memSession = (Session)_sessions.get(idInCluster);
+
+ //check if we need to reload the session -
+ //as an optimization, don't reload on every access
+ //to reduce the load on the database. This introduces a window of
+ //possibility that the node may decide that the session is local to it,
+ //when the session has actually been live on another node, and then
+ //re-migrated to this node. This should be an extremely rare occurrence,
+ //as load-balancers are generally well-behaved and consistently send
+ //sessions to the same node, changing only iff that node fails.
+ //Session data = null;
+ long now = System.currentTimeMillis();
+ if (LOG.isDebugEnabled())
+ {
+ if (memSession==null)
+ LOG.debug("getSession("+idInCluster+"): not in session map,"+
+ " now="+now+
+ " lastSaved="+(memSession==null?0:memSession._lastSaved)+
+ " interval="+(_saveIntervalSec * 1000L));
+ else
+ LOG.debug("getSession("+idInCluster+"): in session map, "+
+ " hashcode="+memSession.hashCode()+
+ " now="+now+
+ " lastSaved="+(memSession==null?0:memSession._lastSaved)+
+ " interval="+(_saveIntervalSec * 1000L)+
+ " lastNode="+memSession._lastNode+
+ " thisNode="+getSessionIdManager().getWorkerName()+
+ " difference="+(now - memSession._lastSaved));
+ }
+
+ try
+ {
+ if (memSession==null)
+ {
+ LOG.debug("getSession("+idInCluster+"): no session in session map. Reloading session data from db.");
+ session = loadSession(idInCluster, canonicalize(_context.getContextPath()), getVirtualHost(_context));
+ }
+ else if ((now - memSession._lastSaved) >= (_saveIntervalSec * 1000L))
+ {
+ LOG.debug("getSession("+idInCluster+"): stale session. Reloading session data from db.");
+ session = loadSession(idInCluster, canonicalize(_context.getContextPath()), getVirtualHost(_context));
+ }
+ else
+ {
+ LOG.debug("getSession("+idInCluster+"): session in session map");
+ session = memSession;
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.warn("Unable to load session "+idInCluster, e);
+ return null;
+ }
+
+
+ //If we have a session
+ if (session != null)
+ {
+ //If the session was last used on a different node, or session doesn't exist on this node
+ if (!session.getLastNode().equals(getSessionIdManager().getWorkerName()) || memSession==null)
+ {
+ //if session doesn't expire, or has not already expired, update it and put it in this nodes' memory
+ if (session._expiryTime <= 0 || session._expiryTime > now)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("getSession("+idInCluster+"): lastNode="+session.getLastNode()+" thisNode="+getSessionIdManager().getWorkerName());
+
+ session.setLastNode(getSessionIdManager().getWorkerName());
+ _sessions.put(idInCluster, session);
+
+ //update in db
+ try
+ {
+ updateSessionNode(session);
+ session.didActivate();
+ }
+ catch (Exception e)
+ {
+ LOG.warn("Unable to update freshly loaded session "+idInCluster, e);
+ return null;
+ }
+ }
+ else
+ {
+ LOG.debug("getSession ({}): Session has expired", idInCluster);
+ //ensure that the session id for the expired session is deleted so that a new session with the
+ //same id cannot be created (because the idInUse() test would succeed)
+ _jdbcSessionIdMgr.removeSession(idInCluster);
+ session=null;
+ }
+
+ }
+ else
+ {
+ //the session loaded from the db and the one in memory are the same, so keep using the one in memory
+ session = memSession;
+ LOG.debug("getSession({}): Session not stale {}", idInCluster,session);
+ }
+ }
+ else
+ {
+ //No session in db with matching id and context path.
+ LOG.debug("getSession({}): No session in database matching id={}",idInCluster,idInCluster);
+ }
+
+ return session;
+ }
+ }
+
+
+ /**
+ * Get the number of sessions.
+ *
+ * @see org.eclipse.jetty.server.session.AbstractSessionManager#getSessions()
+ */
+ @Override
+ public int getSessions()
+ {
+ return _sessions.size();
+ }
+
+
+ /**
+ * Start the session manager.
+ *
+ * @see org.eclipse.jetty.server.session.AbstractSessionManager#doStart()
+ */
+ @Override
+ public void doStart() throws Exception
+ {
+ if (_sessionIdManager==null)
+ throw new IllegalStateException("No session id manager defined");
+
+ _jdbcSessionIdMgr = (JDBCSessionIdManager)_sessionIdManager;
+ _sessionTableSchema = _jdbcSessionIdMgr.getSessionTableSchema();
+
+ _sessions = new ConcurrentHashMap<String, Session>();
+
+ super.doStart();
+ }
+
+
+ /**
+ * Stop the session manager.
+ *
+ * @see org.eclipse.jetty.server.session.AbstractSessionManager#doStop()
+ */
+ @Override
+ public void doStop() throws Exception
+ {
+ super.doStop();
+ _sessions.clear();
+ _sessions = null;
+ }
+
+ @Override
+ protected void shutdownSessions()
+ {
+ //Save the current state of all of our sessions,
+ //do NOT delete them (so other nodes can manage them)
+ long gracefulStopMs = getContextHandler().getServer().getStopTimeout();
+ long stopTime = 0;
+ if (gracefulStopMs > 0)
+ stopTime = System.nanoTime() + (TimeUnit.NANOSECONDS.convert(gracefulStopMs, TimeUnit.MILLISECONDS));
+
+ ArrayList<Session> sessions = (_sessions == null? new ArrayList<Session>() :new ArrayList<Session>(_sessions.values()) );
+
+ // loop while there are sessions, and while there is stop time remaining, or if no stop time, just 1 loop
+ while (sessions.size() > 0 && ((stopTime > 0 && (System.nanoTime() < stopTime)) || (stopTime == 0)))
+ {
+ for (Session session : sessions)
+ {
+ try
+ {
+ session.save(false);
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+ _sessions.remove(session.getClusterId());
+ }
+
+ //check if we should terminate our loop if we're not using the stop timer
+ if (stopTime == 0)
+ break;
+
+ // Get any sessions that were added by other requests during processing and go around the loop again
+ sessions=new ArrayList<Session>(_sessions.values());
+ }
+ }
+
+
+ /**
+ *
+ * @see org.eclipse.jetty.server.SessionManager#renewSessionId(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
+ */
+ public void renewSessionId (String oldClusterId, String oldNodeId, String newClusterId, String newNodeId)
+ {
+ Session session = null;
+ try
+ {
+ session = (Session)_sessions.remove(oldClusterId);
+ if (session != null)
+ {
+ synchronized (session)
+ {
+ session.setClusterId(newClusterId); //update ids
+ session.setNodeId(newNodeId);
+ _sessions.put(newClusterId, session); //put it into list in memory
+ updateSession(session); //update database
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.warn(e);
+ }
+
+ super.renewSessionId(oldClusterId, oldNodeId, newClusterId, newNodeId);
+ }
+
+
+
+ /**
+ * Invalidate a session.
+ *
+ * @param idInCluster
+ */
+ protected void invalidateSession (String idInCluster)
+ {
+ Session session = (Session)_sessions.get(idInCluster);
+
+ if (session != null)
+ {
+ session.invalidate();
+ }
+ }
+
+ /**
+ * Delete an existing session, both from the in-memory map and
+ * the database.
+ *
+ * @see org.eclipse.jetty.server.session.AbstractSessionManager#removeSession(java.lang.String)
+ */
+ @Override
+ protected boolean removeSession(String idInCluster)
+ {
+ Session session = (Session)_sessions.remove(idInCluster);
+ try
+ {
+ if (session != null)
+ deleteSession(session);
+ }
+ catch (Exception e)
+ {
+ LOG.warn("Problem deleting session id="+idInCluster, e);
+ }
+ return session!=null;
+ }
+
+
+ /**
+ * Add a newly created session to our in-memory list for this node and persist it.
+ *
+ * @see org.eclipse.jetty.server.session.AbstractSessionManager#addSession(org.eclipse.jetty.server.session.AbstractSession)
+ */
+ @Override
+ protected void addSession(AbstractSession session)
+ {
+ if (session==null)
+ return;
+
+ _sessions.put(session.getClusterId(), (Session)session);
+
+ try
+ {
+ synchronized (session)
+ {
+ session.willPassivate();
+ storeSession(((JDBCSessionManager.Session)session));
+ session.didActivate();
+ }
+ }
+ catch (Exception e)
+ {
+ LOG.warn("Unable to store new session id="+session.getId() , e);
+ }
+ }
+
+
+ /**
+ * Make a new Session.
+ *
+ * @see org.eclipse.jetty.server.session.AbstractSessionManager#newSession(javax.servlet.http.HttpServletRequest)
+ */
+ @Override
+ protected AbstractSession newSession(HttpServletRequest request)
+ {
+ return new Session(request);
+ }
+
+
+ /**
+ * @param sessionId
+ * @param rowId
+ * @param created
+ * @param accessed
+ * @param maxInterval
+ * @return
+ */
+ protected AbstractSession newSession (String sessionId, String rowId, long created, long accessed, long maxInterval)
+ {
+ return new Session(sessionId, rowId, created, accessed, maxInterval);
+ }
+
+ /* ------------------------------------------------------------ */
+ /** Remove session from manager
+ * @param session The session to remove
+ * @param invalidate True if {@link HttpSessionListener#sessionDestroyed(HttpSessionEvent)} and
+ * {@link SessionIdManager#invalidateAll(String)} should be called.
+ */
+ @Override
+ public boolean removeSession(AbstractSession session, boolean invalidate)
+ {
+ // Remove session from context and global maps
+ boolean removed = super.removeSession(session, invalidate);
+
+ if (removed)
+ {
+ if (!invalidate)
+ {
+ session.willPassivate();
+ }
+ }
+
+ return removed;
+ }
+
+
+ /**
+ * Expire any Sessions we have in memory matching the list of
+ * expired Session ids.
+ *
+ * @param sessionIds
+ */
+ protected Set<String> expire (Set<String> sessionIds)
+ {
+ //don't attempt to scavenge if we are shutting down
+ if (isStopping() || isStopped())
+ return null;
+
+
+ Thread thread=Thread.currentThread();
+ ClassLoader old_loader=thread.getContextClassLoader();
+
+ Set<String> successfullyExpiredIds = new HashSet<String>();
+ try
+ {
+ Iterator<?> itor = sessionIds.iterator();
+ while (itor.hasNext())
+ {
+ String sessionId = (String)itor.next();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Expiring session id "+sessionId);
+
+ Session session = (Session)_sessions.get(sessionId);
+
+ //if session is not in our memory, then fetch from db so we can call the usual listeners on it
+ if (session == null)
+ {
+ if (LOG.isDebugEnabled())LOG.debug("Force loading session id "+sessionId);
+ session = loadSession(sessionId, canonicalize(_context.getContextPath()), getVirtualHost(_context));
+ if (session != null)
+ {
+ //loaded an expired session last managed on this node for this context, add it to the list so we can
+ //treat it like a normal expired session
+ _sessions.put(session.getClusterId(), session);
+ }
+ else
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Unrecognized session id="+sessionId);
+ continue;
+ }
+ }
+
+ if (session != null)
+ {
+ session.timeout();
+ successfullyExpiredIds.add(session.getClusterId());
+ }
+ }
+ return successfullyExpiredIds;
+ }
+ catch (Throwable t)
+ {
+ LOG.warn("Problem expiring sessions", t);
+ return successfullyExpiredIds;
+ }
+ finally
+ {
+ thread.setContextClassLoader(old_loader);
+ }
+ }
+
+
+ /**
+ * Load a session from the database
+ * @param id
+ * @return the session data that was loaded
+ * @throws Exception
+ */
+ protected Session loadSession (final String id, final String canonicalContextPath, final String vhost)
+ throws Exception
+ {
+ final AtomicReference<Session> _reference = new AtomicReference<Session>();
+ final AtomicReference<Exception> _exception = new AtomicReference<Exception>();
+ Runnable load = new Runnable()
+ {
+ /**
+ * @see java.lang.Runnable#run()
+ */
+ @SuppressWarnings("unchecked")
+ public void run()
+ {
+ try (Connection connection = getConnection();
+ PreparedStatement statement = _sessionTableSchema.getLoadStatement(connection, id, canonicalContextPath, vhost);
+ ResultSet result = statement.executeQuery())
+ {
+ Session session = null;
+ if (result.next())
+ {
+ long maxInterval = result.getLong(_sessionTableSchema.getMaxIntervalColumn());
+ if (maxInterval == JDBCSessionIdManager.MAX_INTERVAL_NOT_SET)
+ {
+ maxInterval = getMaxInactiveInterval(); //if value not saved for maxInactiveInterval, use current value from sessionmanager
+ }
+ session = (Session)newSession(id, result.getString(_sessionTableSchema.getRowIdColumn()),
+ result.getLong(_sessionTableSchema.getCreateTimeColumn()),
+ result.getLong(_sessionTableSchema.getAccessTimeColumn()),
+ maxInterval);
+ session.setCookieSet(result.getLong(_sessionTableSchema.getCookieTimeColumn()));
+ session.setLastAccessedTime(result.getLong(_sessionTableSchema.getLastAccessTimeColumn()));
+ session.setLastNode(result.getString(_sessionTableSchema.getLastNodeColumn()));
+ session.setLastSaved(result.getLong(_sessionTableSchema.getLastSavedTimeColumn()));
+ session.setExpiryTime(result.getLong(_sessionTableSchema.getExpiryTimeColumn()));
+ session.setCanonicalContext(result.getString(_sessionTableSchema.getContextPathColumn()));
+ session.setVirtualHost(result.getString(_sessionTableSchema.getVirtualHostColumn()));
+
+ try (InputStream is = ((JDBCSessionIdManager)getSessionIdManager())._dbAdaptor.getBlobInputStream(result, _sessionTableSchema.getMapColumn());
+ ClassLoadingObjectInputStream ois = new ClassLoadingObjectInputStream(is))
+ {
+ Object o = ois.readObject();
+ session.addAttributes((Map<String,Object>)o);
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("LOADED session "+session);
+ }
+ else
+ if (LOG.isDebugEnabled())
+ LOG.debug("Failed to load session "+id);
+ _reference.set(session);
+ }
+ catch (Exception e)
+ {
+ _exception.set(e);
+ }
+ }
+ };
+
+ if (_context==null)
+ load.run();
+ else
+ _context.getContextHandler().handle(load);
+
+ if (_exception.get()!=null)
+ {
+ //if the session could not be restored, take its id out of the pool of currently-in-use
+ //session ids
+ _jdbcSessionIdMgr.removeSession(id);
+ throw _exception.get();
+ }
+
+ return _reference.get();
+ }
+
+ /**
+ * Insert a session into the database.
+ *
+ * @param session
+ * @throws Exception
+ */
+ protected void storeSession (Session session)
+ throws Exception
+ {
+ if (session==null)
+ return;
+
+ //put into the database
+ try (Connection connection = getConnection();
+ PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._insertSession))
+ {
+ String rowId = calculateRowId(session);
+
+ long now = System.currentTimeMillis();
+ connection.setAutoCommit(true);
+ statement.setString(1, rowId); //rowId
+ statement.setString(2, session.getClusterId()); //session id
+ statement.setString(3, session.getCanonicalContext()); //context path
+ statement.setString(4, session.getVirtualHost()); //first vhost
+ statement.setString(5, getSessionIdManager().getWorkerName());//my node id
+ statement.setLong(6, session.getAccessed());//accessTime
+ statement.setLong(7, session.getLastAccessedTime()); //lastAccessTime
+ statement.setLong(8, session.getCreationTime()); //time created
+ statement.setLong(9, session.getCookieSet());//time cookie was set
+ statement.setLong(10, now); //last saved time
+ statement.setLong(11, session.getExpiryTime());
+ statement.setLong(12, session.getMaxInactiveInterval());
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(baos);
+ oos.writeObject(session.getAttributeMap());
+ oos.flush();
+ byte[] bytes = baos.toByteArray();
+
+ ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
+ statement.setBinaryStream(13, bais, bytes.length);//attribute map as blob
+
+
+ statement.executeUpdate();
+ session.setRowId(rowId); //set it on the in-memory data as well as in db
+ session.setLastSaved(now);
+ }
+ if (LOG.isDebugEnabled())
+ LOG.debug("Stored session "+session);
+ }
+
+
+ /**
+ * Update data on an existing persisted session.
+ *
+ * @param data the session
+ * @throws Exception
+ */
+ protected void updateSession (Session data)
+ throws Exception
+ {
+ if (data==null)
+ return;
+
+ try (Connection connection = getConnection();
+ PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._updateSession))
+ {
+ long now = System.currentTimeMillis();
+ connection.setAutoCommit(true);
+ statement.setString(1, data.getClusterId());
+ statement.setString(2, getSessionIdManager().getWorkerName());//my node id
+ statement.setLong(3, data.getAccessed());//accessTime
+ statement.setLong(4, data.getLastAccessedTime()); //lastAccessTime
+ statement.setLong(5, now); //last saved time
+ statement.setLong(6, data.getExpiryTime());
+ statement.setLong(7, data.getMaxInactiveInterval());
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(baos);
+ oos.writeObject(data.getAttributeMap());
+ oos.flush();
+ byte[] bytes = baos.toByteArray();
+ ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
+
+ statement.setBinaryStream(8, bais, bytes.length);//attribute map as blob
+ statement.setString(9, data.getRowId()); //rowId
+ statement.executeUpdate();
+
+ data.setLastSaved(now);
+ }
+ if (LOG.isDebugEnabled())
+ LOG.debug("Updated session "+data);
+ }
+
+
+ /**
+ * Update the node on which the session was last seen to be my node.
+ *
+ * @param data the session
+ * @throws Exception
+ */
+ protected void updateSessionNode (Session data)
+ throws Exception
+ {
+ String nodeId = getSessionIdManager().getWorkerName();
+ try (Connection connection = getConnection();
+ PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._updateSessionNode))
+ {
+ connection.setAutoCommit(true);
+ statement.setString(1, nodeId);
+ statement.setString(2, data.getRowId());
+ statement.executeUpdate();
+ }
+ if (LOG.isDebugEnabled())
+ LOG.debug("Updated last node for session id="+data.getId()+", lastNode = "+nodeId);
+ }
+
+ /**
+ * Persist the time the session was last accessed.
+ *
+ * @param data the session
+ * @throws Exception
+ */
+ private void updateSessionAccessTime (Session data)
+ throws Exception
+ {
+ try (Connection connection = getConnection();
+ PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._updateSessionAccessTime))
+ {
+ long now = System.currentTimeMillis();
+ connection.setAutoCommit(true);
+ statement.setString(1, getSessionIdManager().getWorkerName());
+ statement.setLong(2, data.getAccessed());
+ statement.setLong(3, data.getLastAccessedTime());
+ statement.setLong(4, now);
+ statement.setLong(5, data.getExpiryTime());
+ statement.setLong(6, data.getMaxInactiveInterval());
+ statement.setString(7, data.getRowId());
+
+ statement.executeUpdate();
+ data.setLastSaved(now);
+ }
+ if (LOG.isDebugEnabled())
+ LOG.debug("Updated access time session id="+data.getId()+" with lastsaved="+data.getLastSaved());
+ }
+
+
+
+
+ /**
+ * Delete a session from the database. Should only be called
+ * when the session has been invalidated.
+ *
+ * @param data
+ * @throws Exception
+ */
+ protected void deleteSession (Session data)
+ throws Exception
+ {
+ try (Connection connection = getConnection();
+ PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._deleteSession))
+ {
+ connection.setAutoCommit(true);
+ statement.setString(1, data.getRowId());
+ statement.executeUpdate();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Deleted Session "+data);
+ }
+ }
+
+
+
+ /**
+ * Get a connection from the driver.
+ * @return
+ * @throws SQLException
+ */
+ private Connection getConnection ()
+ throws SQLException
+ {
+ return ((JDBCSessionIdManager)getSessionIdManager()).getConnection();
+ }
+
+ /**
+ * Calculate a unique id for this session across the cluster.
+ *
+ * Unique id is composed of: contextpath_virtualhost0_sessionid
+ * @param data
+ * @return
+ */
+ private String calculateRowId (Session data)
+ {
+ String rowId = canonicalize(_context.getContextPath());
+ rowId = rowId + "_" + getVirtualHost(_context);
+ rowId = rowId+"_"+data.getId();
+ return rowId;
+ }
+
+ /**
+ * Get the first virtual host for the context.
+ *
+ * Used to help identify the exact session/contextPath.
+ *
+ * @return 0.0.0.0 if no virtual host is defined
+ */
+ private static String getVirtualHost (ContextHandler.Context context)
+ {
+ String vhost = "0.0.0.0";
+
+ if (context==null)
+ return vhost;
+
+ String [] vhosts = context.getContextHandler().getVirtualHosts();
+ if (vhosts==null || vhosts.length==0 || vhosts[0]==null)
+ return vhost;
+
+ return vhosts[0];
+ }
+
+ /**
+ * Make an acceptable file name from a context path.
+ *
+ * @param path
+ * @return
+ */
+ private static String canonicalize (String path)
+ {
+ if (path==null)
+ return "";
+
+ return path.replace('/', '_').replace('.','_').replace('\\','_');
+ }
+}