//
//  ========================================================================
//  Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//


package org.eclipse.jetty.server.session;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.ObjectOutputStream;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashSet;
import java.util.Set;

import org.eclipse.jetty.util.ClassLoadingObjectInputStream;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;

/**
 * JDBCSessionDataStore
 *
 * Session data stored in database
 */
@ManagedObject
public class JDBCSessionDataStore extends AbstractSessionDataStore
{
    final static Logger LOG = Log.getLogger("org.eclipse.jetty.server.session");
    
    /**
     * Used for Oracle and other databases where "" is treated as NULL
     */
    public static final String NULL_CONTEXT_PATH = "/";
    
    protected boolean _initialized = false;
    protected DatabaseAdaptor _dbAdaptor;
    protected SessionTableSchema _sessionTableSchema;
    protected boolean _schemaProvided;


    
    /**
     * SessionTableSchema
     *
     */
    public static class SessionTableSchema
    {      
        public final static int MAX_INTERVAL_NOT_SET = -999;
        
        protected DatabaseAdaptor _dbAdaptor;
        protected String _schemaName = null;
        protected String _tableName = "JettySessions";
        protected String _idColumn = "sessionId";
        protected String _contextPathColumn = "contextPath";
        protected String _virtualHostColumn = "virtualHost"; 
        protected String _lastNodeColumn = "lastNode";
        protected String _accessTimeColumn = "accessTime"; 
        protected String _lastAccessTimeColumn = "lastAccessTime";
        protected String _createTimeColumn = "createTime";
        protected String _cookieTimeColumn = "cookieTime";
        protected String _lastSavedTimeColumn = "lastSavedTime";
        protected String _expiryTimeColumn = "expiryTime";
        protected String _maxIntervalColumn = "maxInterval";
        protected String _mapColumn = "map";

        
        
        protected void setDatabaseAdaptor(DatabaseAdaptor dbadaptor)
        {
            _dbAdaptor = dbadaptor;
        }
        public String getSchemaName()
        {
            return _schemaName;
        }
        public void setSchemaName(String schemaName)
        {
            checkNotNull(schemaName);
            _schemaName = schemaName;
        }
        
        public String getTableName()
        {
            return _tableName;
        }
        public void setTableName(String tableName)
        {
            checkNotNull(tableName);
            _tableName = tableName;
        }
     
        private String getSchemaTableName()
        {
            return (getSchemaName()!=null?getSchemaName()+".":"")+getTableName();
        }
        
        public String getIdColumn()
        {
            return _idColumn;
        }
        public void setIdColumn(String idColumn)
        {
            checkNotNull(idColumn);
            _idColumn = idColumn;
        }
        public String getContextPathColumn()
        {
            return _contextPathColumn;
        }
        public void setContextPathColumn(String contextPathColumn)
        {
            checkNotNull(contextPathColumn);
            _contextPathColumn = contextPathColumn;
        }
        public String getVirtualHostColumn()
        {
            return _virtualHostColumn;
        }
        public void setVirtualHostColumn(String virtualHostColumn)
        {
            checkNotNull(virtualHostColumn);
            _virtualHostColumn = virtualHostColumn;
        }
        public String getLastNodeColumn()
        {
            return _lastNodeColumn;
        }
        public void setLastNodeColumn(String lastNodeColumn)
        {
            checkNotNull(lastNodeColumn);
            _lastNodeColumn = lastNodeColumn;
        }
        public String getAccessTimeColumn()
        {
            return _accessTimeColumn;
        }
        public void setAccessTimeColumn(String accessTimeColumn)
        {
            checkNotNull(accessTimeColumn);
            _accessTimeColumn = accessTimeColumn;
        }
        public String getLastAccessTimeColumn()
        {
            return _lastAccessTimeColumn;
        }
        public void setLastAccessTimeColumn(String lastAccessTimeColumn)
        {
            checkNotNull(lastAccessTimeColumn);
            _lastAccessTimeColumn = lastAccessTimeColumn;
        }
        public String getCreateTimeColumn()
        {
            return _createTimeColumn;
        }
        public void setCreateTimeColumn(String createTimeColumn)
        {
            checkNotNull(createTimeColumn);
            _createTimeColumn = createTimeColumn;
        }
        public String getCookieTimeColumn()
        {
            return _cookieTimeColumn;
        }
        public void setCookieTimeColumn(String cookieTimeColumn)
        {
            checkNotNull(cookieTimeColumn);
            _cookieTimeColumn = cookieTimeColumn;
        }
        public String getLastSavedTimeColumn()
        {
            return _lastSavedTimeColumn;
        }
        public void setLastSavedTimeColumn(String lastSavedTimeColumn)
        {
            checkNotNull(lastSavedTimeColumn);
            _lastSavedTimeColumn = lastSavedTimeColumn;
        }
        public String getExpiryTimeColumn()
        {
            return _expiryTimeColumn;
        }
        public void setExpiryTimeColumn(String expiryTimeColumn)
        {
            checkNotNull(expiryTimeColumn);
            _expiryTimeColumn = expiryTimeColumn;
        }
        public String getMaxIntervalColumn()
        {
            return _maxIntervalColumn;
        }
        public void setMaxIntervalColumn(String maxIntervalColumn)
        {
            checkNotNull(maxIntervalColumn);
            _maxIntervalColumn = maxIntervalColumn;
        }
        public String getMapColumn()
        {
            return _mapColumn;
        }
        public void setMapColumn(String mapColumn)
        {
            checkNotNull(mapColumn);
            _mapColumn = mapColumn;
        }
        
        public String getCreateStatementAsString ()
        {
            if (_dbAdaptor == null)
                throw new IllegalStateException ("No DBAdaptor");
            
            String blobType = _dbAdaptor.getBlobType();
            String longType = _dbAdaptor.getLongType();
            String stringType = _dbAdaptor.getStringType();
            
            return "create table "+_tableName+" ("+_idColumn+" "+stringType+"(120), "+
                    _contextPathColumn+" "+stringType+"(60), "+_virtualHostColumn+" "+stringType+"(60), "+_lastNodeColumn+" "+stringType+"(60), "+_accessTimeColumn+" "+longType+", "+
                    _lastAccessTimeColumn+" "+longType+", "+_createTimeColumn+" "+longType+", "+_cookieTimeColumn+" "+longType+", "+
                    _lastSavedTimeColumn+" "+longType+", "+_expiryTimeColumn+" "+longType+", "+_maxIntervalColumn+" "+longType+", "+
                    _mapColumn+" "+blobType+", primary key("+_idColumn+", "+_contextPathColumn+","+_virtualHostColumn+"))";
        }
        
        public String getCreateIndexOverExpiryStatementAsString (String indexName)
        {
            return "create index "+indexName+" on "+getSchemaTableName()+" ("+getExpiryTimeColumn()+")";
        }
        
        public String getCreateIndexOverSessionStatementAsString (String indexName)
        {
            return "create index "+indexName+" on "+getSchemaTableName()+" ("+getIdColumn()+", "+getContextPathColumn()+")";
        }
        
        public String getAlterTableForMaxIntervalAsString ()
        {
            if (_dbAdaptor == null)
                throw new IllegalStateException ("No DBAdaptor");
            String longType = _dbAdaptor.getLongType();
            String stem = "alter table "+getSchemaTableName()+" add "+getMaxIntervalColumn()+" "+longType;
            if (_dbAdaptor.getDBName().contains("oracle"))
                return stem + " default "+ MAX_INTERVAL_NOT_SET + " not null";
            else
                return stem +" not null default "+ MAX_INTERVAL_NOT_SET;
        }
        
        private void checkNotNull(String s)
        {
            if (s == null)
                throw new IllegalArgumentException(s);
        }
        public String getInsertSessionStatementAsString()
        {
           return "insert into "+getSchemaTableName()+
            " ("+getIdColumn()+", "+getContextPathColumn()+", "+getVirtualHostColumn()+", "+getLastNodeColumn()+
            ", "+getAccessTimeColumn()+", "+getLastAccessTimeColumn()+", "+getCreateTimeColumn()+", "+getCookieTimeColumn()+
            ", "+getLastSavedTimeColumn()+", "+getExpiryTimeColumn()+", "+getMaxIntervalColumn()+", "+getMapColumn()+") "+
            " values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
        }

        public PreparedStatement getUpdateSessionStatement(Connection connection, String id, SessionContext context)
                throws SQLException
        {
            String s =  "update "+getSchemaTableName()+
                    " set "+getLastNodeColumn()+" = ?, "+getAccessTimeColumn()+" = ?, "+
                    getLastAccessTimeColumn()+" = ?, "+getLastSavedTimeColumn()+" = ?, "+getExpiryTimeColumn()+" = ?, "+
                    getMaxIntervalColumn()+" = ?, "+getMapColumn()+" = ? where "+getIdColumn()+" = ? and "+getContextPathColumn()+
                    " = ? and "+getVirtualHostColumn()+" = ?";

            String cp = context.getCanonicalContextPath();
            if (_dbAdaptor.isEmptyStringNull() && StringUtil.isBlank(cp))
               cp = NULL_CONTEXT_PATH;
  
            PreparedStatement statement = connection.prepareStatement(s);
            statement.setString(8, id);
            statement.setString(9, cp);
            statement.setString(10, context.getVhost());
            return statement;
        }

      
        public PreparedStatement getExpiredSessionsStatement (Connection connection, String canonicalContextPath, String vhost, long expiry)
        throws SQLException
        {
            // TODO expiry should be a delay rather than an absolute time.
            
            if (_dbAdaptor == null)
                throw new IllegalStateException("No DB adaptor");

            String cp = canonicalContextPath;
            if (_dbAdaptor.isEmptyStringNull() && StringUtil.isBlank(cp))
                    cp = NULL_CONTEXT_PATH;

            PreparedStatement statement = connection.prepareStatement("select "+getIdColumn()+", "+getExpiryTimeColumn()+
                                                                      " from "+getSchemaTableName()+" where "+getContextPathColumn()+" = ? and "+
                                                                      getVirtualHostColumn()+" = ? and "+
                                                                      getExpiryTimeColumn()+" >0 and "+getExpiryTimeColumn()+" <= ?");

            statement.setString(1, cp);
            statement.setString(2, vhost);
            statement.setLong(3, expiry);
            return statement;
        }
      
        
        public PreparedStatement getMyExpiredSessionsStatement (Connection connection, SessionContext sessionContext, long expiry)
        throws SQLException
        {
            // TODO expiry should be a delay rather than an absolute time.
            
            if (_dbAdaptor == null)
                throw new IllegalStateException("No DB adaptor");

            String cp = sessionContext.getCanonicalContextPath();
            if (_dbAdaptor.isEmptyStringNull() && StringUtil.isBlank(cp))
                    cp = NULL_CONTEXT_PATH;

            PreparedStatement statement = connection.prepareStatement("select "+getIdColumn()+", "+getExpiryTimeColumn()+
                                                                      " from "+getSchemaTableName()+" where "+
                                                                      getLastNodeColumn()+" = ? and "+
                                                                      getContextPathColumn()+" = ? and "+
                                                                      getVirtualHostColumn()+" = ? and "+
                                                                      getExpiryTimeColumn()+" >0 and "+getExpiryTimeColumn()+" <= ?");

            statement.setString(1, sessionContext.getWorkerName());
            statement.setString(2, cp);
            statement.setString(3,  sessionContext.getVhost());
            statement.setLong(4, expiry);
            return statement;
        }
    
        
        public PreparedStatement getAllAncientExpiredSessionsStatement (Connection connection)
        throws SQLException
        {
            if (_dbAdaptor == null)
                throw new IllegalStateException("No DB adaptor");

            PreparedStatement statement = connection.prepareStatement("select "+getIdColumn()+", "+getContextPathColumn()+", "+getVirtualHostColumn()+
                                                                      " from "+getSchemaTableName()+
                                                                      " where "+getExpiryTimeColumn()+" >0 and "+getExpiryTimeColumn()+" <= ?");
            return statement;
        }
     
     
        public PreparedStatement getCheckSessionExistsStatement (Connection connection, SessionContext context)
        throws SQLException
        {
            if (_dbAdaptor == null)
                throw new IllegalStateException("No DB adaptor");

            String cp = context.getCanonicalContextPath();
            if (_dbAdaptor.isEmptyStringNull() && StringUtil.isBlank(cp))
                    cp = NULL_CONTEXT_PATH;

            PreparedStatement statement = connection.prepareStatement("select "+getIdColumn()+", "+getExpiryTimeColumn()+
                                                                      " from "+getSchemaTableName()+
                                                                      " where "+getIdColumn()+" = ? and "+
                                                                      getContextPathColumn()+" = ? and "+
                                                                      getVirtualHostColumn()+" = ?");
            statement.setString(2, cp);
            statement.setString(3, context.getVhost());
            return statement;
        }


        
        public PreparedStatement getLoadStatement (Connection connection, String id, SessionContext contextId)
        throws SQLException
        { 
            if (_dbAdaptor == null)
                throw new IllegalStateException("No DB adaptor");


            String cp = contextId.getCanonicalContextPath();
            if (_dbAdaptor.isEmptyStringNull()&& StringUtil.isBlank(cp))
                    cp = NULL_CONTEXT_PATH;

            PreparedStatement statement = connection.prepareStatement("select * from "+getSchemaTableName()+
                                                                      " where "+getIdColumn()+" = ? and "+getContextPathColumn()+
                                                                      " = ? and "+getVirtualHostColumn()+" = ?");
            statement.setString(1, id);
            statement.setString(2, cp);
            statement.setString(3, contextId.getVhost());

            return statement;
        }

        
        
        public PreparedStatement getUpdateStatement (Connection connection, String id, SessionContext contextId)
        throws SQLException
        {
            if (_dbAdaptor == null)
                throw new IllegalStateException("No DB adaptor");

            String cp = contextId.getCanonicalContextPath();
            if (_dbAdaptor.isEmptyStringNull() && StringUtil.isBlank(cp))
                    cp = NULL_CONTEXT_PATH;
            
            String s = "update "+getSchemaTableName()+
                    " set "+getLastNodeColumn()+" = ?, "+getAccessTimeColumn()+" = ?, "+
                    getLastAccessTimeColumn()+" = ?, "+getLastSavedTimeColumn()+" = ?, "+getExpiryTimeColumn()+" = ?, "+
                    getMaxIntervalColumn()+" = ?, "+getMapColumn()+" = ? where "+getIdColumn()+" = ? and "+getContextPathColumn()+
                    " = ? and "+getVirtualHostColumn()+" = ?";


            PreparedStatement statement = connection.prepareStatement(s);
            statement.setString(8, id);
            statement.setString(9, cp);
            statement.setString(10, contextId.getVhost());

            return statement;
        }
        
        


        public PreparedStatement getDeleteStatement (Connection connection, String id, SessionContext contextId)
        throws Exception
        { 
            if (_dbAdaptor == null)

                throw new IllegalStateException("No DB adaptor");

            String cp = contextId.getCanonicalContextPath();
            if (_dbAdaptor.isEmptyStringNull() && StringUtil.isBlank(cp))
                    cp = NULL_CONTEXT_PATH;

            PreparedStatement statement = connection.prepareStatement("delete from "+getSchemaTableName()+
                                                                      " where "+getIdColumn()+" = ? and "+getContextPathColumn()+
                                                                      " = ? and "+getVirtualHostColumn()+" = ?");
            statement.setString(1, id);
            statement.setString(2, cp);
            statement.setString(3, contextId.getVhost());

            return statement;

        }

        
        /**
         * Set up the tables in the database
         * @throws SQLException if unable to prepare tables
         */
        public void prepareTables()
        throws SQLException
        {
            try (Connection connection = _dbAdaptor.getConnection();
                 Statement statement = connection.createStatement())
            {
                //make the id table
                connection.setAutoCommit(true);
                DatabaseMetaData metaData = connection.getMetaData();
                _dbAdaptor.adaptTo(metaData);
                    
                
                //make the session table if necessary
                String tableName = _dbAdaptor.convertIdentifier(getTableName());
                String schemaName = _dbAdaptor.convertIdentifier(getSchemaName());
                try (ResultSet result = metaData.getTables(null, schemaName, tableName, null))
                {
                    if (!result.next())
                    {
                        //table does not exist, so create it
                        statement.executeUpdate(getCreateStatementAsString());
                    }
                    else
                    {
                        //session table exists, check it has maxinterval column
                        ResultSet colResult = null;
                        try
                        {
                            colResult = metaData.getColumns(null, schemaName, tableName, 
                                                            _dbAdaptor.convertIdentifier(getMaxIntervalColumn()));
                        }
                        catch (SQLException s)
                        {
                            LOG.warn("Problem checking if "+getTableName()+
                                     " table contains "+getMaxIntervalColumn()+" column. Ensure table contains column definition: \""
                                    + getMaxIntervalColumn()+" long not null default -999\"");
                            throw s;
                        }
                        try
                        {
                            if (!colResult.next())
                            {
                                try
                                {
                                    //add the maxinterval column
                                    statement.executeUpdate(getAlterTableForMaxIntervalAsString());
                                }
                                catch (SQLException s)
                                {
                                    LOG.warn("Problem adding "+getMaxIntervalColumn()+
                                             " column. Ensure table contains column definition: \""+getMaxIntervalColumn()+
                                             " long not null default -999\"");
                                    throw s;
                                }
                            }
                        }
                        finally
                        {
                            colResult.close();
                        }
                    }
                }
                //make some indexes on the JettySessions table
                String index1 = "idx_"+getTableName()+"_expiry";
                String index2 = "idx_"+getTableName()+"_session";

                boolean index1Exists = false;
                boolean index2Exists = false;
                try (ResultSet result = metaData.getIndexInfo(null, schemaName, tableName, false, true))
                {
                    while (result.next())
                    {
                        String idxName = result.getString("INDEX_NAME");
                        if (index1.equalsIgnoreCase(idxName))
                            index1Exists = true;
                        else if (index2.equalsIgnoreCase(idxName))
                            index2Exists = true;
                    }
                }
                if (!index1Exists)
                    statement.executeUpdate(getCreateIndexOverExpiryStatementAsString(index1));
                if (!index2Exists)
                    statement.executeUpdate(getCreateIndexOverSessionStatementAsString(index2));
            }
        }

        @Override
        public String toString()
        {
            return String.format("%s[%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s]",super.toString(),
                                 _schemaName,_tableName,_idColumn,_contextPathColumn,_virtualHostColumn,_cookieTimeColumn,_createTimeColumn,
                                 _expiryTimeColumn,_accessTimeColumn,_lastAccessTimeColumn,_lastNodeColumn,_lastSavedTimeColumn,_maxIntervalColumn);
        }
        
        
        
    }
    
    
   
  
    public JDBCSessionDataStore ()
    {
        super ();
    }

  



    @Override
    protected void doStart() throws Exception
    {         
        if (_dbAdaptor == null)
            throw new IllegalStateException("No jdbc config");
        
        initialize();
        super.doStart();
    }




    @Override
    protected void doStop() throws Exception
    {
        super.doStop();
        _initialized = false;
        if (!_schemaProvided)
            _sessionTableSchema = null;
    }




    public void initialize () throws Exception
    {
        if (!_initialized)
        {
            _initialized = true;
   
            //taking the defaults if one not set
            if (_sessionTableSchema == null)
            {
                _sessionTableSchema = new SessionTableSchema();
                addBean(_sessionTableSchema,true);
            }
            
            _dbAdaptor.initialize();
            _sessionTableSchema.setDatabaseAdaptor(_dbAdaptor);
            _sessionTableSchema.prepareTables();
        }
    }


    @Override
    public SessionData doLoad(String id) throws Exception
    {
        try (Connection connection = _dbAdaptor.getConnection();
                PreparedStatement statement = _sessionTableSchema.getLoadStatement(connection, id, _context);
                ResultSet result = statement.executeQuery())
        {
            SessionData data = null;
            if (result.next())
            {                    
                data = newSessionData(id,
                                      result.getLong(_sessionTableSchema.getCreateTimeColumn()), 
                                      result.getLong(_sessionTableSchema.getAccessTimeColumn()), 
                                      result.getLong(_sessionTableSchema.getLastAccessTimeColumn()), 
                                      result.getLong(_sessionTableSchema.getMaxIntervalColumn()));
                data.setCookieSet(result.getLong(_sessionTableSchema.getCookieTimeColumn()));
                data.setLastNode(result.getString(_sessionTableSchema.getLastNodeColumn()));
                data.setLastSaved(result.getLong(_sessionTableSchema.getLastSavedTimeColumn()));
                data.setExpiry(result.getLong(_sessionTableSchema.getExpiryTimeColumn()));
                data.setContextPath(_context.getCanonicalContextPath());          
                data.setVhost(_context.getVhost());

                try (InputStream is = _dbAdaptor.getBlobInputStream(result, _sessionTableSchema.getMapColumn());
                        ClassLoadingObjectInputStream ois = new ClassLoadingObjectInputStream(is))
                {
                    SessionData.deserializeAttributes(data, ois);
                }
                catch (Exception e)
                {
                    throw new UnreadableSessionDataException (id, _context, e);
                }

                if (LOG.isDebugEnabled())
                    LOG.debug("LOADED session {}", data);
            }
            else
                if (LOG.isDebugEnabled())
                    LOG.debug("No session {}", id);

            return data;
        }
    }



    @Override
    public boolean delete(String id) throws Exception
    {   
        try (Connection connection = _dbAdaptor.getConnection();
             PreparedStatement statement = _sessionTableSchema.getDeleteStatement(connection, id, _context))
        {
            connection.setAutoCommit(true);
            int rows = statement.executeUpdate();
            if (LOG.isDebugEnabled())
                LOG.debug("Deleted Session {}:{}",id,(rows>0));
            
            return rows > 0;
        }
    }



    @Override
    public void doStore(String id, SessionData data, long lastSaveTime) throws Exception
    {
        if (data==null || id==null)
            return;

        if (lastSaveTime <= 0)
        {     
            doInsert(id, data);
        }
        else
        {
            doUpdate(id, data);            
        }
    }


    protected void doInsert (String id, SessionData data) 
    throws Exception
    {
        String s = _sessionTableSchema.getInsertSessionStatementAsString();


        try (Connection connection = _dbAdaptor.getConnection())        
        {
            connection.setAutoCommit(true);
            try  (PreparedStatement statement = connection.prepareStatement(s))
            {
                statement.setString(1, id); //session id
                
                String cp = _context.getCanonicalContextPath();
                if (_dbAdaptor.isEmptyStringNull() && StringUtil.isBlank(cp))
                        cp = NULL_CONTEXT_PATH;
                
                statement.setString(2, cp); //context path
                      
                statement.setString(3, _context.getVhost()); //first vhost
                statement.setString(4, data.getLastNode());//my node id
                statement.setLong(5, data.getAccessed());//accessTime
                statement.setLong(6, data.getLastAccessed()); //lastAccessTime
                statement.setLong(7, data.getCreated()); //time created
                statement.setLong(8, data.getCookieSet());//time cookie was set
                statement.setLong(9, data.getLastSaved()); //last saved time
                statement.setLong(10, data.getExpiry());
                statement.setLong(11, data.getMaxInactiveMs());

                try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
                     ObjectOutputStream oos = new ObjectOutputStream(baos))
                {
                    SessionData.serializeAttributes(data, oos);
                    byte[] bytes = baos.toByteArray();
                    ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
                    statement.setBinaryStream(12, bais, bytes.length);//attribute map as blob
                    statement.executeUpdate();
                    if (LOG.isDebugEnabled())
                        LOG.debug("Inserted session "+data);
                }
            }
        }
    }

    
    protected void doUpdate (String id, SessionData data)
            throws Exception
    {
        try (Connection connection = _dbAdaptor.getConnection())        
        {
            connection.setAutoCommit(true);
            try (PreparedStatement statement = _sessionTableSchema.getUpdateSessionStatement(connection, data.getId(), _context))
            {
                statement.setString(1, data.getLastNode());//should be my node id
                statement.setLong(2, data.getAccessed());//accessTime
                statement.setLong(3, data.getLastAccessed()); //lastAccessTime
                statement.setLong(4, data.getLastSaved()); //last saved time
                statement.setLong(5, data.getExpiry());
                statement.setLong(6, data.getMaxInactiveMs());

                try(ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    ObjectOutputStream oos = new ObjectOutputStream(baos))
                {
                    SessionData.serializeAttributes(data, oos);
                    byte[] bytes = baos.toByteArray();
                    try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes))
                    {
                        statement.setBinaryStream(7, bais, bytes.length);//attribute map as blob
                        statement.executeUpdate();

                        if (LOG.isDebugEnabled())
                            LOG.debug("Updated session "+data);
                    }
                }
            }
        }
    }


    @Override
    public Set<String> doGetExpired(Set<String> candidates)
    {
        if (LOG.isDebugEnabled())
            LOG.debug("Getting expired sessions at time {}", System.currentTimeMillis());

        long now = System.currentTimeMillis();
        
        Set<String> expiredSessionKeys = new HashSet<>();
        try (Connection connection = _dbAdaptor.getConnection())
        {
            connection.setAutoCommit(true);
            
            /*
             * 1. Select sessions managed by this node for our context that have expired
             */
            long upperBound = now;
            if (LOG.isDebugEnabled())
                LOG.debug ("{}- Pass 1: Searching for sessions for context {} managed by me and expired before {}",  _context.getWorkerName(), _context.getCanonicalContextPath(),upperBound);

            try (PreparedStatement statement = _sessionTableSchema.getExpiredSessionsStatement(connection, _context.getCanonicalContextPath(), _context.getVhost(), upperBound))
            {
                try (ResultSet result = statement.executeQuery())
                {
                    while (result.next())
                    {
                        String sessionId = result.getString(_sessionTableSchema.getIdColumn());
                        long exp = result.getLong(_sessionTableSchema.getExpiryTimeColumn());
                        expiredSessionKeys.add(sessionId);
                        if (LOG.isDebugEnabled()) LOG.debug (_context.getCanonicalContextPath()+"- Found expired sessionId="+sessionId);
                    }
                }
            }

            /*
             *  2. Select sessions for any node or context that have expired 
             *  at least 1 graceperiod since the last expiry check. If we haven't done previous expiry checks, then check
             *  those that have expired at least 3 graceperiod ago.
             */
            try (PreparedStatement selectExpiredSessions = _sessionTableSchema.getAllAncientExpiredSessionsStatement(connection))
            {
                if (_lastExpiryCheckTime <= 0)
                    upperBound = (now - (3*(1000L * _gracePeriodSec)));
                else
                    upperBound =  _lastExpiryCheckTime - (1000L * _gracePeriodSec);

                if (LOG.isDebugEnabled()) LOG.debug("{}- Pass 2: Searching for sessions expired before {}",_context.getWorkerName(), upperBound);

                selectExpiredSessions.setLong(1, upperBound);
                try (ResultSet result = selectExpiredSessions.executeQuery())
                {
                    while (result.next())
                    {
                        String sessionId = result.getString(_sessionTableSchema.getIdColumn());
                        String ctxtpth = result.getString(_sessionTableSchema.getContextPathColumn());
                        String vh = result.getString(_sessionTableSchema.getVirtualHostColumn());
                        expiredSessionKeys.add(sessionId);
                        if (LOG.isDebugEnabled()) LOG.debug ("{}- Found expired sessionId=",_context.getWorkerName(), sessionId);
                    }
                }
            }


            Set<String> notExpiredInDB = new HashSet<>();
            for (String k: candidates)
            {
                //there are some keys that the session store thought had expired, but were not
                //found in our sweep either because it is no longer in the db, or its
                //expiry time was updated
                if (!expiredSessionKeys.contains(k))
                    notExpiredInDB.add(k);
            }


            if (!notExpiredInDB.isEmpty())
            {
                //we have some sessions to check
                try (PreparedStatement checkSessionExists = _sessionTableSchema.getCheckSessionExistsStatement(connection, _context))
                {
                    for (String k: notExpiredInDB)
                    {
                        checkSessionExists.setString(1, k);
                        try (ResultSet result = checkSessionExists.executeQuery())
                        {        
                            if (!result.next())
                            {
                                //session doesn't exist any more, can be expired
                                expiredSessionKeys.add(k);
                            }
                            //else its expiry time has not been reached
                        }
                        catch (Exception e)
                        {
                            LOG.warn("{} Problem checking if potentially expired session {} exists in db", _context.getWorkerName(), k,e);
                        }
                    }

                }
            }

            return expiredSessionKeys;
        }
        catch (Exception e)
        {
            LOG.warn(e);
            return expiredSessionKeys; //return whatever we got
        } 
    }
    
    
    public void setDatabaseAdaptor (DatabaseAdaptor dbAdaptor)
    {
        checkStarted();
        updateBean(_dbAdaptor, dbAdaptor);
        _dbAdaptor = dbAdaptor;
    }
    
    public void setSessionTableSchema (SessionTableSchema schema)
    {
        checkStarted();       
        updateBean(_sessionTableSchema, schema);
        _sessionTableSchema = schema;
        _schemaProvided = true;
    }

  

   @Override
   @ManagedAttribute(value="does this store serialize sessions", readonly=true)
   public boolean isPassivating()
   {
       return true;
   }




   @Override
   public boolean exists(String id)
   throws Exception
   {
       try (Connection connection = _dbAdaptor.getConnection())
       {
           connection.setAutoCommit(true);

           //non-expired session exists?
           try (PreparedStatement checkSessionExists = _sessionTableSchema.getCheckSessionExistsStatement(connection, _context))
           {
               checkSessionExists.setString(1, id);
               try (ResultSet result = checkSessionExists.executeQuery())
               {        
                   if (!result.next())
                   {
                       return false; //no such session
                   }
                   else
                   {
                       long expiry = result.getLong(_sessionTableSchema.getExpiryTimeColumn());
                       if (expiry <= 0) //never expires
                           return true;
                       else
                           return (expiry > System.currentTimeMillis()); //hasn't already expired
                   }
               }
           }
       }
   }
}
