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);
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         super.renewSessionId(oldClusterId, oldNodeId, newClusterId, newNodeId);
652     }
653 
654     
655 
656     /**
657      * Invalidate a session.
658      *
659      * @param idInCluster
660      */
661     protected void invalidateSession (String idInCluster)
662     {
663         Session session = null;
664         synchronized (this)
665         {
666             session = (Session)_sessions.get(idInCluster);
667         }
668 
669         if (session != null)
670         {
671             session.invalidate();
672         }
673     }
674 
675     /**
676      * Delete an existing session, both from the in-memory map and
677      * the database.
678      *
679      * @see org.eclipse.jetty.server.session.AbstractSessionManager#removeSession(java.lang.String)
680      */
681     @Override
682     protected boolean removeSession(String idInCluster)
683     {
684         synchronized (this)
685         {
686             Session session = (Session)_sessions.remove(idInCluster);
687             try
688             {
689                 if (session != null)
690                     deleteSession(session);
691             }
692             catch (Exception e)
693             {
694                 LOG.warn("Problem deleting session id="+idInCluster, e);
695             }
696             return session!=null;
697         }
698     }
699 
700 
701     /**
702      * Add a newly created session to our in-memory list for this node and persist it.
703      *
704      * @see org.eclipse.jetty.server.session.AbstractSessionManager#addSession(org.eclipse.jetty.server.session.AbstractSession)
705      */
706     @Override
707     protected void addSession(AbstractSession session)
708     {
709         if (session==null)
710             return;
711 
712         synchronized (this)
713         {
714             _sessions.put(session.getClusterId(), session);
715         }
716 
717         //TODO or delay the store until exit out of session? If we crash before we store it
718         //then session data will be lost.
719         try
720         {
721             synchronized (session)
722             {
723                 session.willPassivate();
724                 storeSession(((JDBCSessionManager.Session)session));
725                 session.didActivate();
726             }
727         }
728         catch (Exception e)
729         {
730             LOG.warn("Unable to store new session id="+session.getId() , e);
731         }
732     }
733 
734 
735     /**
736      * Make a new Session.
737      *
738      * @see org.eclipse.jetty.server.session.AbstractSessionManager#newSession(javax.servlet.http.HttpServletRequest)
739      */
740     @Override
741     protected AbstractSession newSession(HttpServletRequest request)
742     {
743         return new Session(request);
744     }
745 
746     /* ------------------------------------------------------------ */
747     /** Remove session from manager
748      * @param session The session to remove
749      * @param invalidate True if {@link HttpSessionListener#sessionDestroyed(HttpSessionEvent)} and
750      * {@link SessionIdManager#invalidateAll(String)} should be called.
751      */
752     @Override
753     public void removeSession(AbstractSession session, boolean invalidate)
754     {
755         // Remove session from context and global maps
756         boolean removed = false;
757 
758         synchronized (this)
759         {
760             //take this session out of the map of sessions for this context         
761             if (_sessions.containsKey(session.getClusterId()))
762             {
763                 removed = true;
764                 removeSession(session.getClusterId());
765             }
766         }
767 
768         if (removed)
769         {
770             // Remove session from all context and global id maps
771             _sessionIdManager.removeSession(session);
772 
773             if (invalidate)
774                 _sessionIdManager.invalidateAll(session.getClusterId());
775 
776             if (invalidate && !_sessionListeners.isEmpty())
777             {
778                 HttpSessionEvent event=new HttpSessionEvent(session);
779                 for (HttpSessionListener l : _sessionListeners)
780                     l.sessionDestroyed(event);
781             }
782             if (!invalidate)
783             {
784                 session.willPassivate();
785             }
786         }
787     }
788 
789 
790     /**
791      * Expire any Sessions we have in memory matching the list of
792      * expired Session ids.
793      *
794      * @param sessionIds
795      */
796     protected Set<String> expire (Set<String> sessionIds)
797     {
798         //don't attempt to scavenge if we are shutting down
799         if (isStopping() || isStopped())
800             return null;
801 
802         
803         Thread thread=Thread.currentThread();
804         ClassLoader old_loader=thread.getContextClassLoader();
805         
806         Set<String> successfullyExpiredIds = new HashSet<String>();
807         try
808         {
809             Iterator<?> itor = sessionIds.iterator();
810             while (itor.hasNext())
811             {
812                 String sessionId = (String)itor.next();
813                 if (LOG.isDebugEnabled())
814                     LOG.debug("Expiring session id "+sessionId);
815 
816                 Session session = (Session)_sessions.get(sessionId);
817 
818                 //if session is not in our memory, then fetch from db so we can call the usual listeners on it
819                 if (session == null)
820                 {
821                     if (LOG.isDebugEnabled())LOG.debug("Force loading session id "+sessionId);
822                     session = loadSession(sessionId, canonicalize(_context.getContextPath()), getVirtualHost(_context));
823                     if (session != null)
824                     {
825                         //loaded an expired session last managed on this node for this context, add it to the list so we can 
826                         //treat it like a normal expired session
827                         synchronized (this)
828                         {
829                             _sessions.put(session.getClusterId(), session);
830                         }
831                     }
832                     else
833                     {
834                         if (LOG.isDebugEnabled())
835                             LOG.debug("Unrecognized session id="+sessionId);
836                         continue;
837                     }
838                 }
839                
840                 if (session != null)
841                 {
842                     session.timeout();
843                     successfullyExpiredIds.add(session.getClusterId());
844                 }
845             }
846             return successfullyExpiredIds;
847         }
848         catch (Throwable t)
849         {
850             LOG.warn("Problem expiring sessions", t);
851             return successfullyExpiredIds;
852         }
853         finally
854         {
855             thread.setContextClassLoader(old_loader);
856         }
857     }
858     
859   
860     /**
861      * Load a session from the database
862      * @param id
863      * @return the session data that was loaded
864      * @throws Exception
865      */
866     protected Session loadSession (final String id, final String canonicalContextPath, final String vhost)
867     throws Exception
868     {
869         final AtomicReference<Session> _reference = new AtomicReference<Session>();
870         final AtomicReference<Exception> _exception = new AtomicReference<Exception>();
871         Runnable load = new Runnable()
872         {
873             /** 
874              * @see java.lang.Runnable#run()
875              */
876             @SuppressWarnings("unchecked")
877             public void run()
878             {
879                 try (Connection connection = getConnection();
880                         PreparedStatement statement = _jdbcSessionIdMgr._dbAdaptor.getLoadStatement(connection, id, canonicalContextPath, vhost);
881                         ResultSet result = statement.executeQuery())
882                 {
883                     Session session = null;
884                     if (result.next())
885                     {                    
886                         long maxInterval = result.getLong("maxInterval");
887                         if (maxInterval == JDBCSessionIdManager.MAX_INTERVAL_NOT_SET)
888                         {
889                             maxInterval = getMaxInactiveInterval(); //if value not saved for maxInactiveInterval, use current value from sessionmanager
890                         }
891                         session = new Session(id, result.getString(_jdbcSessionIdMgr._sessionTableRowId), 
892                                                   result.getLong("createTime"), 
893                                                   result.getLong("accessTime"), 
894                                                   maxInterval);
895                         session.setCookieSet(result.getLong("cookieTime"));
896                         session.setLastAccessedTime(result.getLong("lastAccessTime"));
897                         session.setLastNode(result.getString("lastNode"));
898                         session.setLastSaved(result.getLong("lastSavedTime"));
899                         session.setExpiryTime(result.getLong("expiryTime"));
900                         session.setCanonicalContext(result.getString("contextPath"));
901                         session.setVirtualHost(result.getString("virtualHost"));
902                                            
903                         try (InputStream is = ((JDBCSessionIdManager)getSessionIdManager())._dbAdaptor.getBlobInputStream(result, "map");
904                                 ClassLoadingObjectInputStream ois = new ClassLoadingObjectInputStream(is))
905                         {
906                             Object o = ois.readObject();
907                             session.addAttributes((Map<String,Object>)o);
908                         }
909 
910                         if (LOG.isDebugEnabled())
911                             LOG.debug("LOADED session "+session);
912                     }
913                     else
914                         if (LOG.isDebugEnabled())
915                             LOG.debug("Failed to load session "+id);
916                     _reference.set(session);
917                 }
918                 catch (Exception e)
919                 {
920                     _exception.set(e);
921                 }
922             }
923         };
924 
925         if (_context==null)
926             load.run();
927         else
928             _context.getContextHandler().handle(load);
929 
930         if (_exception.get()!=null)
931         {
932             //if the session could not be restored, take its id out of the pool of currently-in-use
933             //session ids
934             _jdbcSessionIdMgr.removeSession(id);
935             throw _exception.get();
936         }
937 
938         return _reference.get();
939     }
940 
941     /**
942      * Insert a session into the database.
943      *
944      * @param session
945      * @throws Exception
946      */
947     protected void storeSession (Session session)
948     throws Exception
949     {
950         if (session==null)
951             return;
952 
953         //put into the database
954         try (Connection connection = getConnection();
955                 PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._insertSession))
956         {
957             String rowId = calculateRowId(session);
958 
959             long now = System.currentTimeMillis();
960             connection.setAutoCommit(true);
961             statement.setString(1, rowId); //rowId
962             statement.setString(2, session.getId()); //session id
963             statement.setString(3, session.getCanonicalContext()); //context path
964             statement.setString(4, session.getVirtualHost()); //first vhost
965             statement.setString(5, getSessionIdManager().getWorkerName());//my node id
966             statement.setLong(6, session.getAccessed());//accessTime
967             statement.setLong(7, session.getLastAccessedTime()); //lastAccessTime
968             statement.setLong(8, session.getCreationTime()); //time created
969             statement.setLong(9, session.getCookieSet());//time cookie was set
970             statement.setLong(10, now); //last saved time
971             statement.setLong(11, session.getExpiryTime());
972             statement.setLong(12, session.getMaxInactiveInterval());
973 
974             ByteArrayOutputStream baos = new ByteArrayOutputStream();
975             ObjectOutputStream oos = new ObjectOutputStream(baos);
976             oos.writeObject(session.getAttributeMap());
977             oos.flush();
978             byte[] bytes = baos.toByteArray();
979 
980             ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
981             statement.setBinaryStream(13, bais, bytes.length);//attribute map as blob
982            
983 
984             statement.executeUpdate();
985             session.setRowId(rowId); //set it on the in-memory data as well as in db
986             session.setLastSaved(now);
987         }
988         if (LOG.isDebugEnabled())
989             LOG.debug("Stored session "+session);
990     }
991 
992 
993     /**
994      * Update data on an existing persisted session.
995      *
996      * @param data the session
997      * @throws Exception
998      */
999     protected void updateSession (Session data)
1000     throws Exception
1001     {
1002         if (data==null)
1003             return;
1004 
1005         try (Connection connection = getConnection();
1006                 PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._updateSession))
1007         {
1008             long now = System.currentTimeMillis();
1009             connection.setAutoCommit(true);
1010             statement.setString(1, data.getId());
1011             statement.setString(2, getSessionIdManager().getWorkerName());//my node id
1012             statement.setLong(3, data.getAccessed());//accessTime
1013             statement.setLong(4, data.getLastAccessedTime()); //lastAccessTime
1014             statement.setLong(5, now); //last saved time
1015             statement.setLong(6, data.getExpiryTime());
1016             statement.setLong(7, data.getMaxInactiveInterval());
1017 
1018             ByteArrayOutputStream baos = new ByteArrayOutputStream();
1019             ObjectOutputStream oos = new ObjectOutputStream(baos);
1020             oos.writeObject(data.getAttributeMap());
1021             oos.flush();
1022             byte[] bytes = baos.toByteArray();
1023             ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
1024 
1025             statement.setBinaryStream(8, bais, bytes.length);//attribute map as blob
1026             statement.setString(9, data.getRowId()); //rowId
1027             statement.executeUpdate();
1028 
1029             data.setLastSaved(now);
1030         }
1031         if (LOG.isDebugEnabled())
1032             LOG.debug("Updated session "+data);
1033     }
1034 
1035 
1036     /**
1037      * Update the node on which the session was last seen to be my node.
1038      *
1039      * @param data the session
1040      * @throws Exception
1041      */
1042     protected void updateSessionNode (Session data)
1043     throws Exception
1044     {
1045         String nodeId = getSessionIdManager().getWorkerName();
1046         try (Connection connection = getConnection();
1047                 PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._updateSessionNode))
1048         {
1049             connection.setAutoCommit(true);
1050             statement.setString(1, nodeId);
1051             statement.setString(2, data.getRowId());
1052             statement.executeUpdate();
1053         }
1054         if (LOG.isDebugEnabled())
1055             LOG.debug("Updated last node for session id="+data.getId()+", lastNode = "+nodeId);
1056     }
1057 
1058     /**
1059      * Persist the time the session was last accessed.
1060      *
1061      * @param data the session
1062      * @throws Exception
1063      */
1064     private void updateSessionAccessTime (Session data)
1065     throws Exception
1066     {
1067         try (Connection connection = getConnection();
1068                 PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._updateSessionAccessTime))
1069         {
1070             long now = System.currentTimeMillis();
1071             connection.setAutoCommit(true);
1072             statement.setString(1, getSessionIdManager().getWorkerName());
1073             statement.setLong(2, data.getAccessed());
1074             statement.setLong(3, data.getLastAccessedTime());
1075             statement.setLong(4, now);
1076             statement.setLong(5, data.getExpiryTime());
1077             statement.setLong(6, data.getMaxInactiveInterval());
1078             statement.setString(7, data.getRowId());
1079           
1080             statement.executeUpdate();
1081             data.setLastSaved(now);
1082         }
1083         if (LOG.isDebugEnabled())
1084             LOG.debug("Updated access time session id="+data.getId());
1085     }
1086 
1087 
1088 
1089 
1090     /**
1091      * Delete a session from the database. Should only be called
1092      * when the session has been invalidated.
1093      *
1094      * @param data
1095      * @throws Exception
1096      */
1097     protected void deleteSession (Session data)
1098     throws Exception
1099     {
1100         try (Connection connection = getConnection();
1101                 PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._deleteSession))
1102         {
1103             connection.setAutoCommit(true);
1104             statement.setString(1, data.getRowId());
1105             statement.executeUpdate();
1106             if (LOG.isDebugEnabled())
1107                 LOG.debug("Deleted Session "+data);
1108         }
1109     }
1110 
1111 
1112 
1113     /**
1114      * Get a connection from the driver.
1115      * @return
1116      * @throws SQLException
1117      */
1118     private Connection getConnection ()
1119     throws SQLException
1120     {
1121         return ((JDBCSessionIdManager)getSessionIdManager()).getConnection();
1122     }
1123 
1124     /**
1125      * Calculate a unique id for this session across the cluster.
1126      *
1127      * Unique id is composed of: contextpath_virtualhost0_sessionid
1128      * @param data
1129      * @return
1130      */
1131     private String calculateRowId (Session data)
1132     {
1133         String rowId = canonicalize(_context.getContextPath());
1134         rowId = rowId + "_" + getVirtualHost(_context);
1135         rowId = rowId+"_"+data.getId();
1136         return rowId;
1137     }
1138 
1139     /**
1140      * Get the first virtual host for the context.
1141      *
1142      * Used to help identify the exact session/contextPath.
1143      *
1144      * @return 0.0.0.0 if no virtual host is defined
1145      */
1146     private static String getVirtualHost (ContextHandler.Context context)
1147     {
1148         String vhost = "0.0.0.0";
1149 
1150         if (context==null)
1151             return vhost;
1152 
1153         String [] vhosts = context.getContextHandler().getVirtualHosts();
1154         if (vhosts==null || vhosts.length==0 || vhosts[0]==null)
1155             return vhost;
1156 
1157         return vhosts[0];
1158     }
1159 
1160     /**
1161      * Make an acceptable file name from a context path.
1162      *
1163      * @param path
1164      * @return
1165      */
1166     private static String canonicalize (String path)
1167     {
1168         if (path==null)
1169             return "";
1170 
1171         return path.replace('/', '_').replace('.','_').replace('\\','_');
1172     }
1173 }