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