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