View Javadoc

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