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