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