View Javadoc

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