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     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     throws Exception
520     {           
521         initializeDatabase();
522         prepareTables();   
523         cleanExpiredSessions();
524         super.doStart();
525         if (LOG.isDebugEnabled()) 
526             LOG.debug("Scavenging interval = "+getScavengeInterval()+" sec");
527         _timer=new Timer("JDBCSessionScavenger", true);
528         setScavengeInterval(getScavengeInterval());
529     }
530 
531     /** 
532      * Stop the scavenger.
533      */
534     @Override
535     public void doStop () 
536     throws Exception
537     {
538         synchronized(this)
539         {
540             if (_task!=null)
541                 _task.cancel();
542             if (_timer!=null)
543                 _timer.cancel();
544             _timer=null;
545         }
546         _sessionIds.clear();
547         super.doStop();
548     }
549   
550     /**
551      * Get a connection from the driver or datasource.
552      * 
553      * @return the connection for the datasource
554      * @throws SQLException
555      */
556     protected Connection getConnection ()
557     throws SQLException
558     {
559         if (_datasource != null)
560             return _datasource.getConnection();
561         else
562             return DriverManager.getConnection(_connectionUrl);
563     }
564     
565     
566    
567     
568     
569     /**
570      * Set up the tables in the database
571      * @throws SQLException
572      */
573     private void prepareTables()
574     throws SQLException
575     {
576         _createSessionIdTable = "create table "+_sessionIdTable+" (id varchar(120), primary key(id))";
577         _selectBoundedExpiredSessions = "select * from "+_sessionTable+" where expiryTime >= ? and expiryTime <= ?";
578         _selectExpiredSessions = "select * from "+_sessionTable+" where expiryTime >0 and expiryTime <= ?";
579         _deleteOldExpiredSessions = "delete from "+_sessionTable+" where expiryTime >0 and expiryTime <= ?";
580 
581         _insertId = "insert into "+_sessionIdTable+" (id)  values (?)";
582         _deleteId = "delete from "+_sessionIdTable+" where id = ?";
583         _queryId = "select * from "+_sessionIdTable+" where id = ?";
584 
585         Connection connection = null;
586         try
587         {
588             //make the id table
589             connection = getConnection();
590             connection.setAutoCommit(true);
591             DatabaseMetaData metaData = connection.getMetaData();
592             _dbAdaptor = new DatabaseAdaptor(metaData);
593             _sessionTableRowId = _dbAdaptor.getRowIdColumnName();
594 
595             //checking for table existence is case-sensitive, but table creation is not
596             String tableName = _dbAdaptor.convertIdentifier(_sessionIdTable);
597             ResultSet result = metaData.getTables(null, null, tableName, null);
598             if (!result.next())
599             {
600                 //table does not exist, so create it
601                 connection.createStatement().executeUpdate(_createSessionIdTable);
602             }
603             
604             //make the session table if necessary
605             tableName = _dbAdaptor.convertIdentifier(_sessionTable);   
606             result = metaData.getTables(null, null, tableName, null);
607             if (!result.next())
608             {
609                 //table does not exist, so create it
610                 String blobType = _dbAdaptor.getBlobType();
611                 String longType = _dbAdaptor.getLongType();
612                 _createSessionTable = "create table "+_sessionTable+" ("+_sessionTableRowId+" varchar(120), sessionId varchar(120), "+
613                                            " contextPath varchar(60), virtualHost varchar(60), lastNode varchar(60), accessTime "+longType+", "+
614                                            " lastAccessTime "+longType+", createTime "+longType+", cookieTime "+longType+", "+
615                                            " lastSavedTime "+longType+", expiryTime "+longType+", map "+blobType+", primary key("+_sessionTableRowId+"))";
616                 connection.createStatement().executeUpdate(_createSessionTable);
617             }
618             
619             //make some indexes on the JettySessions table
620             String index1 = "idx_"+_sessionTable+"_expiry";
621             String index2 = "idx_"+_sessionTable+"_session";
622             
623             result = metaData.getIndexInfo(null, null, tableName, false, false);
624             boolean index1Exists = false;
625             boolean index2Exists = false;
626             while (result.next())
627             {
628                 String idxName = result.getString("INDEX_NAME");
629                 if (index1.equalsIgnoreCase(idxName))
630                     index1Exists = true;
631                 else if (index2.equalsIgnoreCase(idxName))
632                     index2Exists = true;
633             }
634             if (!(index1Exists && index2Exists))
635             {
636                 Statement statement = connection.createStatement();
637                 if (!index1Exists)
638                     statement.executeUpdate("create index "+index1+" on "+_sessionTable+" (expiryTime)");
639                 if (!index2Exists)
640                     statement.executeUpdate("create index "+index2+" on "+_sessionTable+" (sessionId, contextPath)");
641             }
642 
643             //set up some strings representing the statements for session manipulation
644             _insertSession = "insert into "+_sessionTable+
645             " ("+_sessionTableRowId+", sessionId, contextPath, virtualHost, lastNode, accessTime, lastAccessTime, createTime, cookieTime, lastSavedTime, expiryTime, map) "+
646             " values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
647 
648             _deleteSession = "delete from "+_sessionTable+
649             " where "+_sessionTableRowId+" = ?";
650             
651             _updateSession = "update "+_sessionTable+
652             " set lastNode = ?, accessTime = ?, lastAccessTime = ?, lastSavedTime = ?, expiryTime = ?, map = ? where "+_sessionTableRowId+" = ?";
653 
654             _updateSessionNode = "update "+_sessionTable+
655             " set lastNode = ? where "+_sessionTableRowId+" = ?";
656 
657             _updateSessionAccessTime = "update "+_sessionTable+
658             " set lastNode = ?, accessTime = ?, lastAccessTime = ?, lastSavedTime = ?, expiryTime = ? where "+_sessionTableRowId+" = ?";
659 
660             
661         }
662         finally
663         {
664             if (connection != null)
665                 connection.close();
666         }
667     }
668     
669     /**
670      * Insert a new used session id into the table.
671      * 
672      * @param id
673      * @throws SQLException
674      */
675     private void insert (String id)
676     throws SQLException 
677     {
678         Connection connection = null;
679         try
680         {
681             connection = getConnection();
682             connection.setAutoCommit(true);            
683             PreparedStatement query = connection.prepareStatement(_queryId);
684             query.setString(1, id);
685             ResultSet result = query.executeQuery();
686             //only insert the id if it isn't in the db already 
687             if (!result.next())
688             {
689                 PreparedStatement statement = connection.prepareStatement(_insertId);
690                 statement.setString(1, id);
691                 statement.executeUpdate();
692             }
693         }
694         finally
695         {
696             if (connection != null)
697                 connection.close();
698         }
699     }
700     
701     /**
702      * Remove a session id from the table.
703      * 
704      * @param id
705      * @throws SQLException
706      */
707     private void delete (String id)
708     throws SQLException
709     {
710         Connection connection = null;
711         try
712         {
713             connection = getConnection();
714             connection.setAutoCommit(true);
715             PreparedStatement statement = connection.prepareStatement(_deleteId);
716             statement.setString(1, id);
717             statement.executeUpdate();
718         }
719         finally
720         {
721             if (connection != null)
722                 connection.close();
723         }
724     }
725     
726     
727     /**
728      * Check if a session id exists.
729      * 
730      * @param id
731      * @return
732      * @throws SQLException
733      */
734     private boolean exists (String id)
735     throws SQLException
736     {
737         Connection connection = null;
738         try
739         {
740             connection = getConnection();
741             connection.setAutoCommit(true);
742             PreparedStatement statement = connection.prepareStatement(_queryId);
743             statement.setString(1, id);
744             ResultSet result = statement.executeQuery();
745             return result.next();
746         }
747         finally
748         {
749             if (connection != null)
750                 connection.close();
751         }
752     }
753     
754     /**
755      * Look for sessions in the database that have expired.
756      * 
757      * We do this in the SessionIdManager and not the SessionManager so
758      * that we only have 1 scavenger, otherwise if there are n SessionManagers
759      * there would be n scavengers, all contending for the database.
760      * 
761      * We look first for sessions that expired in the previous interval, then
762      * for sessions that expired previously - these are old sessions that no
763      * node is managing any more and have become stuck in the database.
764      */
765     private void scavenge ()
766     {
767         Connection connection = null;
768         List<String> expiredSessionIds = new ArrayList<String>();
769         try
770         {            
771             if (LOG.isDebugEnabled()) 
772                 LOG.debug("Scavenge sweep started at "+System.currentTimeMillis());
773             if (_lastScavengeTime > 0)
774             {
775                 connection = getConnection();
776                 connection.setAutoCommit(true);
777                 //"select sessionId from JettySessions where expiryTime > (lastScavengeTime - scanInterval) and expiryTime < lastScavengeTime";
778                 PreparedStatement statement = connection.prepareStatement(_selectBoundedExpiredSessions);
779                 long lowerBound = (_lastScavengeTime - _scavengeIntervalMs);
780                 long upperBound = _lastScavengeTime;
781                 if (LOG.isDebugEnabled()) 
782                     LOG.debug (" Searching for sessions expired between "+lowerBound + " and "+upperBound);
783                 
784                 statement.setLong(1, lowerBound);
785                 statement.setLong(2, upperBound);
786                 ResultSet result = statement.executeQuery();
787                 while (result.next())
788                 {
789                     String sessionId = result.getString("sessionId");
790                     expiredSessionIds.add(sessionId);
791                     if (LOG.isDebugEnabled()) LOG.debug (" Found expired sessionId="+sessionId); 
792                 }
793 
794                 //tell the SessionManagers to expire any sessions with a matching sessionId in memory
795                 Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
796                 for (int i=0; contexts!=null && i<contexts.length; i++)
797                 {
798 
799                     SessionHandler sessionHandler = (SessionHandler)((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
800                     if (sessionHandler != null) 
801                     { 
802                         SessionManager manager = sessionHandler.getSessionManager();
803                         if (manager != null && manager instanceof JDBCSessionManager)
804                         {
805                             ((JDBCSessionManager)manager).expire(expiredSessionIds);
806                         }
807                     }
808                 }
809 
810                 //find all sessions that have expired at least a couple of scanIntervals ago and just delete them
811                 upperBound = _lastScavengeTime - (2 * _scavengeIntervalMs);
812                 if (upperBound > 0)
813                 {
814                     if (LOG.isDebugEnabled()) LOG.debug("Deleting old expired sessions expired before "+upperBound);
815                     statement = connection.prepareStatement(_deleteOldExpiredSessions);
816                     statement.setLong(1, upperBound);
817                     int rows = statement.executeUpdate();
818                     if (LOG.isDebugEnabled()) LOG.debug("Deleted "+rows+" rows of old sessions expired before "+upperBound);
819                 }
820             }
821         }
822         catch (Exception e)
823         {
824             if (isRunning())    
825                 LOG.warn("Problem selecting expired sessions", e);
826             else
827                 LOG.ignore(e);
828         }
829         finally
830         {           
831             _lastScavengeTime=System.currentTimeMillis();
832             if (LOG.isDebugEnabled()) LOG.debug("Scavenge sweep ended at "+_lastScavengeTime);
833             if (connection != null)
834             {
835                 try
836                 {
837                 connection.close();
838                 }
839                 catch (SQLException e)
840                 {
841                     LOG.warn(e);
842                 }
843             }
844         }
845     }
846     
847     /**
848      * Get rid of sessions and sessionids from sessions that have already expired
849      * @throws Exception
850      */
851     private void cleanExpiredSessions ()
852     throws Exception
853     {
854         Connection connection = null;
855         List<String> expiredSessionIds = new ArrayList<String>();
856         try
857         {     
858             connection = getConnection();
859             connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
860             connection.setAutoCommit(false);
861 
862             PreparedStatement statement = connection.prepareStatement(_selectExpiredSessions);
863             long now = System.currentTimeMillis();
864             if (LOG.isDebugEnabled()) LOG.debug ("Searching for sessions expired before {}", now);
865 
866             statement.setLong(1, now);
867             ResultSet result = statement.executeQuery();
868             while (result.next())
869             {
870                 String sessionId = result.getString("sessionId");
871                 expiredSessionIds.add(sessionId);
872                 if (LOG.isDebugEnabled()) LOG.debug ("Found expired sessionId={}", sessionId); 
873             }
874             
875             Statement sessionsTableStatement = null;
876             Statement sessionIdsTableStatement = null;
877 
878             if (!expiredSessionIds.isEmpty())
879             {
880                 sessionsTableStatement = connection.createStatement();
881                 sessionsTableStatement.executeUpdate(createCleanExpiredSessionsSql("delete from "+_sessionTable+" where sessionId in ", expiredSessionIds));
882                 sessionIdsTableStatement = connection.createStatement();
883                 sessionIdsTableStatement.executeUpdate(createCleanExpiredSessionsSql("delete from "+_sessionIdTable+" where id in ", expiredSessionIds));
884             }
885             connection.commit();
886 
887             synchronized (_sessionIds)
888             {
889                 _sessionIds.removeAll(expiredSessionIds); //in case they were in our local cache of session ids
890             }
891         }
892         catch (Exception e)
893         {
894             if (connection != null)
895                 connection.rollback();
896             throw e;
897         }
898         finally
899         {
900             try
901             {
902                 if (connection != null)
903                     connection.close();
904             }
905             catch (SQLException e)
906             {
907                 LOG.warn(e);
908             }
909         }
910     }
911     
912     
913     /**
914      * 
915      * @param sql
916      * @param connection
917      * @param expiredSessionIds
918      * @throws Exception
919      */
920     private String createCleanExpiredSessionsSql (String sql,Collection<String> expiredSessionIds)
921     throws Exception
922     {
923         StringBuffer buff = new StringBuffer();
924         buff.append(sql);
925         buff.append("(");
926         Iterator<String> itor = expiredSessionIds.iterator();
927         while (itor.hasNext())
928         {
929             buff.append("'"+(itor.next())+"'");
930             if (itor.hasNext())
931                 buff.append(",");
932         }
933         buff.append(")");
934         
935         if (LOG.isDebugEnabled()) LOG.debug("Cleaning expired sessions with: {}", buff);
936         return buff.toString();
937     }
938     
939     private void initializeDatabase ()
940     throws Exception
941     {
942         if (_datasource != null)
943             return; //already set up
944         
945         if (_jndiName!=null)
946         {
947             InitialContext ic = new InitialContext();
948             _datasource = (DataSource)ic.lookup(_jndiName);
949         }
950         else if ( _driver != null && _connectionUrl != null )
951         {
952             DriverManager.registerDriver(_driver);
953         }
954         else if (_driverClassName != null && _connectionUrl != null)
955         {
956             Class.forName(_driverClassName);
957         }
958         else
959             throw new IllegalStateException("No database configured for sessions");
960     }
961     
962    
963 }