View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  
20  package org.eclipse.jetty.server.session;
21  
22  import java.io.ByteArrayInputStream;
23  import java.io.ByteArrayOutputStream;
24  import java.io.InputStream;
25  import java.io.ObjectOutputStream;
26  import java.sql.Connection;
27  import java.sql.PreparedStatement;
28  import java.sql.ResultSet;
29  import java.sql.SQLException;
30  import java.util.ArrayList;
31  import java.util.HashSet;
32  import java.util.Iterator;
33  import java.util.Map;
34  import java.util.Set;
35  import java.util.concurrent.ConcurrentHashMap;
36  import java.util.concurrent.TimeUnit;
37  import java.util.concurrent.atomic.AtomicReference;
38  
39  import javax.servlet.http.HttpServletRequest;
40  import javax.servlet.http.HttpSessionEvent;
41  import javax.servlet.http.HttpSessionListener;
42  
43  import org.eclipse.jetty.server.SessionIdManager;
44  import org.eclipse.jetty.server.handler.ContextHandler;
45  import org.eclipse.jetty.server.session.JDBCSessionIdManager.SessionTableSchema;
46  import org.eclipse.jetty.util.ClassLoadingObjectInputStream;
47  import org.eclipse.jetty.util.log.Log;
48  import org.eclipse.jetty.util.log.Logger;
49  
50  /**
51   * JDBCSessionManager
52   *
53   * SessionManager that persists sessions to a database to enable clustering.
54   *
55   * Session data is persisted to the JettySessions table:
56   *
57   * rowId (unique in cluster: webapp name/path + virtualhost + sessionId)
58   * contextPath (of the context owning the session)
59   * sessionId (unique in a context)
60   * lastNode (name of node last handled session)
61   * accessTime (time in milliseconds session was accessed)
62   * lastAccessTime (previous time in milliseconds session was accessed)
63   * createTime (time in milliseconds session created)
64   * cookieTime (time in milliseconds session cookie created)
65   * lastSavedTime (last time in milliseconds session access times were saved)
66   * expiryTime (time in milliseconds that the session is due to expire)
67   * map (attribute map)
68   *
69   * As an optimization, to prevent thrashing the database, we do not persist
70   * the accessTime and lastAccessTime every time the session is accessed. Rather,
71   * we write it out every so often. The frequency is controlled by the saveIntervalSec
72   * field.
73   */
74  public class JDBCSessionManager extends AbstractSessionManager
75  {
76      private static final Logger LOG = Log.getLogger(JDBCSessionManager.class);
77  
78      private ConcurrentHashMap<String, Session> _sessions;
79      protected JDBCSessionIdManager _jdbcSessionIdMgr = null;
80      protected long _saveIntervalSec = 60; //only persist changes to session access times every 60 secs
81      protected SessionTableSchema _sessionTableSchema;
82  
83     
84  
85  
86      /**
87       * Session
88       *
89       * Session instance.
90       */
91      public class Session extends AbstractSession
92      {
93          private static final long serialVersionUID = 5208464051134226143L;
94          
95          /**
96           * If dirty, session needs to be (re)persisted
97           */
98          protected boolean _dirty=false;
99          
100         
101         /**
102          * Time in msec since the epoch that a session cookie was set for this session
103          */
104         protected long _cookieSet;
105         
106         
107         /**
108          * Time in msec since the epoch that the session will expire
109          */
110         protected long _expiryTime;
111         
112         
113         /**
114          * Time in msec since the epoch that the session was last persisted
115          */
116         protected long _lastSaved;
117         
118         
119         /**
120          * Unique identifier of the last node to host the session
121          */
122         protected String _lastNode;
123         
124         
125         /**
126          * Virtual host for context (used to help distinguish 2 sessions with same id on different contexts)
127          */
128         protected String _virtualHost;
129         
130         
131         /**
132          * Unique row in db for session
133          */
134         protected String _rowId;
135         
136         
137         /**
138          * Mangled context name (used to help distinguish 2 sessions with same id on different contexts)
139          */
140         protected String _canonicalContext;
141         
142    
143         /**
144          * Session from a request.
145          *
146          * @param request
147          */
148         protected Session (HttpServletRequest request)
149         {
150             super(JDBCSessionManager.this,request);
151             int maxInterval=getMaxInactiveInterval();
152             _expiryTime = (maxInterval <= 0 ? 0 : (System.currentTimeMillis() + maxInterval*1000L));
153             _virtualHost = JDBCSessionManager.getVirtualHost(_context);
154             _canonicalContext = canonicalize(_context.getContextPath());
155             _lastNode = getSessionIdManager().getWorkerName();
156         }
157         
158         
159         /**
160          * Session restored from database
161          * @param sessionId
162          * @param rowId
163          * @param created
164          * @param accessed
165          */
166         protected Session (String sessionId, String rowId, long created, long accessed, long maxInterval)
167         {
168             super(JDBCSessionManager.this, created, accessed, sessionId);
169             _rowId = rowId;
170             super.setMaxInactiveInterval((int)maxInterval); //restore the session's previous inactivity interval setting
171             _expiryTime = (maxInterval <= 0 ? 0 : (System.currentTimeMillis() + maxInterval*1000L));
172         }
173         
174         
175         protected synchronized String getRowId()
176         {
177             return _rowId;
178         }
179         
180         protected synchronized void setRowId(String rowId)
181         {
182             _rowId = rowId;
183         }
184         
185         public synchronized void setVirtualHost (String vhost)
186         {
187             _virtualHost=vhost;
188         }
189 
190         public synchronized String getVirtualHost ()
191         {
192             return _virtualHost;
193         }
194         
195         public synchronized long getLastSaved ()
196         {
197             return _lastSaved;
198         }
199 
200         public synchronized void setLastSaved (long time)
201         {
202             _lastSaved=time;
203         }
204 
205         public synchronized void setExpiryTime (long time)
206         {
207             _expiryTime=time;
208         }
209 
210         public synchronized long getExpiryTime ()
211         {
212             return _expiryTime;
213         }
214         
215 
216         public synchronized void setCanonicalContext(String str)
217         {
218             _canonicalContext=str;
219         }
220 
221         public synchronized String getCanonicalContext ()
222         {
223             return _canonicalContext;
224         }
225         
226         public void setCookieSet (long ms)
227         {
228             _cookieSet = ms;
229         }
230 
231         public synchronized long getCookieSet ()
232         {
233             return _cookieSet;
234         }
235 
236         public synchronized void setLastNode (String node)
237         {
238             _lastNode=node;
239         }
240 
241         public synchronized String getLastNode ()
242         {
243             return _lastNode;
244         }
245 
246         @Override
247         public void setAttribute (String name, Object value)
248         {
249             updateAttribute(name, value);
250             _dirty = true;
251         }
252 
253         @Override
254         public void removeAttribute (String name)
255         {
256             super.removeAttribute(name);
257             _dirty=true;
258         }
259 
260         @Override
261         protected void cookieSet()
262         {
263             _cookieSet = getAccessed();
264         }
265 
266         /**
267          * Entry to session.
268          * Called by SessionHandler on inbound request and the session already exists in this node's memory.
269          *
270          * @see org.eclipse.jetty.server.session.AbstractSession#access(long)
271          */
272         @Override
273         protected boolean access(long time)
274         {
275             synchronized (this)
276             {
277                 if (super.access(time))
278                 {
279                     int maxInterval=getMaxInactiveInterval();
280                     _expiryTime = (maxInterval <= 0 ? 0 : (time + maxInterval*1000L));
281                     return true;
282                 }
283                 return false;
284             }
285         }
286         
287         
288         
289 
290 
291         /** 
292          * Change the max idle time for this session. This recalculates the expiry time.
293          * @see org.eclipse.jetty.server.session.AbstractSession#setMaxInactiveInterval(int)
294          */
295         @Override
296         public void setMaxInactiveInterval(int secs)
297         {
298             synchronized (this)
299             {
300                 super.setMaxInactiveInterval(secs);
301                 int maxInterval=getMaxInactiveInterval();
302                 _expiryTime = (maxInterval <= 0 ? 0 : (System.currentTimeMillis() + maxInterval*1000L));
303                 //force the session to be written out right now
304                 try
305                 {
306                     updateSessionAccessTime(this);
307                 }
308                 catch (Exception e)
309                 {
310                     LOG.warn("Problem saving changed max idle time for session "+ this, e);
311                 }
312             }
313         }
314 
315 
316         /**
317          * Exit from session
318          * @see org.eclipse.jetty.server.session.AbstractSession#complete()
319          */
320         @Override
321         protected void complete()
322         {
323             synchronized (this)
324             {
325                 super.complete();
326                 try
327                 {
328                     if (isValid())
329                     {
330                         if (_dirty)
331                         {
332                             //The session attributes have changed, write to the db, ensuring
333                             //http passivation/activation listeners called
334                             save(true);
335                         }
336                         else if ((getAccessed() - _lastSaved) >= (getSaveInterval() * 1000L))
337                         {
338                             updateSessionAccessTime(this);
339                         }
340                     }
341                 }
342                 catch (Exception e)
343                 {
344                     LOG.warn("Problem persisting changed session data id="+getId(), e);
345                 }
346                 finally
347                 {
348                     _dirty=false;
349                 }
350             }
351         }
352 
353         protected void save() throws Exception
354         {
355             synchronized (this)
356             {
357                 try
358                 {
359                     updateSession(this);
360                 }
361                 finally
362                 {
363                     _dirty = false;
364                 }
365             }
366         }
367 
368         protected void save (boolean reactivate) throws Exception
369         {
370             synchronized (this)
371             {
372                 if (_dirty)
373                 {
374                     //The session attributes have changed, write to the db, ensuring
375                     //http passivation/activation listeners called
376                     willPassivate();                      
377                     updateSession(this);
378                     if (reactivate)
379                         didActivate();  
380                 }
381             }
382         }
383 
384         
385         @Override
386         protected void timeout() throws IllegalStateException
387         {
388             if (LOG.isDebugEnabled())
389                 LOG.debug("Timing out session id="+getClusterId());
390             super.timeout();
391         }
392         
393         
394         @Override
395         public String toString ()
396         {
397             return "Session rowId="+_rowId+",id="+getId()+",lastNode="+_lastNode+
398                             ",created="+getCreationTime()+",accessed="+getAccessed()+
399                             ",lastAccessed="+getLastAccessedTime()+",cookieSet="+_cookieSet+
400                             ",maxInterval="+getMaxInactiveInterval()+",lastSaved="+_lastSaved+",expiry="+_expiryTime;
401         }
402     }
403 
404 
405 
406 
407     /**
408      * Set the time in seconds which is the interval between
409      * saving the session access time to the database.
410      *
411      * This is an optimization that prevents the database from
412      * being overloaded when a session is accessed very frequently.
413      *
414      * On session exit, if the session attributes have NOT changed,
415      * the time at which we last saved the accessed
416      * time is compared to the current accessed time. If the interval
417      * is at least saveIntervalSecs, then the access time will be
418      * persisted to the database.
419      *
420      * If any session attribute does change, then the attributes and
421      * the accessed time are persisted.
422      *
423      * @param sec
424      */
425     public void setSaveInterval (long sec)
426     {
427         _saveIntervalSec=sec;
428     }
429 
430     public long getSaveInterval ()
431     {
432         return _saveIntervalSec;
433     }
434 
435 
436 
437     /**
438      * A method that can be implemented in subclasses to support
439      * distributed caching of sessions. This method will be
440      * called whenever the session is written to the database
441      * because the session data has changed.
442      *
443      * This could be used eg with a JMS backplane to notify nodes
444      * that the session has changed and to delete the session from
445      * the node's cache, and re-read it from the database.
446      * @param session
447      */
448     public void cacheInvalidate (Session session)
449     {
450 
451     }
452 
453 
454     /**
455      * A session has been requested by its id on this node.
456      *
457      * Load the session by id AND context path from the database.
458      * Multiple contexts may share the same session id (due to dispatching)
459      * but they CANNOT share the same contents.
460      *
461      * Check if last node id is my node id, if so, then the session we have
462      * in memory cannot be stale. If another node used the session last, then
463      * we need to refresh from the db.
464      *
465      * NOTE: this method will go to the database, so if you only want to check
466      * for the existence of a Session in memory, use _sessions.get(id) instead.
467      *
468      * @see org.eclipse.jetty.server.session.AbstractSessionManager#getSession(java.lang.String)
469      */
470     @Override
471     public Session getSession(String idInCluster)
472     {
473         Session session = null;
474         
475         synchronized (this)
476         {
477             Session memSession = (Session)_sessions.get(idInCluster);
478             
479             //check if we need to reload the session -
480             //as an optimization, don't reload on every access
481             //to reduce the load on the database. This introduces a window of
482             //possibility that the node may decide that the session is local to it,
483             //when the session has actually been live on another node, and then
484             //re-migrated to this node. This should be an extremely rare occurrence,
485             //as load-balancers are generally well-behaved and consistently send
486             //sessions to the same node, changing only iff that node fails.
487             //Session data = null;
488             long now = System.currentTimeMillis();
489             if (LOG.isDebugEnabled())
490             {
491                 if (memSession==null)
492                     LOG.debug("getSession("+idInCluster+"): not in session map,"+
493                             " now="+now+
494                             " lastSaved="+(memSession==null?0:memSession._lastSaved)+
495                             " interval="+(_saveIntervalSec * 1000L));
496                 else
497                     LOG.debug("getSession("+idInCluster+"): in session map, "+
498                             " hashcode="+memSession.hashCode()+
499                             " now="+now+
500                             " lastSaved="+(memSession==null?0:memSession._lastSaved)+
501                             " interval="+(_saveIntervalSec * 1000L)+
502                             " lastNode="+memSession._lastNode+
503                             " thisNode="+getSessionIdManager().getWorkerName()+
504                             " difference="+(now - memSession._lastSaved));
505             }
506 
507             try
508             {
509                 if (memSession==null)
510                 {
511                     LOG.debug("getSession("+idInCluster+"): no session in session map. Reloading session data from db.");
512                     session = loadSession(idInCluster, canonicalize(_context.getContextPath()), getVirtualHost(_context));
513                 }
514                 else if ((now - memSession._lastSaved) >= (_saveIntervalSec * 1000L))
515                 {
516                     LOG.debug("getSession("+idInCluster+"): stale session. Reloading session data from db.");
517                     session = loadSession(idInCluster, canonicalize(_context.getContextPath()), getVirtualHost(_context));
518                 }
519                 else
520                 {
521                     LOG.debug("getSession("+idInCluster+"): session in session map");
522                     session = memSession;
523                 }
524             }
525             catch (Exception e)
526             {
527                 LOG.warn("Unable to load session "+idInCluster, e);
528                 return null;
529             }
530 
531 
532             //If we have a session
533             if (session != null)
534             {
535                 //If the session was last used on a different node, or session doesn't exist on this node
536                 if (!session.getLastNode().equals(getSessionIdManager().getWorkerName()) || memSession==null)
537                 {
538                     //if session doesn't expire, or has not already expired, update it and put it in this nodes' memory
539                     if (session._expiryTime <= 0 || session._expiryTime > now)
540                     {
541                         if (LOG.isDebugEnabled()) 
542                             LOG.debug("getSession("+idInCluster+"): lastNode="+session.getLastNode()+" thisNode="+getSessionIdManager().getWorkerName());
543 
544                         session.setLastNode(getSessionIdManager().getWorkerName());                            
545                         _sessions.put(idInCluster, session);
546 
547                         //update in db
548                         try
549                         {
550                             updateSessionNode(session);
551                             session.didActivate();
552                         }
553                         catch (Exception e)
554                         {
555                             LOG.warn("Unable to update freshly loaded session "+idInCluster, e);
556                             return null;
557                         }
558                     }
559                     else
560                     {
561                         LOG.debug("getSession ({}): Session has expired", idInCluster);  
562                         //ensure that the session id for the expired session is deleted so that a new session with the 
563                         //same id cannot be created (because the idInUse() test would succeed)
564                         _jdbcSessionIdMgr.removeSession(idInCluster);
565                         session=null;
566                     }
567 
568                 }
569                 else
570                 {
571                     //the session loaded from the db and the one in memory are the same, so keep using the one in memory
572                     session = memSession;
573                     LOG.debug("getSession({}): Session not stale {}", idInCluster,session);
574                 }
575             }
576             else
577             {
578                 //No session in db with matching id and context path.
579                 LOG.debug("getSession({}): No session in database matching id={}",idInCluster,idInCluster);
580             }
581 
582             return session;
583         }
584     }
585     
586 
587     /**
588      * Get the number of sessions.
589      *
590      * @see org.eclipse.jetty.server.session.AbstractSessionManager#getSessions()
591      */
592     @Override
593     public int getSessions()
594     {
595         return _sessions.size();
596     }
597 
598 
599     /**
600      * Start the session manager.
601      *
602      * @see org.eclipse.jetty.server.session.AbstractSessionManager#doStart()
603      */
604     @Override
605     public void doStart() throws Exception
606     {
607         if (_sessionIdManager==null)
608             throw new IllegalStateException("No session id manager defined");
609 
610         _jdbcSessionIdMgr = (JDBCSessionIdManager)_sessionIdManager;
611         _sessionTableSchema = _jdbcSessionIdMgr.getSessionTableSchema();
612 
613         _sessions = new ConcurrentHashMap<String, Session>();
614 
615         super.doStart();
616     }
617 
618 
619     /**
620      * Stop the session manager.
621      *
622      * @see org.eclipse.jetty.server.session.AbstractSessionManager#doStop()
623      */
624     @Override
625     public void doStop() throws Exception
626     {
627         super.doStop();
628         _sessions.clear();
629         _sessions = null;
630     }
631 
632     @Override
633     protected void shutdownSessions()
634     {
635         //Save the current state of all of our sessions,
636         //do NOT delete them (so other nodes can manage them)
637         long gracefulStopMs = getContextHandler().getServer().getStopTimeout();
638         long stopTime = 0;
639         if (gracefulStopMs > 0)
640             stopTime = System.nanoTime() + (TimeUnit.NANOSECONDS.convert(gracefulStopMs, TimeUnit.MILLISECONDS));        
641 
642         ArrayList<Session> sessions = (_sessions == null? new ArrayList<Session>() :new ArrayList<Session>(_sessions.values()) );
643 
644         // loop while there are sessions, and while there is stop time remaining, or if no stop time, just 1 loop
645         while (sessions.size() > 0 && ((stopTime > 0 && (System.nanoTime() < stopTime)) || (stopTime == 0)))
646         {
647             for (Session session : sessions)
648             {
649                 try
650                 {
651                     session.save(false);
652                 }
653                 catch (Exception e)
654                 {
655                     LOG.warn(e);
656                 }
657                 _sessions.remove(session.getClusterId());
658             }
659 
660             //check if we should terminate our loop if we're not using the stop timer
661             if (stopTime == 0)
662                 break;
663             
664             // Get any sessions that were added by other requests during processing and go around the loop again
665             sessions=new ArrayList<Session>(_sessions.values());
666         }
667     }
668 
669     
670     /**
671      * 
672      * @see org.eclipse.jetty.server.SessionManager#renewSessionId(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
673      */
674     public void renewSessionId (String oldClusterId, String oldNodeId, String newClusterId, String newNodeId)
675     {
676         Session session = null;
677         try
678         {
679             session = (Session)_sessions.remove(oldClusterId);
680             if (session != null)
681             {
682                 synchronized (session)
683                 {
684                     session.setClusterId(newClusterId); //update ids
685                     session.setNodeId(newNodeId);
686                     _sessions.put(newClusterId, session); //put it into list in memory
687                     updateSession(session); //update database
688                 }
689             }
690         }
691         catch (Exception e)
692         {
693             LOG.warn(e);
694         }
695 
696         super.renewSessionId(oldClusterId, oldNodeId, newClusterId, newNodeId);
697     }
698 
699     
700 
701     /**
702      * Invalidate a session.
703      *
704      * @param idInCluster
705      */
706     protected void invalidateSession (String idInCluster)
707     {
708         Session session = (Session)_sessions.get(idInCluster);
709 
710         if (session != null)
711         {
712             session.invalidate();
713         }
714     }
715 
716     /**
717      * Delete an existing session, both from the in-memory map and
718      * the database.
719      *
720      * @see org.eclipse.jetty.server.session.AbstractSessionManager#removeSession(java.lang.String)
721      */
722     @Override
723     protected boolean removeSession(String idInCluster)
724     {
725         Session session = (Session)_sessions.remove(idInCluster);
726         try
727         {
728             if (session != null)
729                 deleteSession(session);
730         }
731         catch (Exception e)
732         {
733             LOG.warn("Problem deleting session id="+idInCluster, e);
734         }
735         return session!=null;
736     }
737 
738 
739     /**
740      * Add a newly created session to our in-memory list for this node and persist it.
741      *
742      * @see org.eclipse.jetty.server.session.AbstractSessionManager#addSession(org.eclipse.jetty.server.session.AbstractSession)
743      */
744     @Override
745     protected void addSession(AbstractSession session)
746     {
747         if (session==null)
748             return;
749 
750         _sessions.put(session.getClusterId(), (Session)session);
751 
752         try
753         {
754             synchronized (session)
755             {
756                 session.willPassivate();
757                 storeSession(((JDBCSessionManager.Session)session));
758                 session.didActivate();
759             }
760         }
761         catch (Exception e)
762         {
763             LOG.warn("Unable to store new session id="+session.getId() , e);
764         }
765     }
766 
767 
768     /**
769      * Make a new Session.
770      *
771      * @see org.eclipse.jetty.server.session.AbstractSessionManager#newSession(javax.servlet.http.HttpServletRequest)
772      */
773     @Override
774     protected AbstractSession newSession(HttpServletRequest request)
775     {
776         return new Session(request);
777     }
778     
779     
780     /**
781      * @param sessionId
782      * @param rowId
783      * @param created
784      * @param accessed
785      * @param maxInterval
786      * @return
787      */
788     protected AbstractSession newSession (String sessionId, String rowId, long created, long accessed, long maxInterval)
789     {
790         return new Session(sessionId, rowId, created, accessed, maxInterval);
791     }
792 
793     /* ------------------------------------------------------------ */
794     /** Remove session from manager
795      * @param session The session to remove
796      * @param invalidate True if {@link HttpSessionListener#sessionDestroyed(HttpSessionEvent)} and
797      * {@link SessionIdManager#invalidateAll(String)} should be called.
798      */
799     @Override
800     public boolean removeSession(AbstractSession session, boolean invalidate)
801     {
802         // Remove session from context and global maps
803         boolean removed = super.removeSession(session, invalidate);
804 
805         if (removed)
806         {
807             if (!invalidate)
808             {
809                 session.willPassivate();
810             }
811         }
812         
813         return removed;
814     }
815 
816 
817     /**
818      * Expire any Sessions we have in memory matching the list of
819      * expired Session ids.
820      *
821      * @param sessionIds
822      */
823     protected Set<String> expire (Set<String> sessionIds)
824     {
825         //don't attempt to scavenge if we are shutting down
826         if (isStopping() || isStopped())
827             return null;
828 
829         
830         Thread thread=Thread.currentThread();
831         ClassLoader old_loader=thread.getContextClassLoader();
832         
833         Set<String> successfullyExpiredIds = new HashSet<String>();
834         try
835         {
836             Iterator<?> itor = sessionIds.iterator();
837             while (itor.hasNext())
838             {
839                 String sessionId = (String)itor.next();
840                 if (LOG.isDebugEnabled())
841                     LOG.debug("Expiring session id "+sessionId);
842 
843                 Session session = (Session)_sessions.get(sessionId);
844 
845                 //if session is not in our memory, then fetch from db so we can call the usual listeners on it
846                 if (session == null)
847                 {
848                     if (LOG.isDebugEnabled())LOG.debug("Force loading session id "+sessionId);
849                     session = loadSession(sessionId, canonicalize(_context.getContextPath()), getVirtualHost(_context));
850                     if (session != null)
851                     {
852                         //loaded an expired session last managed on this node for this context, add it to the list so we can 
853                         //treat it like a normal expired session
854                         _sessions.put(session.getClusterId(), session);
855                     }
856                     else
857                     {
858                         if (LOG.isDebugEnabled())
859                             LOG.debug("Unrecognized session id="+sessionId);
860                         continue;
861                     }
862                 }
863 
864                 if (session != null)
865                 {
866                     session.timeout();
867                     successfullyExpiredIds.add(session.getClusterId());
868                 }
869             }
870             return successfullyExpiredIds;
871         }
872         catch (Throwable t)
873         {
874             LOG.warn("Problem expiring sessions", t);
875             return successfullyExpiredIds;
876         }
877         finally
878         {
879             thread.setContextClassLoader(old_loader);
880         }
881     }
882     
883   
884     /**
885      * Load a session from the database
886      * @param id
887      * @return the session data that was loaded
888      * @throws Exception
889      */
890     protected Session loadSession (final String id, final String canonicalContextPath, final String vhost)
891     throws Exception
892     {
893         final AtomicReference<Session> _reference = new AtomicReference<Session>();
894         final AtomicReference<Exception> _exception = new AtomicReference<Exception>();
895         Runnable load = new Runnable()
896         {
897             /** 
898              * @see java.lang.Runnable#run()
899              */
900             @SuppressWarnings("unchecked")
901             public void run()
902             {
903                 try (Connection connection = getConnection();
904                         PreparedStatement statement = _sessionTableSchema.getLoadStatement(connection, id, canonicalContextPath, vhost);
905                         ResultSet result = statement.executeQuery())
906                 {
907                     Session session = null;
908                     if (result.next())
909                     {                    
910                         long maxInterval = result.getLong(_sessionTableSchema.getMaxIntervalColumn());
911                         if (maxInterval == JDBCSessionIdManager.MAX_INTERVAL_NOT_SET)
912                         {
913                             maxInterval = getMaxInactiveInterval(); //if value not saved for maxInactiveInterval, use current value from sessionmanager
914                         }
915                         session = (Session)newSession(id, result.getString(_sessionTableSchema.getRowIdColumn()), 
916                                                   result.getLong(_sessionTableSchema.getCreateTimeColumn()), 
917                                                   result.getLong(_sessionTableSchema.getAccessTimeColumn()), 
918                                                   maxInterval);
919                         session.setCookieSet(result.getLong(_sessionTableSchema.getCookieTimeColumn()));
920                         session.setLastAccessedTime(result.getLong(_sessionTableSchema.getLastAccessTimeColumn()));
921                         session.setLastNode(result.getString(_sessionTableSchema.getLastNodeColumn()));
922                         session.setLastSaved(result.getLong(_sessionTableSchema.getLastSavedTimeColumn()));
923                         session.setExpiryTime(result.getLong(_sessionTableSchema.getExpiryTimeColumn()));
924                         session.setCanonicalContext(result.getString(_sessionTableSchema.getContextPathColumn()));
925                         session.setVirtualHost(result.getString(_sessionTableSchema.getVirtualHostColumn()));
926                                            
927                         try (InputStream is = ((JDBCSessionIdManager)getSessionIdManager())._dbAdaptor.getBlobInputStream(result, _sessionTableSchema.getMapColumn());
928                                 ClassLoadingObjectInputStream ois = new ClassLoadingObjectInputStream(is))
929                         {
930                             Object o = ois.readObject();
931                             session.addAttributes((Map<String,Object>)o);
932                         }
933 
934                         if (LOG.isDebugEnabled())
935                             LOG.debug("LOADED session "+session);
936                     }
937                     else
938                         if (LOG.isDebugEnabled())
939                             LOG.debug("Failed to load session "+id);
940                     _reference.set(session);
941                 }
942                 catch (Exception e)
943                 {
944                     _exception.set(e);
945                 }
946             }
947         };
948 
949         if (_context==null)
950             load.run();
951         else
952             _context.getContextHandler().handle(load);
953 
954         if (_exception.get()!=null)
955         {
956             //if the session could not be restored, take its id out of the pool of currently-in-use
957             //session ids
958             _jdbcSessionIdMgr.removeSession(id);
959             throw _exception.get();
960         }
961 
962         return _reference.get();
963     }
964 
965     /**
966      * Insert a session into the database.
967      *
968      * @param session
969      * @throws Exception
970      */
971     protected void storeSession (Session session)
972     throws Exception
973     {
974         if (session==null)
975             return;
976 
977         //put into the database
978         try (Connection connection = getConnection();
979                 PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._insertSession))
980         {
981             String rowId = calculateRowId(session);
982 
983             long now = System.currentTimeMillis();
984             connection.setAutoCommit(true);
985             statement.setString(1, rowId); //rowId
986             statement.setString(2, session.getClusterId()); //session id
987             statement.setString(3, session.getCanonicalContext()); //context path
988             statement.setString(4, session.getVirtualHost()); //first vhost
989             statement.setString(5, getSessionIdManager().getWorkerName());//my node id
990             statement.setLong(6, session.getAccessed());//accessTime
991             statement.setLong(7, session.getLastAccessedTime()); //lastAccessTime
992             statement.setLong(8, session.getCreationTime()); //time created
993             statement.setLong(9, session.getCookieSet());//time cookie was set
994             statement.setLong(10, now); //last saved time
995             statement.setLong(11, session.getExpiryTime());
996             statement.setLong(12, session.getMaxInactiveInterval());
997 
998             ByteArrayOutputStream baos = new ByteArrayOutputStream();
999             ObjectOutputStream oos = new ObjectOutputStream(baos);
1000             oos.writeObject(session.getAttributeMap());
1001             oos.flush();
1002             byte[] bytes = baos.toByteArray();
1003 
1004             ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
1005             statement.setBinaryStream(13, bais, bytes.length);//attribute map as blob
1006            
1007 
1008             statement.executeUpdate();
1009             session.setRowId(rowId); //set it on the in-memory data as well as in db
1010             session.setLastSaved(now);
1011         }
1012         if (LOG.isDebugEnabled())
1013             LOG.debug("Stored session "+session);
1014     }
1015 
1016 
1017     /**
1018      * Update data on an existing persisted session.
1019      *
1020      * @param data the session
1021      * @throws Exception
1022      */
1023     protected void updateSession (Session data)
1024     throws Exception
1025     {
1026         if (data==null)
1027             return;
1028 
1029         try (Connection connection = getConnection();
1030                 PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._updateSession))
1031         {
1032             long now = System.currentTimeMillis();
1033             connection.setAutoCommit(true);
1034             statement.setString(1, data.getClusterId());
1035             statement.setString(2, getSessionIdManager().getWorkerName());//my node id
1036             statement.setLong(3, data.getAccessed());//accessTime
1037             statement.setLong(4, data.getLastAccessedTime()); //lastAccessTime
1038             statement.setLong(5, now); //last saved time
1039             statement.setLong(6, data.getExpiryTime());
1040             statement.setLong(7, data.getMaxInactiveInterval());
1041 
1042             ByteArrayOutputStream baos = new ByteArrayOutputStream();
1043             ObjectOutputStream oos = new ObjectOutputStream(baos);
1044             oos.writeObject(data.getAttributeMap());
1045             oos.flush();
1046             byte[] bytes = baos.toByteArray();
1047             ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
1048 
1049             statement.setBinaryStream(8, bais, bytes.length);//attribute map as blob
1050             statement.setString(9, data.getRowId()); //rowId
1051             statement.executeUpdate();
1052 
1053             data.setLastSaved(now);
1054         }
1055         if (LOG.isDebugEnabled())
1056             LOG.debug("Updated session "+data);
1057     }
1058 
1059 
1060     /**
1061      * Update the node on which the session was last seen to be my node.
1062      *
1063      * @param data the session
1064      * @throws Exception
1065      */
1066     protected void updateSessionNode (Session data)
1067     throws Exception
1068     {
1069         String nodeId = getSessionIdManager().getWorkerName();
1070         try (Connection connection = getConnection();
1071                 PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._updateSessionNode))
1072         {
1073             connection.setAutoCommit(true);
1074             statement.setString(1, nodeId);
1075             statement.setString(2, data.getRowId());
1076             statement.executeUpdate();
1077         }
1078         if (LOG.isDebugEnabled())
1079             LOG.debug("Updated last node for session id="+data.getId()+", lastNode = "+nodeId);
1080     }
1081 
1082     /**
1083      * Persist the time the session was last accessed.
1084      *
1085      * @param data the session
1086      * @throws Exception
1087      */
1088     private void updateSessionAccessTime (Session data)
1089     throws Exception
1090     {
1091         try (Connection connection = getConnection();
1092                 PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._updateSessionAccessTime))
1093         {
1094             long now = System.currentTimeMillis();
1095             connection.setAutoCommit(true);
1096             statement.setString(1, getSessionIdManager().getWorkerName());
1097             statement.setLong(2, data.getAccessed());
1098             statement.setLong(3, data.getLastAccessedTime());
1099             statement.setLong(4, now);
1100             statement.setLong(5, data.getExpiryTime());
1101             statement.setLong(6, data.getMaxInactiveInterval());
1102             statement.setString(7, data.getRowId());
1103           
1104             statement.executeUpdate();
1105             data.setLastSaved(now);
1106         }
1107         if (LOG.isDebugEnabled())
1108             LOG.debug("Updated access time session id="+data.getId()+" with lastsaved="+data.getLastSaved());
1109     }
1110 
1111 
1112 
1113 
1114     /**
1115      * Delete a session from the database. Should only be called
1116      * when the session has been invalidated.
1117      *
1118      * @param data
1119      * @throws Exception
1120      */
1121     protected void deleteSession (Session data)
1122     throws Exception
1123     {
1124         try (Connection connection = getConnection();
1125                 PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._deleteSession))
1126         {
1127             connection.setAutoCommit(true);
1128             statement.setString(1, data.getRowId());
1129             statement.executeUpdate();
1130             if (LOG.isDebugEnabled())
1131                 LOG.debug("Deleted Session "+data);
1132         }
1133     }
1134 
1135 
1136 
1137     /**
1138      * Get a connection from the driver.
1139      * @return
1140      * @throws SQLException
1141      */
1142     private Connection getConnection ()
1143     throws SQLException
1144     {
1145         return ((JDBCSessionIdManager)getSessionIdManager()).getConnection();
1146     }
1147 
1148     /**
1149      * Calculate a unique id for this session across the cluster.
1150      *
1151      * Unique id is composed of: contextpath_virtualhost0_sessionid
1152      * @param data
1153      * @return
1154      */
1155     private String calculateRowId (Session data)
1156     {
1157         String rowId = canonicalize(_context.getContextPath());
1158         rowId = rowId + "_" + getVirtualHost(_context);
1159         rowId = rowId+"_"+data.getId();
1160         return rowId;
1161     }
1162 
1163     /**
1164      * Get the first virtual host for the context.
1165      *
1166      * Used to help identify the exact session/contextPath.
1167      *
1168      * @return 0.0.0.0 if no virtual host is defined
1169      */
1170     private static String getVirtualHost (ContextHandler.Context context)
1171     {
1172         String vhost = "0.0.0.0";
1173 
1174         if (context==null)
1175             return vhost;
1176 
1177         String [] vhosts = context.getContextHandler().getVirtualHosts();
1178         if (vhosts==null || vhosts.length==0 || vhosts[0]==null)
1179             return vhost;
1180 
1181         return vhosts[0];
1182     }
1183 
1184     /**
1185      * Make an acceptable file name from a context path.
1186      *
1187      * @param path
1188      * @return
1189      */
1190     private static String canonicalize (String path)
1191     {
1192         if (path==null)
1193             return "";
1194 
1195         return path.replace('/', '_').replace('.','_').replace('\\','_');
1196     }
1197 }