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