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