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