]> WPIA git - gigi.git/blob - lib/jetty/org/eclipse/jetty/server/session/JDBCSessionIdManager.java
Update notes about password security
[gigi.git] / lib / jetty / org / eclipse / jetty / server / session / JDBCSessionIdManager.java
1 //
2 //  ========================================================================
3 //  Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
4 //  ------------------------------------------------------------------------
5 //  All rights reserved. This program and the accompanying materials
6 //  are made available under the terms of the Eclipse Public License v1.0
7 //  and Apache License v2.0 which accompanies this distribution.
8 //
9 //      The Eclipse Public License is available at
10 //      http://www.eclipse.org/legal/epl-v10.html
11 //
12 //      The Apache License v2.0 is available at
13 //      http://www.opensource.org/licenses/apache2.0.php
14 //
15 //  You may elect to redistribute this code under either of these licenses.
16 //  ========================================================================
17 //
18
19 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.HashSet;
33 import java.util.Locale;
34 import java.util.Random;
35 import java.util.Set;
36 import java.util.concurrent.TimeUnit;
37
38 import javax.naming.InitialContext;
39 import javax.servlet.http.HttpServletRequest;
40 import javax.servlet.http.HttpSession;
41 import javax.sql.DataSource;
42
43 import org.eclipse.jetty.server.Handler;
44 import org.eclipse.jetty.server.Server;
45 import org.eclipse.jetty.server.SessionManager;
46 import org.eclipse.jetty.server.handler.ContextHandler;
47 import org.eclipse.jetty.util.log.Logger;
48 import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
49 import org.eclipse.jetty.util.thread.Scheduler;
50
51
52
53 /**
54  * JDBCSessionIdManager
55  *
56  * SessionIdManager implementation that uses a database to store in-use session ids,
57  * to support distributed sessions.
58  *
59  */
60 public class JDBCSessionIdManager extends AbstractSessionIdManager
61 {
62     final static Logger LOG = SessionHandler.LOG;
63     public final static int MAX_INTERVAL_NOT_SET = -999;
64
65     protected final HashSet<String> _sessionIds = new HashSet<String>();
66     protected Server _server;
67     protected Driver _driver;
68     protected String _driverClassName;
69     protected String _connectionUrl;
70     protected DataSource _datasource;
71     protected String _jndiName;
72
73     protected int _deleteBlockSize = 10; //number of ids to include in where 'in' clause
74
75     protected Scheduler.Task _task; //scavenge task
76     protected Scheduler _scheduler;
77     protected Scavenger _scavenger;
78     protected boolean _ownScheduler;
79     protected long _lastScavengeTime;
80     protected long _scavengeIntervalMs = 1000L * 60 * 10; //10mins
81
82
83     protected String _createSessionIdTable;
84     protected String _createSessionTable;
85
86     protected String _selectBoundedExpiredSessions;
87     private String _selectExpiredSessions;
88     
89     protected String _insertId;
90     protected String _deleteId;
91     protected String _queryId;
92
93     protected  String _insertSession;
94     protected  String _deleteSession;
95     protected  String _updateSession;
96     protected  String _updateSessionNode;
97     protected  String _updateSessionAccessTime;
98
99     protected DatabaseAdaptor _dbAdaptor = new DatabaseAdaptor();
100     protected SessionIdTableSchema _sessionIdTableSchema = new SessionIdTableSchema();
101     protected SessionTableSchema _sessionTableSchema = new SessionTableSchema();
102     
103   
104
105  
106     /**
107      * SessionTableSchema
108      *
109      */
110     public static class SessionTableSchema
111     {        
112         protected DatabaseAdaptor _dbAdaptor;
113         protected String _tableName = "JettySessions";
114         protected String _rowIdColumn = "rowId";
115         protected String _idColumn = "sessionId";
116         protected String _contextPathColumn = "contextPath";
117         protected String _virtualHostColumn = "virtualHost"; 
118         protected String _lastNodeColumn = "lastNode";
119         protected String _accessTimeColumn = "accessTime"; 
120         protected String _lastAccessTimeColumn = "lastAccessTime";
121         protected String _createTimeColumn = "createTime";
122         protected String _cookieTimeColumn = "cookieTime";
123         protected String _lastSavedTimeColumn = "lastSavedTime";
124         protected String _expiryTimeColumn = "expiryTime";
125         protected String _maxIntervalColumn = "maxInterval";
126         protected String _mapColumn = "map";
127         
128         
129         protected void setDatabaseAdaptor(DatabaseAdaptor dbadaptor)
130         {
131             _dbAdaptor = dbadaptor;
132         }
133         
134         
135         public String getTableName()
136         {
137             return _tableName;
138         }
139         public void setTableName(String tableName)
140         {
141             checkNotNull(tableName);
142             _tableName = tableName;
143         }
144         public String getRowIdColumn()
145         {       
146             if ("rowId".equals(_rowIdColumn) && _dbAdaptor.isRowIdReserved())
147                 _rowIdColumn = "srowId";
148             return _rowIdColumn;
149         }
150         public void setRowIdColumn(String rowIdColumn)
151         {
152             checkNotNull(rowIdColumn);
153             if (_dbAdaptor == null)
154                 throw new IllegalStateException ("DbAdaptor is null");
155             
156             if (_dbAdaptor.isRowIdReserved() && "rowId".equals(rowIdColumn))
157                 throw new IllegalArgumentException("rowId is reserved word for Oracle");
158             
159             _rowIdColumn = rowIdColumn;
160         }
161         public String getIdColumn()
162         {
163             return _idColumn;
164         }
165         public void setIdColumn(String idColumn)
166         {
167             checkNotNull(idColumn);
168             _idColumn = idColumn;
169         }
170         public String getContextPathColumn()
171         {
172             return _contextPathColumn;
173         }
174         public void setContextPathColumn(String contextPathColumn)
175         {
176             checkNotNull(contextPathColumn);
177             _contextPathColumn = contextPathColumn;
178         }
179         public String getVirtualHostColumn()
180         {
181             return _virtualHostColumn;
182         }
183         public void setVirtualHostColumn(String virtualHostColumn)
184         {
185             checkNotNull(virtualHostColumn);
186             _virtualHostColumn = virtualHostColumn;
187         }
188         public String getLastNodeColumn()
189         {
190             return _lastNodeColumn;
191         }
192         public void setLastNodeColumn(String lastNodeColumn)
193         {
194             checkNotNull(lastNodeColumn);
195             _lastNodeColumn = lastNodeColumn;
196         }
197         public String getAccessTimeColumn()
198         {
199             return _accessTimeColumn;
200         }
201         public void setAccessTimeColumn(String accessTimeColumn)
202         {
203             checkNotNull(accessTimeColumn);
204             _accessTimeColumn = accessTimeColumn;
205         }
206         public String getLastAccessTimeColumn()
207         {
208             return _lastAccessTimeColumn;
209         }
210         public void setLastAccessTimeColumn(String lastAccessTimeColumn)
211         {
212             checkNotNull(lastAccessTimeColumn);
213             _lastAccessTimeColumn = lastAccessTimeColumn;
214         }
215         public String getCreateTimeColumn()
216         {
217             return _createTimeColumn;
218         }
219         public void setCreateTimeColumn(String createTimeColumn)
220         {
221             checkNotNull(createTimeColumn);
222             _createTimeColumn = createTimeColumn;
223         }
224         public String getCookieTimeColumn()
225         {
226             return _cookieTimeColumn;
227         }
228         public void setCookieTimeColumn(String cookieTimeColumn)
229         {
230             checkNotNull(cookieTimeColumn);
231             _cookieTimeColumn = cookieTimeColumn;
232         }
233         public String getLastSavedTimeColumn()
234         {
235             return _lastSavedTimeColumn;
236         }
237         public void setLastSavedTimeColumn(String lastSavedTimeColumn)
238         {
239             checkNotNull(lastSavedTimeColumn);
240             _lastSavedTimeColumn = lastSavedTimeColumn;
241         }
242         public String getExpiryTimeColumn()
243         {
244             return _expiryTimeColumn;
245         }
246         public void setExpiryTimeColumn(String expiryTimeColumn)
247         {
248             checkNotNull(expiryTimeColumn);
249             _expiryTimeColumn = expiryTimeColumn;
250         }
251         public String getMaxIntervalColumn()
252         {
253             return _maxIntervalColumn;
254         }
255         public void setMaxIntervalColumn(String maxIntervalColumn)
256         {
257             checkNotNull(maxIntervalColumn);
258             _maxIntervalColumn = maxIntervalColumn;
259         }
260         public String getMapColumn()
261         {
262             return _mapColumn;
263         }
264         public void setMapColumn(String mapColumn)
265         {
266             checkNotNull(mapColumn);
267             _mapColumn = mapColumn;
268         }
269         
270         public String getCreateStatementAsString ()
271         {
272             if (_dbAdaptor == null)
273                 throw new IllegalStateException ("No DBAdaptor");
274             
275             String blobType = _dbAdaptor.getBlobType();
276             String longType = _dbAdaptor.getLongType();
277             
278             return "create table "+_tableName+" ("+getRowIdColumn()+" varchar(120), "+_idColumn+" varchar(120), "+
279                     _contextPathColumn+" varchar(60), "+_virtualHostColumn+" varchar(60), "+_lastNodeColumn+" varchar(60), "+_accessTimeColumn+" "+longType+", "+
280                     _lastAccessTimeColumn+" "+longType+", "+_createTimeColumn+" "+longType+", "+_cookieTimeColumn+" "+longType+", "+
281                     _lastSavedTimeColumn+" "+longType+", "+_expiryTimeColumn+" "+longType+", "+_maxIntervalColumn+" "+longType+", "+
282                     _mapColumn+" "+blobType+", primary key("+getRowIdColumn()+"))";
283         }
284         
285         public String getCreateIndexOverExpiryStatementAsString (String indexName)
286         {
287             return "create index "+indexName+" on "+getTableName()+" ("+getExpiryTimeColumn()+")";
288         }
289         
290         public String getCreateIndexOverSessionStatementAsString (String indexName)
291         {
292             return "create index "+indexName+" on "+getTableName()+" ("+getIdColumn()+", "+getContextPathColumn()+")";
293         }
294         
295         public String getAlterTableForMaxIntervalAsString ()
296         {
297             if (_dbAdaptor == null)
298                 throw new IllegalStateException ("No DBAdaptor");
299             String longType = _dbAdaptor.getLongType();
300             return "alter table "+getTableName()+" add "+getMaxIntervalColumn()+" "+longType+" not null default "+MAX_INTERVAL_NOT_SET;
301         }
302         
303         private void checkNotNull(String s)
304         {
305             if (s == null)
306                 throw new IllegalArgumentException(s);
307         }
308         public String getInsertSessionStatementAsString()
309         {
310            return "insert into "+getTableName()+
311             " ("+getRowIdColumn()+", "+getIdColumn()+", "+getContextPathColumn()+", "+getVirtualHostColumn()+", "+getLastNodeColumn()+
312             ", "+getAccessTimeColumn()+", "+getLastAccessTimeColumn()+", "+getCreateTimeColumn()+", "+getCookieTimeColumn()+
313             ", "+getLastSavedTimeColumn()+", "+getExpiryTimeColumn()+", "+getMaxIntervalColumn()+", "+getMapColumn()+") "+
314             " values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
315         }
316         public String getDeleteSessionStatementAsString()
317         {
318             return "delete from "+getTableName()+
319             " where "+getRowIdColumn()+" = ?";
320         }
321         public String getUpdateSessionStatementAsString()
322         {
323             return "update "+getTableName()+
324                     " set "+getIdColumn()+" = ?, "+getLastNodeColumn()+" = ?, "+getAccessTimeColumn()+" = ?, "+
325                     getLastAccessTimeColumn()+" = ?, "+getLastSavedTimeColumn()+" = ?, "+getExpiryTimeColumn()+" = ?, "+
326                     getMaxIntervalColumn()+" = ?, "+getMapColumn()+" = ? where "+getRowIdColumn()+" = ?";
327         }
328         public String getUpdateSessionNodeStatementAsString()
329         {
330             return "update "+getTableName()+
331                     " set "+getLastNodeColumn()+" = ? where "+getRowIdColumn()+" = ?";
332         }
333         public String getUpdateSessionAccessTimeStatementAsString()
334         {
335            return "update "+getTableName()+
336             " set "+getLastNodeColumn()+" = ?, "+getAccessTimeColumn()+" = ?, "+getLastAccessTimeColumn()+" = ?, "+
337                    getLastSavedTimeColumn()+" = ?, "+getExpiryTimeColumn()+" = ?, "+getMaxIntervalColumn()+" = ? where "+getRowIdColumn()+" = ?";
338         }
339         
340         public String getBoundedExpiredSessionsStatementAsString()
341         {
342             return "select * from "+getTableName()+" where "+getLastNodeColumn()+" = ? and "+getExpiryTimeColumn()+" >= ? and "+getExpiryTimeColumn()+" <= ?";
343         }
344         
345         public String getSelectExpiredSessionsStatementAsString()
346         {
347             return "select * from "+getTableName()+" where "+getExpiryTimeColumn()+" >0 and "+getExpiryTimeColumn()+" <= ?";
348         }
349      
350         public PreparedStatement getLoadStatement (Connection connection, String rowId, String contextPath, String virtualHosts)
351         throws SQLException
352         { 
353             if (_dbAdaptor == null)
354                 throw new IllegalStateException("No DB adaptor");
355
356
357             if (contextPath == null || "".equals(contextPath))
358             {
359                 if (_dbAdaptor.isEmptyStringNull())
360                 {
361                     PreparedStatement statement = connection.prepareStatement("select * from "+getTableName()+
362                                                                               " where "+getIdColumn()+" = ? and "+
363                                                                               getContextPathColumn()+" is null and "+
364                                                                               getVirtualHostColumn()+" = ?");
365                     statement.setString(1, rowId);
366                     statement.setString(2, virtualHosts);
367
368                     return statement;
369                 }
370             }
371
372             PreparedStatement statement = connection.prepareStatement("select * from "+getTableName()+
373                                                                       " where "+getIdColumn()+" = ? and "+getContextPathColumn()+
374                                                                       " = ? and "+getVirtualHostColumn()+" = ?");
375             statement.setString(1, rowId);
376             statement.setString(2, contextPath);
377             statement.setString(3, virtualHosts);
378
379             return statement;
380         }
381     }
382     
383     
384     
385     /**
386      * SessionIdTableSchema
387      *
388      */
389     public static class SessionIdTableSchema
390     {
391         protected DatabaseAdaptor _dbAdaptor;
392         protected String _tableName = "JettySessionIds";
393         protected String _idColumn = "id";
394
395         public void setDatabaseAdaptor(DatabaseAdaptor dbAdaptor)
396         {
397             _dbAdaptor = dbAdaptor;
398         }
399         public String getIdColumn()
400         {
401             return _idColumn;
402         }
403
404         public void setIdColumn(String idColumn)
405         {
406             checkNotNull(idColumn);
407             _idColumn = idColumn;
408         }
409
410         public String getTableName()
411         {
412             return _tableName;
413         }
414
415         public void setTableName(String tableName)
416         {
417             checkNotNull(tableName);
418             _tableName = tableName;
419         }
420
421         public String getInsertStatementAsString ()
422         {
423             return "insert into "+_tableName+" ("+_idColumn+")  values (?)";
424         }
425
426         public String getDeleteStatementAsString ()
427         {
428             return "delete from "+_tableName+" where "+_idColumn+" = ?";
429         }
430
431         public String getSelectStatementAsString ()
432         {
433             return  "select * from "+_tableName+" where "+_idColumn+" = ?";
434         }
435         
436         public String getCreateStatementAsString ()
437         {
438             return "create table "+_tableName+" ("+_idColumn+" varchar(120), primary key("+_idColumn+"))";
439         }
440         
441         private void checkNotNull(String s)
442         {
443             if (s == null)
444                 throw new IllegalArgumentException(s);
445         }
446     }
447
448
449     /**
450      * DatabaseAdaptor
451      *
452      * Handles differences between databases.
453      *
454      * Postgres uses the getBytes and setBinaryStream methods to access
455      * a "bytea" datatype, which can be up to 1Gb of binary data. MySQL
456      * is happy to use the "blob" type and getBlob() methods instead.
457      *
458      * TODO if the differences become more major it would be worthwhile
459      * refactoring this class.
460      */
461     public static class DatabaseAdaptor
462     {
463         String _dbName;
464         boolean _isLower;
465         boolean _isUpper;
466         
467         protected String _blobType; //if not set, is deduced from the type of the database at runtime
468         protected String _longType; //if not set, is deduced from the type of the database at runtime
469
470
471         public DatabaseAdaptor ()
472         {           
473         }
474         
475         
476         public void adaptTo(DatabaseMetaData dbMeta)  
477         throws SQLException
478         {
479             _dbName = dbMeta.getDatabaseProductName().toLowerCase(Locale.ENGLISH);
480             LOG.debug ("Using database {}",_dbName);
481             _isLower = dbMeta.storesLowerCaseIdentifiers();
482             _isUpper = dbMeta.storesUpperCaseIdentifiers(); 
483         }
484         
485        
486         public void setBlobType(String blobType)
487         {
488             _blobType = blobType;
489         }
490         
491         public String getBlobType ()
492         {
493             if (_blobType != null)
494                 return _blobType;
495
496             if (_dbName.startsWith("postgres"))
497                 return "bytea";
498
499             return "blob";
500         }
501         
502
503         public void setLongType(String longType)
504         {
505             _longType = longType;
506         }
507         
508
509         public String getLongType ()
510         {
511             if (_longType != null)
512                 return _longType;
513
514             if (_dbName == null)
515                 throw new IllegalStateException ("DbAdaptor missing metadata");
516             
517             if (_dbName.startsWith("oracle"))
518                 return "number(20)";
519
520             return "bigint";
521         }
522         
523
524         /**
525          * Convert a camel case identifier into either upper or lower
526          * depending on the way the db stores identifiers.
527          *
528          * @param identifier
529          * @return the converted identifier
530          */
531         public String convertIdentifier (String identifier)
532         {
533             if (_dbName == null)
534                 throw new IllegalStateException ("DbAdaptor missing metadata");
535             
536             if (_isLower)
537                 return identifier.toLowerCase(Locale.ENGLISH);
538             if (_isUpper)
539                 return identifier.toUpperCase(Locale.ENGLISH);
540
541             return identifier;
542         }
543
544         public String getDBName ()
545         {
546             return _dbName;
547         }
548
549
550         public InputStream getBlobInputStream (ResultSet result, String columnName)
551         throws SQLException
552         {
553             if (_dbName == null)
554                 throw new IllegalStateException ("DbAdaptor missing metadata");
555             
556             if (_dbName.startsWith("postgres"))
557             {
558                 byte[] bytes = result.getBytes(columnName);
559                 return new ByteArrayInputStream(bytes);
560             }
561
562             Blob blob = result.getBlob(columnName);
563             return blob.getBinaryStream();
564         }
565
566
567         public boolean isEmptyStringNull ()
568         {
569             if (_dbName == null)
570                 throw new IllegalStateException ("DbAdaptor missing metadata");
571             
572             return (_dbName.startsWith("oracle"));
573         }
574         
575         /**
576          * rowId is a reserved word for Oracle, so change the name of this column
577          * @return true if db in use is oracle
578          */
579         public boolean isRowIdReserved ()
580         {
581             if (_dbName == null)
582                 throw new IllegalStateException ("DbAdaptor missing metadata");
583             
584             return (_dbName != null && _dbName.startsWith("oracle"));
585         }
586     }
587
588     
589     /**
590      * Scavenger
591      *
592      */
593     protected class Scavenger implements Runnable
594     {
595
596         @Override
597         public void run()
598         {
599            try
600            {
601                scavenge();
602            }
603            finally
604            {
605                if (_scheduler != null && _scheduler.isRunning())
606                    _scheduler.schedule(this, _scavengeIntervalMs, TimeUnit.MILLISECONDS);
607            }
608         }
609     }
610
611
612     public JDBCSessionIdManager(Server server)
613     {
614         super();
615         _server=server;
616     }
617
618     public JDBCSessionIdManager(Server server, Random random)
619     {
620        super(random);
621        _server=server;
622     }
623
624     /**
625      * Configure jdbc connection information via a jdbc Driver
626      *
627      * @param driverClassName
628      * @param connectionUrl
629      */
630     public void setDriverInfo (String driverClassName, String connectionUrl)
631     {
632         _driverClassName=driverClassName;
633         _connectionUrl=connectionUrl;
634     }
635
636     /**
637      * Configure jdbc connection information via a jdbc Driver
638      *
639      * @param driverClass
640      * @param connectionUrl
641      */
642     public void setDriverInfo (Driver driverClass, String connectionUrl)
643     {
644         _driver=driverClass;
645         _connectionUrl=connectionUrl;
646     }
647
648
649     public void setDatasource (DataSource ds)
650     {
651         _datasource = ds;
652     }
653
654     public DataSource getDataSource ()
655     {
656         return _datasource;
657     }
658
659     public String getDriverClassName()
660     {
661         return _driverClassName;
662     }
663
664     public String getConnectionUrl ()
665     {
666         return _connectionUrl;
667     }
668
669     public void setDatasourceName (String jndi)
670     {
671         _jndiName=jndi;
672     }
673
674     public String getDatasourceName ()
675     {
676         return _jndiName;
677     }
678
679     /**
680      * @param name
681      * @deprecated see DbAdaptor.setBlobType
682      */
683     public void setBlobType (String name)
684     {
685         _dbAdaptor.setBlobType(name);
686     }
687
688     public DatabaseAdaptor getDbAdaptor()
689     {
690         return _dbAdaptor;
691     }
692
693     public void setDbAdaptor(DatabaseAdaptor dbAdaptor)
694     {
695         if (dbAdaptor == null)
696             throw new IllegalStateException ("DbAdaptor cannot be null");
697         
698         _dbAdaptor = dbAdaptor;
699     }
700
701     /**
702      * @return
703      * @deprecated see DbAdaptor.getBlobType
704      */
705     public String getBlobType ()
706     {
707         return _dbAdaptor.getBlobType();
708     }
709
710     /**
711      * @return
712      * @deprecated see DbAdaptor.getLogType
713      */
714     public String getLongType()
715     {
716         return _dbAdaptor.getLongType();
717     }
718
719     /**
720      * @param longType
721      * @deprecated see DbAdaptor.setLongType
722      */
723     public void setLongType(String longType)
724     {
725        _dbAdaptor.setLongType(longType);
726     }
727     
728     public SessionIdTableSchema getSessionIdTableSchema()
729     {
730         return _sessionIdTableSchema;
731     }
732
733     public void setSessionIdTableSchema(SessionIdTableSchema sessionIdTableSchema)
734     {
735         if (sessionIdTableSchema == null)
736             throw new IllegalArgumentException("Null SessionIdTableSchema");
737         
738         _sessionIdTableSchema = sessionIdTableSchema;
739     }
740
741     public SessionTableSchema getSessionTableSchema()
742     {
743         return _sessionTableSchema;
744     }
745
746     public void setSessionTableSchema(SessionTableSchema sessionTableSchema)
747     {
748         _sessionTableSchema = sessionTableSchema;
749     }
750
751     public void setDeleteBlockSize (int bsize)
752     {
753         this._deleteBlockSize = bsize;
754     }
755
756     public int getDeleteBlockSize ()
757     {
758         return this._deleteBlockSize;
759     }
760     
761     public void setScavengeInterval (long sec)
762     {
763         if (sec<=0)
764             sec=60;
765
766         long old_period=_scavengeIntervalMs;
767         long period=sec*1000L;
768
769         _scavengeIntervalMs=period;
770
771         //add a bit of variability into the scavenge time so that not all
772         //nodes with the same scavenge time sync up
773         long tenPercent = _scavengeIntervalMs/10;
774         if ((System.currentTimeMillis()%2) == 0)
775             _scavengeIntervalMs += tenPercent;
776
777         if (LOG.isDebugEnabled())
778             LOG.debug("Scavenging every "+_scavengeIntervalMs+" ms");
779         
780         //if (_timer!=null && (period!=old_period || _task==null))
781         if (_scheduler != null && (period!=old_period || _task==null))
782         {
783             synchronized (this)
784             {
785                 if (_task!=null)
786                     _task.cancel();
787                 if (_scavenger == null)
788                     _scavenger = new Scavenger();
789                 _task = _scheduler.schedule(_scavenger,_scavengeIntervalMs,TimeUnit.MILLISECONDS);
790             }
791         }
792     }
793
794     public long getScavengeInterval ()
795     {
796         return _scavengeIntervalMs/1000;
797     }
798
799
800     @Override
801     public void addSession(HttpSession session)
802     {
803         if (session == null)
804             return;
805
806         synchronized (_sessionIds)
807         {
808             String id = ((JDBCSessionManager.Session)session).getClusterId();
809             try
810             {
811                 insert(id);
812                 _sessionIds.add(id);
813             }
814             catch (Exception e)
815             {
816                 LOG.warn("Problem storing session id="+id, e);
817             }
818         }
819     }
820     
821   
822     public void addSession(String id)
823     {
824         if (id == null)
825             return;
826
827         synchronized (_sessionIds)
828         {           
829             try
830             {
831                 insert(id);
832                 _sessionIds.add(id);
833             }
834             catch (Exception e)
835             {
836                 LOG.warn("Problem storing session id="+id, e);
837             }
838         }
839     }
840
841
842
843     @Override
844     public void removeSession(HttpSession session)
845     {
846         if (session == null)
847             return;
848
849         removeSession(((JDBCSessionManager.Session)session).getClusterId());
850     }
851
852
853
854     public void removeSession (String id)
855     {
856
857         if (id == null)
858             return;
859
860         synchronized (_sessionIds)
861         {
862             if (LOG.isDebugEnabled())
863                 LOG.debug("Removing sessionid="+id);
864             try
865             {
866                 _sessionIds.remove(id);
867                 delete(id);
868             }
869             catch (Exception e)
870             {
871                 LOG.warn("Problem removing session id="+id, e);
872             }
873         }
874
875     }
876
877
878     @Override
879     public boolean idInUse(String id)
880     {
881         if (id == null)
882             return false;
883
884         String clusterId = getClusterId(id);
885         boolean inUse = false;
886         synchronized (_sessionIds)
887         {
888             inUse = _sessionIds.contains(clusterId);
889         }
890
891         
892         if (inUse)
893             return true; //optimisation - if this session is one we've been managing, we can check locally
894
895         //otherwise, we need to go to the database to check
896         try
897         {
898             return exists(clusterId);
899         }
900         catch (Exception e)
901         {
902             LOG.warn("Problem checking inUse for id="+clusterId, e);
903             return false;
904         }
905     }
906
907     /**
908      * Invalidate the session matching the id on all contexts.
909      *
910      * @see org.eclipse.jetty.server.SessionIdManager#invalidateAll(java.lang.String)
911      */
912     @Override
913     public void invalidateAll(String id)
914     {
915         //take the id out of the list of known sessionids for this node
916         removeSession(id);
917
918         synchronized (_sessionIds)
919         {
920             //tell all contexts that may have a session object with this id to
921             //get rid of them
922             Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
923             for (int i=0; contexts!=null && i<contexts.length; i++)
924             {
925                 SessionHandler sessionHandler = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
926                 if (sessionHandler != null)
927                 {
928                     SessionManager manager = sessionHandler.getSessionManager();
929
930                     if (manager != null && manager instanceof JDBCSessionManager)
931                     {
932                         ((JDBCSessionManager)manager).invalidateSession(id);
933                     }
934                 }
935             }
936         }
937     }
938
939
940     @Override
941     public void renewSessionId (String oldClusterId, String oldNodeId, HttpServletRequest request)
942     {
943         //generate a new id
944         String newClusterId = newSessionId(request.hashCode());
945
946         synchronized (_sessionIds)
947         {
948             removeSession(oldClusterId);//remove the old one from the list (and database)
949             addSession(newClusterId); //add in the new session id to the list (and database)
950
951             //tell all contexts to update the id 
952             Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
953             for (int i=0; contexts!=null && i<contexts.length; i++)
954             {
955                 SessionHandler sessionHandler = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
956                 if (sessionHandler != null) 
957                 {
958                     SessionManager manager = sessionHandler.getSessionManager();
959
960                     if (manager != null && manager instanceof JDBCSessionManager)
961                     {
962                         ((JDBCSessionManager)manager).renewSessionId(oldClusterId, oldNodeId, newClusterId, getNodeId(newClusterId, request));
963                     }
964                 }
965             }
966         }
967     }
968
969
970     /**
971      * Start up the id manager.
972      *
973      * Makes necessary database tables and starts a Session
974      * scavenger thread.
975      */
976     @Override
977     public void doStart()
978     throws Exception
979     {           
980         initializeDatabase();
981         prepareTables();   
982         super.doStart();
983         if (LOG.isDebugEnabled()) 
984             LOG.debug("Scavenging interval = "+getScavengeInterval()+" sec");
985         
986          //try and use a common scheduler, fallback to own
987          _scheduler =_server.getBean(Scheduler.class);
988          if (_scheduler == null)
989          {
990              _scheduler = new ScheduledExecutorScheduler();
991              _ownScheduler = true;
992              _scheduler.start();
993          }
994   
995         setScavengeInterval(getScavengeInterval());
996     }
997
998     /**
999      * Stop the scavenger.
1000      */
1001     @Override
1002     public void doStop ()
1003     throws Exception
1004     {
1005         synchronized(this)
1006         {
1007             if (_task!=null)
1008                 _task.cancel();
1009             _task=null;
1010             if (_ownScheduler && _scheduler !=null)
1011                 _scheduler.stop();
1012             _scheduler=null;
1013         }
1014         _sessionIds.clear();
1015         super.doStop();
1016     }
1017
1018     /**
1019      * Get a connection from the driver or datasource.
1020      *
1021      * @return the connection for the datasource
1022      * @throws SQLException
1023      */
1024     protected Connection getConnection ()
1025     throws SQLException
1026     {
1027         if (_datasource != null)
1028             return _datasource.getConnection();
1029         else
1030             return DriverManager.getConnection(_connectionUrl);
1031     }
1032     
1033
1034
1035
1036
1037
1038     /**
1039      * Set up the tables in the database
1040      * @throws SQLException
1041      */
1042     /**
1043      * @throws SQLException
1044      */
1045     private void prepareTables()
1046     throws SQLException
1047     {
1048         if (_sessionIdTableSchema == null)
1049             throw new IllegalStateException ("No SessionIdTableSchema");
1050         
1051         if (_sessionTableSchema == null)
1052             throw new IllegalStateException ("No SessionTableSchema");
1053         
1054         try (Connection connection = getConnection();
1055              Statement statement = connection.createStatement())
1056         {
1057             //make the id table
1058             connection.setAutoCommit(true);
1059             DatabaseMetaData metaData = connection.getMetaData();
1060             _dbAdaptor.adaptTo(metaData);
1061             _sessionTableSchema.setDatabaseAdaptor(_dbAdaptor);
1062             _sessionIdTableSchema.setDatabaseAdaptor(_dbAdaptor);
1063             
1064             _createSessionIdTable = _sessionIdTableSchema.getCreateStatementAsString();
1065             _insertId = _sessionIdTableSchema.getInsertStatementAsString();
1066             _deleteId =  _sessionIdTableSchema.getDeleteStatementAsString();
1067             _queryId = _sessionIdTableSchema.getSelectStatementAsString();
1068             
1069             //checking for table existence is case-sensitive, but table creation is not
1070             String tableName = _dbAdaptor.convertIdentifier(_sessionIdTableSchema.getTableName());
1071             try (ResultSet result = metaData.getTables(null, null, tableName, null))
1072             {
1073                 if (!result.next())
1074                 {
1075                     //table does not exist, so create it
1076                     statement.executeUpdate(_createSessionIdTable);
1077                 }
1078             }         
1079             
1080             //make the session table if necessary
1081             tableName = _dbAdaptor.convertIdentifier(_sessionTableSchema.getTableName());
1082             try (ResultSet result = metaData.getTables(null, null, tableName, null))
1083             {
1084                 if (!result.next())
1085                 {
1086                     //table does not exist, so create it
1087                     _createSessionTable = _sessionTableSchema.getCreateStatementAsString();
1088                     statement.executeUpdate(_createSessionTable);
1089                 }
1090                 else
1091                 {
1092                     //session table exists, check it has maxinterval column
1093                     ResultSet colResult = null;
1094                     try
1095                     {
1096                         colResult = metaData.getColumns(null, null,
1097                                                         _dbAdaptor.convertIdentifier(_sessionTableSchema.getTableName()), 
1098                                                         _dbAdaptor.convertIdentifier(_sessionTableSchema.getMaxIntervalColumn()));
1099                     }
1100                     catch (SQLException s)
1101                     {
1102                         LOG.warn("Problem checking if "+_sessionTableSchema.getTableName()+
1103                                  " table contains "+_sessionTableSchema.getMaxIntervalColumn()+" column. Ensure table contains column definition: \""
1104                                 +_sessionTableSchema.getMaxIntervalColumn()+" long not null default -999\"");
1105                         throw s;
1106                     }
1107                     try
1108                     {
1109                         if (!colResult.next())
1110                         {
1111                             try
1112                             {
1113                                 //add the maxinterval column
1114                                 statement.executeUpdate(_sessionTableSchema.getAlterTableForMaxIntervalAsString());
1115                             }
1116                             catch (SQLException s)
1117                             {
1118                                 LOG.warn("Problem adding "+_sessionTableSchema.getMaxIntervalColumn()+
1119                                          " column. Ensure table contains column definition: \""+_sessionTableSchema.getMaxIntervalColumn()+
1120                                          " long not null default -999\"");
1121                                 throw s;
1122                             }
1123                         }
1124                     }
1125                     finally
1126                     {
1127                         colResult.close();
1128                     }
1129                 }
1130             }
1131             //make some indexes on the JettySessions table
1132             String index1 = "idx_"+_sessionTableSchema.getTableName()+"_expiry";
1133             String index2 = "idx_"+_sessionTableSchema.getTableName()+"_session";
1134
1135             boolean index1Exists = false;
1136             boolean index2Exists = false;
1137             try (ResultSet result = metaData.getIndexInfo(null, null, tableName, false, false))
1138             {
1139                 while (result.next())
1140                 {
1141                     String idxName = result.getString("INDEX_NAME");
1142                     if (index1.equalsIgnoreCase(idxName))
1143                         index1Exists = true;
1144                     else if (index2.equalsIgnoreCase(idxName))
1145                         index2Exists = true;
1146                 }
1147             }
1148             if (!index1Exists)
1149                 statement.executeUpdate(_sessionTableSchema.getCreateIndexOverExpiryStatementAsString(index1));
1150             if (!index2Exists)
1151                 statement.executeUpdate(_sessionTableSchema.getCreateIndexOverSessionStatementAsString(index2));
1152
1153             //set up some strings representing the statements for session manipulation
1154             _insertSession = _sessionTableSchema.getInsertSessionStatementAsString();
1155             _deleteSession = _sessionTableSchema.getDeleteSessionStatementAsString();
1156             _updateSession = _sessionTableSchema.getUpdateSessionStatementAsString();
1157             _updateSessionNode = _sessionTableSchema.getUpdateSessionNodeStatementAsString();
1158             _updateSessionAccessTime = _sessionTableSchema.getUpdateSessionAccessTimeStatementAsString();
1159             _selectBoundedExpiredSessions = _sessionTableSchema.getBoundedExpiredSessionsStatementAsString();
1160             _selectExpiredSessions = _sessionTableSchema.getSelectExpiredSessionsStatementAsString();
1161         }
1162     }
1163
1164     /**
1165      * Insert a new used session id into the table.
1166      *
1167      * @param id
1168      * @throws SQLException
1169      */
1170     private void insert (String id)
1171     throws SQLException
1172     {
1173         try (Connection connection = getConnection();
1174                 PreparedStatement query = connection.prepareStatement(_queryId))
1175         {
1176             connection.setAutoCommit(true);
1177             query.setString(1, id);
1178             try (ResultSet result = query.executeQuery())
1179             {
1180                 //only insert the id if it isn't in the db already
1181                 if (!result.next())
1182                 {
1183                     try (PreparedStatement statement = connection.prepareStatement(_insertId))
1184                     {
1185                         statement.setString(1, id);
1186                         statement.executeUpdate();
1187                     }
1188                 }
1189             }
1190         }
1191     }
1192
1193     /**
1194      * Remove a session id from the table.
1195      *
1196      * @param id
1197      * @throws SQLException
1198      */
1199     private void delete (String id)
1200     throws SQLException
1201     {
1202         try (Connection connection = getConnection();
1203                 PreparedStatement statement = connection.prepareStatement(_deleteId))
1204         {
1205             connection.setAutoCommit(true);
1206             statement.setString(1, id);
1207             statement.executeUpdate();
1208         }
1209     }
1210
1211
1212     /**
1213      * Check if a session id exists.
1214      *
1215      * @param id
1216      * @return
1217      * @throws SQLException
1218      */
1219     private boolean exists (String id)
1220     throws SQLException
1221     {
1222         try (Connection connection = getConnection();
1223                 PreparedStatement statement = connection.prepareStatement(_queryId))
1224         {
1225             connection.setAutoCommit(true);
1226             statement.setString(1, id);
1227             try (ResultSet result = statement.executeQuery())
1228             {
1229                 return result.next();
1230             }
1231         }
1232     }
1233
1234     /**
1235      * Look for sessions in the database that have expired.
1236      *
1237      * We do this in the SessionIdManager and not the SessionManager so
1238      * that we only have 1 scavenger, otherwise if there are n SessionManagers
1239      * there would be n scavengers, all contending for the database.
1240      *
1241      * We look first for sessions that expired in the previous interval, then
1242      * for sessions that expired previously - these are old sessions that no
1243      * node is managing any more and have become stuck in the database.
1244      */
1245     private void scavenge ()
1246     {
1247         Connection connection = null;
1248         try
1249         {
1250             if (LOG.isDebugEnabled())
1251                 LOG.debug(getWorkerName()+"- Scavenge sweep started at "+System.currentTimeMillis());
1252             if (_lastScavengeTime > 0)
1253             {
1254                 connection = getConnection();
1255                 connection.setAutoCommit(true);
1256                 Set<String> expiredSessionIds = new HashSet<String>();
1257                 
1258                 
1259                 //Pass 1: find sessions for which we were last managing node that have just expired since last pass
1260                 long lowerBound = (_lastScavengeTime - _scavengeIntervalMs);
1261                 long upperBound = _lastScavengeTime;
1262                 if (LOG.isDebugEnabled())
1263                     LOG.debug (getWorkerName()+"- Pass 1: Searching for sessions expired between "+lowerBound + " and "+upperBound);
1264
1265                 try (PreparedStatement statement = connection.prepareStatement(_selectBoundedExpiredSessions))
1266                 {
1267                     statement.setString(1, getWorkerName());
1268                     statement.setLong(2, lowerBound);
1269                     statement.setLong(3, upperBound);
1270                     try (ResultSet result = statement.executeQuery())
1271                     {
1272                         while (result.next())
1273                         {
1274                             String sessionId = result.getString(_sessionTableSchema.getIdColumn());
1275                             expiredSessionIds.add(sessionId);
1276                             if (LOG.isDebugEnabled()) LOG.debug ("Found expired sessionId="+sessionId);
1277                         }
1278                     }
1279                 }
1280                 scavengeSessions(expiredSessionIds, false);
1281
1282
1283                 //Pass 2: find sessions that have expired a while ago for which this node was their last manager
1284                 try (PreparedStatement selectExpiredSessions = connection.prepareStatement(_selectExpiredSessions))
1285                 {
1286                     expiredSessionIds.clear();
1287                     upperBound = _lastScavengeTime - (2 * _scavengeIntervalMs);
1288                     if (upperBound > 0)
1289                     {
1290                         if (LOG.isDebugEnabled()) LOG.debug(getWorkerName()+"- Pass 2: Searching for sessions expired before "+upperBound);
1291                         selectExpiredSessions.setLong(1, upperBound);
1292                         try (ResultSet result = selectExpiredSessions.executeQuery())
1293                         {
1294                             while (result.next())
1295                             {
1296                                 String sessionId = result.getString(_sessionTableSchema.getIdColumn());
1297                                 String lastNode = result.getString(_sessionTableSchema.getLastNodeColumn());
1298                                 if ((getWorkerName() == null && lastNode == null) || (getWorkerName() != null && getWorkerName().equals(lastNode)))
1299                                     expiredSessionIds.add(sessionId);
1300                                 if (LOG.isDebugEnabled()) LOG.debug ("Found expired sessionId="+sessionId+" last managed by "+getWorkerName());
1301                             }
1302                         }
1303                         scavengeSessions(expiredSessionIds, false);
1304                     }
1305
1306
1307                     //Pass 3:
1308                     //find all sessions that have expired at least a couple of scanIntervals ago
1309                     //if we did not succeed in loading them (eg their related context no longer exists, can't be loaded etc) then
1310                     //they are simply deleted
1311                     upperBound = _lastScavengeTime - (3 * _scavengeIntervalMs);
1312                     expiredSessionIds.clear();
1313                     if (upperBound > 0)
1314                     {
1315                         if (LOG.isDebugEnabled()) LOG.debug(getWorkerName()+"- Pass 3: searching for sessions expired before "+upperBound);
1316                         selectExpiredSessions.setLong(1, upperBound);
1317                         try (ResultSet result = selectExpiredSessions.executeQuery())
1318                         {
1319                             while (result.next())
1320                             {
1321                                 String sessionId = result.getString(_sessionTableSchema.getIdColumn());
1322                                 expiredSessionIds.add(sessionId);
1323                                 if (LOG.isDebugEnabled()) LOG.debug ("Found expired sessionId="+sessionId);
1324                             }
1325                         }
1326                         scavengeSessions(expiredSessionIds, true);
1327                     }
1328                 }
1329             }
1330         }
1331         catch (Exception e)
1332         {
1333             if (isRunning())
1334                 LOG.warn("Problem selecting expired sessions", e);
1335             else
1336                 LOG.ignore(e);
1337         }
1338         finally
1339         {
1340             _lastScavengeTime=System.currentTimeMillis();
1341             if (LOG.isDebugEnabled()) LOG.debug(getWorkerName()+"- Scavenge sweep ended at "+_lastScavengeTime);
1342             if (connection != null)
1343             {
1344                 try
1345                 {
1346                     connection.close();
1347                 }
1348                 catch (SQLException e)
1349                 {
1350                     LOG.warn(e);
1351                 }
1352             }
1353         }
1354     }
1355     
1356     
1357     /**
1358      * @param expiredSessionIds
1359      */
1360     private void scavengeSessions (Set<String> expiredSessionIds, boolean forceDelete)
1361     {       
1362         Set<String> remainingIds = new HashSet<String>(expiredSessionIds);
1363         Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
1364         for (int i=0; contexts!=null && i<contexts.length; i++)
1365         {
1366             SessionHandler sessionHandler = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
1367             if (sessionHandler != null)
1368             {
1369                 SessionManager manager = sessionHandler.getSessionManager();
1370                 if (manager != null && manager instanceof JDBCSessionManager)
1371                 {
1372                     Set<String> successfullyExpiredIds = ((JDBCSessionManager)manager).expire(expiredSessionIds);
1373                     if (successfullyExpiredIds != null)
1374                         remainingIds.removeAll(successfullyExpiredIds);
1375                 }
1376             }
1377         }
1378
1379         //Any remaining ids are of those sessions that no context removed
1380         if (!remainingIds.isEmpty() && forceDelete)
1381         {
1382             LOG.info("Forcibly deleting unrecoverable expired sessions {}", remainingIds);
1383             try
1384             {
1385                 //ensure they aren't in the local list of in-use session ids
1386                 synchronized (_sessionIds)
1387                 {
1388                     _sessionIds.removeAll(remainingIds);
1389                 }
1390                 
1391                 cleanExpiredSessionIds(remainingIds);
1392             }
1393             catch (Exception e)
1394             {
1395                 LOG.warn("Error removing expired session ids", e);
1396             }
1397         }
1398     }
1399
1400
1401    
1402     
1403     private void cleanExpiredSessionIds (Set<String> expiredIds)
1404     throws Exception
1405     {
1406         if (expiredIds == null || expiredIds.isEmpty())
1407             return;
1408
1409         String[] ids = expiredIds.toArray(new String[expiredIds.size()]);
1410         try (Connection con = getConnection())
1411         {
1412             con.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
1413             con.setAutoCommit(false);
1414
1415             int start = 0;
1416             int end = 0;
1417             int blocksize = _deleteBlockSize;
1418             int block = 0;
1419        
1420             try (Statement statement = con.createStatement())
1421             {
1422                 while (end < ids.length)
1423                 {
1424                     start = block*blocksize;
1425                     if ((ids.length -  start)  >= blocksize)
1426                         end = start + blocksize;
1427                     else
1428                         end = ids.length;
1429
1430                     //take them out of the sessionIds table
1431                     statement.executeUpdate(fillInClause("delete from "+_sessionIdTableSchema.getTableName()+" where "+_sessionIdTableSchema.getIdColumn()+" in ", ids, start, end));
1432                     //take them out of the sessions table
1433                     statement.executeUpdate(fillInClause("delete from "+_sessionTableSchema.getTableName()+" where "+_sessionTableSchema.getIdColumn()+" in ", ids, start, end));
1434                     block++;
1435                 }
1436             }
1437             catch (Exception e)
1438             {
1439                 con.rollback();
1440                 throw e;
1441             }
1442             con.commit();
1443         }
1444     }
1445
1446     
1447     
1448     /**
1449      * 
1450      * @param sql
1451      * @param atoms
1452      * @throws Exception
1453      */
1454     private String fillInClause (String sql, String[] literals, int start, int end)
1455     throws Exception
1456     {
1457         StringBuffer buff = new StringBuffer();
1458         buff.append(sql);
1459         buff.append("(");
1460         for (int i=start; i<end; i++)
1461         {
1462             buff.append("'"+(literals[i])+"'");
1463             if (i+1<end)
1464                 buff.append(",");
1465         }
1466         buff.append(")");
1467         return buff.toString();
1468     }
1469     
1470     
1471     
1472     private void initializeDatabase ()
1473     throws Exception
1474     {
1475         if (_datasource != null)
1476             return; //already set up
1477         
1478         if (_jndiName!=null)
1479         {
1480             InitialContext ic = new InitialContext();
1481             _datasource = (DataSource)ic.lookup(_jndiName);
1482         }
1483         else if ( _driver != null && _connectionUrl != null )
1484         {
1485             DriverManager.registerDriver(_driver);
1486         }
1487         else if (_driverClassName != null && _connectionUrl != null)
1488         {
1489             Class.forName(_driverClassName);
1490         }
1491         else
1492             throw new IllegalStateException("No database configured for sessions");
1493     }
1494     
1495    
1496 }