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
20package org.eclipse.jetty.server.session;
21
22import java.io.ByteArrayInputStream;
23import java.io.ByteArrayOutputStream;
24import java.io.IOException;
25import java.io.InputStream;
26import java.io.ObjectInputStream;
27import java.io.ObjectOutputStream;
28import java.sql.Connection;
29import java.sql.PreparedStatement;
30import java.sql.ResultSet;
31import java.sql.SQLException;
32import java.sql.Statement;
33import java.util.HashMap;
34import java.util.List;
35import java.util.ListIterator;
36import java.util.Map;
37import java.util.Set;
38import java.util.concurrent.ConcurrentHashMap;
39import java.util.concurrent.atomic.AtomicReference;
40
41import javax.servlet.SessionTrackingMode;
42import javax.servlet.http.HttpServletRequest;
43import javax.servlet.http.HttpSessionEvent;
44import javax.servlet.http.HttpSessionListener;
45
46import org.eclipse.jetty.server.SessionIdManager;
47import org.eclipse.jetty.server.handler.ContextHandler;
48import org.eclipse.jetty.util.log.Log;
49import 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 */
75public 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                    {
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
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
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
595    public void doStop() throws Exception
596    {
597        _sessions.clear();
598        _sessions = null;
599
600        super.doStop();
601    }
602
603    @Override
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     */
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
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
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
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
1167    private static String canonicalize (String path)
1168    {
1169        if (path==null)
1170            return "";
1171
1172        return path.replace('/', '_').replace('.','_').replace('\\','_');
1173    }
1174}
1175