View Javadoc

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