View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2012 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     public void removeSession(HttpSession session)
392     {
393         if (session == null)
394             return;
395         
396         removeSession(((JDBCSessionManager.Session)session).getClusterId());
397     }
398     
399     
400     
401     public void removeSession (String id)
402     {
403 
404         if (id == null)
405             return;
406         
407         synchronized (_sessionIds)
408         {  
409             if (LOG.isDebugEnabled())
410                 LOG.debug("Removing session id="+id);
411             try
412             {               
413                 _sessionIds.remove(id);
414                 delete(id);
415             }
416             catch (Exception e)
417             {
418                 LOG.warn("Problem removing session id="+id, e);
419             }
420         }
421         
422     }
423     
424 
425     /** 
426      * Get the session id without any node identifier suffix.
427      * 
428      * @see org.eclipse.jetty.server.SessionIdManager#getClusterId(java.lang.String)
429      */
430     public String getClusterId(String nodeId)
431     {
432         int dot=nodeId.lastIndexOf('.');
433         return (dot>0)?nodeId.substring(0,dot):nodeId;
434     }
435     
436 
437     /** 
438      * Get the session id, including this node's id as a suffix.
439      * 
440      * @see org.eclipse.jetty.server.SessionIdManager#getNodeId(java.lang.String, javax.servlet.http.HttpServletRequest)
441      */
442     public String getNodeId(String clusterId, HttpServletRequest request)
443     {
444         if (_workerName!=null)
445             return clusterId+'.'+_workerName;
446 
447         return clusterId;
448     }
449 
450 
451     public boolean idInUse(String id)
452     {
453         if (id == null)
454             return false;
455         
456         String clusterId = getClusterId(id);
457         boolean inUse = false;
458         synchronized (_sessionIds)
459         {
460             inUse = _sessionIds.contains(clusterId);
461         }
462         
463         
464         if (inUse)
465             return true; //optimisation - if this session is one we've been managing, we can check locally
466 
467         //otherwise, we need to go to the database to check
468         try
469         {
470             return exists(clusterId);
471         }
472         catch (Exception e)
473         {
474             LOG.warn("Problem checking inUse for id="+clusterId, e);
475             return false;
476         }
477     }
478 
479     /** 
480      * Invalidate the session matching the id on all contexts.
481      * 
482      * @see org.eclipse.jetty.server.SessionIdManager#invalidateAll(java.lang.String)
483      */
484     public void invalidateAll(String id)
485     {            
486         //take the id out of the list of known sessionids for this node
487         removeSession(id);
488         
489         synchronized (_sessionIds)
490         {
491             //tell all contexts that may have a session object with this id to
492             //get rid of them
493             Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
494             for (int i=0; contexts!=null && i<contexts.length; i++)
495             {
496                 SessionHandler sessionHandler = (SessionHandler)((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
497                 if (sessionHandler != null) 
498                 {
499                     SessionManager manager = sessionHandler.getSessionManager();
500 
501                     if (manager != null && manager instanceof JDBCSessionManager)
502                     {
503                         ((JDBCSessionManager)manager).invalidateSession(id);
504                     }
505                 }
506             }
507         }
508     }
509 
510 
511     /** 
512      * Start up the id manager.
513      * 
514      * Makes necessary database tables and starts a Session
515      * scavenger thread.
516      */
517     @Override
518     public void doStart()
519     {
520         try
521         {            
522             initializeDatabase();
523             prepareTables();   
524             cleanExpiredSessions();
525             super.doStart();
526             if (LOG.isDebugEnabled()) 
527                 LOG.debug("Scavenging interval = "+getScavengeInterval()+" sec");
528             _timer=new Timer("JDBCSessionScavenger", true);
529             setScavengeInterval(getScavengeInterval());
530         }
531         catch (Exception e)
532         {
533             LOG.warn("Problem initialising JettySessionIds table", e);
534         }
535     }
536     
537     /** 
538      * Stop the scavenger.
539      */
540     @Override
541     public void doStop () 
542     throws Exception
543     {
544         synchronized(this)
545         {
546             if (_task!=null)
547                 _task.cancel();
548             if (_timer!=null)
549                 _timer.cancel();
550             _timer=null;
551         }
552         _sessionIds.clear();
553         super.doStop();
554     }
555   
556     /**
557      * Get a connection from the driver or datasource.
558      * 
559      * @return the connection for the datasource
560      * @throws SQLException
561      */
562     protected Connection getConnection ()
563     throws SQLException
564     {
565         if (_datasource != null)
566             return _datasource.getConnection();
567         else
568             return DriverManager.getConnection(_connectionUrl);
569     }
570     
571     
572    
573     
574     
575     /**
576      * Set up the tables in the database
577      * @throws SQLException
578      */
579     private void prepareTables()
580     throws SQLException
581     {
582         _createSessionIdTable = "create table "+_sessionIdTable+" (id varchar(120), primary key(id))";
583         _selectBoundedExpiredSessions = "select * from "+_sessionTable+" where expiryTime >= ? and expiryTime <= ?";
584         _selectExpiredSessions = "select * from "+_sessionTable+" where expiryTime >0 and expiryTime <= ?";
585         _deleteOldExpiredSessions = "delete from "+_sessionTable+" where expiryTime >0 and expiryTime <= ?";
586 
587         _insertId = "insert into "+_sessionIdTable+" (id)  values (?)";
588         _deleteId = "delete from "+_sessionIdTable+" where id = ?";
589         _queryId = "select * from "+_sessionIdTable+" where id = ?";
590 
591         Connection connection = null;
592         try
593         {
594             //make the id table
595             connection = getConnection();
596             connection.setAutoCommit(true);
597             DatabaseMetaData metaData = connection.getMetaData();
598             _dbAdaptor = new DatabaseAdaptor(metaData);
599             _sessionTableRowId = _dbAdaptor.getRowIdColumnName();
600 
601             //checking for table existence is case-sensitive, but table creation is not
602             String tableName = _dbAdaptor.convertIdentifier(_sessionIdTable);
603             ResultSet result = metaData.getTables(null, null, tableName, null);
604             if (!result.next())
605             {
606                 //table does not exist, so create it
607                 connection.createStatement().executeUpdate(_createSessionIdTable);
608             }
609             
610             //make the session table if necessary
611             tableName = _dbAdaptor.convertIdentifier(_sessionTable);   
612             result = metaData.getTables(null, null, tableName, null);
613             if (!result.next())
614             {
615                 //table does not exist, so create it
616                 String blobType = _dbAdaptor.getBlobType();
617                 String longType = _dbAdaptor.getLongType();
618                 _createSessionTable = "create table "+_sessionTable+" ("+_sessionTableRowId+" varchar(120), sessionId varchar(120), "+
619                                            " contextPath varchar(60), virtualHost varchar(60), lastNode varchar(60), accessTime "+longType+", "+
620                                            " lastAccessTime "+longType+", createTime "+longType+", cookieTime "+longType+", "+
621                                            " lastSavedTime "+longType+", expiryTime "+longType+", map "+blobType+", primary key("+_sessionTableRowId+"))";
622                 connection.createStatement().executeUpdate(_createSessionTable);
623             }
624             
625             //make some indexes on the JettySessions table
626             String index1 = "idx_"+_sessionTable+"_expiry";
627             String index2 = "idx_"+_sessionTable+"_session";
628             
629             result = metaData.getIndexInfo(null, null, tableName, false, false);
630             boolean index1Exists = false;
631             boolean index2Exists = false;
632             while (result.next())
633             {
634                 String idxName = result.getString("INDEX_NAME");
635                 if (index1.equalsIgnoreCase(idxName))
636                     index1Exists = true;
637                 else if (index2.equalsIgnoreCase(idxName))
638                     index2Exists = true;
639             }
640             if (!(index1Exists && index2Exists))
641             {
642                 Statement statement = connection.createStatement();
643                 if (!index1Exists)
644                     statement.executeUpdate("create index "+index1+" on "+_sessionTable+" (expiryTime)");
645                 if (!index2Exists)
646                     statement.executeUpdate("create index "+index2+" on "+_sessionTable+" (sessionId, contextPath)");
647             }
648 
649             //set up some strings representing the statements for session manipulation
650             _insertSession = "insert into "+_sessionTable+
651             " ("+_sessionTableRowId+", sessionId, contextPath, virtualHost, lastNode, accessTime, lastAccessTime, createTime, cookieTime, lastSavedTime, expiryTime, map) "+
652             " values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
653 
654             _deleteSession = "delete from "+_sessionTable+
655             " where "+_sessionTableRowId+" = ?";
656             
657             _updateSession = "update "+_sessionTable+
658             " set lastNode = ?, accessTime = ?, lastAccessTime = ?, lastSavedTime = ?, expiryTime = ?, map = ? where "+_sessionTableRowId+" = ?";
659 
660             _updateSessionNode = "update "+_sessionTable+
661             " set lastNode = ? where "+_sessionTableRowId+" = ?";
662 
663             _updateSessionAccessTime = "update "+_sessionTable+
664             " set lastNode = ?, accessTime = ?, lastAccessTime = ?, lastSavedTime = ?, expiryTime = ? where "+_sessionTableRowId+" = ?";
665 
666             
667         }
668         finally
669         {
670             if (connection != null)
671                 connection.close();
672         }
673     }
674     
675     /**
676      * Insert a new used session id into the table.
677      * 
678      * @param id
679      * @throws SQLException
680      */
681     private void insert (String id)
682     throws SQLException 
683     {
684         Connection connection = null;
685         try
686         {
687             connection = getConnection();
688             connection.setAutoCommit(true);            
689             PreparedStatement query = connection.prepareStatement(_queryId);
690             query.setString(1, id);
691             ResultSet result = query.executeQuery();
692             //only insert the id if it isn't in the db already 
693             if (!result.next())
694             {
695                 PreparedStatement statement = connection.prepareStatement(_insertId);
696                 statement.setString(1, id);
697                 statement.executeUpdate();
698             }
699         }
700         finally
701         {
702             if (connection != null)
703                 connection.close();
704         }
705     }
706     
707     /**
708      * Remove a session id from the table.
709      * 
710      * @param id
711      * @throws SQLException
712      */
713     private void delete (String id)
714     throws SQLException
715     {
716         Connection connection = null;
717         try
718         {
719             connection = getConnection();
720             connection.setAutoCommit(true);
721             PreparedStatement statement = connection.prepareStatement(_deleteId);
722             statement.setString(1, id);
723             statement.executeUpdate();
724         }
725         finally
726         {
727             if (connection != null)
728                 connection.close();
729         }
730     }
731     
732     
733     /**
734      * Check if a session id exists.
735      * 
736      * @param id
737      * @return
738      * @throws SQLException
739      */
740     private boolean exists (String id)
741     throws SQLException
742     {
743         Connection connection = null;
744         try
745         {
746             connection = getConnection();
747             connection.setAutoCommit(true);
748             PreparedStatement statement = connection.prepareStatement(_queryId);
749             statement.setString(1, id);
750             ResultSet result = statement.executeQuery();
751             return result.next();
752         }
753         finally
754         {
755             if (connection != null)
756                 connection.close();
757         }
758     }
759     
760     /**
761      * Look for sessions in the database that have expired.
762      * 
763      * We do this in the SessionIdManager and not the SessionManager so
764      * that we only have 1 scavenger, otherwise if there are n SessionManagers
765      * there would be n scavengers, all contending for the database.
766      * 
767      * We look first for sessions that expired in the previous interval, then
768      * for sessions that expired previously - these are old sessions that no
769      * node is managing any more and have become stuck in the database.
770      */
771     private void scavenge ()
772     {
773         Connection connection = null;
774         List<String> expiredSessionIds = new ArrayList<String>();
775         try
776         {            
777             if (LOG.isDebugEnabled()) 
778                 LOG.debug("Scavenge sweep started at "+System.currentTimeMillis());
779             if (_lastScavengeTime > 0)
780             {
781                 connection = getConnection();
782                 connection.setAutoCommit(true);
783                 //"select sessionId from JettySessions where expiryTime > (lastScavengeTime - scanInterval) and expiryTime < lastScavengeTime";
784                 PreparedStatement statement = connection.prepareStatement(_selectBoundedExpiredSessions);
785                 long lowerBound = (_lastScavengeTime - _scavengeIntervalMs);
786                 long upperBound = _lastScavengeTime;
787                 if (LOG.isDebugEnabled()) 
788                     LOG.debug (" Searching for sessions expired between "+lowerBound + " and "+upperBound);
789                 
790                 statement.setLong(1, lowerBound);
791                 statement.setLong(2, upperBound);
792                 ResultSet result = statement.executeQuery();
793                 while (result.next())
794                 {
795                     String sessionId = result.getString("sessionId");
796                     expiredSessionIds.add(sessionId);
797                     if (LOG.isDebugEnabled()) LOG.debug (" Found expired sessionId="+sessionId); 
798                 }
799 
800                 //tell the SessionManagers to expire any sessions with a matching sessionId in memory
801                 Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
802                 for (int i=0; contexts!=null && i<contexts.length; i++)
803                 {
804 
805                     SessionHandler sessionHandler = (SessionHandler)((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
806                     if (sessionHandler != null) 
807                     { 
808                         SessionManager manager = sessionHandler.getSessionManager();
809                         if (manager != null && manager instanceof JDBCSessionManager)
810                         {
811                             ((JDBCSessionManager)manager).expire(expiredSessionIds);
812                         }
813                     }
814                 }
815 
816                 //find all sessions that have expired at least a couple of scanIntervals ago and just delete them
817                 upperBound = _lastScavengeTime - (2 * _scavengeIntervalMs);
818                 if (upperBound > 0)
819                 {
820                     if (LOG.isDebugEnabled()) LOG.debug("Deleting old expired sessions expired before "+upperBound);
821                     statement = connection.prepareStatement(_deleteOldExpiredSessions);
822                     statement.setLong(1, upperBound);
823                     int rows = statement.executeUpdate();
824                     if (LOG.isDebugEnabled()) LOG.debug("Deleted "+rows+" rows");
825                 }
826             }
827         }
828         catch (Exception e)
829         {
830             if (isRunning())    
831                 LOG.warn("Problem selecting expired sessions", e);
832             else
833                 LOG.ignore(e);
834         }
835         finally
836         {           
837             _lastScavengeTime=System.currentTimeMillis();
838             if (LOG.isDebugEnabled()) LOG.debug("Scavenge sweep ended at "+_lastScavengeTime);
839             if (connection != null)
840             {
841                 try
842                 {
843                 connection.close();
844                 }
845                 catch (SQLException e)
846                 {
847                     LOG.warn(e);
848                 }
849             }
850         }
851     }
852     
853     /**
854      * Get rid of sessions and sessionids from sessions that have already expired
855      * @throws Exception
856      */
857     private void cleanExpiredSessions ()
858     throws Exception
859     {
860         Connection connection = null;
861         List<String> expiredSessionIds = new ArrayList<String>();
862         try
863         {     
864             connection = getConnection();
865             connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
866             connection.setAutoCommit(false);
867 
868             PreparedStatement statement = connection.prepareStatement(_selectExpiredSessions);
869             long now = System.currentTimeMillis();
870             if (LOG.isDebugEnabled()) LOG.debug ("Searching for sessions expired before {}", now);
871 
872             statement.setLong(1, now);
873             ResultSet result = statement.executeQuery();
874             while (result.next())
875             {
876                 String sessionId = result.getString("sessionId");
877                 expiredSessionIds.add(sessionId);
878                 if (LOG.isDebugEnabled()) LOG.debug ("Found expired sessionId={}", sessionId); 
879             }
880             
881             Statement sessionsTableStatement = null;
882             Statement sessionIdsTableStatement = null;
883 
884             if (!expiredSessionIds.isEmpty())
885             {
886                 sessionsTableStatement = connection.createStatement();
887                 sessionsTableStatement.executeUpdate(createCleanExpiredSessionsSql("delete from "+_sessionTable+" where sessionId in ", expiredSessionIds));
888                 sessionIdsTableStatement = connection.createStatement();
889                 sessionIdsTableStatement.executeUpdate(createCleanExpiredSessionsSql("delete from "+_sessionIdTable+" where id in ", expiredSessionIds));
890             }
891             connection.commit();
892 
893             synchronized (_sessionIds)
894             {
895                 _sessionIds.removeAll(expiredSessionIds); //in case they were in our local cache of session ids
896             }
897         }
898         catch (Exception e)
899         {
900             if (connection != null)
901                 connection.rollback();
902             throw e;
903         }
904         finally
905         {
906             try
907             {
908                 if (connection != null)
909                     connection.close();
910             }
911             catch (SQLException e)
912             {
913                 LOG.warn(e);
914             }
915         }
916     }
917     
918     
919     /**
920      * 
921      * @param sql
922      * @param connection
923      * @param expiredSessionIds
924      * @throws Exception
925      */
926     private String createCleanExpiredSessionsSql (String sql,Collection<String> expiredSessionIds)
927     throws Exception
928     {
929         StringBuffer buff = new StringBuffer();
930         buff.append(sql);
931         buff.append("(");
932         Iterator<String> itor = expiredSessionIds.iterator();
933         while (itor.hasNext())
934         {
935             buff.append("'"+(itor.next())+"'");
936             if (itor.hasNext())
937                 buff.append(",");
938         }
939         buff.append(")");
940         
941         if (LOG.isDebugEnabled()) LOG.debug("Cleaning expired sessions with: {}", buff);
942         return buff.toString();
943     }
944     
945     private void initializeDatabase ()
946     throws Exception
947     {
948         if (_datasource != null)
949             return; //already set up
950         
951         if (_jndiName!=null)
952         {
953             InitialContext ic = new InitialContext();
954             _datasource = (DataSource)ic.lookup(_jndiName);
955         }
956         else if ( _driver != null && _connectionUrl != null )
957         {
958             DriverManager.registerDriver(_driver);
959         }
960         else if (_driverClassName != null && _connectionUrl != null)
961         {
962             Class.forName(_driverClassName);
963         }
964         else
965             throw new IllegalStateException("No database configured for sessions");
966     }
967     
968    
969 }