View Javadoc

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