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