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