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