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