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