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