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