/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.catalina.session;


import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Deque;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.Engine;
import org.apache.catalina.Globals;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.Manager;
import org.apache.catalina.Session;
import org.apache.catalina.SessionIdGenerator;
import org.apache.catalina.util.LifecycleMBeanBase;
import org.apache.catalina.util.SessionIdGeneratorBase;
import org.apache.catalina.util.StandardSessionIdGenerator;
import org.apache.catalina.util.ToStringUtil;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.res.StringManager;


/**
 * Minimal implementation of the <b>Manager</b> interface that supports
 * no session persistence or distributable capabilities.  This class may
 * be subclassed to create more sophisticated Manager implementations.
 *
 * @author Craig R. McClanahan
 */
public abstract class ManagerBase extends LifecycleMBeanBase implements Manager {

    private final Log log = LogFactory.getLog(ManagerBase.class); // must not be static

    // ----------------------------------------------------- Instance Variables

    /**
     * The Context with which this Manager is associated.
     */
    private Context context;


    /**
     * The descriptive name of this Manager implementation (for logging).
     */
    private static final String name = "ManagerBase";


    /**
     * The Java class name of the secure random number generator class to be
     * used when generating session identifiers. The random number generator
     * class must be self-seeding and have a zero-argument constructor. If not
     * specified, an instance of {@link java.security.SecureRandom} will be
     * generated.
     */
    protected String secureRandomClass = null;

    /**
     * The name of the algorithm to use to create instances of
     * {@link java.security.SecureRandom} which are used to generate session
     * IDs. If no algorithm is specified, SHA1PRNG is used. If SHA1PRNG is not
     * available, the platform default will be used. To use the platform default
     * (which may be SHA1PRNG), specify the empty string. If an invalid
     * algorithm and/or provider is specified the SecureRandom instances will be
     * created using the defaults. If that fails, the SecureRandom instances
     * will be created using platform defaults.
     */
    protected String secureRandomAlgorithm = SessionIdGeneratorBase.DEFAULT_SECURE_RANDOM_ALGORITHM;

    /**
     * The name of the provider to use to create instances of
     * {@link java.security.SecureRandom} which are used to generate session
     * IDs. If no provider is specified the platform default is used. If an
     * invalid algorithm and/or provider is specified the SecureRandom instances
     * will be created using the defaults. If that fails, the SecureRandom
     * instances will be created using platform defaults.
     */
    protected String secureRandomProvider = null;

    protected SessionIdGenerator sessionIdGenerator = null;
    protected Class<? extends SessionIdGenerator> sessionIdGeneratorClass = null;

    /**
     * The longest time (in seconds) that an expired session had been alive.
     */
    protected volatile int sessionMaxAliveTime;
    private final Object sessionMaxAliveTimeUpdateLock = new Object();


    protected static final int TIMING_STATS_CACHE_SIZE = 100;

    protected final Deque<SessionTiming> sessionCreationTiming =
            new LinkedList<>();

    protected final Deque<SessionTiming> sessionExpirationTiming =
            new LinkedList<>();

    /**
     * Number of sessions that have expired.
     */
    protected final AtomicLong expiredSessions = new AtomicLong(0);


    /**
     * The set of currently active Sessions for this Manager, keyed by
     * session identifier.
     */
    protected Map<String, Session> sessions = new ConcurrentHashMap<>();

    // Number of sessions created by this manager
    protected long sessionCounter=0;

    protected volatile int maxActive=0;

    private final Object maxActiveUpdateLock = new Object();

    /**
     * The maximum number of active Sessions allowed, or -1 for no limit.
     */
    protected int maxActiveSessions = -1;

    /**
     * Number of session creations that failed due to maxActiveSessions.
     */
    protected int rejectedSessions = 0;

    // number of duplicated session ids - anything >0 means we have problems
    protected volatile int duplicates=0;

    /**
     * Processing time during session expiration.
     */
    protected long processingTime = 0;

    /**
     * Iteration count for background processing.
     */
    private int count = 0;


    /**
     * Frequency of the session expiration, and related manager operations.
     * Manager operations will be done once for the specified amount of
     * backgroundProcess calls (ie, the lower the amount, the most often the
     * checks will occur).
     */
    protected int processExpiresFrequency = 6;

    /**
     * The string manager for this package.
     */
    protected static final StringManager sm = StringManager.getManager(ManagerBase.class);

    /**
     * The property change support for this component.
     */
    protected final PropertyChangeSupport support =
            new PropertyChangeSupport(this);

    private Pattern sessionAttributeNamePattern;

    private Pattern sessionAttributeValueClassNamePattern;

    private boolean warnOnSessionAttributeFilterFailure;

    private boolean notifyBindingListenerOnUnchangedValue;

    private boolean notifyAttributeListenerOnUnchangedValue = true;

    /**
     * Determines whether sessions managed by this manager shall persist (serialize)
     * authentication information or not.
     */
    private boolean persistAuthentication = false;


    // ------------------------------------------------------------ Constructors

    public ManagerBase() {
        if (Globals.IS_SECURITY_ENABLED) {
            // Minimum set required for default distribution/persistence to work
            // plus String
            // plus SerializablePrincipal and String[] (required for authentication persistence)
            setSessionAttributeValueClassNameFilter(
                    "java\\.lang\\.(?:Boolean|Integer|Long|Number|String)"
                    + "|org\\.apache\\.catalina\\.realm\\.GenericPrincipal\\$SerializablePrincipal"
                    + "|\\[Ljava.lang.String;");
            setWarnOnSessionAttributeFilterFailure(true);
        }
    }


    // -------------------------------------------------------------- Properties

    @Override
    public boolean getNotifyAttributeListenerOnUnchangedValue() {
        return notifyAttributeListenerOnUnchangedValue;
    }



    @Override
    public void setNotifyAttributeListenerOnUnchangedValue(boolean notifyAttributeListenerOnUnchangedValue) {
        this.notifyAttributeListenerOnUnchangedValue = notifyAttributeListenerOnUnchangedValue;
    }


    @Override
    public boolean getNotifyBindingListenerOnUnchangedValue() {
        return notifyBindingListenerOnUnchangedValue;
    }


    @Override
    public void setNotifyBindingListenerOnUnchangedValue(boolean notifyBindingListenerOnUnchangedValue) {
        this.notifyBindingListenerOnUnchangedValue = notifyBindingListenerOnUnchangedValue;
    }


    /**
     * Obtain the regular expression used to filter session attribute based on
     * attribute name. The regular expression is anchored so it must match the
     * entire name
     *
     * @return The regular expression currently used to filter attribute names.
     *         {@code null} means no filter is applied. If an empty string is
     *         specified then no names will match the filter and all attributes
     *         will be blocked.
     */
    public String getSessionAttributeNameFilter() {
        if (sessionAttributeNamePattern == null) {
            return null;
        }
        return sessionAttributeNamePattern.toString();
    }


    /**
     * Set the regular expression to use to filter session attributes based on
     * attribute name. The regular expression is anchored so it must match the
     * entire name.
     *
     * @param sessionAttributeNameFilter The regular expression to use to filter
     *        session attributes based on attribute name. Use {@code null} if no
     *        filtering is required. If an empty string is specified then no
     *        names will match the filter and all attributes will be blocked.
     *
     * @throws PatternSyntaxException If the expression is not valid
     */
    public void setSessionAttributeNameFilter(String sessionAttributeNameFilter)
            throws PatternSyntaxException {
        if (sessionAttributeNameFilter == null || sessionAttributeNameFilter.length() == 0) {
            sessionAttributeNamePattern = null;
        } else {
            sessionAttributeNamePattern = Pattern.compile(sessionAttributeNameFilter);
        }
    }


    /**
     * Provides {@link #getSessionAttributeNameFilter()} as a pre-compiled
     * regular expression pattern.
     *
     * @return The pre-compiled pattern used to filter session attributes based
     *         on attribute name. {@code null} means no filter is applied.
     */
    protected Pattern getSessionAttributeNamePattern() {
        return sessionAttributeNamePattern;
    }


    /**
     * Obtain the regular expression used to filter session attribute based on
     * the implementation class of the value. The regular expression is anchored
     * and must match the fully qualified class name.
     *
     * @return The regular expression currently used to filter class names.
     *         {@code null} means no filter is applied. If an empty string is
     *         specified then no names will match the filter and all attributes
     *         will be blocked.
     */
    public String getSessionAttributeValueClassNameFilter() {
        if (sessionAttributeValueClassNamePattern == null) {
            return null;
        }
        return sessionAttributeValueClassNamePattern.toString();
    }


    /**
     * Provides {@link #getSessionAttributeValueClassNameFilter()} as a
     * pre-compiled regular expression pattern.
     *
     * @return The pre-compiled pattern used to filter session attributes based
     *         on the implementation class name of the value. {@code null} means
     *         no filter is applied.
     */
    protected Pattern getSessionAttributeValueClassNamePattern() {
        return sessionAttributeValueClassNamePattern;
    }


    /**
     * Set the regular expression to use to filter classes used for session
     * attributes. The regular expression is anchored and must match the fully
     * qualified class name.
     *
     * @param sessionAttributeValueClassNameFilter The regular expression to use
     *            to filter session attributes based on class name. Use {@code
     *            null} if no filtering is required. If an empty string is
     *           specified then no names will match the filter and all
     *           attributes will be blocked.
     *
     * @throws PatternSyntaxException If the expression is not valid
     */
    public void setSessionAttributeValueClassNameFilter(String sessionAttributeValueClassNameFilter)
            throws PatternSyntaxException {
        if (sessionAttributeValueClassNameFilter == null ||
                sessionAttributeValueClassNameFilter.length() == 0) {
            sessionAttributeValueClassNamePattern = null;
        } else {
            sessionAttributeValueClassNamePattern =
                    Pattern.compile(sessionAttributeValueClassNameFilter);
        }
    }


    /**
     * Should a warn level log message be generated if a session attribute is
     * not persisted / replicated / restored.
     *
     * @return {@code true} if a warn level log message should be generated
     */
    public boolean getWarnOnSessionAttributeFilterFailure() {
        return warnOnSessionAttributeFilterFailure;
    }


    /**
     * Configure whether or not a warn level log message should be generated if
     * a session attribute is not persisted / replicated / restored.
     *
     * @param warnOnSessionAttributeFilterFailure {@code true} if the
     *            warn level message should be generated
     *
     */
    public void setWarnOnSessionAttributeFilterFailure(
            boolean warnOnSessionAttributeFilterFailure) {
        this.warnOnSessionAttributeFilterFailure = warnOnSessionAttributeFilterFailure;
    }


    @Override
    public Context getContext() {
        return context;
    }


    @Override
    public void setContext(Context context) {
        if (this.context == context) {
            // NO-OP
            return;
        }
        if (!getState().equals(LifecycleState.NEW)) {
            throw new IllegalStateException(sm.getString("managerBase.setContextNotNew"));
        }
        Context oldContext = this.context;
        this.context = context;
        support.firePropertyChange("context", oldContext, this.context);
    }


    /**
     * @return The name of the implementation class.
     */
    public String getClassName() {
        return this.getClass().getName();
    }


    @Override
    public SessionIdGenerator getSessionIdGenerator() {
        if (sessionIdGenerator != null) {
            return sessionIdGenerator;
        } else if (sessionIdGeneratorClass != null) {
            try {
                sessionIdGenerator = sessionIdGeneratorClass.getConstructor().newInstance();
                return sessionIdGenerator;
            } catch(ReflectiveOperationException ex) {
                // Ignore
            }
        }
        return null;
    }


    @Override
    public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator) {
        this.sessionIdGenerator = sessionIdGenerator;
        sessionIdGeneratorClass = sessionIdGenerator.getClass();
    }


    /**
     * @return The descriptive short name of this Manager implementation.
     */
    public String getName() {
        return name;
    }

    /**
     * @return The secure random number generator class name.
     */
    public String getSecureRandomClass() {
        return this.secureRandomClass;
    }


    /**
     * Set the secure random number generator class name.
     *
     * @param secureRandomClass The new secure random number generator class
     *                          name
     */
    public void setSecureRandomClass(String secureRandomClass) {

        String oldSecureRandomClass = this.secureRandomClass;
        this.secureRandomClass = secureRandomClass;
        support.firePropertyChange("secureRandomClass", oldSecureRandomClass,
                                   this.secureRandomClass);

    }


    /**
     * @return The secure random number generator algorithm name.
     */
    public String getSecureRandomAlgorithm() {
        return secureRandomAlgorithm;
    }


    /**
     * Set the secure random number generator algorithm name.
     *
     * @param secureRandomAlgorithm The new secure random number generator
     *                              algorithm name
     */
    public void setSecureRandomAlgorithm(String secureRandomAlgorithm) {
        this.secureRandomAlgorithm = secureRandomAlgorithm;
    }


    /**
     * @return The secure random number generator provider name.
     */
    public String getSecureRandomProvider() {
        return secureRandomProvider;
    }


    /**
     * Set the secure random number generator provider name.
     *
     * @param secureRandomProvider The new secure random number generator
     *                             provider name
     */
    public void setSecureRandomProvider(String secureRandomProvider) {
        this.secureRandomProvider = secureRandomProvider;
    }


    @Override
    public int getRejectedSessions() {
        return rejectedSessions;
    }


    @Override
    public long getExpiredSessions() {
        return expiredSessions.get();
    }


    @Override
    public void setExpiredSessions(long expiredSessions) {
        this.expiredSessions.set(expiredSessions);
    }

    public long getProcessingTime() {
        return processingTime;
    }


    public void setProcessingTime(long processingTime) {
        this.processingTime = processingTime;
    }

    /**
     * @return The frequency of manager checks.
     */
    public int getProcessExpiresFrequency() {
        return this.processExpiresFrequency;
    }

    /**
     * Set the manager checks frequency.
     *
     * @param processExpiresFrequency the new manager checks frequency
     */
    public void setProcessExpiresFrequency(int processExpiresFrequency) {

        if (processExpiresFrequency <= 0) {
            return;
        }

        int oldProcessExpiresFrequency = this.processExpiresFrequency;
        this.processExpiresFrequency = processExpiresFrequency;
        support.firePropertyChange("processExpiresFrequency",
                                   Integer.valueOf(oldProcessExpiresFrequency),
                                   Integer.valueOf(this.processExpiresFrequency));

    }


    /**
     * Return whether sessions managed by this manager shall persist authentication
     * information or not.
     *
     * @return {@code true}, sessions managed by this manager shall persist
     *         authentication information; {@code false} otherwise
     */
    public boolean getPersistAuthentication() {
        return this.persistAuthentication;
    }

    /**
     * Set whether sessions managed by this manager shall persist authentication
     * information or not.
     *
     * @param persistAuthentication if {@code true}, sessions managed by this manager
     *                              shall persist authentication information
     */
    public void setPersistAuthentication(boolean persistAuthentication) {
        this.persistAuthentication = persistAuthentication;
    }


    // --------------------------------------------------------- Public Methods

    /**
     * {@inheritDoc}
     * <p>
     * Direct call to {@link #processExpires()}
     */
    @Override
    public void backgroundProcess() {
        count = (count + 1) % processExpiresFrequency;
        if (count == 0) {
            processExpires();
        }
    }

    /**
     * Invalidate all sessions that have expired.
     */
    public void processExpires() {

        long timeNow = System.currentTimeMillis();
        Session sessions[] = findSessions();
        int expireHere = 0 ;

        if(log.isDebugEnabled()) {
            log.debug("Start expire sessions " + getName() + " at " + timeNow + " sessioncount " + sessions.length);
        }
        for (Session session : sessions) {
            if (session != null && !session.isValid()) {
                expireHere++;
            }
        }
        long timeEnd = System.currentTimeMillis();
        if(log.isDebugEnabled()) {
            log.debug("End expire sessions " + getName() + " processingTime " + (timeEnd - timeNow) + " expired sessions: " + expireHere);
        }
        processingTime += ( timeEnd - timeNow );

    }


    @Override
    protected void initInternal() throws LifecycleException {
        super.initInternal();

        if (context == null) {
            throw new LifecycleException(sm.getString("managerBase.contextNull"));
        }
    }


    @Override
    protected void startInternal() throws LifecycleException {

        // Ensure caches for timing stats are the right size by filling with
        // nulls.
        while (sessionCreationTiming.size() < TIMING_STATS_CACHE_SIZE) {
            sessionCreationTiming.add(null);
        }
        while (sessionExpirationTiming.size() < TIMING_STATS_CACHE_SIZE) {
            sessionExpirationTiming.add(null);
        }

        /* Create sessionIdGenerator if not explicitly configured */
        SessionIdGenerator sessionIdGenerator = getSessionIdGenerator();
        if (sessionIdGenerator == null) {
            sessionIdGenerator = new StandardSessionIdGenerator();
            setSessionIdGenerator(sessionIdGenerator);
        }

        sessionIdGenerator.setJvmRoute(getJvmRoute());
        if (sessionIdGenerator instanceof SessionIdGeneratorBase) {
            SessionIdGeneratorBase sig = (SessionIdGeneratorBase)sessionIdGenerator;
            sig.setSecureRandomAlgorithm(getSecureRandomAlgorithm());
            sig.setSecureRandomClass(getSecureRandomClass());
            sig.setSecureRandomProvider(getSecureRandomProvider());
        }

        if (sessionIdGenerator instanceof Lifecycle) {
            ((Lifecycle) sessionIdGenerator).start();
        } else {
            // Force initialization of the random number generator
            if (log.isDebugEnabled()) {
                log.debug("Force random number initialization starting");
            }
            sessionIdGenerator.generateSessionId();
            if (log.isDebugEnabled()) {
                log.debug("Force random number initialization completed");
            }
        }
    }


    @Override
    protected void stopInternal() throws LifecycleException {
        if (sessionIdGenerator instanceof Lifecycle) {
            ((Lifecycle) sessionIdGenerator).stop();
        }
    }


    @Override
    public void add(Session session) {
        sessions.put(session.getIdInternal(), session);
        int size = getActiveSessions();
        if( size > maxActive ) {
            synchronized(maxActiveUpdateLock) {
                if( size > maxActive ) {
                    maxActive = size;
                }
            }
        }
    }


    @Override
    public void addPropertyChangeListener(PropertyChangeListener listener) {
        support.addPropertyChangeListener(listener);
    }


    @Override
    public Session createSession(String sessionId) {

        if ((maxActiveSessions >= 0) &&
                (getActiveSessions() >= maxActiveSessions)) {
            rejectedSessions++;
            throw new TooManyActiveSessionsException(
                    sm.getString("managerBase.createSession.ise"),
                    maxActiveSessions);
        }

        // Recycle or create a Session instance
        Session session = createEmptySession();

        // Initialize the properties of the new session and return it
        session.setNew(true);
        session.setValid(true);
        session.setCreationTime(System.currentTimeMillis());
        session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);
        String id = sessionId;
        if (id == null) {
            id = generateSessionId();
        }
        session.setId(id);
        sessionCounter++;

        SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
        synchronized (sessionCreationTiming) {
            sessionCreationTiming.add(timing);
            sessionCreationTiming.poll();
        }
        return session;
    }


    @Override
    public Session createEmptySession() {
        return getNewSession();
    }


    @Override
    public Session findSession(String id) throws IOException {
        if (id == null) {
            return null;
        }
        return sessions.get(id);
    }


    @Override
    public Session[] findSessions() {
        return sessions.values().toArray(new Session[0]);
    }


    @Override
    public void remove(Session session) {
        remove(session, false);
    }


    @Override
    public void remove(Session session, boolean update) {
        // If the session has expired - as opposed to just being removed from
        // the manager because it is being persisted - update the expired stats
        if (update) {
            long timeNow = System.currentTimeMillis();
            int timeAlive =
                (int) (timeNow - session.getCreationTimeInternal())/1000;
            updateSessionMaxAliveTime(timeAlive);
            expiredSessions.incrementAndGet();
            SessionTiming timing = new SessionTiming(timeNow, timeAlive);
            synchronized (sessionExpirationTiming) {
                sessionExpirationTiming.add(timing);
                sessionExpirationTiming.poll();
            }
        }

        if (session.getIdInternal() != null) {
            sessions.remove(session.getIdInternal());
        }
    }


    @Override
    public void removePropertyChangeListener(PropertyChangeListener listener) {
        support.removePropertyChangeListener(listener);
    }


    @Override
    public void changeSessionId(Session session) {
        rotateSessionId(session);
    }


    @Override
    public String rotateSessionId(Session session) {
        String newId = generateSessionId();
        changeSessionId(session, newId, true, true);
        return newId;
    }


    @Override
    public void changeSessionId(Session session, String newId) {
        changeSessionId(session, newId, true, true);
    }


    protected void changeSessionId(Session session, String newId,
            boolean notifySessionListeners, boolean notifyContainerListeners) {
        String oldId = session.getIdInternal();
        session.setId(newId, false);
        session.tellChangedSessionId(newId, oldId,
                notifySessionListeners, notifyContainerListeners);
    }


    /**
     * {@inheritDoc}
     * <p>
     * This implementation excludes session attributes from distribution if the:
     * <ul>
     * <li>attribute name matches {@link #getSessionAttributeNameFilter()}</li>
     * </ul>
     */
    @Override
    public boolean willAttributeDistribute(String name, Object value) {
        Pattern sessionAttributeNamePattern = getSessionAttributeNamePattern();
        if (sessionAttributeNamePattern != null) {
            if (!sessionAttributeNamePattern.matcher(name).matches()) {
                if (getWarnOnSessionAttributeFilterFailure() || log.isDebugEnabled()) {
                    String msg = sm.getString("managerBase.sessionAttributeNameFilter",
                            name, sessionAttributeNamePattern);
                    if (getWarnOnSessionAttributeFilterFailure()) {
                        log.warn(msg);
                    } else {
                        log.debug(msg);
                    }
                }
                return false;
            }
        }

        Pattern sessionAttributeValueClassNamePattern = getSessionAttributeValueClassNamePattern();
        if (value != null && sessionAttributeValueClassNamePattern != null) {
            if (!sessionAttributeValueClassNamePattern.matcher(
                    value.getClass().getName()).matches()) {
                if (getWarnOnSessionAttributeFilterFailure() || log.isDebugEnabled()) {
                    String msg = sm.getString("managerBase.sessionAttributeValueClassNameFilter",
                            name, value.getClass().getName(), sessionAttributeValueClassNamePattern);
                    if (getWarnOnSessionAttributeFilterFailure()) {
                        log.warn(msg);
                    } else {
                        log.debug(msg);
                    }
                }
                return false;
            }
        }

        return true;
    }


    // ------------------------------------------------------ Protected Methods


    /**
     * Get new session class to be used in the doLoad() method.
     * @return a new session for use with this manager
     */
    protected StandardSession getNewSession() {
        return new StandardSession(this);
    }


    /**
     * Generate and return a new session identifier.
     * @return a new session id
     */
    protected String generateSessionId() {

        String result = null;

        do {
            if (result != null) {
                // Not thread-safe but if one of multiple increments is lost
                // that is not a big deal since the fact that there was any
                // duplicate is a much bigger issue.
                duplicates++;
            }

            result = sessionIdGenerator.generateSessionId();

        } while (sessions.containsKey(result));

        return result;
    }


    // ------------------------------------------------------ Protected Methods


    /**
     * Retrieve the enclosing Engine for this Manager.
     *
     * @return an Engine object (or null).
     */
    public Engine getEngine() {
        Engine e = null;
        for (Container c = getContext(); e == null && c != null ; c = c.getParent()) {
            if (c instanceof Engine) {
                e = (Engine)c;
            }
        }
        return e;
    }


    /**
     * Retrieve the JvmRoute for the enclosing Engine.
     * @return the JvmRoute or null.
     */
    public String getJvmRoute() {
        Engine e = getEngine();
        return e == null ? null : e.getJvmRoute();
    }


    // -------------------------------------------------------- Package Methods


    @Override
    public void setSessionCounter(long sessionCounter) {
        this.sessionCounter = sessionCounter;
    }


    @Override
    public long getSessionCounter() {
        return sessionCounter;
    }


    /**
     * Number of duplicated session IDs generated by the random source.
     * Anything bigger than 0 means problems.
     *
     * @return The count of duplicates
     */
    public int getDuplicates() {
        return duplicates;
    }


    public void setDuplicates(int duplicates) {
        this.duplicates = duplicates;
    }


    @Override
    public int getActiveSessions() {
        return sessions.size();
    }


    @Override
    public int getMaxActive() {
        return maxActive;
    }


    @Override
    public void setMaxActive(int maxActive) {
        synchronized (maxActiveUpdateLock) {
            this.maxActive = maxActive;
        }
    }


    /**
     * @return The maximum number of active Sessions allowed, or -1 for no
     *         limit.
     */
    public int getMaxActiveSessions() {
        return this.maxActiveSessions;
    }


    /**
     * Set the maximum number of active Sessions allowed, or -1 for
     * no limit.
     *
     * @param max The new maximum number of sessions
     */
    public void setMaxActiveSessions(int max) {

        int oldMaxActiveSessions = this.maxActiveSessions;
        this.maxActiveSessions = max;
        support.firePropertyChange("maxActiveSessions",
                                   Integer.valueOf(oldMaxActiveSessions),
                                   Integer.valueOf(this.maxActiveSessions));

    }


    @Override
    public int getSessionMaxAliveTime() {
        return sessionMaxAliveTime;
    }


    @Override
    public void setSessionMaxAliveTime(int sessionMaxAliveTime) {
        synchronized (sessionMaxAliveTimeUpdateLock) {
            this.sessionMaxAliveTime = sessionMaxAliveTime;
        }
    }


    /**
     * Updates the sessionMaxAliveTime attribute if the candidate value is
     * larger than the current value.
     *
     * @param sessionAliveTime  The candidate value (in seconds) for the new
     *                          sessionMaxAliveTime value.
     */
    public void updateSessionMaxAliveTime(int sessionAliveTime) {
        if (sessionAliveTime > this.sessionMaxAliveTime) {
            synchronized (sessionMaxAliveTimeUpdateLock) {
                if (sessionAliveTime > this.sessionMaxAliveTime) {
                    this.sessionMaxAliveTime = sessionAliveTime;
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * Based on the last 100 sessions to expire. If less than 100 sessions have
     * expired then all available data is used.
     */
    @Override
    public int getSessionAverageAliveTime() {
        // Copy current stats
        List<SessionTiming> copy;
        synchronized (sessionExpirationTiming) {
            copy = new ArrayList<>(sessionExpirationTiming);
        }

        // Init
        int counter = 0;
        int result = 0;

        // Calculate average
        for (SessionTiming timing : copy) {
            if (timing != null) {
                int timeAlive = timing.getDuration();
                counter++;
                // Very careful not to overflow - probably not necessary
                result =
                    (result * ((counter - 1)/counter)) + (timeAlive/counter);
            }
        }
        return result;
    }


    /**
     * {@inheritDoc}<p>
     * Based on the creation time of the previous 100 sessions created. If less
     * than 100 sessions have been created then all available data is used.
     */
    @Override
    public int getSessionCreateRate() {
        // Copy current stats
        List<SessionTiming> copy;
        synchronized (sessionCreationTiming) {
            copy = new ArrayList<>(sessionCreationTiming);
        }

        return calculateRate(copy);
    }


    /**
     * {@inheritDoc}
     * <p>
     * Based on the expiry time of the previous 100 sessions expired. If less
     * than 100 sessions have expired then all available data is used.
     *
     * @return  The current rate (in sessions per minute) of session expiration
     */
    @Override
    public int getSessionExpireRate() {
        // Copy current stats
        List<SessionTiming> copy;
        synchronized (sessionExpirationTiming) {
            copy = new ArrayList<>(sessionExpirationTiming);
        }

        return calculateRate(copy);
    }


    private static int calculateRate(List<SessionTiming> sessionTiming) {
        // Init
        long now = System.currentTimeMillis();
        long oldest = now;
        int counter = 0;
        int result = 0;

        // Calculate rate
        for (SessionTiming timing : sessionTiming) {
            if (timing != null) {
                counter++;
                if (timing.getTimestamp() < oldest) {
                    oldest = timing.getTimestamp();
                }
            }
        }
        if (counter > 0) {
            if (oldest < now) {
                result = (1000*60*counter)/(int) (now - oldest);
            } else {
                // Better than reporting zero
                result = Integer.MAX_VALUE;
            }
        }
        return result;
    }


    /**
     * For debugging.
     *
     * @return A space separated list of all session IDs currently active
     */
    public String listSessionIds() {
        StringBuilder sb = new StringBuilder();
        for (String s : sessions.keySet()) {
            sb.append(s).append(' ');
        }
        return sb.toString();
    }


    /**
     * For debugging.
     *
     * @param sessionId The ID for the session of interest
     * @param key       The key for the attribute to obtain
     *
     * @return The attribute value for the specified session, if found, null
     *         otherwise
     */
    public String getSessionAttribute( String sessionId, String key ) {
        Session s = sessions.get(sessionId);
        if (s == null) {
            if (log.isInfoEnabled()) {
                log.info(sm.getString("managerBase.sessionNotFound", sessionId));
            }
            return null;
        }
        Object o=s.getSession().getAttribute(key);
        if( o==null ) {
            return null;
        }
        return o.toString();
    }


    /**
     * Returns information about the session with the given session id.
     *
     * <p>The session information is organized as a HashMap, mapping
     * session attribute names to the String representation of their values.
     *
     * @param sessionId Session id
     *
     * @return HashMap mapping session attribute names to the String
     * representation of their values, or null if no session with the
     * specified id exists, or if the session does not have any attributes
     */
    public HashMap<String, String> getSession(String sessionId) {
        Session s = sessions.get(sessionId);
        if (s == null) {
            if (log.isInfoEnabled()) {
                log.info(sm.getString("managerBase.sessionNotFound", sessionId));
            }
            return null;
        }

        Enumeration<String> ee = s.getSession().getAttributeNames();
        if (ee == null || !ee.hasMoreElements()) {
            return null;
        }

        HashMap<String, String> map = new HashMap<>();
        while (ee.hasMoreElements()) {
            String attrName = ee.nextElement();
            map.put(attrName, getSessionAttribute(sessionId, attrName));
        }

        return map;
    }


    public void expireSession( String sessionId ) {
        Session s = sessions.get(sessionId);
        if (s == null) {
            if (log.isInfoEnabled()) {
                log.info(sm.getString("managerBase.sessionNotFound", sessionId));
            }
            return;
        }
        s.expire();
    }

    public long getThisAccessedTimestamp( String sessionId ) {
        Session s = sessions.get(sessionId);
        if (s == null) {
            if (log.isInfoEnabled()) {
                log.info(sm.getString("managerBase.sessionNotFound", sessionId));
            }
            return -1;
        }
        return s.getThisAccessedTime();
    }

    public String getThisAccessedTime( String sessionId ) {
        Session s = sessions.get(sessionId);
        if (s == null) {
            if (log.isInfoEnabled()) {
                log.info(sm.getString("managerBase.sessionNotFound", sessionId));
            }
            return "";
        }
        return new Date(s.getThisAccessedTime()).toString();
    }

    public long getLastAccessedTimestamp( String sessionId ) {
        Session s = sessions.get(sessionId);
        if (s == null) {
            if (log.isInfoEnabled()) {
                log.info(sm.getString("managerBase.sessionNotFound", sessionId));
            }
            return -1;
        }
        return s.getLastAccessedTime();
    }

    public String getLastAccessedTime( String sessionId ) {
        Session s = sessions.get(sessionId);
        if (s == null) {
            if (log.isInfoEnabled()) {
                log.info(sm.getString("managerBase.sessionNotFound", sessionId));
            }
            return "";
        }
        return new Date(s.getLastAccessedTime()).toString();
    }

    public String getCreationTime( String sessionId ) {
        Session s = sessions.get(sessionId);
        if (s == null) {
            if (log.isInfoEnabled()) {
                log.info(sm.getString("managerBase.sessionNotFound", sessionId));
            }
            return "";
        }
        return new Date(s.getCreationTime()).toString();
    }

    public long getCreationTimestamp( String sessionId ) {
        Session s = sessions.get(sessionId);
        if (s == null) {
            if (log.isInfoEnabled()) {
                log.info(sm.getString("managerBase.sessionNotFound", sessionId));
            }
            return -1;
        }
        return s.getCreationTime();
    }


    @Override
    public String toString() {
        return ToStringUtil.toString(this, context);
    }


    // -------------------- JMX and Registration  --------------------
    @Override
    public String getObjectNameKeyProperties() {

        StringBuilder name = new StringBuilder("type=Manager");

        name.append(",host=");
        name.append(context.getParent().getName());

        name.append(",context=");
        String contextName = context.getName();
        if (!contextName.startsWith("/")) {
            name.append('/');
        }
        name.append(contextName);

        return name.toString();
    }

    @Override
    public String getDomainInternal() {
        return context.getDomain();
    }


    // ----------------------------------------------------------- Inner classes

    protected static final class SessionTiming {
        private final long timestamp;
        private final int duration;

        public SessionTiming(long timestamp, int duration) {
            this.timestamp = timestamp;
            this.duration = duration;
        }

        /**
         * @return Time stamp associated with this piece of timing information
         *         in milliseconds.
         */
        public long getTimestamp() {
            return timestamp;
        }

        /**
         * @return Duration associated with this piece of timing information in
         *         seconds.
         */
        public int getDuration() {
            return duration;
        }
    }
}
