/**
 * @version $Revision: 1.1.2.2 $
 */
 
package com.iplanet.server.http.session;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Properties;
import javax.servlet.ServletContext;
import javax.servlet.http.*;

import java.io.IOException;

import com.iplanet.server.http.util.LogUtil;
import com.iplanet.server.http.util.ResUtil;
import com.iplanet.server.http.util.CfgUtil;
import com.iplanet.server.http.util.LockManager;
import com.iplanet.server.http.servlet.NSServletRunner;
import com.iplanet.server.http.servlet.VirtualServer;


/**
 * iPlanet Web Server 6.0's session manager implementation.
 *
 * @deprecated
 */
public class IWSSessionManager extends IWSHttpSessionManager
{
    /**
     * Amount of time in seconds a session can remain inactive before
     * it can be removed, with a default value of <tt>30 minutes</tt>
     */
    private int _timeOut = IWSHttpSessionManager.DEFAULT_TIMEOUT;

    /**
     * The hash table to store all the current sessions
     */
    private Hashtable _sessions = null;

    /**
     * The default maximum number of sessions allowed.
     */
    private int _maxSessions = 1000;

    /**
     * Indicates whether failover mode is on or not
     */
    private boolean _failoverEnabled = false;

    /**
     * Indicates success/failure of the initialization of this object.
     */
    private boolean _initialized   = false;

    /**
     * A running count of the number of sessions that have been reaped
     * since the webserver started up.
     */
    private int _reapCount = 0;

    /**
     * Data store that implements session persistence.
     */
    private SessionDataStore _store = null;

    /**
     * A utility class which takes care of internationalizing the
     * messages sent to the error log.
     */
    private static ResUtil _res = ResUtil.getDefaultResUtil();

    /**
     * Indicates whether there is more than one webserver process
     * running for this webserver instance (i.e. MaxProcs > 1).
     *
     * A store is always needed in multi-process mode and failoverEnabled_
     * is always set to true in this mode.
     */
    private static boolean _multiProcessMode = !(CfgUtil.isSingleProcess());

    /**
     * The default store that will be used (in multi-process mode) if none is 
     * configured.
     */
    private static final String DEFAULT_STORE_CLASS = "com.iplanet.server.http.session.FileStore";

    /**
     * Implements a cross-process locking mechanism for use in maintaining 
     * session integrity for all the sessions that are managed by this
     * manager. A cross-process locking mechanism is used only when the 
     * webserver is running in multi-process mode.
     *
     * This manager may be managing sessions for a single web-app or for
     * all the web-apps in a particular virtual server.
     */
    private LockManager _mpLockMgr = null;

    /**
     * Implements a cross-process locking mechanism for use in ensuring 
     * that the data store is reaped by one process at a time.
     *
     * This is used only when the webserver is running in multi-process mode.
     */
    private LockManager _mpStoreLockMgr = null;


    public IWSSessionManager()
    {
        _sessions = new Hashtable();
    }


    public void init(Properties config)
    {
        if (config != null || _initialized)
        {
            String s = config.getProperty("timeOut");
        
            if (s != null)
            {
                try
                {
                    _timeOut = Integer.parseInt(s);
                    if (_timeOut < 0)
                        _timeOut = -1;
                }
                catch (NumberFormatException nfe)
                {
                    LogUtil.logWarning(_res.getProp("session.IWSSessionManager.msg_timeoutNotValid", s));
                }
            }
        
            s = config.getProperty("maxSessions");
            if (s != null)
            {
                try
                {
                    _maxSessions = Integer.parseInt(s);
                }
                catch (NumberFormatException nfe)
                {
                    LogUtil.logWarning(_res.getProp("session.IWSSessionManager.msg_maxSessionsNotValid ", s));
                }
            }
            Integer i = new Integer(_maxSessions);
            LogUtil.logInfo(_res.getProp("session.IWSSessionManager.msg_Settings", i));
            _initialized = true;

            s = config.getProperty("session-failover-enabled");
            if (s != null)
                _failoverEnabled = Boolean.valueOf(s).booleanValue();


            if (_multiProcessMode)
            {
                //
                // Inform the user that a default store will be configured
                // if the session-data-store directive is missing and that
                // session-failover-enabled will always be set to true in
                // multi-process mode
                //
                LogUtil.logInfo(_res.getProp("session.IWSSessionManager.msg_mpMode"));

                // Failover is ALWAYS enabled in multi-process mode
                if (!_failoverEnabled)
                    _failoverEnabled = true;

                // Create and initialize the cross-process lock manager

                s = config.getProperty(VirtualServer.SESSION_CONTEXT_NAME);
                if (s == null)
                {
                    // Old style servlet session managers don't set a
                    // context name so use a default name
                    s = "default-iwssessionmanager";
                }

                // Check if the XML configuration specifies the number
                // of cross-process locks to use
                int nLocks = LockManager.DEFAULT_NUM_LOCKS;
                String locks = config.getProperty("maxLocks");
                if (locks != null)
                {
                    try
                    {
                        nLocks = Integer.parseInt(locks);
                    }
                    catch (NumberFormatException nfe)
                    {
                        LogUtil.logWarning(_res.getProp("session.IWSSessionManager.msg_maxLocksNotValid ", new Integer(LockManager.DEFAULT_NUM_LOCKS)));
                    }
                }

                _mpLockMgr = new LockManager(s, nLocks);

                // Create a single cross-process lock to ensure that the store
                // is reaped by a process at a time
                StringBuffer storeLockName = new StringBuffer(s).append("_store");
                _mpStoreLockMgr = new LockManager(storeLockName.toString(), 1);
            }

            s = config.getProperty("session-data-store");

            // If the webserver is in multi-process mode, then a store
            // MUST be configured. If not, a default store is used.
            if (s == null && _multiProcessMode)
                s = DEFAULT_STORE_CLASS;

            if (s != null)
            {
                try
                {
                    _store = (SessionDataStore) Class.forName(s).newInstance();
                    _store.setManager(this);

                    // Don't allow multiple processes to concurrently 
                    // initialize the store

                    if (_multiProcessMode)
                        _mpStoreLockMgr.lock("store");

                    if (_store.init(config) == false)
                        _initialized = false;

                    if (_multiProcessMode)
                        _mpStoreLockMgr.unlock("store");
                }
                catch (Exception e)
                {
                    LogUtil.logInfo(_res.getProp("session.IWSSessionManager.msg_BadClass", LogUtil.getStackTrace(e)));
                    _initialized = false;
                }
            }

        }
    }

    public HttpSession createSession(String id)
    {
        return createSession(id, null);
    }

    public HttpSession createSession(String id, ServletContext context)
    {
        if (!_initialized)
            return null;
        
        String mangledID = null;
        if (_multiProcessMode)
        {
            mangledID = IWSHttpSession.getMangledString(id);
            lock(mangledID);
        }

        IWSHttpSession sn = new IWSHttpSession(id, _timeOut, this, context);
        sn.setNew();
        
        if (_sessions.size() <= _maxSessions)
        {
            if (!_failoverEnabled)
                _sessions.put(id, sn);
        }
        else
        {
            Integer i = new Integer(_maxSessions);
            LogUtil.logInfo(_res.getProp("session.IWSSessionManager.msg_TooManySessions", i));
            sn = null;
        }

        if (_store != null && sn != null)
            _store.save(sn);

        if (LogUtil.enableTrace)
            LogUtil.TRACE(7, "IWSSessionManager: newSession " + id);

        if (sn == null)
        {
            unlock(mangledID);
        }

        return sn;
    }

    /**
     * Returns the current timeout value for sessions.
     *
     * @return  <i>_timeOut</i>
     */ 
    public int getDefaultTimeOut()
    {
        return _timeOut;
    }

    /**
     * Deletes the given session.
     *
     * @param   session which is to be deleted
     * @see     #reaper
     */
    public void deleteSession(HttpSession session)
    {
        if (!_initialized || session == null )
            return;

        if (session instanceof IWSHttpSession)
        {
            IWSHttpSession sn = (IWSHttpSession) session;
            sn.removeAllObjects();      // remove its bound objects
            if (_store != null)
                _store.remove(sn);
            String id = sn.getId();
            if (!_failoverEnabled)
                _sessions.remove(id);

            if (LogUtil.enableTrace)
                LogUtil.TRACE(7, "IWSSessionManager: deleteSession " + id);

            sn.close();
        }
    }

    public HttpSession getSession(String id)
    {
        return getSession(id, null);
    }

    /**
     * Extracts the session with a given id from the hashtable.
     *
     * It returns <code>null</code> if it cannot find a session with the 
     * given id.
     *
     * @param   id  a session id to search for
     * @return  the Session object with the given session id
     */
    public HttpSession getSession(String id, ServletContext context)
    {
        IWSHttpSession session = null;
        boolean newSession = false;
        
        if (_initialized && id != null)
        {
            String mangledID = null;
            if (_multiProcessMode)
            {
                mangledID = IWSHttpSession.getMangledString(id);
                lock(mangledID);
            }

            if (!_failoverEnabled)
                session = (IWSHttpSession)_sessions.get(id);

            if (session == null)
                newSession = true;

            if (_store != null && (_failoverEnabled || newSession))
            {
                IWSHttpSession tmp = new IWSHttpSession(id, _timeOut, this,
                                                        context);
                session = _store.load(tmp);
                if (session != null)
                {
                    session.setIWSContext(this, context);
                    if (!_failoverEnabled)
                        _sessions.put(id, session);
                }
            }
            if (session != null)
            {
                // Update the access time of the last (previous) request
                session.updateAccessTime();

                // This is an existing session and not a new one
                session.unsetNew();

                if (session._isInvalid())
                {
                    // Even though the session was in the hashtable or loaded
                    // from the store, check if it has expired and if so, 
                    // delete it and return null to the user
                    deleteSession(session);
                    session = null;
                }
            }

            if (session == null)
            {
                unlock(mangledID);
                reaped(mangledID);
            }
        }
        
        return (HttpSession) session;
    }

    /**
     * Deletes all the timedout sessions.
     *
     * This method is called by <code>SessionReaper</code> thread. 
     * The <i>reapInterval</i> is the amount of time in seconds 
     * <code>SessionReaper</code> thread sleeps before calling 
     * <code>reaper</code> method again.
     *
     * @see     #deleteSession
     */
    public synchronized void reaper()
    {
        if (!_initialized)
            return;

        ServletContext context = null;
        int count = 0;
        int totalActiveSessions = 0;

        long currentTime = System.currentTimeMillis();

        // The _sessions hashtable is used only in single process
        // non-failover mode (failover is ALWAYS enabled in multi-process mode)
        if (!_failoverEnabled)
        {
            Enumeration e = _sessions.elements();
            if (e != null)
            {
                while (e.hasMoreElements())
                {
                    IWSHttpSession session = (IWSHttpSession)e.nextElement();

                    if (session.isInvalid(currentTime))
                    {
                        _reapCount++;

                        if (context == null)
                            context = session.getServletContext();
                        deleteSession(session);
                        session = null;

                        count++;
                    }
                    else 
                        totalActiveSessions++;
                }
            }
        }
        if (_store != null)
        {
            // Prevent multiple processes from reaping the store simultaneously
            if (_multiProcessMode)
                _mpStoreLockMgr.lock("store");

            //
            // The store must lock/unlock each session (using the session 
            // managers lock/unlock/reaped methods) as it checks the session for
            // expiry or purges it.
            //
            // FileStore implements per-session locking. JdbcStore DOES NOT and
            // hence is unsafe for use in multi-process mode.
            //
            _store.reap(currentTime);

            if (_multiProcessMode)
                _mpStoreLockMgr.unlock("store");
        }
        
        // log stats for this round of reaper run.
        if (count > 0 && LogUtil.enableTrace) {
            String ctx = (context != null) ? context.toString() : "";

            int formLoginSessionCount = 0;
            if (getHandleFormLoginSessions())
                formLoginSessionCount = totalActiveSessions;

            if (LogUtil.enableTrace) {
                LogUtil.TRACE(5, "session reaper stats: " + ctx +
                        " number of live sessions = " + totalActiveSessions + 
                        ", number of sessions expired/total = " + count + "/" + _reapCount + 
                        ", number of live form-login sessions = " + formLoginSessionCount);
            }
        }
    }
    
    /**
     * A hint to the session manager to passivate the session as the 
     * request is complete.
     * 
     * @param session HttpSession object
     */ 
    public void update(HttpSession session)
    {
        if (!_initialized)
            return;
        
        if (session instanceof IWSHttpSession)
        {
            IWSHttpSession sn = (IWSHttpSession) session;
            if (sn.isValid())          // Save valid sessions only
            {
                sn.unsetNew();

                if (LogUtil.enableTrace)
                    LogUtil.TRACE(7, "IWSSessionManager: update " + sn.getId());

                if (_store != null)
                    _store.save(sn);
            }

            // Release the cross-process lock that was acquired in
            // [get/create]Session
            if (_multiProcessMode)
                unlock(sn.getMangledId());  // Even if sn was invalidated the
                                            // unlock still needs to be done
        }
    }

    /**
     * Gracefully shutdown, waiting for the reaper thread to stop.
     *
     * If the multiprocess mode is enabled then the session-data-store
     * is checked for expired sessions and those sessions are purged. Only
     * one process can expire sessions from the store at any time.
     */
    public void close()
    {
        if (_initialized)
        {
            // Stop the reaper thread
            super.close();

            if (_store != null)
            {
                if (_multiProcessMode)
                    _mpStoreLockMgr.lock("store");

                _store.reap(System.currentTimeMillis());

                if (_multiProcessMode)
                    _mpStoreLockMgr.unlock("store");
            }

            if (_multiProcessMode)
            {
                // Destroy the cross-process locks
                if (_mpLockMgr != null)
                    _mpLockMgr.destroy();

                if (_mpStoreLockMgr != null)
                    _mpStoreLockMgr.destroy();
            }
        }
    }

    // stats methods
    public int getMaxSession()
    {
        return _maxSessions;
    }
    
    public int getSessionCount()
    {
        return _sessions.size();
    }

    public int getSessionReapCount()
    {
        return _reapCount;
    }

    protected boolean lock(String id)
    {
        boolean status = false;
        if (_multiProcessMode && _mpLockMgr != null && id != null)
            status = _mpLockMgr.lock(id);
        return status;
    }

    public boolean unlock(String id)
    {
        boolean status = false;
        if (_multiProcessMode && _mpLockMgr != null && id != null)
            status = _mpLockMgr.unlock(id);
        return status;
    }

    protected void reaped(String id)
    {
        if (_multiProcessMode && _mpLockMgr != null && id != null)
            _mpLockMgr.remove(id);
    }

    /**
     * Indicate whether a persistent store has been configured.
     */
    protected boolean hasPersistence()
    {
        return (_store != null);
    }
}
