View Javadoc

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