View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  
20  package org.eclipse.jetty.server.session;
21  
22  import java.io.ByteArrayInputStream;
23  import java.io.ByteArrayOutputStream;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.ObjectInputStream;
27  import java.io.ObjectOutputStream;
28  import java.sql.Connection;
29  import java.sql.PreparedStatement;
30  import java.sql.ResultSet;
31  import java.sql.SQLException;
32  import java.sql.Statement;
33  import java.util.HashMap;
34  import java.util.List;
35  import java.util.ListIterator;
36  import java.util.Map;
37  import java.util.Set;
38  import java.util.concurrent.ConcurrentHashMap;
39  import java.util.concurrent.atomic.AtomicReference;
40  
41  import javax.servlet.SessionTrackingMode;
42  import javax.servlet.http.HttpServletRequest;
43  import javax.servlet.http.HttpSessionEvent;
44  import javax.servlet.http.HttpSessionListener;
45  
46  import org.eclipse.jetty.server.SessionIdManager;
47  import org.eclipse.jetty.server.handler.ContextHandler;
48  import org.eclipse.jetty.util.log.Log;
49  import org.eclipse.jetty.util.log.Logger;
50  
51  /**
52   * JDBCSessionManager
53   *
54   * SessionManager that persists sessions to a database to enable clustering.
55   *
56   * Session data is persisted to the JettySessions table:
57   *
58   * rowId (unique in cluster: webapp name/path + virtualhost + sessionId)
59   * contextPath (of the context owning the session)
60   * sessionId (unique in a context)
61   * lastNode (name of node last handled session)
62   * accessTime (time in milliseconds session was accessed)
63   * lastAccessTime (previous time in milliseconds session was accessed)
64   * createTime (time in milliseconds session created)
65   * cookieTime (time in milliseconds session cookie created)
66   * lastSavedTime (last time in milliseconds session access times were saved)
67   * expiryTime (time in milliseconds that the session is due to expire)
68   * map (attribute map)
69   *
70   * As an optimization, to prevent thrashing the database, we do not persist
71   * the accessTime and lastAccessTime every time the session is accessed. Rather,
72   * we write it out every so often. The frequency is controlled by the saveIntervalSec
73   * field.
74   */
75  public class JDBCSessionManager extends AbstractSessionManager
76  {
77      private static final Logger LOG = Log.getLogger(JDBCSessionManager.class);
78  
79      private ConcurrentHashMap<String, AbstractSession> _sessions;
80      protected JDBCSessionIdManager _jdbcSessionIdMgr = null;
81      protected long _saveIntervalSec = 60; //only persist changes to session access times every 60 secs
82  
83     
84  
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          */
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          */
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         
173         protected synchronized String getRowId()
174         {
175             return _rowId;
176         }
177         
178         protected synchronized void setRowId(String rowId)
179         {
180             _rowId = rowId;
181         }
182         
183         public synchronized void setVirtualHost (String vhost)
184         {
185             _virtualHost=vhost;
186         }
187 
188         public synchronized String getVirtualHost ()
189         {
190             return _virtualHost;
191         }
192         
193         public synchronized long getLastSaved ()
194         {
195             return _lastSaved;
196         }
197 
198         public synchronized void setLastSaved (long time)
199         {
200             _lastSaved=time;
201         }
202 
203         public synchronized void setExpiryTime (long time)
204         {
205             _expiryTime=time;
206         }
207 
208         public synchronized long getExpiryTime ()
209         {
210             return _expiryTime;
211         }
212         
213 
214         public synchronized void setCanonicalContext(String str)
215         {
216             _canonicalContext=str;
217         }
218 
219         public synchronized String getCanonicalContext ()
220         {
221             return _canonicalContext;
222         }
223         
224         public void setCookieSet (long ms)
225         {
226             _cookieSet = ms;
227         }
228 
229         public synchronized long getCookieSet ()
230         {
231             return _cookieSet;
232         }
233 
234         public synchronized void setLastNode (String node)
235         {
236             _lastNode=node;
237         }
238 
239         public synchronized String getLastNode ()
240         {
241             return _lastNode;
242         }
243 
244         @Override
245         public void setAttribute (String name, Object value)
246         {
247             super.setAttribute(name, value);
248             _dirty=true;
249         }
250 
251         @Override
252         public void removeAttribute (String name)
253         {
254             super.removeAttribute(name);
255             _dirty=true;
256         }
257 
258         @Override
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
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
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
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
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     {
354         public ClassLoadingObjectInputStream(java.io.InputStream in) throws IOException
355         {
356             super(in);
357         }
358 
359         public ClassLoadingObjectInputStream () throws IOException
360         {
361             super();
362         }
363 
364         @Override
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      */
397     public void setSaveInterval (long sec)
398     {
399         _saveIntervalSec=sec;
400     }
401 
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      */
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
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                        LOG.debug("getSession({}): Session not stale {}", idInCluster,session);
538                 }
539                 else
540                 {
541                     //No session in db with matching id and context path.
542                     LOG.debug("getSession({}): No session in database matching id={}",idInCluster,idInCluster);
543                 }
544 
545                 return session;
546         }
547     }
548 
549     /**
550      * Get the number of sessions.
551      *
552      * @see org.eclipse.jetty.server.session.AbstractSessionManager#getSessions()
553      */
554     @Override
555     public int getSessions()
556     {
557         int size = 0;
558         synchronized (this)
559         {
560             size = _sessions.size();
561         }
562         return size;
563     }
564 
565 
566     /**
567      * Start the session manager.
568      *
569      * @see org.eclipse.jetty.server.session.AbstractSessionManager#doStart()
570      */
571     @Override
572     public void doStart() throws Exception
573     {
574         if (_sessionIdManager==null)
575             throw new IllegalStateException("No session id manager defined");
576 
577         _jdbcSessionIdMgr = (JDBCSessionIdManager)_sessionIdManager;
578         
579         _sessions = new ConcurrentHashMap<String, AbstractSession>();
580 
581         super.doStart();
582     }
583 
584 
585     /**
586      * Stop the session manager.
587      *
588      * @see org.eclipse.jetty.server.session.AbstractSessionManager#doStop()
589      */
590     @Override
591     public void doStop() throws Exception
592     {
593         _sessions.clear();
594         _sessions = null;
595 
596         super.doStop();
597     }
598 
599     @Override
600     protected void invalidateSessions()
601     {
602         //Do nothing - we don't want to remove and
603         //invalidate all the sessions because this
604         //method is called from doStop(), and just
605         //because this context is stopping does not
606         //mean that we should remove the session from
607         //any other nodes
608     }
609 
610 
611     /**
612      * Invalidate a session.
613      *
614      * @param idInCluster
615      */
616     protected void invalidateSession (String idInCluster)
617     {
618         Session session = null;
619         synchronized (this)
620         {
621             session = (Session)_sessions.get(idInCluster);
622         }
623 
624         if (session != null)
625         {
626             session.invalidate();
627         }
628     }
629 
630     /**
631      * Delete an existing session, both from the in-memory map and
632      * the database.
633      *
634      * @see org.eclipse.jetty.server.session.AbstractSessionManager#removeSession(java.lang.String)
635      */
636     @Override
637     protected boolean removeSession(String idInCluster)
638     {
639         synchronized (this)
640         {
641             Session session = (Session)_sessions.remove(idInCluster);
642             try
643             {
644                 if (session != null)
645                     deleteSession(session);
646             }
647             catch (Exception e)
648             {
649                 LOG.warn("Problem deleting session id="+idInCluster, e);
650             }
651             return session!=null;
652         }
653     }
654 
655 
656     /**
657      * Add a newly created session to our in-memory list for this node and persist it.
658      *
659      * @see org.eclipse.jetty.server.session.AbstractSessionManager#addSession(org.eclipse.jetty.server.session.AbstractSession)
660      */
661     @Override
662     protected void addSession(AbstractSession session)
663     {
664         if (session==null)
665             return;
666 
667         synchronized (this)
668         {
669             _sessions.put(session.getClusterId(), session);
670         }
671 
672         //TODO or delay the store until exit out of session? If we crash before we store it
673         //then session data will be lost.
674         try
675         {
676             synchronized (session)
677             {
678                 session.willPassivate();
679                 storeSession(((JDBCSessionManager.Session)session));
680                 session.didActivate();
681             }
682         }
683         catch (Exception e)
684         {
685             LOG.warn("Unable to store new session id="+session.getId() , e);
686         }
687     }
688 
689 
690     /**
691      * Make a new Session.
692      *
693      * @see org.eclipse.jetty.server.session.AbstractSessionManager#newSession(javax.servlet.http.HttpServletRequest)
694      */
695     @Override
696     protected AbstractSession newSession(HttpServletRequest request)
697     {
698         return new Session(request);
699     }
700 
701     /* ------------------------------------------------------------ */
702     /** Remove session from manager
703      * @param session The session to remove
704      * @param invalidate True if {@link HttpSessionListener#sessionDestroyed(HttpSessionEvent)} and
705      * {@link SessionIdManager#invalidateAll(String)} should be called.
706      */
707     @Override
708     public void removeSession(AbstractSession session, boolean invalidate)
709     {
710         // Remove session from context and global maps
711         boolean removed = false;
712 
713         synchronized (this)
714         {
715             //take this session out of the map of sessions for this context
716             if (getSession(session.getClusterId()) != null)
717             {
718                 removed = true;
719                 removeSession(session.getClusterId());
720             }
721         }
722 
723         if (removed)
724         {
725             // Remove session from all context and global id maps
726             _sessionIdManager.removeSession(session);
727 
728             if (invalidate)
729                 _sessionIdManager.invalidateAll(session.getClusterId());
730 
731             if (invalidate && !_sessionListeners.isEmpty())
732             {
733                 HttpSessionEvent event=new HttpSessionEvent(session);
734                 for (HttpSessionListener l : _sessionListeners)
735                     l.sessionDestroyed(event);
736             }
737             if (!invalidate)
738             {
739                 session.willPassivate();
740             }
741         }
742     }
743 
744 
745     /**
746      * Expire any Sessions we have in memory matching the list of
747      * expired Session ids.
748      *
749      * @param sessionIds
750      */
751     protected void expire (List<?> sessionIds)
752     {
753         //don't attempt to scavenge if we are shutting down
754         if (isStopping() || isStopped())
755             return;
756 
757         //Remove any sessions we already have in memory that match the ids
758         Thread thread=Thread.currentThread();
759         ClassLoader old_loader=thread.getContextClassLoader();
760         ListIterator<?> itor = sessionIds.listIterator();
761 
762         try
763         {
764             while (itor.hasNext())
765             {
766                 String sessionId = (String)itor.next();
767                 if (LOG.isDebugEnabled())
768                     LOG.debug("Expiring session id "+sessionId);
769 
770                 Session session = (Session)_sessions.get(sessionId);
771                 if (session != null)
772                 {
773                     session.timeout();
774                     itor.remove();
775                 }
776                 else
777                 {
778                     if (LOG.isDebugEnabled())
779                         LOG.debug("Unrecognized session id="+sessionId);
780                 }
781             }
782         }
783         catch (Throwable t)
784         {
785             LOG.warn("Problem expiring sessions", t);
786         }
787         finally
788         {
789             thread.setContextClassLoader(old_loader);
790         }
791     }
792 
793 
794     /**
795      * Load a session from the database
796      * @param id
797      * @return the session data that was loaded
798      * @throws Exception
799      */
800     protected Session loadSession (final String id, final String canonicalContextPath, final String vhost)
801     throws Exception
802     {
803         final AtomicReference<Session> _reference = new AtomicReference<Session>();
804         final AtomicReference<Exception> _exception = new AtomicReference<Exception>();
805         Runnable load = new Runnable()
806         {
807             @SuppressWarnings("unchecked")
808             public void run()
809             {
810                 Session session = null;
811                 Connection connection=null;
812                 PreparedStatement statement = null;
813                 try
814                 {
815                     connection = getConnection();
816                     statement = _jdbcSessionIdMgr._dbAdaptor.getLoadStatement(connection, id, canonicalContextPath, vhost);
817                     ResultSet result = statement.executeQuery();
818                     if (result.next())
819                     {                    
820                         session = new Session(id, result.getString(_jdbcSessionIdMgr._sessionTableRowId), result.getLong("createTime"), result.getLong("accessTime"));
821                         session.setCookieSet(result.getLong("cookieTime"));
822                         session.setLastAccessedTime(result.getLong("lastAccessTime"));
823                         session.setLastNode(result.getString("lastNode"));
824                         session.setLastSaved(result.getLong("lastSavedTime"));
825                         session.setExpiryTime(result.getLong("expiryTime"));
826                         session.setCanonicalContext(result.getString("contextPath"));
827                         session.setVirtualHost(result.getString("virtualHost"));
828                                            
829                         InputStream is = ((JDBCSessionIdManager)getSessionIdManager())._dbAdaptor.getBlobInputStream(result, "map");
830                         ClassLoadingObjectInputStream ois = new ClassLoadingObjectInputStream (is);
831                         Object o = ois.readObject();
832                         session.addAttributes((Map<String,Object>)o);
833                         ois.close();
834 
835                         if (LOG.isDebugEnabled())
836                             LOG.debug("LOADED session "+session);
837                     }
838                     _reference.set(session);
839                 }
840                 catch (Exception e)
841                 {
842                     _exception.set(e);
843                 }
844                 finally
845                 {
846                     if (statement!=null)
847                     {
848                         try { statement.close(); }
849                         catch(Exception e) { LOG.warn(e); }
850                     }
851 
852                     if (connection!=null)
853                     {
854                         try { connection.close();}
855                         catch(Exception e) { LOG.warn(e); }
856                     }
857                 }
858             }
859         };
860 
861         if (_context==null)
862             load.run();
863         else
864             _context.getContextHandler().handle(load);
865 
866         if (_exception.get()!=null)
867         {
868             //if the session could not be restored, take its id out of the pool of currently-in-use
869             //session ids
870             _jdbcSessionIdMgr.removeSession(id);
871             throw _exception.get();
872         }
873 
874         return _reference.get();
875     }
876 
877     /**
878      * Insert a session into the database.
879      *
880      * @param data
881      * @throws Exception
882      */
883     protected void storeSession (Session session)
884     throws Exception
885     {
886         if (session==null)
887             return;
888 
889         //put into the database
890         Connection connection = getConnection();
891         PreparedStatement statement = null;
892         try
893         {
894             String rowId = calculateRowId(session);
895 
896             long now = System.currentTimeMillis();
897             connection.setAutoCommit(true);
898             statement = connection.prepareStatement(_jdbcSessionIdMgr._insertSession);
899             statement.setString(1, rowId); //rowId
900             statement.setString(2, session.getId()); //session id
901             statement.setString(3, session.getCanonicalContext()); //context path
902             statement.setString(4, session.getVirtualHost()); //first vhost
903             statement.setString(5, getSessionIdManager().getWorkerName());//my node id
904             statement.setLong(6, session.getAccessed());//accessTime
905             statement.setLong(7, session.getLastAccessedTime()); //lastAccessTime
906             statement.setLong(8, session.getCreationTime()); //time created
907             statement.setLong(9, session.getCookieSet());//time cookie was set
908             statement.setLong(10, now); //last saved time
909             statement.setLong(11, session.getExpiryTime());
910 
911             ByteArrayOutputStream baos = new ByteArrayOutputStream();
912             ObjectOutputStream oos = new ObjectOutputStream(baos);
913             oos.writeObject(session.getAttributeMap());
914             byte[] bytes = baos.toByteArray();
915 
916             ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
917             statement.setBinaryStream(12, bais, bytes.length);//attribute map as blob
918 
919             statement.executeUpdate();
920             session.setRowId(rowId); //set it on the in-memory data as well as in db
921             session.setLastSaved(now);
922 
923 
924             if (LOG.isDebugEnabled())
925                 LOG.debug("Stored session "+session);
926         }
927         finally
928         {
929             if (statement!=null)
930             {
931                 try { statement.close(); }
932                 catch(Exception e) { LOG.warn(e); }
933             }
934 
935             if (connection!=null)
936                 connection.close();
937         }
938     }
939 
940 
941     /**
942      * Update data on an existing persisted session.
943      *
944      * @param data the session
945      * @throws Exception
946      */
947     protected void updateSession (Session data)
948     throws Exception
949     {
950         if (data==null)
951             return;
952 
953         Connection connection = getConnection();
954         PreparedStatement statement = null;
955         try
956         {
957             long now = System.currentTimeMillis();
958             connection.setAutoCommit(true);
959             statement = connection.prepareStatement(_jdbcSessionIdMgr._updateSession);
960             statement.setString(1, getSessionIdManager().getWorkerName());//my node id
961             statement.setLong(2, data.getAccessed());//accessTime
962             statement.setLong(3, data.getLastAccessedTime()); //lastAccessTime
963             statement.setLong(4, now); //last saved time
964             statement.setLong(5, data.getExpiryTime());
965 
966             ByteArrayOutputStream baos = new ByteArrayOutputStream();
967             ObjectOutputStream oos = new ObjectOutputStream(baos);
968             oos.writeObject(data.getAttributeMap());
969             byte[] bytes = baos.toByteArray();
970             ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
971 
972             statement.setBinaryStream(6, bais, bytes.length);//attribute map as blob
973             statement.setString(7, data.getRowId()); //rowId
974             statement.executeUpdate();
975 
976             data.setLastSaved(now);
977             if (LOG.isDebugEnabled())
978                 LOG.debug("Updated session "+data);
979         }
980         finally
981         {
982             if (statement!=null)
983             {
984                 try { statement.close(); }
985                 catch(Exception e) { LOG.warn(e); }
986             }
987 
988             if (connection!=null)
989                 connection.close();
990         }
991     }
992 
993 
994     /**
995      * Update the node on which the session was last seen to be my node.
996      *
997      * @param data the session
998      * @throws Exception
999      */
1000     protected void updateSessionNode (Session data)
1001     throws Exception
1002     {
1003         String nodeId = getSessionIdManager().getWorkerName();
1004         Connection connection = getConnection();
1005         PreparedStatement statement = null;
1006         try
1007         {
1008             connection.setAutoCommit(true);
1009             statement = connection.prepareStatement(_jdbcSessionIdMgr._updateSessionNode);
1010             statement.setString(1, nodeId);
1011             statement.setString(2, data.getRowId());
1012             statement.executeUpdate();
1013             statement.close();
1014             if (LOG.isDebugEnabled())
1015                 LOG.debug("Updated last node for session id="+data.getId()+", lastNode = "+nodeId);
1016         }
1017         finally
1018         {
1019             if (statement!=null)
1020             {
1021                 try { statement.close(); }
1022                 catch(Exception e) { LOG.warn(e); }
1023             }
1024 
1025             if (connection!=null)
1026                 connection.close();
1027         }
1028     }
1029 
1030     /**
1031      * Persist the time the session was last accessed.
1032      *
1033      * @param data the session
1034      * @throws Exception
1035      */
1036     private void updateSessionAccessTime (Session data)
1037     throws Exception
1038     {
1039         Connection connection = getConnection();
1040         PreparedStatement statement = null;
1041         try
1042         {
1043             long now = System.currentTimeMillis();
1044             connection.setAutoCommit(true);
1045             statement = connection.prepareStatement(_jdbcSessionIdMgr._updateSessionAccessTime);
1046             statement.setString(1, getSessionIdManager().getWorkerName());
1047             statement.setLong(2, data.getAccessed());
1048             statement.setLong(3, data.getLastAccessedTime());
1049             statement.setLong(4, now);
1050             statement.setLong(5, data.getExpiryTime());
1051             statement.setString(6, data.getRowId());
1052             statement.executeUpdate();
1053             data.setLastSaved(now);
1054             statement.close();
1055             if (LOG.isDebugEnabled())
1056                 LOG.debug("Updated access time session id="+data.getId());
1057         }
1058         finally
1059         {
1060             if (statement!=null)
1061             {
1062                 try { statement.close(); }
1063                 catch(Exception e) { LOG.warn(e); }
1064             }
1065 
1066             if (connection!=null)
1067                 connection.close();
1068         }
1069     }
1070 
1071 
1072 
1073 
1074     /**
1075      * Delete a session from the database. Should only be called
1076      * when the session has been invalidated.
1077      *
1078      * @param data
1079      * @throws Exception
1080      */
1081     protected void deleteSession (Session data)
1082     throws Exception
1083     {
1084         Connection connection = getConnection();
1085         PreparedStatement statement = null;
1086         try
1087         {
1088             connection.setAutoCommit(true);
1089             statement = connection.prepareStatement(_jdbcSessionIdMgr._deleteSession);
1090             statement.setString(1, data.getRowId());
1091             statement.executeUpdate();
1092             if (LOG.isDebugEnabled())
1093                 LOG.debug("Deleted Session "+data);
1094         }
1095         finally
1096         {
1097             if (statement!=null)
1098             {
1099                 try { statement.close(); }
1100                 catch(Exception e) { LOG.warn(e); }
1101             }
1102 
1103             if (connection!=null)
1104                 connection.close();
1105         }
1106     }
1107 
1108 
1109 
1110     /**
1111      * Get a connection from the driver.
1112      * @return
1113      * @throws SQLException
1114      */
1115     private Connection getConnection ()
1116     throws SQLException
1117     {
1118         return ((JDBCSessionIdManager)getSessionIdManager()).getConnection();
1119     }
1120 
1121     /**
1122      * Calculate a unique id for this session across the cluster.
1123      *
1124      * Unique id is composed of: contextpath_virtualhost0_sessionid
1125      * @param data
1126      * @return
1127      */
1128     private String calculateRowId (Session data)
1129     {
1130         String rowId = canonicalize(_context.getContextPath());
1131         rowId = rowId + "_" + getVirtualHost(_context);
1132         rowId = rowId+"_"+data.getId();
1133         return rowId;
1134     }
1135 
1136     /**
1137      * Get the first virtual host for the context.
1138      *
1139      * Used to help identify the exact session/contextPath.
1140      *
1141      * @return 0.0.0.0 if no virtual host is defined
1142      */
1143     private static String getVirtualHost (ContextHandler.Context context)
1144     {
1145         String vhost = "0.0.0.0";
1146 
1147         if (context==null)
1148             return vhost;
1149 
1150         String [] vhosts = context.getContextHandler().getVirtualHosts();
1151         if (vhosts==null || vhosts.length==0 || vhosts[0]==null)
1152             return vhost;
1153 
1154         return vhosts[0];
1155     }
1156 
1157     /**
1158      * Make an acceptable file name from a context path.
1159      *
1160      * @param path
1161      * @return
1162      */
1163     private static String canonicalize (String path)
1164     {
1165         if (path==null)
1166             return "";
1167 
1168         return path.replace('/', '_').replace('.','_').replace('\\','_');
1169     }
1170 }