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.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.HashSet;
31  import java.util.Iterator;
32  import java.util.Map;
33  import java.util.Set;
34  import java.util.concurrent.ConcurrentHashMap;
35  import java.util.concurrent.atomic.AtomicReference;
36  
37  import javax.servlet.http.HttpServletRequest;
38  import javax.servlet.http.HttpSessionEvent;
39  import javax.servlet.http.HttpSessionListener;
40  
41  import org.eclipse.jetty.server.SessionIdManager;
42  import org.eclipse.jetty.server.handler.ContextHandler;
43  import org.eclipse.jetty.util.ClassLoadingObjectInputStream;
44  import org.eclipse.jetty.util.log.Log;
45  import org.eclipse.jetty.util.log.Logger;
46  
47  /**
48   * JDBCSessionManager
49   *
50   * SessionManager that persists sessions to a database to enable clustering.
51   *
52   * Session data is persisted to the JettySessions table:
53   *
54   * rowId (unique in cluster: webapp name/path + virtualhost + sessionId)
55   * contextPath (of the context owning the session)
56   * sessionId (unique in a context)
57   * lastNode (name of node last handled session)
58   * accessTime (time in milliseconds session was accessed)
59   * lastAccessTime (previous time in milliseconds session was accessed)
60   * createTime (time in milliseconds session created)
61   * cookieTime (time in milliseconds session cookie created)
62   * lastSavedTime (last time in milliseconds session access times were saved)
63   * expiryTime (time in milliseconds that the session is due to expire)
64   * map (attribute map)
65   *
66   * As an optimization, to prevent thrashing the database, we do not persist
67   * the accessTime and lastAccessTime every time the session is accessed. Rather,
68   * we write it out every so often. The frequency is controlled by the saveIntervalSec
69   * field.
70   */
71  public class JDBCSessionManager extends AbstractSessionManager
72  {
73      private static final Logger LOG = Log.getLogger(JDBCSessionManager.class);
74  
75      private ConcurrentHashMap<String, AbstractSession> _sessions;
76      protected JDBCSessionIdManager _jdbcSessionIdMgr = null;
77      protected long _saveIntervalSec = 60; //only persist changes to session access times every 60 secs
78  
79     
80  
81  
82      /**
83       * Session
84       *
85       * Session instance.
86       */
87      public class Session extends AbstractSession
88      {
89          private static final long serialVersionUID = 5208464051134226143L;
90          
91          /**
92           * If dirty, session needs to be (re)persisted
93           */
94          private boolean _dirty=false;
95          
96          
97          /**
98           * Time in msec since the epoch that a session cookie was set for this session
99           */
100         private long _cookieSet;
101         
102         
103         /**
104          * Time in msec since the epoch that the session will expire
105          */
106         private long _expiryTime;
107         
108         
109         /**
110          * Time in msec since the epoch that the session was last persisted
111          */
112         private long _lastSaved;
113         
114         
115         /**
116          * Unique identifier of the last node to host the session
117          */
118         private String _lastNode;
119         
120         
121         /**
122          * Virtual host for context (used to help distinguish 2 sessions with same id on different contexts)
123          */
124         private String _virtualHost;
125         
126         
127         /**
128          * Unique row in db for session
129          */
130         private String _rowId;
131         
132         
133         /**
134          * Mangled context name (used to help distinguish 2 sessions with same id on different contexts)
135          */
136         private String _canonicalContext;
137         
138    
139         /**
140          * Session from a request.
141          *
142          * @param request
143          */
144         protected Session (HttpServletRequest request)
145         {
146             super(JDBCSessionManager.this,request);
147             int maxInterval=getMaxInactiveInterval();
148             _expiryTime = (maxInterval <= 0 ? 0 : (System.currentTimeMillis() + maxInterval*1000L));
149             _virtualHost = JDBCSessionManager.getVirtualHost(_context);
150             _canonicalContext = canonicalize(_context.getContextPath());
151             _lastNode = getSessionIdManager().getWorkerName();
152         }
153         
154         
155         /**
156          * Session restored from database
157          * @param sessionId
158          * @param rowId
159          * @param created
160          * @param accessed
161          */
162         protected Session (String sessionId, String rowId, long created, long accessed, long maxInterval)
163         {
164             super(JDBCSessionManager.this, created, accessed, sessionId);
165             _rowId = rowId;
166             super.setMaxInactiveInterval((int)maxInterval); //restore the session's previous inactivity interval setting
167             _expiryTime = (maxInterval <= 0 ? 0 : (System.currentTimeMillis() + maxInterval*1000L));
168         }
169         
170         
171         protected synchronized String getRowId()
172         {
173             return _rowId;
174         }
175         
176         protected synchronized void setRowId(String rowId)
177         {
178             _rowId = rowId;
179         }
180         
181         public synchronized void setVirtualHost (String vhost)
182         {
183             _virtualHost=vhost;
184         }
185 
186         public synchronized String getVirtualHost ()
187         {
188             return _virtualHost;
189         }
190         
191         public synchronized long getLastSaved ()
192         {
193             return _lastSaved;
194         }
195 
196         public synchronized void setLastSaved (long time)
197         {
198             _lastSaved=time;
199         }
200 
201         public synchronized void setExpiryTime (long time)
202         {
203             _expiryTime=time;
204         }
205 
206         public synchronized long getExpiryTime ()
207         {
208             return _expiryTime;
209         }
210         
211 
212         public synchronized void setCanonicalContext(String str)
213         {
214             _canonicalContext=str;
215         }
216 
217         public synchronized String getCanonicalContext ()
218         {
219             return _canonicalContext;
220         }
221         
222         public void setCookieSet (long ms)
223         {
224             _cookieSet = ms;
225         }
226 
227         public synchronized long getCookieSet ()
228         {
229             return _cookieSet;
230         }
231 
232         public synchronized void setLastNode (String node)
233         {
234             _lastNode=node;
235         }
236 
237         public synchronized String getLastNode ()
238         {
239             return _lastNode;
240         }
241 
242         @Override
243         public void setAttribute (String name, Object value)
244         {
245             _dirty = (updateAttribute(name, value) || _dirty);
246         }
247 
248         @Override
249         public void removeAttribute (String name)
250         {
251             super.removeAttribute(name);
252             _dirty=true;
253         }
254 
255         @Override
256         protected void cookieSet()
257         {
258             _cookieSet = getAccessed();
259         }
260 
261         /**
262          * Entry to session.
263          * Called by SessionHandler on inbound request and the session already exists in this node's memory.
264          *
265          * @see org.eclipse.jetty.server.session.AbstractSession#access(long)
266          */
267         @Override
268         protected boolean access(long time)
269         {
270             synchronized (this)
271             {
272                 if (super.access(time))
273                 {
274                     int maxInterval=getMaxInactiveInterval();
275                     _expiryTime = (maxInterval <= 0 ? 0 : (time + maxInterval*1000L));
276                     return true;
277                 }
278                 return false;
279             }
280         }
281         
282         
283         
284 
285 
286         /** 
287          * Change the max idle time for this session. This recalculates the expiry time.
288          * @see org.eclipse.jetty.server.session.AbstractSession#setMaxInactiveInterval(int)
289          */
290         @Override
291         public void setMaxInactiveInterval(int secs)
292         {
293             synchronized (this)
294             {
295                 super.setMaxInactiveInterval(secs);
296                 int maxInterval=getMaxInactiveInterval();
297                 _expiryTime = (maxInterval <= 0 ? 0 : (System.currentTimeMillis() + maxInterval*1000L));
298                 //force the session to be written out right now
299                 try
300                 {
301                     updateSessionAccessTime(this);
302                 }
303                 catch (Exception e)
304                 {
305                     LOG.warn("Problem saving changed max idle time for session "+ this, e);
306                 }
307             }
308         }
309 
310 
311         /**
312          * Exit from session
313          * @see org.eclipse.jetty.server.session.AbstractSession#complete()
314          */
315         @Override
316         protected void complete()
317         {
318             synchronized (this)
319             {
320                 super.complete();
321                 try
322                 {
323                     if (isValid())
324                     {
325                         if (_dirty)
326                         {
327                             //The session attributes have changed, write to the db, ensuring
328                             //http passivation/activation listeners called
329                             willPassivate();                      
330                             updateSession(this);
331                             didActivate();
332                         }
333                         else if ((getAccessed() - _lastSaved) >= (getSaveInterval() * 1000L))
334                         {
335                             updateSessionAccessTime(this);
336                         }
337                     }
338                 }
339                 catch (Exception e)
340                 {
341                     LOG.warn("Problem persisting changed session data id="+getId(), e);
342                 }
343                 finally
344                 {
345                     _dirty=false;
346                 }
347             }
348         }
349 
350         protected void save() throws Exception
351         {
352             synchronized (this)
353             {
354                 try
355                 {
356                     updateSession(this);
357                 }
358                 finally
359                 {
360                     _dirty = false;
361                 }
362             }
363         }
364         
365         @Override
366         protected void timeout() throws IllegalStateException
367         {
368             if (LOG.isDebugEnabled())
369                 LOG.debug("Timing out session id="+getClusterId());
370             super.timeout();
371         }
372         
373         
374         @Override
375         public String toString ()
376         {
377             return "Session rowId="+_rowId+",id="+getId()+",lastNode="+_lastNode+
378                             ",created="+getCreationTime()+",accessed="+getAccessed()+
379                             ",lastAccessed="+getLastAccessedTime()+",cookieSet="+_cookieSet+
380                             ",maxInterval="+getMaxInactiveInterval()+",lastSaved="+_lastSaved+",expiry="+_expiryTime;
381         }
382     }
383 
384 
385 
386 
387     /**
388      * Set the time in seconds which is the interval between
389      * saving the session access time to the database.
390      *
391      * This is an optimization that prevents the database from
392      * being overloaded when a session is accessed very frequently.
393      *
394      * On session exit, if the session attributes have NOT changed,
395      * the time at which we last saved the accessed
396      * time is compared to the current accessed time. If the interval
397      * is at least saveIntervalSecs, then the access time will be
398      * persisted to the database.
399      *
400      * If any session attribute does change, then the attributes and
401      * the accessed time are persisted.
402      *
403      * @param sec
404      */
405     public void setSaveInterval (long sec)
406     {
407         _saveIntervalSec=sec;
408     }
409 
410     public long getSaveInterval ()
411     {
412         return _saveIntervalSec;
413     }
414 
415 
416 
417     /**
418      * A method that can be implemented in subclasses to support
419      * distributed caching of sessions. This method will be
420      * called whenever the session is written to the database
421      * because the session data has changed.
422      *
423      * This could be used eg with a JMS backplane to notify nodes
424      * that the session has changed and to delete the session from
425      * the node's cache, and re-read it from the database.
426      * @param session
427      */
428     public void cacheInvalidate (Session session)
429     {
430 
431     }
432 
433 
434     /**
435      * A session has been requested by its id on this node.
436      *
437      * Load the session by id AND context path from the database.
438      * Multiple contexts may share the same session id (due to dispatching)
439      * but they CANNOT share the same contents.
440      *
441      * Check if last node id is my node id, if so, then the session we have
442      * in memory cannot be stale. If another node used the session last, then
443      * we need to refresh from the db.
444      *
445      * NOTE: this method will go to the database, so if you only want to check
446      * for the existence of a Session in memory, use _sessions.get(id) instead.
447      *
448      * @see org.eclipse.jetty.server.session.AbstractSessionManager#getSession(java.lang.String)
449      */
450     @Override
451     public Session getSession(String idInCluster)
452     {
453         Session session = null;
454         Session memSession = (Session)_sessions.get(idInCluster);
455 
456         synchronized (this)
457         {
458                 //check if we need to reload the session -
459                 //as an optimization, don't reload on every access
460                 //to reduce the load on the database. This introduces a window of
461                 //possibility that the node may decide that the session is local to it,
462                 //when the session has actually been live on another node, and then
463                 //re-migrated to this node. This should be an extremely rare occurrence,
464                 //as load-balancers are generally well-behaved and consistently send
465                 //sessions to the same node, changing only iff that node fails.
466                 //Session data = null;
467                 long now = System.currentTimeMillis();
468                 if (LOG.isDebugEnabled())
469                 {
470                     if (memSession==null)
471                         LOG.debug("getSession("+idInCluster+"): not in session map,"+
472                                 " now="+now+
473                                 " lastSaved="+(memSession==null?0:memSession._lastSaved)+
474                                 " interval="+(_saveIntervalSec * 1000L));
475                     else
476                         LOG.debug("getSession("+idInCluster+"): in session map, "+
477                                 " now="+now+
478                                 " lastSaved="+(memSession==null?0:memSession._lastSaved)+
479                                 " interval="+(_saveIntervalSec * 1000L)+
480                                 " lastNode="+memSession._lastNode+
481                                 " thisNode="+getSessionIdManager().getWorkerName()+
482                                 " difference="+(now - memSession._lastSaved));
483                 }
484 
485                 try
486                 {
487                     if (memSession==null)
488                     {
489                         LOG.debug("getSession("+idInCluster+"): no session in session map. Reloading session data from db.");
490                         session = loadSession(idInCluster, canonicalize(_context.getContextPath()), getVirtualHost(_context));
491                     }
492                     else if ((now - memSession._lastSaved) >= (_saveIntervalSec * 1000L))
493                     {
494                         LOG.debug("getSession("+idInCluster+"): stale session. Reloading session data from db.");
495                         session = loadSession(idInCluster, canonicalize(_context.getContextPath()), getVirtualHost(_context));
496                     }
497                     else
498                     {
499                         LOG.debug("getSession("+idInCluster+"): session in session map");
500                         session = memSession;
501                     }
502                 }
503                 catch (Exception e)
504                 {
505                     LOG.warn("Unable to load session "+idInCluster, e);
506                     return null;
507                 }
508 
509                 
510                 //If we have a session
511                 if (session != null)
512                 {
513                     //If the session was last used on a different node, or session doesn't exist on this node
514                     if (!session.getLastNode().equals(getSessionIdManager().getWorkerName()) || memSession==null)
515                     {
516                         //if session doesn't expire, or has not already expired, update it and put it in this nodes' memory
517                         if (session._expiryTime <= 0 || session._expiryTime > now)
518                         {
519                             if (LOG.isDebugEnabled()) 
520                                 LOG.debug("getSession("+idInCluster+"): lastNode="+session.getLastNode()+" thisNode="+getSessionIdManager().getWorkerName());
521                             
522                             session.setLastNode(getSessionIdManager().getWorkerName());                            
523                             _sessions.put(idInCluster, session);
524                             
525                             //update in db
526                             try
527                             {
528                                 updateSessionNode(session);
529                                 session.didActivate();
530                             }
531                             catch (Exception e)
532                             {
533                                 LOG.warn("Unable to update freshly loaded session "+idInCluster, e);
534                                 return null;
535                             }
536                         }
537                         else
538                         {
539                             LOG.debug("getSession ({}): Session has expired", idInCluster);  
540                             //ensure that the session id for the expired session is deleted so that a new session with the 
541                             //same id cannot be created (because the idInUse() test would succeed)
542                             _jdbcSessionIdMgr.removeSession(idInCluster);
543                             session=null;
544                         }
545 
546                     }
547                     else
548                        LOG.debug("getSession({}): Session not stale {}", idInCluster,session);
549                 }
550                 else
551                 {
552                     //No session in db with matching id and context path.
553                     LOG.debug("getSession({}): No session in database matching id={}",idInCluster,idInCluster);
554                 }
555 
556                 return session;
557         }
558     }
559     
560 
561     /**
562      * Get the number of sessions.
563      *
564      * @see org.eclipse.jetty.server.session.AbstractSessionManager#getSessions()
565      */
566     @Override
567     public int getSessions()
568     {
569         int size = 0;
570         synchronized (this)
571         {
572             size = _sessions.size();
573         }
574         return size;
575     }
576 
577 
578     /**
579      * Start the session manager.
580      *
581      * @see org.eclipse.jetty.server.session.AbstractSessionManager#doStart()
582      */
583     @Override
584     public void doStart() throws Exception
585     {
586         if (_sessionIdManager==null)
587             throw new IllegalStateException("No session id manager defined");
588 
589         _jdbcSessionIdMgr = (JDBCSessionIdManager)_sessionIdManager;
590 
591         _sessions = new ConcurrentHashMap<String, AbstractSession>();
592 
593         super.doStart();
594     }
595 
596 
597     /**
598      * Stop the session manager.
599      *
600      * @see org.eclipse.jetty.server.session.AbstractSessionManager#doStop()
601      */
602     @Override
603     public void doStop() throws Exception
604     {
605         _sessions.clear();
606         _sessions = null;
607 
608         super.doStop();
609     }
610 
611     @Override
612     protected void invalidateSessions()
613     {
614         //Do nothing - we don't want to remove and
615         //invalidate all the sessions because this
616         //method is called from doStop(), and just
617         //because this context is stopping does not
618         //mean that we should remove the session from
619         //any other nodes
620     }
621 
622     
623     /**
624      * 
625      * @see org.eclipse.jetty.server.SessionManager#renewSessionId(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
626      */
627     public void renewSessionId (String oldClusterId, String oldNodeId, String newClusterId, String newNodeId)
628     {
629         Session session = null;
630         synchronized (this)
631         {
632             try
633             {
634                 session = (Session)_sessions.remove(oldClusterId);
635                 if (session != null)
636                 {
637                     session.setClusterId(newClusterId); //update ids
638                     session.setNodeId(newNodeId);
639                     _sessions.put(newClusterId, session); //put it into list in memory
640                     session.save(); //update database
641                 }
642             }
643             catch (Exception e)
644             {
645                 LOG.warn(e);
646             }
647         }
648 
649         super.renewSessionId(oldClusterId, oldNodeId, newClusterId, newNodeId);
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 session
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 }