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  package org.eclipse.jetty.server.session;
20  
21  import java.io.ByteArrayInputStream;
22  import java.io.InputStream;
23  import java.sql.Blob;
24  import java.sql.Connection;
25  import java.sql.DatabaseMetaData;
26  import java.sql.Driver;
27  import java.sql.DriverManager;
28  import java.sql.PreparedStatement;
29  import java.sql.ResultSet;
30  import java.sql.SQLException;
31  import java.sql.Statement;
32  import java.util.ArrayList;
33  import java.util.Collection;
34  import java.util.HashSet;
35  import java.util.Iterator;
36  import java.util.List;
37  import java.util.Locale;
38  import java.util.Random;
39  import java.util.Timer;
40  import java.util.TimerTask;
41  
42  import javax.naming.InitialContext;
43  import javax.servlet.http.HttpServletRequest;
44  import javax.servlet.http.HttpSession;
45  import javax.sql.DataSource;
46  
47  import org.eclipse.jetty.server.Handler;
48  import org.eclipse.jetty.server.Server;
49  import org.eclipse.jetty.server.SessionManager;
50  import org.eclipse.jetty.server.handler.ContextHandler;
51  import org.eclipse.jetty.util.log.Logger;
52  
53  
54  
55  /**
56   * JDBCSessionIdManager
57   *
58   * SessionIdManager implementation that uses a database to store in-use session ids,
59   * to support distributed sessions.
60   *
61   */
62  public class JDBCSessionIdManager extends AbstractSessionIdManager
63  {
64      final static Logger LOG = SessionHandler.LOG;
65  
66      protected final HashSet<String> _sessionIds = new HashSet<String>();
67      protected Server _server;
68      protected Driver _driver;
69      protected String _driverClassName;
70      protected String _connectionUrl;
71      protected DataSource _datasource;
72      protected String _jndiName;
73      protected String _sessionIdTable = "JettySessionIds";
74      protected String _sessionTable = "JettySessions";
75      protected String _sessionTableRowId = "rowId";
76  
77      protected Timer _timer; //scavenge timer
78      protected TimerTask _task; //scavenge task
79      protected long _lastScavengeTime;
80      protected long _scavengeIntervalMs = 1000L * 60 * 10; //10mins
81      protected String _blobType; //if not set, is deduced from the type of the database at runtime
82      protected String _longType; //if not set, is deduced from the type of the database at runtime
83  
84      protected String _createSessionIdTable;
85      protected String _createSessionTable;
86  
87      protected String _selectBoundedExpiredSessions;
88      protected String _deleteOldExpiredSessions;
89  
90      protected String _insertId;
91      protected String _deleteId;
92      protected String _queryId;
93  
94      protected  String _insertSession;
95      protected  String _deleteSession;
96      protected  String _updateSession;
97      protected  String _updateSessionNode;
98      protected  String _updateSessionAccessTime;
99  
100     protected DatabaseAdaptor _dbAdaptor;
101 
102     private String _selectExpiredSessions;
103 
104 
105     /**
106      * DatabaseAdaptor
107      *
108      * Handles differences between databases.
109      *
110      * Postgres uses the getBytes and setBinaryStream methods to access
111      * a "bytea" datatype, which can be up to 1Gb of binary data. MySQL
112      * is happy to use the "blob" type and getBlob() methods instead.
113      *
114      * TODO if the differences become more major it would be worthwhile
115      * refactoring this class.
116      */
117     public class DatabaseAdaptor
118     {
119         String _dbName;
120         boolean _isLower;
121         boolean _isUpper;
122        
123 
124 
125         public DatabaseAdaptor (DatabaseMetaData dbMeta)
126         throws SQLException
127         {
128             _dbName = dbMeta.getDatabaseProductName().toLowerCase(Locale.ENGLISH);
129             LOG.debug ("Using database {}",_dbName);
130             _isLower = dbMeta.storesLowerCaseIdentifiers();
131             _isUpper = dbMeta.storesUpperCaseIdentifiers();            
132         }
133 
134         /**
135          * Convert a camel case identifier into either upper or lower
136          * depending on the way the db stores identifiers.
137          *
138          * @param identifier
139          * @return the converted identifier
140          */
141         public String convertIdentifier (String identifier)
142         {
143             if (_isLower)
144                 return identifier.toLowerCase(Locale.ENGLISH);
145             if (_isUpper)
146                 return identifier.toUpperCase(Locale.ENGLISH);
147 
148             return identifier;
149         }
150 
151         public String getDBName ()
152         {
153             return _dbName;
154         }
155 
156         public String getBlobType ()
157         {
158             if (_blobType != null)
159                 return _blobType;
160 
161             if (_dbName.startsWith("postgres"))
162                 return "bytea";
163 
164             return "blob";
165         }
166 
167         public String getLongType ()
168         {
169             if (_longType != null)
170                 return _longType;
171 
172             if (_dbName.startsWith("oracle"))
173                 return "number(20)";
174 
175             return "bigint";
176         }
177 
178         public InputStream getBlobInputStream (ResultSet result, String columnName)
179         throws SQLException
180         {
181             if (_dbName.startsWith("postgres"))
182             {
183                 byte[] bytes = result.getBytes(columnName);
184                 return new ByteArrayInputStream(bytes);
185             }
186 
187             Blob blob = result.getBlob(columnName);
188             return blob.getBinaryStream();
189         }
190 
191         /**
192          * rowId is a reserved word for Oracle, so change the name of this column
193          * @return
194          */
195         public String getRowIdColumnName ()
196         {
197             if (_dbName != null && _dbName.startsWith("oracle"))
198                 return "srowId";
199 
200             return "rowId";
201         }
202 
203 
204         public boolean isEmptyStringNull ()
205         {
206             return (_dbName.startsWith("oracle"));
207         }
208 
209         public PreparedStatement getLoadStatement (Connection connection, String rowId, String contextPath, String virtualHosts)
210         throws SQLException
211         {
212             if (contextPath == null || "".equals(contextPath))
213             {
214                 if (isEmptyStringNull())
215                 {
216                     PreparedStatement statement = connection.prepareStatement("select * from "+_sessionTable+
217                     " where sessionId = ? and contextPath is null and virtualHost = ?");
218                     statement.setString(1, rowId);
219                     statement.setString(2, virtualHosts);
220 
221                     return statement;
222                 }
223             }
224 
225 
226 
227             PreparedStatement statement = connection.prepareStatement("select * from "+_sessionTable+
228             " where sessionId = ? and contextPath = ? and virtualHost = ?");
229             statement.setString(1, rowId);
230             statement.setString(2, contextPath);
231             statement.setString(3, virtualHosts);
232 
233             return statement;
234         }
235     }
236 
237 
238 
239     public JDBCSessionIdManager(Server server)
240     {
241         super();
242         _server=server;
243     }
244 
245     public JDBCSessionIdManager(Server server, Random random)
246     {
247        super(random);
248        _server=server;
249     }
250 
251     /**
252      * Configure jdbc connection information via a jdbc Driver
253      *
254      * @param driverClassName
255      * @param connectionUrl
256      */
257     public void setDriverInfo (String driverClassName, String connectionUrl)
258     {
259         _driverClassName=driverClassName;
260         _connectionUrl=connectionUrl;
261     }
262 
263     /**
264      * Configure jdbc connection information via a jdbc Driver
265      *
266      * @param driverClass
267      * @param connectionUrl
268      */
269     public void setDriverInfo (Driver driverClass, String connectionUrl)
270     {
271         _driver=driverClass;
272         _connectionUrl=connectionUrl;
273     }
274 
275 
276     public void setDatasource (DataSource ds)
277     {
278         _datasource = ds;
279     }
280 
281     public DataSource getDataSource ()
282     {
283         return _datasource;
284     }
285 
286     public String getDriverClassName()
287     {
288         return _driverClassName;
289     }
290 
291     public String getConnectionUrl ()
292     {
293         return _connectionUrl;
294     }
295 
296     public void setDatasourceName (String jndi)
297     {
298         _jndiName=jndi;
299     }
300 
301     public String getDatasourceName ()
302     {
303         return _jndiName;
304     }
305 
306     public void setBlobType (String name)
307     {
308         _blobType = name;
309     }
310 
311     public String getBlobType ()
312     {
313         return _blobType;
314     }
315 
316 
317 
318     public String getLongType()
319     {
320         return _longType;
321     }
322 
323     public void setLongType(String longType)
324     {
325         this._longType = longType;
326     }
327 
328     public void setScavengeInterval (long sec)
329     {
330         if (sec<=0)
331             sec=60;
332 
333         long old_period=_scavengeIntervalMs;
334         long period=sec*1000L;
335 
336         _scavengeIntervalMs=period;
337 
338         //add a bit of variability into the scavenge time so that not all
339         //nodes with the same scavenge time sync up
340         long tenPercent = _scavengeIntervalMs/10;
341         if ((System.currentTimeMillis()%2) == 0)
342             _scavengeIntervalMs += tenPercent;
343 
344         if (LOG.isDebugEnabled())
345             LOG.debug("Scavenging every "+_scavengeIntervalMs+" ms");
346         if (_timer!=null && (period!=old_period || _task==null))
347         {
348             synchronized (this)
349             {
350                 if (_task!=null)
351                     _task.cancel();
352                 _task = new TimerTask()
353                 {
354                     @Override
355                     public void run()
356                     {
357                         scavenge();
358                     }
359                 };
360                 _timer.schedule(_task,_scavengeIntervalMs,_scavengeIntervalMs);
361             }
362         }
363     }
364 
365     public long getScavengeInterval ()
366     {
367         return _scavengeIntervalMs/1000;
368     }
369 
370 
371     public void addSession(HttpSession session)
372     {
373         if (session == null)
374             return;
375 
376         synchronized (_sessionIds)
377         {
378             String id = ((JDBCSessionManager.Session)session).getClusterId();
379             try
380             {
381                 insert(id);
382                 _sessionIds.add(id);
383             }
384             catch (Exception e)
385             {
386                 LOG.warn("Problem storing session id="+id, e);
387             }
388         }
389     }
390     
391   
392     public void addSession(String id)
393     {
394         if (id == null)
395             return;
396 
397         synchronized (_sessionIds)
398         {           
399             try
400             {
401                 insert(id);
402                 _sessionIds.add(id);
403             }
404             catch (Exception e)
405             {
406                 LOG.warn("Problem storing session id="+id, e);
407             }
408         }
409     }
410 
411 
412 
413     public void removeSession(HttpSession session)
414     {
415         if (session == null)
416             return;
417 
418         removeSession(((JDBCSessionManager.Session)session).getClusterId());
419     }
420 
421 
422 
423     public void removeSession (String id)
424     {
425 
426         if (id == null)
427             return;
428 
429         synchronized (_sessionIds)
430         {
431             if (LOG.isDebugEnabled())
432                 LOG.debug("Removing session id="+id);
433             try
434             {
435                 _sessionIds.remove(id);
436                 delete(id);
437             }
438             catch (Exception e)
439             {
440                 LOG.warn("Problem removing session id="+id, e);
441             }
442         }
443 
444     }
445 
446 
447     /**
448      * Get the session id without any node identifier suffix.
449      *
450      * @see org.eclipse.jetty.server.SessionIdManager#getClusterId(java.lang.String)
451      */
452     public String getClusterId(String nodeId)
453     {
454         int dot=nodeId.lastIndexOf('.');
455         return (dot>0)?nodeId.substring(0,dot):nodeId;
456     }
457 
458 
459     /**
460      * Get the session id, including this node's id as a suffix.
461      *
462      * @see org.eclipse.jetty.server.SessionIdManager#getNodeId(java.lang.String, javax.servlet.http.HttpServletRequest)
463      */
464     public String getNodeId(String clusterId, HttpServletRequest request)
465     {
466         if (_workerName!=null)
467             return clusterId+'.'+_workerName;
468 
469         return clusterId;
470     }
471 
472 
473     public boolean idInUse(String id)
474     {
475         if (id == null)
476             return false;
477 
478         String clusterId = getClusterId(id);
479         boolean inUse = false;
480         synchronized (_sessionIds)
481         {
482             inUse = _sessionIds.contains(clusterId);
483         }
484 
485         
486         if (inUse)
487             return true; //optimisation - if this session is one we've been managing, we can check locally
488 
489         //otherwise, we need to go to the database to check
490         try
491         {
492             return exists(clusterId);
493         }
494         catch (Exception e)
495         {
496             LOG.warn("Problem checking inUse for id="+clusterId, e);
497             return false;
498         }
499     }
500 
501     /**
502      * Invalidate the session matching the id on all contexts.
503      *
504      * @see org.eclipse.jetty.server.SessionIdManager#invalidateAll(java.lang.String)
505      */
506     public void invalidateAll(String id)
507     {
508         //take the id out of the list of known sessionids for this node
509         removeSession(id);
510 
511         synchronized (_sessionIds)
512         {
513             //tell all contexts that may have a session object with this id to
514             //get rid of them
515             Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
516             for (int i=0; contexts!=null && i<contexts.length; i++)
517             {
518                 SessionHandler sessionHandler = (SessionHandler)((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
519                 if (sessionHandler != null)
520                 {
521                     SessionManager manager = sessionHandler.getSessionManager();
522 
523                     if (manager != null && manager instanceof JDBCSessionManager)
524                     {
525                         ((JDBCSessionManager)manager).invalidateSession(id);
526                     }
527                 }
528             }
529         }
530     }
531 
532 
533     public void renewSessionId (String oldClusterId, String oldNodeId, HttpServletRequest request)
534     {
535         //generate a new id
536         String newClusterId = newSessionId(request.hashCode());
537 
538         synchronized (_sessionIds)
539         {
540             removeSession(oldClusterId);//remove the old one from the list (and database)
541             addSession(newClusterId); //add in the new session id to the list (and database)
542 
543             //tell all contexts to update the id 
544             Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
545             for (int i=0; contexts!=null && i<contexts.length; i++)
546             {
547                 SessionHandler sessionHandler = (SessionHandler)((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
548                 if (sessionHandler != null) 
549                 {
550                     SessionManager manager = sessionHandler.getSessionManager();
551 
552                     if (manager != null && manager instanceof JDBCSessionManager)
553                     {
554                         ((JDBCSessionManager)manager).renewSessionId(oldClusterId, oldNodeId, newClusterId, getNodeId(newClusterId, request));
555                     }
556                 }
557             }
558         }
559     }
560 
561 
562     /**
563      * Start up the id manager.
564      *
565      * Makes necessary database tables and starts a Session
566      * scavenger thread.
567      */
568     @Override
569     public void doStart()
570     {
571         try
572         {
573             initializeDatabase();
574             prepareTables();
575             cleanExpiredSessions();
576             super.doStart();
577             if (LOG.isDebugEnabled())
578                 LOG.debug("Scavenging interval = "+getScavengeInterval()+" sec");
579             _timer=new Timer("JDBCSessionScavenger", true);
580             setScavengeInterval(getScavengeInterval());
581         }
582         catch (Exception e)
583         {
584             LOG.warn("Problem initialising JettySessionIds table", e);
585         }
586     }
587 
588     /**
589      * Stop the scavenger.
590      */
591     @Override
592     public void doStop ()
593     throws Exception
594     {
595         synchronized(this)
596         {
597             if (_task!=null)
598                 _task.cancel();
599             _task=null;
600             if (_timer!=null)
601                 _timer.cancel();
602             _timer=null;
603         }
604         _sessionIds.clear();
605         super.doStop();
606     }
607 
608     /**
609      * Get a connection from the driver or datasource.
610      *
611      * @return the connection for the datasource
612      * @throws SQLException
613      */
614     protected Connection getConnection ()
615     throws SQLException
616     {
617         if (_datasource != null)
618             return _datasource.getConnection();
619         else
620             return DriverManager.getConnection(_connectionUrl);
621     }
622     
623 
624 
625 
626 
627 
628     /**
629      * Set up the tables in the database
630      * @throws SQLException
631      */
632     private void prepareTables()
633     throws SQLException
634     {
635         _createSessionIdTable = "create table "+_sessionIdTable+" (id varchar(120), primary key(id))";
636         _selectBoundedExpiredSessions = "select * from "+_sessionTable+" where expiryTime >= ? and expiryTime <= ?";
637         _selectExpiredSessions = "select * from "+_sessionTable+" where expiryTime >0 and expiryTime <= ?";
638         _deleteOldExpiredSessions = "delete from "+_sessionTable+" where expiryTime >0 and expiryTime <= ?";
639 
640         _insertId = "insert into "+_sessionIdTable+" (id)  values (?)";
641         _deleteId = "delete from "+_sessionIdTable+" where id = ?";
642         _queryId = "select * from "+_sessionIdTable+" where id = ?";
643 
644         Connection connection = null;
645         try
646         {
647             //make the id table
648             connection = getConnection();
649             connection.setAutoCommit(true);
650             DatabaseMetaData metaData = connection.getMetaData();
651             _dbAdaptor = new DatabaseAdaptor(metaData);
652             _sessionTableRowId = _dbAdaptor.getRowIdColumnName();
653 
654             //checking for table existence is case-sensitive, but table creation is not
655             String tableName = _dbAdaptor.convertIdentifier(_sessionIdTable);
656             ResultSet result = metaData.getTables(null, null, tableName, null);
657             if (!result.next())
658             {
659                 //table does not exist, so create it
660                 connection.createStatement().executeUpdate(_createSessionIdTable);
661             }
662 
663             //make the session table if necessary
664             tableName = _dbAdaptor.convertIdentifier(_sessionTable);
665             result = metaData.getTables(null, null, tableName, null);
666             if (!result.next())
667             {
668                 //table does not exist, so create it
669                 String blobType = _dbAdaptor.getBlobType();
670                 String longType = _dbAdaptor.getLongType();
671                 _createSessionTable = "create table "+_sessionTable+" ("+_sessionTableRowId+" varchar(120), sessionId varchar(120), "+
672                                            " contextPath varchar(60), virtualHost varchar(60), lastNode varchar(60), accessTime "+longType+", "+
673                                            " lastAccessTime "+longType+", createTime "+longType+", cookieTime "+longType+", "+
674                                            " lastSavedTime "+longType+", expiryTime "+longType+", map "+blobType+", primary key("+_sessionTableRowId+"))";
675                 connection.createStatement().executeUpdate(_createSessionTable);
676             }
677 
678             //make some indexes on the JettySessions table
679             String index1 = "idx_"+_sessionTable+"_expiry";
680             String index2 = "idx_"+_sessionTable+"_session";
681 
682             result = metaData.getIndexInfo(null, null, tableName, false, false);
683             boolean index1Exists = false;
684             boolean index2Exists = false;
685             while (result.next())
686             {
687                 String idxName = result.getString("INDEX_NAME");
688                 if (index1.equalsIgnoreCase(idxName))
689                     index1Exists = true;
690                 else if (index2.equalsIgnoreCase(idxName))
691                     index2Exists = true;
692             }
693             if (!(index1Exists && index2Exists))
694             {
695                 Statement statement = connection.createStatement();
696                 if (!index1Exists)
697                     statement.executeUpdate("create index "+index1+" on "+_sessionTable+" (expiryTime)");
698                 if (!index2Exists)
699                     statement.executeUpdate("create index "+index2+" on "+_sessionTable+" (sessionId, contextPath)");
700             }
701 
702             //set up some strings representing the statements for session manipulation
703             _insertSession = "insert into "+_sessionTable+
704             " ("+_sessionTableRowId+", sessionId, contextPath, virtualHost, lastNode, accessTime, lastAccessTime, createTime, cookieTime, lastSavedTime, expiryTime, map) "+
705             " values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
706 
707             _deleteSession = "delete from "+_sessionTable+
708             " where "+_sessionTableRowId+" = ?";
709 
710             _updateSession = "update "+_sessionTable+
711             " set sessionId = ?, lastNode = ?, accessTime = ?, lastAccessTime = ?, lastSavedTime = ?, expiryTime = ?, map = ? where "+_sessionTableRowId+" = ?";
712 
713             _updateSessionNode = "update "+_sessionTable+
714             " set lastNode = ? where "+_sessionTableRowId+" = ?";
715 
716             _updateSessionAccessTime = "update "+_sessionTable+
717             " set lastNode = ?, accessTime = ?, lastAccessTime = ?, lastSavedTime = ?, expiryTime = ? where "+_sessionTableRowId+" = ?";
718 
719 
720         }
721         finally
722         {
723             if (connection != null)
724                 connection.close();
725         }
726     }
727 
728     /**
729      * Insert a new used session id into the table.
730      *
731      * @param id
732      * @throws SQLException
733      */
734     private void insert (String id)
735     throws SQLException
736     {
737         Connection connection = null;
738         try
739         {
740             connection = getConnection();
741             connection.setAutoCommit(true);
742             PreparedStatement query = connection.prepareStatement(_queryId);
743             query.setString(1, id);
744             ResultSet result = query.executeQuery();
745             //only insert the id if it isn't in the db already
746             if (!result.next())
747             {
748                 PreparedStatement statement = connection.prepareStatement(_insertId);
749                 statement.setString(1, id);
750                 statement.executeUpdate();
751             }
752         }
753         finally
754         {
755             if (connection != null)
756                 connection.close();
757         }
758     }
759 
760     /**
761      * Remove a session id from the table.
762      *
763      * @param id
764      * @throws SQLException
765      */
766     private void delete (String id)
767     throws SQLException
768     {
769         Connection connection = null;
770         try
771         {
772             connection = getConnection();
773             connection.setAutoCommit(true);
774             PreparedStatement statement = connection.prepareStatement(_deleteId);
775             statement.setString(1, id);
776             statement.executeUpdate();
777         }
778         finally
779         {
780             if (connection != null)
781                 connection.close();
782         }
783     }
784 
785 
786     /**
787      * Check if a session id exists.
788      *
789      * @param id
790      * @return
791      * @throws SQLException
792      */
793     private boolean exists (String id)
794     throws SQLException
795     {
796         Connection connection = null;
797         try
798         {
799             connection = getConnection();
800             connection.setAutoCommit(true);
801             PreparedStatement statement = connection.prepareStatement(_queryId);
802             statement.setString(1, id);
803             ResultSet result = statement.executeQuery();
804             return result.next();
805         }
806         finally
807         {
808             if (connection != null)
809                 connection.close();
810         }
811     }
812 
813     /**
814      * Look for sessions in the database that have expired.
815      *
816      * We do this in the SessionIdManager and not the SessionManager so
817      * that we only have 1 scavenger, otherwise if there are n SessionManagers
818      * there would be n scavengers, all contending for the database.
819      *
820      * We look first for sessions that expired in the previous interval, then
821      * for sessions that expired previously - these are old sessions that no
822      * node is managing any more and have become stuck in the database.
823      */
824     private void scavenge ()
825     {
826         Connection connection = null;
827         List<String> expiredSessionIds = new ArrayList<String>();
828         try
829         {
830             if (LOG.isDebugEnabled())
831                 LOG.debug("Scavenge sweep started at "+System.currentTimeMillis());
832             if (_lastScavengeTime > 0)
833             {
834                 connection = getConnection();
835                 connection.setAutoCommit(true);
836                 //"select sessionId from JettySessions where expiryTime > (lastScavengeTime - scanInterval) and expiryTime < lastScavengeTime";
837                 PreparedStatement statement = connection.prepareStatement(_selectBoundedExpiredSessions);
838                 long lowerBound = (_lastScavengeTime - _scavengeIntervalMs);
839                 long upperBound = _lastScavengeTime;
840                 if (LOG.isDebugEnabled())
841                     LOG.debug (" Searching for sessions expired between "+lowerBound + " and "+upperBound);
842 
843                 statement.setLong(1, lowerBound);
844                 statement.setLong(2, upperBound);
845                 ResultSet result = statement.executeQuery();
846                 while (result.next())
847                 {
848                     String sessionId = result.getString("sessionId");
849                     expiredSessionIds.add(sessionId);
850                     if (LOG.isDebugEnabled()) LOG.debug (" Found expired sessionId="+sessionId);
851                 }
852 
853                 //tell the SessionManagers to expire any sessions with a matching sessionId in memory
854                 Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
855                 for (int i=0; contexts!=null && i<contexts.length; i++)
856                 {
857 
858                     SessionHandler sessionHandler = (SessionHandler)((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
859                     if (sessionHandler != null)
860                     {
861                         SessionManager manager = sessionHandler.getSessionManager();
862                         if (manager != null && manager instanceof JDBCSessionManager)
863                         {
864                             ((JDBCSessionManager)manager).expire(expiredSessionIds);
865                         }
866                     }
867                 }
868 
869                 //find all sessions that have expired at least a couple of scanIntervals ago and just delete them
870                 upperBound = _lastScavengeTime - (2 * _scavengeIntervalMs);
871                 if (upperBound > 0)
872                 {
873                     if (LOG.isDebugEnabled()) LOG.debug("Deleting old expired sessions expired before "+upperBound);
874                     statement = connection.prepareStatement(_deleteOldExpiredSessions);
875                     statement.setLong(1, upperBound);
876                     int rows = statement.executeUpdate();
877                     if (LOG.isDebugEnabled()) LOG.debug("Deleted "+rows+" rows");
878                 }
879             }
880         }
881         catch (Exception e)
882         {
883             if (isRunning())
884                 LOG.warn("Problem selecting expired sessions", e);
885             else
886                 LOG.ignore(e);
887         }
888         finally
889         {
890             _lastScavengeTime=System.currentTimeMillis();
891             if (LOG.isDebugEnabled()) LOG.debug("Scavenge sweep ended at "+_lastScavengeTime);
892             if (connection != null)
893             {
894                 try
895                 {
896                 connection.close();
897                 }
898                 catch (SQLException e)
899                 {
900                     LOG.warn(e);
901                 }
902             }
903         }
904     }
905     
906     /**
907      * Get rid of sessions and sessionids from sessions that have already expired
908      * @throws Exception
909      */
910     private void cleanExpiredSessions ()
911     throws Exception
912     {
913         Connection connection = null;
914         List<String> expiredSessionIds = new ArrayList<String>();
915         try
916         {     
917             connection = getConnection();
918             connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
919             connection.setAutoCommit(false);
920 
921             PreparedStatement statement = connection.prepareStatement(_selectExpiredSessions);
922             long now = System.currentTimeMillis();
923             if (LOG.isDebugEnabled()) LOG.debug ("Searching for sessions expired before {}", now);
924 
925             statement.setLong(1, now);
926             ResultSet result = statement.executeQuery();
927             while (result.next())
928             {
929                 String sessionId = result.getString("sessionId");
930                 expiredSessionIds.add(sessionId);
931                 if (LOG.isDebugEnabled()) LOG.debug ("Found expired sessionId={}", sessionId); 
932             }
933             
934             Statement sessionsTableStatement = null;
935             Statement sessionIdsTableStatement = null;
936 
937             if (!expiredSessionIds.isEmpty())
938             {
939                 sessionsTableStatement = connection.createStatement();
940                 sessionsTableStatement.executeUpdate(createCleanExpiredSessionsSql("delete from "+_sessionTable+" where sessionId in ", expiredSessionIds));
941                 sessionIdsTableStatement = connection.createStatement();
942                 sessionIdsTableStatement.executeUpdate(createCleanExpiredSessionsSql("delete from "+_sessionIdTable+" where id in ", expiredSessionIds));
943             }
944             connection.commit();
945 
946             synchronized (_sessionIds)
947             {
948                 _sessionIds.removeAll(expiredSessionIds); //in case they were in our local cache of session ids
949             }
950         }
951         catch (Exception e)
952         {
953             if (connection != null)
954                 connection.rollback();
955             throw e;
956         }
957         finally
958         {
959             try
960             {
961                 if (connection != null)
962                     connection.close();
963             }
964             catch (SQLException e)
965             {
966                 LOG.warn(e);
967             }
968         }
969     }
970     
971     
972     /**
973      * 
974      * @param sql
975      * @param connection
976      * @param expiredSessionIds
977      * @throws Exception
978      */
979     private String createCleanExpiredSessionsSql (String sql,Collection<String> expiredSessionIds)
980     throws Exception
981     {
982         StringBuffer buff = new StringBuffer();
983         buff.append(sql);
984         buff.append("(");
985         Iterator<String> itor = expiredSessionIds.iterator();
986         while (itor.hasNext())
987         {
988             buff.append("'"+(itor.next())+"'");
989             if (itor.hasNext())
990                 buff.append(",");
991         }
992         buff.append(")");
993         
994         if (LOG.isDebugEnabled()) LOG.debug("Cleaning expired sessions with: {}", buff);
995         return buff.toString();
996     }
997     
998     private void initializeDatabase ()
999     throws Exception
1000     {
1001         if (_datasource != null)
1002             return; //already set up
1003         
1004         if (_jndiName!=null)
1005         {
1006             InitialContext ic = new InitialContext();
1007             _datasource = (DataSource)ic.lookup(_jndiName);
1008         }
1009         else if ( _driver != null && _connectionUrl != null )
1010         {
1011             DriverManager.registerDriver(_driver);
1012         }
1013         else if (_driverClassName != null && _connectionUrl != null)
1014         {
1015             Class.forName(_driverClassName);
1016         }
1017         else
1018             throw new IllegalStateException("No database configured for sessions");
1019     }
1020     
1021    
1022 }