/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.as.web.session;

import static org.jboss.as.web.WebMessages.MESSAGES;

import java.beans.PropertyChangeSupport;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionActivationListener;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

import org.apache.catalina.Context;
import org.apache.catalina.Globals;
import org.apache.catalina.Manager;
import org.apache.catalina.Session;
import org.apache.catalina.SessionEvent;
import org.apache.catalina.SessionListener;
import org.apache.catalina.security.SecurityUtil;
import org.apache.catalina.session.StandardSession;
import org.apache.catalina.session.StandardSessionFacade;
import org.apache.catalina.util.Enumerator;
import org.apache.catalina.util.StringManager;
import org.jboss.as.clustering.web.DistributableSessionMetadata;
import org.jboss.as.clustering.web.DistributedCacheManager;
import org.jboss.as.clustering.web.IncomingDistributableSessionData;
import org.jboss.as.clustering.web.OutgoingDistributableSessionData;
import org.jboss.as.clustering.web.SessionOwnershipSupport;
import org.jboss.as.web.session.notification.ClusteredSessionManagementStatus;
import org.jboss.as.web.session.notification.ClusteredSessionNotificationCause;
import org.jboss.as.web.session.notification.ClusteredSessionNotificationPolicy;
import org.jboss.logging.Logger;
import org.jboss.metadata.web.jboss.ReplicationTrigger;

/**
 * Abstract base class for session clustering based on StandardSession. Different session replication strategies can be
 * implemented by subclasses.
 *
 * @author Ben Wang
 * @author Brian Stansberry
 *
 * @version $Revision: 109139 $
 */
public abstract class ClusteredSession<O extends OutgoingDistributableSessionData> implements HttpSession, Session {
    protected static final boolean ACTIVITY_CHECK = Globals.STRICT_SERVLET_COMPLIANCE
            || Boolean.valueOf(System.getProperty("org.apache.catalina.session.StandardSession.ACTIVITY_CHECK", "false")).booleanValue();

    /**
     * Descriptive information describing this Session implementation.
     */
    protected static final String info = "ClusteredSession/1.0";

    /**
     * Set of attribute names which are not allowed to be replicated/persisted.
     */
    protected static final String[] excludedAttributes = { Globals.SUBJECT_ATTR };

    /**
     * Set containing all members of {@link #excludedAttributes}.
     */
    protected static final Set<String> replicationExcludes = Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(excludedAttributes)));

    /**
     * The method signature for the <code>fireContainerEvent</code> method.
     */
    protected static final Class<?>[] containerEventTypes = { String.class, Object.class };

    protected static final Logger log = Logger.getLogger(ClusteredSession.class);

    /**
     * The dummy HTTP session context used for servlet spec compliance.
     */
    @SuppressWarnings("deprecation")
    protected static javax.servlet.http.HttpSessionContext sessionContext = new javax.servlet.http.HttpSessionContext() {
        @Override
        public Enumeration<String> getIds() {
            return Collections.enumeration(Collections.<String> emptyList());
        }

        @Override
        public HttpSession getSession(String sessionId) {
            return null;
        }
    };

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

    /** Length of time we do full replication as workaround to JBCACHE-1531 */
    private static final long FULL_REPLICATION_WINDOW_LENGTH = 5000;

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

    /**
     * The collection of user data attributes associated with this Session.
     */
    private final Map<String, Object> attributes = new ConcurrentHashMap<String, Object>(16, 0.75f, 2);

    /**
     * The authentication type used to authenticate our cached Principal, if any. NOTE: This value is not included in the
     * serialized version of this object.
     */
    private transient String authType = null;

    /**
     * The <code>java.lang.Method</code> for the <code>fireContainerEvent()</code> method of the
     * <code>org.apache.catalina.core.StandardContext</code> method, if our Context implementation is of this class. This value
     * is computed dynamically the first time it is needed, or after a session reload (since it is declared transient).
     */
    private transient Method containerEventMethod = null;

    /**
     * The time this session was created, in milliseconds since midnight, January 1, 1970 GMT.
     */
    private long creationTime = 0L;

    /**
     * We are currently processing a session expiration, so bypass certain IllegalStateException tests. NOTE: This value is not
     * included in the serialized version of this object.
     */
    private transient volatile boolean expiring = false;

    /**
     * The facade associated with this session. NOTE: This value is not included in the serialized version of this object.
     */
    private transient StandardSessionFacade facade = null;

    /**
     * The session identifier of this Session.
     */
    private String id = null;

    /**
     * The last accessed time for this Session.
     */
    private volatile long lastAccessedTime = creationTime;

    /**
     * The session event listeners for this Session.
     */
    private transient List<SessionListener> listeners = new ArrayList<SessionListener>();

    /**
     * The Manager with which this Session is associated.
     */
    private transient ClusteredSessionManager<O> manager = null;

    /**
     * Our proxy to the distributed cache.
     */
    private transient DistributedCacheManager<O> distributedCacheManager;

    /**
     * The maximum time interval, in seconds, between client requests before the servlet container may invalidate this session.
     * A negative time indicates that the session should never time out.
     */
    private int maxInactiveInterval = -1;

    /**
     * Flag indicating whether this session is new or not.
     */
    private boolean isNew = false;

    /**
     * Flag indicating whether this session is valid or not.
     */
    private volatile boolean isValid = false;

    /**
     * Internal notes associated with this session by Catalina components and event listeners. <b>IMPLEMENTATION NOTE:</b> This
     * object is <em>not</em> saved and restored across session serializations!
     */
    private final transient Map<String, Object> notes = new Hashtable<String, Object>();

    /**
     * The authenticated Principal associated with this session, if any. <b>IMPLEMENTATION NOTE:</b> This object is <i>not</i>
     * saved and restored across session serializations!
     */
    private transient Principal principal = null;

    /**
     * The property change support for this component. NOTE: This value is not included in the serialized version of this
     * object.
     */
    private transient PropertyChangeSupport support = new PropertyChangeSupport(this);

    /**
     * The current accessed time for this session.
     */
    private volatile long thisAccessedTime = creationTime;

    /**
     * The access count for this session.
     */
    private final transient AtomicInteger accessCount;

    /**
     * Policy controlling whether reading/writing attributes requires replication.
     */
    private ReplicationTrigger invalidationPolicy;

    /**
     * If true, means the local in-memory session data contains metadata changes that have not been published to the distributed
     * cache.
     */
    private transient boolean sessionMetadataDirty;

    /**
     * If true, means the local in-memory session data contains attribute changes that have not been published to the
     * distributed cache.
     */
    private transient boolean sessionAttributesDirty;

    /**
     * Object wrapping thisAccessedTime. Create once and mutate so we can store it in JBoss Cache w/o concern that a transaction
     * rollback will revert the cached ref to an older object.
     */
    private final transient AtomicLong timestamp = new AtomicLong(0);

    /**
     * Object wrapping other metadata for this session. Create once and mutate so we can store it in JBoss Cache w/o concern
     * that a transaction rollback will revert the cached ref to an older object.
     */
    private transient volatile DistributableSessionMetadata metadata = new DistributableSessionMetadata();

    /**
     * The last time {@link #setIsOutdated setIsOutdated(true)} was called or <code>0</code> if
     * <code>setIsOutdated(false)</code> was subsequently called.
     */
    private transient volatile long outdatedTime;

    /**
     * Version number to track cache invalidation. If any new version number is greater than this one, it means the data it
     * holds is newer than this one.
     */
    private final AtomicInteger version = new AtomicInteger(0);

    /**
     * The session's id with any jvmRoute removed.
     */
    private transient String realId;

    /**
     * Timestamp when we were last replicated.
     */
    private transient volatile long lastReplicated;

    /**
     * Maximum number of milliseconds this session should be allowed to go unreplicated if access to the session doesn't mark it
     * as dirty.
     */
    private transient long maxUnreplicatedInterval;

    /** True if maxUnreplicatedInterval is 0 or less than maxInactiveInterval */
    private transient boolean alwaysReplicateTimestamp = true;

    /**
     * Whether any of this session's attributes implement HttpSessionActivationListener.
     */
    private transient Boolean hasActivationListener;

    /**
     * Has this session only been accessed once?
     */
    private transient boolean firstAccess;

    /**
     * Policy that drives whether we issue servlet spec notifications.
     */
    private transient ClusteredSessionNotificationPolicy notificationPolicy;

    private transient ClusteredSessionManagementStatus clusterStatus;

    /** True if a call to activate() is needed to offset a preceding passivate() call */
    private transient boolean needsPostReplicateActivation;

    /**
     * True if a getOutgoingSessionData() should include metadata and all attributes no matter what. This is a workaround to
     * JBCACHE-1531. This flag ensures that at least one request gets full replication, whether or not in occurs before
     * this.fullReplicationWindow
     */
    private transient boolean fullReplicationRequired = true;
    /** End of period when we do full replication */
    private transient long fullReplicationWindow = -1;

    /** Coordinate updates from the cluster */
    private transient Lock ownershipLock = new ReentrantLock();

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

    /**
     * Creates a new ClusteredSession.
     *
     * @param manager the manager for this session
     */
    protected ClusteredSession(ClusteredSessionManager<O> manager) {
        super();

        // Initialize access count
        accessCount = ACTIVITY_CHECK ? new AtomicInteger() : null;
        this.firstAccess = true;

        setManager(manager);
        requireFullReplication();
    }

    // ---------------------------------------------------------------- Session

    @Override
    public String getAuthType() {
        return this.authType;
    }

    @Override
    public void setAuthType(String authType) {
        String oldAuthType = this.authType;
        this.authType = authType;
        support.firePropertyChange("authType", oldAuthType, this.authType);
    }

    @Override
    public long getCreationTime() {
        if (!isValidInternal())
            throw MESSAGES.expiredSession();

        return (this.creationTime);
    }

    /**
     * Set the creation time for this session. This method is called by the Manager when an existing Session instance is reused.
     *
     * @param time The new creation time
     */
    @Override
    public void setCreationTime(long time) {
        this.creationTime = time;
        this.lastAccessedTime = time;
        this.thisAccessedTime = time;
        sessionMetadataDirty();
    }

    @Override
    public String getId() {
        return this.id;
    }

    @Override
    public String getIdInternal() {
        return this.id;
    }

    /**
     * Overrides the superclass method to also set the {@link #getRealId() realId} property.
     */
    @Override
    public void setId(String id) {
        // Parse the real id first, as super.setId() calls add(),
        // which depends on having the real id
        parseRealId(id);

        if ((this.id != null) && (manager != null)) {
            manager.remove(this);
        }
        this.id = id;

        this.clusterStatus = new ClusteredSessionManagementStatus(this.realId, true, null, null);

        if (manager != null) {
            manager.add(this);
        }
    }

    @Override
    public long getLastAccessedTime() {
        if (!isValidInternal()) {
            throw MESSAGES.expiredSession();
        }

        return this.lastAccessedTime;
    }

    @Override
    public long getLastAccessedTimeInternal() {
        return (this.lastAccessedTime);
    }

    @Override
    public Manager getManager() {
        return (this.manager);
    }

    @Override
    public void setManager(Manager manager) {
        if ((manager instanceof ClusteredSessionManager<?>) == false)
            throw MESSAGES.invalidManager();
        @SuppressWarnings("unchecked")
        ClusteredSessionManager<O> unchecked = (ClusteredSessionManager<O>) manager;
        this.manager = unchecked;

        this.invalidationPolicy = this.manager.getReplicationTrigger();

        int maxUnrep = this.manager.getMaxUnreplicatedInterval() * 1000;
        setMaxUnreplicatedInterval(maxUnrep);
        this.notificationPolicy = this.manager.getNotificationPolicy();
        establishDistributedCacheManager();
    }

    @Override
    public int getMaxInactiveInterval() {
        return this.maxInactiveInterval;
    }

    /**
     * Overrides the superclass to calculate {@link #getMaxUnreplicatedInterval() maxUnreplicatedInterval}.
     */
    @Override
    public void setMaxInactiveInterval(int interval) {
        this.maxInactiveInterval = interval;
        checkAlwaysReplicateTimestamp();
        sessionMetadataDirty();
    }

    @Override
    public Principal getPrincipal() {
        return this.principal;
    }

    /**
     * Set the authenticated Principal that is associated with this Session. This provides an <code>Authenticator</code> with a
     * means to cache a previously authenticated Principal, and avoid potentially expensive <code>Realm.authenticate()</code>
     * calls on every request.
     *
     * @param principal The new Principal, or <code>null</code> if none
     */
    @Override
    public void setPrincipal(Principal principal) {
        Principal oldPrincipal = this.principal;
        this.principal = principal;
        support.firePropertyChange("principal", oldPrincipal, this.principal);

        if ((oldPrincipal != null && !oldPrincipal.equals(principal)) || (oldPrincipal == null && principal != null)) {
            sessionMetadataDirty();
        }
    }

    @Override
    public void access() {
        this.acquireSessionOwnership();

        this.lastAccessedTime = this.thisAccessedTime;
        this.thisAccessedTime = System.currentTimeMillis();

        if (ACTIVITY_CHECK) {
            accessCount.incrementAndGet();
        }

        // JBAS-3528. If it's not the first access, make sure
        // the 'new' flag is correct
        if (!firstAccess && isNew) {
            setNew(false);
        }
    }

    private void acquireSessionOwnership() {
        SessionOwnershipSupport support = this.distributedCacheManager.getSessionOwnershipSupport();

        if (support != null) {
            try {
                this.ownershipLock.lockInterruptibly();

                try {
                    if (support.acquireSessionOwnership(this.realId, needNewLock()) == SessionOwnershipSupport.LockResult.ACQUIRED_FROM_CLUSTER) {
                        IncomingDistributableSessionData data = this.distributedCacheManager.getSessionData(this.realId, false);
                        if (data != null) {
                            // We may be out of date re: the distributed cache
                            update(data);
                        }
                    }
                } catch (TimeoutException e) {
                    throw MESSAGES.failAcquiringOwnership(realId, e);
                } finally {
                    this.ownershipLock.unlock();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw MESSAGES.interruptedAcquiringOwnership(realId, e);
            }
        }
    }

    private boolean needNewLock() {
        return firstAccess && isNew;
    }

    @Override
    public void endAccess() {
        isNew = false;

        if (ACTIVITY_CHECK) {
            accessCount.decrementAndGet();
        }

        this.lastAccessedTime = this.thisAccessedTime;

        if (firstAccess) {
            firstAccess = false;
            // Tomcat marks the session as non new, but that's not really
            // accurate per SRV.7.2, as the second request hasn't come in yet
            // So, we fix that
            isNew = true;
        }

        this.relinquishSessionOwnership(false);
    }

    private void relinquishSessionOwnership(boolean remove) {
        SessionOwnershipSupport support = this.distributedCacheManager.getSessionOwnershipSupport();

        if (support != null) {
            support.relinquishSessionOwnership(this.realId, remove);
        }
    }

    @Override
    public boolean isNew() {
        if (!isValidInternal())
            throw MESSAGES.expiredSession();

        return (this.isNew);
    }

    @Override
    public void setNew(boolean isNew) {
        this.isNew = isNew;

        // Don't replicate metadata just 'cause its the second request
        // The only effect of this is if someone besides a request
        // deserializes metadata from the distributed cache, this
        // field may be out of date.
        // If a request accesses the session, the access() call will
        // set isNew=false, so the request will see the correct value
        // sessionMetadataDirty();
    }

    /**
     * Overrides the {@link StandardSession#isValid() superclass method} to call @ #isValid(boolean) isValid(true)} .
     */
    @Override
    public boolean isValid() {
        return isValid(true);
    }

    @Override
    public void setValid(boolean isValid) {
        this.isValid = isValid;
        sessionMetadataDirty();
    }

    /**
     * Invalidates this session and unbinds any objects bound to it. Overridden here to remove across the cluster instead of
     * just expiring.
     *
     * @exception IllegalStateException if this method is called on an invalidated session
     */
    @Override
    public void invalidate() {
        if (!isValid())
            throw MESSAGES.expiredSession();

        // Cause this session to expire globally
        boolean notify = true;
        boolean localCall = true;
        boolean localOnly = false;
        expire(notify, localCall, localOnly, ClusteredSessionNotificationCause.INVALIDATE);
        // Preemptively relinquish ownership that was acquired in access() - don't wait for endAccess()
        this.relinquishSessionOwnership(false);
    }

    @Override
    public void expire() {
        boolean notify = true;
        boolean localCall = true;
        boolean localOnly = false;
        expire(notify, localCall, localOnly, ClusteredSessionNotificationCause.INVALIDATE);
    }

    @Override
    public void recycle() {
        if (!isValid) {
            // Thread no longer needs to track this session
            SessionInvalidationTracker.clearInvalidatedSession(id, manager);
        }

        // Reset the instance variables associated with this Session
        attributes.clear();
        setAuthType(null);
        creationTime = 0L;
        expiring = false;
        id = null;
        lastAccessedTime = 0L;
        maxInactiveInterval = -1;
        notes.clear();
        setPrincipal(null);
        isNew = false;
        isValid = false;
        firstAccess = true;
        manager = null;

        listeners.clear();
        support = new PropertyChangeSupport(this);

        invalidationPolicy = ReplicationTrigger.ACCESS;
        outdatedTime = 0;
        sessionAttributesDirty = false;
        sessionMetadataDirty = false;
        realId = null;
        version.set(0);
        hasActivationListener = null;
        lastReplicated = 0;
        maxUnreplicatedInterval = 0;
        alwaysReplicateTimestamp = true;
        this.notificationPolicy = null;
        this.clusterStatus = null;
    }

    @Override
    public void addSessionListener(SessionListener listener) {
        listeners.add(listener);
    }

    @Override
    public void removeSessionListener(SessionListener listener) {
        listeners.remove(listener);
    }

    @Override
    public Object getNote(String name) {
        return (notes.get(name));
    }

    @Override
    public Iterator<String> getNoteNames() {
        return (notes.keySet().iterator());
    }

    @Override
    public void setNote(String name, Object value) {
        notes.put(name, value);
    }

    @Override
    public void removeNote(String name) {
        notes.remove(name);
    }

    // TODO uncomment when work on JBAS-1900 is completed
    // public void removeNote(String name)
    // {
    // // FormAuthenticator removes the username and password because
    // // it assumes they are not needed if the Principal is cached,
    // // but they are needed if the session fails over, so ignore
    // // the removal request.
    // // TODO discuss this on Tomcat dev list to see if a better
    // // way of handling this can be found
    // if (Constants.SESS_USERNAME_NOTE.equals(name)
    // || Constants.SESS_PASSWORD_NOTE.equals(name))
    // {
    // if (log.isDebugEnabled())
    // {
    // log.debug("removeNote(): ignoring removal of note " + name);
    // }
    // }
    // else
    // {
    // super.removeNote(name);
    // }
    //
    // }

    // TODO uncomment when work on JBAS-1900 is completed
    // public void setNote(String name, Object value)
    // {
    // super.setNote(name, value);
    //
    // if (Constants.SESS_USERNAME_NOTE.equals(name)
    // || Constants.SESS_PASSWORD_NOTE.equals(name))
    // {
    // sessionIsDirty();
    // }
    // }

    @Override
    public HttpSession getSession() {
        if (facade == null) {
            if (SecurityUtil.isPackageProtectionEnabled()) {
                final HttpSession fsession = this;
                StandardSessionFacade ssf = AccessController.doPrivileged(new PrivilegedAction<StandardSessionFacade>() {
                    @Override
                    public StandardSessionFacade run() {
                        return new StandardSessionFacade(fsession);
                    }
                });
                this.facade = ssf;
            } else {
                facade = new StandardSessionFacade(this);
            }
        }
        return (facade);
    }

    // ------------------------------------------------------------ HttpSession

    @Override
    public ServletContext getServletContext() {
        if (manager == null)
            return (null);
        Context context = (Context) manager.getContainer();
        if (context == null)
            return (null);
        else
            return (context.getServletContext());
    }

    @Override
    public Object getAttribute(String name) {
        if (!isValid())
            throw MESSAGES.expiredSession();

        return getAttributeInternal(name);
    }

    @Override
    @SuppressWarnings("unchecked")
    public Enumeration<String> getAttributeNames() {
        if (!isValid())
            throw MESSAGES.expiredSession();

        return (new Enumerator(getAttributesInternal().keySet(), true));
    }

    @Override
    public void setAttribute(String name, Object value) {
        // Name cannot be null
        if (name == null)
            throw MESSAGES.expiredSession();

        // Null value is the same as removeAttribute()
        if (value == null) {
            removeAttribute(name);
            return;
        }

        // Validate our current state
        if (!isValidInternal()) {
            throw MESSAGES.expiredSession();
        }

        if (canAttributeBeReplicated(value) == false) {
            throw MESSAGES.failToReplicateAttribute();
        }

        // Construct an event with the new value
        HttpSessionBindingEvent event = null;

        // Call the valueBound() method if necessary
        if (value instanceof HttpSessionBindingListener
                && notificationPolicy.isHttpSessionBindingListenerInvocationAllowed(this.clusterStatus,
                        ClusteredSessionNotificationCause.MODIFY, name, true)) {
            event = new HttpSessionBindingEvent(getSession(), name, value);
            try {
                ((HttpSessionBindingListener) value).valueBound(event);
            } catch (Throwable t) {
                manager.getContainer().getLogger().error(MESSAGES.errorValueBoundEvent(t));
            }
        }

        if (value instanceof HttpSessionActivationListener)
            hasActivationListener = Boolean.TRUE;

        // Replace or add this attribute
        Object unbound = setAttributeInternal(name, value);

        // Call the valueUnbound() method if necessary
        if ((unbound != null)
                && (unbound != value)
                && (unbound instanceof HttpSessionBindingListener)
                && notificationPolicy.isHttpSessionBindingListenerInvocationAllowed(this.clusterStatus,
                        ClusteredSessionNotificationCause.MODIFY, name, true)) {
            try {
                ((HttpSessionBindingListener) unbound).valueUnbound(new HttpSessionBindingEvent(getSession(), name));
            } catch (Throwable t) {
                manager.getContainer().getLogger().error(MESSAGES.errorValueUnboundEvent(t));
            }
        }

        // Notify interested application event listeners
        if (notificationPolicy.isHttpSessionAttributeListenerInvocationAllowed(this.clusterStatus,
                ClusteredSessionNotificationCause.MODIFY, name, true)) {
            Context context = (Context) manager.getContainer();
            Object[] lifecycleListeners = context.getApplicationEventListeners();
            if (lifecycleListeners == null)
                return;
            for (int i = 0; i < lifecycleListeners.length; i++) {
                if (!(lifecycleListeners[i] instanceof HttpSessionAttributeListener))
                    continue;
                HttpSessionAttributeListener listener = (HttpSessionAttributeListener) lifecycleListeners[i];
                try {
                    if (unbound != null) {
                        fireContainerEvent(context, "beforeSessionAttributeReplaced", listener);
                        if (event == null) {
                            event = new HttpSessionBindingEvent(getSession(), name, unbound);
                        }
                        listener.attributeReplaced(event);
                        fireContainerEvent(context, "afterSessionAttributeReplaced", listener);
                    } else {
                        fireContainerEvent(context, "beforeSessionAttributeAdded", listener);
                        if (event == null) {
                            event = new HttpSessionBindingEvent(getSession(), name, value);
                        }
                        listener.attributeAdded(event);
                        fireContainerEvent(context, "afterSessionAttributeAdded", listener);
                    }
                } catch (Throwable t) {
                    try {
                        if (unbound != null) {
                            fireContainerEvent(context, "afterSessionAttributeReplaced", listener);
                        } else {
                            fireContainerEvent(context, "afterSessionAttributeAdded", listener);
                        }
                    } catch (Exception e) {
                        // Ignore
                    }
                    manager.getContainer().getLogger().error(MESSAGES.errorSessionAttributeEvent(t));
                }
            }
        }
    }

    @Override
    public void removeAttribute(String name) {
        // Validate our current state
        if (!isValidInternal())
            throw new IllegalStateException(sm.getString("clusteredSession.removeAttribute.ise"));

        final boolean localCall = true;
        final boolean localOnly = false;
        final boolean notify = true;
        removeAttributeInternal(name, localCall, localOnly, notify, ClusteredSessionNotificationCause.MODIFY);
    }

    @Override
    @SuppressWarnings("deprecation")
    public javax.servlet.http.HttpSessionContext getSessionContext() {
        return (sessionContext);
    }

    @Override
    public Object getValue(String name) {
        return (getAttribute(name));
    }

    @Override
    public String[] getValueNames() {
        if (!isValidInternal())
            throw MESSAGES.expiredSession();

        return (keys());
    }

    @Override
    public void putValue(String name, Object value) {
        setAttribute(name, value);
    }

    @Override
    public void removeValue(String name) {
        removeAttribute(name);
    }

    // ---------------------------------------------------- DistributableSession

    /**
     * Gets the session id with any appended jvmRoute info removed.
     *
     * @see #getUseJK()
     */
    public String getRealId() {
        return realId;
    }

    public boolean getMustReplicateTimestamp() {
        // If the access times are the same, access() was never called
        // on the session
        boolean touched = this.thisAccessedTime != this.lastAccessedTime;
        boolean exceeds = alwaysReplicateTimestamp && touched;

        if (!exceeds && touched && maxUnreplicatedInterval > 0) // -1 means ignore
        {
            long unrepl = System.currentTimeMillis() - lastReplicated;
            exceeds = (unrepl >= maxUnreplicatedInterval);
        }

        return exceeds;
    }

    /**
     * {@inheritDoc}
     */
    public void update(IncomingDistributableSessionData sessionData) {
        assert sessionData != null : MESSAGES.nullSessionData();

        this.version.set(sessionData.getVersion());

        long ts = sessionData.getTimestamp();
        this.lastAccessedTime = this.thisAccessedTime = ts;
        this.timestamp.set(ts);

        DistributableSessionMetadata md = sessionData.getMetadata();
        // TODO -- get rid of these field and delegate to metadata
        this.id = md.getId();
        this.creationTime = md.getCreationTime();
        this.maxInactiveInterval = md.getMaxInactiveInterval();
        this.isNew = md.isNew();
        this.isValid = md.isValid();
        this.metadata = md;

        // Get our id without any jvmRoute appended
        parseRealId(id);

        // We no longer know if we have an activationListener
        hasActivationListener = null;

        // If the session has been replicated, any subsequent
        // access cannot be the first.
        this.firstAccess = false;

        // We don't know when we last replicated our timestamp. We may be
        // getting called due to activation, not deserialization after
        // replication, so this.timestamp may be after the last replication.
        // So use the creation time as a conservative guesstimate. Only downside
        // is we may replicate a timestamp earlier than we need to, which is not
        // a heavy cost.
        this.lastReplicated = this.creationTime;

        this.clusterStatus = new ClusteredSessionManagementStatus(this.realId, true, null, null);

        checkAlwaysReplicateTimestamp();

        populateAttributes(sessionData.getSessionAttributes());

        // TODO uncomment when work on JBAS-1900 is completed
        // // Session notes -- for FORM auth apps, allow replicated session
        // // to be used without requiring a new login
        // // We use the superclass set/removeNote calls here to bypass
        // // the custom logic we've added
        // String username = (String) in.readObject();
        // if (username != null)
        // {
        // super.setNote(Constants.SESS_USERNAME_NOTE, username);
        // }
        // else
        // {
        // super.removeNote(Constants.SESS_USERNAME_NOTE);
        // }
        // String password = (String) in.readObject();
        // if (password != null)
        // {
        // super.setNote(Constants.SESS_PASSWORD_NOTE, password);
        // }
        // else
        // {
        // super.removeNote(Constants.SESS_PASSWORD_NOTE);
        // }

        // We are no longer outdated vis a vis distributed cache
        this.outdatedTime = 0;

        // Requests must publish our full state back to the cluster in case anything got dropped.
        this.requireFullReplication();
    }

    // ------------------------------------------------------------------ Public

    /**
     * Increment our version and propagate ourself to the distributed cache.
     */
    public synchronized void processSessionReplication() {
        // Replicate the session.
        if (log.isTraceEnabled()) {
            log.tracef("processSessionReplication(): session is dirty. Will increment version from: %s and replicate.", getVersion());
        }
        version.incrementAndGet();

        O outgoingData = getOutgoingSessionData();
        distributedCacheManager.storeSessionData(outgoingData);

        sessionAttributesDirty = false;
        sessionMetadataDirty = false;

        lastReplicated = System.currentTimeMillis();

        this.fullReplicationRequired = false;
        if (this.fullReplicationWindow > 0 && System.currentTimeMillis() > this.fullReplicationWindow) {
            this.fullReplicationWindow = -1;
        }
    }

    /**
     * Remove myself from the distributed cache.
     */
    public void removeMyself() {
        getDistributedCacheManager().removeSession(getRealId());
    }

    /**
     * Remove myself from the <t>local</t> instance of the distributed cache.
     */
    public void removeMyselfLocal() {
        getDistributedCacheManager().removeSessionLocal(getRealId());
    }

    /**
     * Gets the sessions creation time, skipping any validity check.
     *
     * @return the creation time
     */
    public long getCreationTimeInternal() {
        return creationTime;
    }

    /**
     * Gets the time {@link #processSessionReplication()} was last called, or <code>0</code> if it has never been called.
     */
    public long getLastReplicated() {
        return lastReplicated;
    }

    /**
     * Gets the maximum period in ms after which a request accessing this session will trigger replication of its timestamp,
     * even if the request doesn't otherwise modify the session. A value of -1 means no limit.
     */
    public long getMaxUnreplicatedInterval() {
        return maxUnreplicatedInterval;
    }

    /**
     * Sets the maximum period in ms after which a request accessing this session will trigger replication of its timestamp,
     * even if the request doesn't otherwise modify the session. A value of -1 means no limit.
     */
    public void setMaxUnreplicatedInterval(long interval) {
        this.maxUnreplicatedInterval = Math.max(interval, -1);
        checkAlwaysReplicateTimestamp();
    }

    /**
     * This is called specifically for failover case using mod_jk where the new session has this node name in there. As a
     * result, it is safe to just replace the id since the backend store is using the "real" id without the node name.
     *
     * @param id
     */
    public void resetIdWithRouteInfo(String id) {
        this.id = id;
        parseRealId(id);
    }

    /**
     * Update our version due to changes in the distributed cache.
     *
     * @param version the distributed cache version
     * @return <code>true</code>
     */
    public boolean setVersionFromDistributedCache(int version) {
        boolean outdated = getVersion() < version;
        if (outdated) {
            outdatedTime = System.currentTimeMillis();
        }
        return outdated;
    }

    /**
     * Check to see if the session data is still valid. Outdated here means that the in-memory data is not in sync with one in
     * the data store.
     *
     * @return
     */
    public boolean isOutdated() {
        // if creationTime == 0 we've neither been synced with the
        // distributed cache nor had creation time set (i.e. brand new session)
        return thisAccessedTime < outdatedTime || this.creationTime == 0;
    }

    public boolean isSessionDirty() {
        return sessionAttributesDirty || sessionMetadataDirty;
    }

    /** Inform any HttpSessionListener of the creation of this session */
    public void tellNew(ClusteredSessionNotificationCause cause) {
        // Notify interested session event listeners
        fireSessionEvent(Session.SESSION_CREATED_EVENT, null);

        // Notify interested application event listeners
        if (notificationPolicy.isHttpSessionListenerInvocationAllowed(this.clusterStatus, cause, true)) {
            Context context = (Context) manager.getContainer();
            Object[] lifecycleListeners = context.getApplicationSessionLifecycleListeners();
            if (lifecycleListeners != null) {
                HttpSessionEvent event = new HttpSessionEvent(getSession());
                for (int i = 0; i < lifecycleListeners.length; i++) {
                    if (!(lifecycleListeners[i] instanceof HttpSessionListener))
                        continue;
                    HttpSessionListener listener = (HttpSessionListener) lifecycleListeners[i];
                    try {
                        fireContainerEvent(context, "beforeSessionCreated", listener);
                        listener.sessionCreated(event);
                        fireContainerEvent(context, "afterSessionCreated", listener);
                    } catch (Throwable t) {
                        try {
                            fireContainerEvent(context, "afterSessionCreated", listener);
                        } catch (Exception e) {
                            // Ignore
                        }
                        manager.getContainer().getLogger().error(MESSAGES.errorSessionEvent(t));
                    }
                }
            }
        }
    }

    /**
     * Returns whether the current session is still valid, but only calls <code>expire</code> for timed-out sessions if
     * <code>expireIfInvalid</code> is <code>true</code>.
     *
     * @param expireIfInvalid <code>true</code> if sessions that have been timed out should be expired
     */
    public boolean isValid(boolean expireIfInvalid) {
        if (this.expiring) {
            return true;
        }

        if (!this.isValid) {
            return false;
        }

        if (ACTIVITY_CHECK && accessCount.get() > 0) {
            return true;
        }

        if (maxInactiveInterval > 0) {
            long timeNow = System.currentTimeMillis();
            int timeIdle = (int) ((timeNow - thisAccessedTime) / 1000L);
            if (timeIdle >= maxInactiveInterval) {
                if (expireIfInvalid) {
                    boolean notify = true;
                    boolean localCall = true;
                    boolean localOnly = true;
                    expire(notify, localCall, localOnly, ClusteredSessionNotificationCause.TIMEOUT);
                } else {
                    return false;
                }
            }
        }

        return (this.isValid);

    }

    /**
     * Expires the session, notifying listeners and possibly the manager.
     * <p>
     * <strong>NOTE:</strong> The manager will only be notified of the expiration if <code>localCall</code> is <code>true</code>
     * ; otherwise it is the responsibility of the caller to notify the manager that the session is expired. (In the case of
     * JBossCacheManager, it is the manager itself that makes such a call, so it of course is aware).
     * </p>
     *
     * @param notify whether servlet spec listeners should be notified
     * @param localCall <code>true</code> if this call originated due to local activity (such as a session invalidation in user
     *        code or an expiration by the local background processing thread); <code>false</code> if the expiration originated
     *        due to some kind of event notification from the cluster.
     * @param localOnly <code>true</code> if the expiration should not be announced to the cluster, <code>false</code> if other
     *        cluster nodes should be made aware of the expiration. Only meaningful if <code>localCall</code> is
     *        <code>true</code>.
     * @param cause the cause of the expiration
     */
    public void expire(boolean notify, boolean localCall, boolean localOnly, ClusteredSessionNotificationCause cause) {
        if (log.isTraceEnabled()) {
            log.tracef("The session has expired with id: %s  -- is expiration local? %s", id, localOnly);
        }

        // If another thread is already doing this, stop
        if (expiring)
            return;

        synchronized (this) {
            // If we had a race to this sync block, another thread may
            // have already completed expiration. If so, don't do it again
            if (!isValid)
                return;

            if (manager == null)
                return;

            expiring = true;

            // SRV.10.6 (2.5) 11.6 (3.0) Propagate listener exceptions
            RuntimeException listenerException = null;

            if (localCall) {
                this.acquireSessionOwnership();
            }

            try {
                // Notify interested application event listeners
                // FIXME - Assumes we call listeners in reverse order
                Context context = (Context) manager.getContainer();
                Object[] lifecycleListeners = context.getApplicationSessionLifecycleListeners();
                if (notify && (lifecycleListeners != null)
                        && notificationPolicy.isHttpSessionListenerInvocationAllowed(this.clusterStatus, cause, localCall)) {
                    HttpSessionEvent event = new HttpSessionEvent(getSession());
                    for (int i = 0; i < lifecycleListeners.length; i++) {
                        int j = (lifecycleListeners.length - 1) - i;
                        if (!(lifecycleListeners[j] instanceof HttpSessionListener))
                            continue;
                        HttpSessionListener listener = (HttpSessionListener) lifecycleListeners[j];
                        try {
                            fireContainerEvent(context, "beforeSessionDestroyed", listener);
                            try {
                                listener.sessionDestroyed(event);
                            } catch (RuntimeException e) {
                                if (listenerException == null) {
                                    listenerException = e;
                                }
                            }
                            fireContainerEvent(context, "afterSessionDestroyed", listener);
                        } catch (Throwable t) {
                            try {
                                fireContainerEvent(context, "afterSessionDestroyed", listener);
                            } catch (Exception e) {
                                // Ignore
                            }
                            manager.getContainer().getLogger().error(MESSAGES.errorSessionEvent(t));
                        }
                    }
                }

                if (ACTIVITY_CHECK) {
                    accessCount.set(0);
                }

                // Notify interested session event listeners.
                if (notify) {
                    fireSessionEvent(Session.SESSION_DESTROYED_EVENT, null);
                }

                // JBAS-1360 -- Unbind any objects associated with this session
                String[] keys = keys();
                for (int i = 0; i < keys.length; i++) {
                    try {
                        removeAttributeInternal(keys[i], localCall, localOnly, notify, cause);
                    } catch (RuntimeException e) {
                        if (listenerException == null) {
                            listenerException = e;
                        }
                    }
                }

                // Remove this session from our manager's active sessions
                // If !localCall, this expire call came from the manager,
                // so don't recurse
                if (localCall) {
                    removeFromManager(localOnly);
                }

                if (listenerException != null) {
                    throw listenerException;
                }
            } finally {
                // We have completed expire of this session
                setValid(false);
                expiring = false;
                if (localCall) {
                    this.relinquishSessionOwnership(true);
                }
            }
        }
    }

    /**
     * Inform any HttpSessionActivationListener that the session will passivate.
     *
     * @param cause cause of the notification (e.g. {@link ClusteredSessionNotificationCause#REPLICATION} or
     *        {@link ClusteredSessionNotificationCause#PASSIVATION}
     */
    public void notifyWillPassivate(ClusteredSessionNotificationCause cause) {
        // Notify interested session event listeners
        fireSessionEvent(Session.SESSION_PASSIVATED_EVENT, null);

        if (hasActivationListener != Boolean.FALSE) {
            boolean hasListener = false;

            // Notify ActivationListeners
            HttpSessionEvent event = null;
            String[] keys = keys();
            Map<String, Object> attrs = getAttributesInternal();
            for (int i = 0; i < keys.length; i++) {
                Object attribute = attrs.get(keys[i]);
                if (attribute instanceof HttpSessionActivationListener) {
                    hasListener = true;

                    if (notificationPolicy.isHttpSessionActivationListenerInvocationAllowed(this.clusterStatus, cause, keys[i])) {
                        if (event == null)
                            event = new HttpSessionEvent(getSession());

                        try {
                            ((HttpSessionActivationListener) attribute).sessionWillPassivate(event);
                        } catch (Throwable t) {
                            manager.getContainer().getLogger().error(MESSAGES.errorSessionActivationEvent(t));
                        }
                    }
                }
            }

            hasActivationListener = hasListener ? Boolean.TRUE : Boolean.FALSE;
        }

        if (cause != ClusteredSessionNotificationCause.PASSIVATION) {
            this.needsPostReplicateActivation = true;
        }
    }

    /**
     * Inform any HttpSessionActivationListener that the session has been activated.
     *
     * @param cause cause of the notification (e.g. {@link ClusteredSessionNotificationCause#REPLICATION} or
     *        {@link ClusteredSessionNotificationCause#PASSIVATION}
     */
    public void notifyDidActivate(ClusteredSessionNotificationCause cause) {
        if (cause == ClusteredSessionNotificationCause.ACTIVATION) {
            this.needsPostReplicateActivation = true;
        }

        // Notify interested session event listeners
        fireSessionEvent(Session.SESSION_ACTIVATED_EVENT, null);

        if (hasActivationListener != Boolean.FALSE) {
            // Notify ActivationListeners

            boolean hasListener = false;

            HttpSessionEvent event = null;
            String[] keys = keys();
            Map<String, Object> attrs = getAttributesInternal();
            for (int i = 0; i < keys.length; i++) {
                Object attribute = attrs.get(keys[i]);
                if (attribute instanceof HttpSessionActivationListener) {
                    hasListener = true;

                    if (notificationPolicy.isHttpSessionActivationListenerInvocationAllowed(this.clusterStatus, cause, keys[i])) {
                        if (event == null)
                            event = new HttpSessionEvent(getSession());
                        try {
                            ((HttpSessionActivationListener) attribute).sessionDidActivate(event);
                        } catch (Throwable t) {
                            manager.getContainer().getLogger().error(MESSAGES.errorSessionActivationEvent(t));
                        }
                    }
                }
            }

            hasActivationListener = hasListener ? Boolean.TRUE : Boolean.FALSE;
        }

        if (cause != ClusteredSessionNotificationCause.ACTIVATION) {
            this.needsPostReplicateActivation = false;
        }
    }

    /**
     * Gets whether the session needs to notify HttpSessionActivationListeners that it has been activated following replication.
     */
    public boolean getNeedsPostReplicateActivation() {
        return needsPostReplicateActivation;
    }

    @Override
    public String toString() {
        return new StringBuilder(getClass().getSimpleName()).append('[').append("id: ").append(id)
                .append(" lastAccessedTime: ").append(lastAccessedTime).append(" version: ").append(version)
                .append(" lastOutdated: ").append(outdatedTime).append(']').toString();
    }

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

    protected abstract Object setAttributeInternal(String name, Object value);

    protected abstract Object removeAttributeInternal(String name, boolean localCall, boolean localOnly);

    protected abstract O getOutgoingSessionData();

    protected Object getAttributeInternal(String name) {
        Object result = getAttributesInternal().get(name);

        // Do dirty check even if result is null, as w/ SET_AND_GET null
        // still makes us dirty (ensures timely replication w/o using ACCESS)
        if (isGetDirty(result)) {
            sessionAttributesDirty();
        }

        return result;
    }

    /**
     * Extension point for subclasses to load the attribute map from the distributed cache.
     */
    protected void populateAttributes(Map<String, Object> distributedCacheAttributes) {
        Map<String, Object> existing = getAttributesInternal();
        Map<String, Object> excluded = removeExcludedAttributes(existing);

        existing.clear();

        existing.putAll(distributedCacheAttributes);
        if (excluded != null)
            existing.putAll(excluded);
    }

    protected final Map<String, Object> getAttributesInternal() {
        return attributes;
    }

    protected final ClusteredSessionManager<O> getManagerInternal() {
        return manager;
    }

    protected final DistributedCacheManager<O> getDistributedCacheManager() {
        return distributedCacheManager;
    }

    protected final void setDistributedCacheManager(DistributedCacheManager<O> distributedCacheManager) {
        this.distributedCacheManager = distributedCacheManager;
    }

    /**
     * Returns whether the attribute's type is one that can be replicated.
     *
     * @param attribute the attribute
     * @return <code>true</code> if <code>attribute</code> is <code>null</code>, <code>Serializable</code> or an array of
     *         primitives.
     */
    protected boolean canAttributeBeReplicated(Object attribute) {
        if (attribute instanceof Serializable || attribute == null)
            return true;
        Class<?> clazz = attribute.getClass().getComponentType();
        return (clazz != null && clazz.isPrimitive());
    }

    /**
     * Removes any attribute whose name is found in {@link #excludedAttributes} from <code>attributes</code> and returns a Map
     * of all such attributes.
     *
     * @param attributes source map from which excluded attributes are to be removed.
     *
     * @return Map that contains any attributes removed from <code>attributes</code>, or <code>null</code> if no attributes were
     *         removed.
     */
    protected final Map<String, Object> removeExcludedAttributes(Map<String, Object> attributes) {
        Map<String, Object> excluded = null;
        for (int i = 0; i < excludedAttributes.length; i++) {
            Object attr = attributes.remove(excludedAttributes[i]);
            if (attr != null) {
                if (log.isTraceEnabled()) {
                    log.tracef("Excluding attribute %s from replication", excludedAttributes[i]);
                }
                if (excluded == null) {
                    excluded = new HashMap<String, Object>();
                }
                excluded.put(excludedAttributes[i], attr);
            }
        }

        return excluded;
    }

    protected final boolean isGetDirty(Object attribute) {
        boolean result = false;
        switch (invalidationPolicy) {
            case SET_AND_GET:
                result = true;
                break;
            case SET_AND_NON_PRIMITIVE_GET:
                result = isMutable(attribute);
                break;
            default:
                // result is false
        }
        return result;
    }

    protected boolean isMutable(Object attribute) {
        return attribute != null
                && !(attribute instanceof String || attribute instanceof Number || attribute instanceof Character || attribute instanceof Boolean);
    }

    /**
     * Gets a reference to the JBossCacheService.
     */
    protected void establishDistributedCacheManager() {
        if (distributedCacheManager == null) {
            distributedCacheManager = getManagerInternal().getDistributedCacheManager();

            // still null???
            if (distributedCacheManager == null) {
                throw MESSAGES.nullDistributedCacheManager();
            }
        }
    }

    protected final void sessionAttributesDirty() {
        if (!sessionAttributesDirty && log.isTraceEnabled())
            log.tracef("Marking session attributes dirty %s", id);

        sessionAttributesDirty = true;
    }

    protected final void setHasActivationListener(boolean hasListener) {
        this.hasActivationListener = Boolean.valueOf(hasListener);
    }

    protected int getVersion() {
        return version.get();
    }

    protected long getSessionTimestamp() {
        this.timestamp.set(this.thisAccessedTime);
        return this.timestamp.get();
    }

    protected boolean isSessionMetadataDirty() {
        return sessionMetadataDirty;
    }

    protected DistributableSessionMetadata getSessionMetadata() {
        this.metadata.setId(id);
        this.metadata.setCreationTime(creationTime);
        this.metadata.setMaxInactiveInterval(maxInactiveInterval);
        this.metadata.setNew(isNew);
        this.metadata.setValid(isValid);

        return this.metadata;
    }

    protected boolean isSessionAttributeMapDirty() {
        return sessionAttributesDirty || isFullReplicationNeeded();
    }

    protected boolean isFullReplicationNeeded() {
        if (fullReplicationRequired) {
            return true;
        }
        return fullReplicationRequired || (fullReplicationWindow > 0 && System.currentTimeMillis() < fullReplicationWindow);
    }

    // ----------------------------------------------------------------- Private

    private void checkAlwaysReplicateTimestamp() {
        this.alwaysReplicateTimestamp = (maxUnreplicatedInterval == 0 || (maxUnreplicatedInterval > 0
                && maxInactiveInterval >= 0 && maxUnreplicatedInterval > (maxInactiveInterval * 1000)));
    }

    private void parseRealId(String sessionId) {
        String newId = this.manager.parse(sessionId).getKey();

        // realId is used in a lot of map lookups, so only replace it
        // if the new id is actually different -- preserve object identity
        if (!newId.equals(realId)) {
            realId = newId;
        }
    }

    /**
     * Remove the attribute from the local cache and possibly the distributed cache, plus notify any listeners
     *
     * @param name the attribute name
     * @param localCall <code>true</code> if this call originated from local activity (e.g. a removeAttribute() in the webapp or
     *        a local session invalidation/expiration), <code>false</code> if it originated due to an remote event in the
     *        distributed cache.
     * @param localOnly <code>true</code> if the removal should not be replicated around the cluster
     * @param notify <code>true</code> if listeners should be notified
     * @param cause the cause of the removal
     */
    private void removeAttributeInternal(String name, boolean localCall, boolean localOnly, boolean notify,
            ClusteredSessionNotificationCause cause) {
        // Remove this attribute from our collection
        Object value = removeAttributeInternal(name, localCall, localOnly);

        // Do we need to do valueUnbound() and attributeRemoved() notification?
        if (!notify || (value == null)) {
            return;
        }

        // Call the valueUnbound() method if necessary
        HttpSessionBindingEvent event = null;
        if (value instanceof HttpSessionBindingListener
                && notificationPolicy.isHttpSessionBindingListenerInvocationAllowed(this.clusterStatus, cause, name, localCall)) {
            event = new HttpSessionBindingEvent(getSession(), name, value);
            ((HttpSessionBindingListener) value).valueUnbound(event);
        }

        // Notify interested application event listeners
        if (notificationPolicy.isHttpSessionAttributeListenerInvocationAllowed(this.clusterStatus, cause, name, localCall)) {
            Context context = (Context) manager.getContainer();
            Object[] lifecycleListeners = context.getApplicationEventListeners();
            if (lifecycleListeners != null) {
                // SRV.10.6 (2.5) 11.6 (3.0) Propagate listener exceptions
                RuntimeException listenerException = null;

                for (int i = 0; i < lifecycleListeners.length; i++) {
                    if (!(lifecycleListeners[i] instanceof HttpSessionAttributeListener))
                        continue;
                    HttpSessionAttributeListener listener = (HttpSessionAttributeListener) lifecycleListeners[i];
                    try {
                        fireContainerEvent(context, "beforeSessionAttributeRemoved", listener);
                        if (event == null) {
                            event = new HttpSessionBindingEvent(getSession(), name, value);
                        }
                        try {
                            listener.attributeRemoved(event);
                        } catch (RuntimeException e) {
                            if (listenerException == null) {
                                listenerException = e;
                            }
                        }
                        fireContainerEvent(context, "afterSessionAttributeRemoved", listener);
                    } catch (Throwable t) {
                        try {
                            fireContainerEvent(context, "afterSessionAttributeRemoved", listener);
                        } catch (Exception e) {
                            // Ignore
                        }
                        manager.getContainer().getLogger().error(MESSAGES.errorSessionAttributeEvent(t));
                    }
                }

                if (listenerException != null) {
                    throw listenerException;
                }
            }
        }
    }

    private String[] keys() {
        Set<String> keySet = getAttributesInternal().keySet();
        return ((String[]) keySet.toArray(new String[keySet.size()]));
    }

    /**
     * Return the <code>isValid</code> flag for this session without any expiration check.
     */
    @Override
    public boolean isValidInternal() {
        return this.isValid || this.expiring;
    }

    /**
     * Fire container events if the Context implementation is the <code>org.apache.catalina.core.StandardContext</code>.
     *
     * @param context Context for which to fire events
     * @param type Event type
     * @param data Event data
     *
     * @exception Exception occurred during event firing
     */
    private void fireContainerEvent(Context context, String type, Object data) throws Exception {

        if (!"org.apache.catalina.core.StandardContext".equals(context.getClass().getName())) {
            return; // Container events are not supported
        }
        // NOTE: Race condition is harmless, so do not synchronize
        if (containerEventMethod == null) {
            containerEventMethod = context.getClass().getMethod("fireContainerEvent", containerEventTypes);
        }
        Object[] containerEventParams = new Object[2];
        containerEventParams[0] = type;
        containerEventParams[1] = data;
        containerEventMethod.invoke(context, containerEventParams);

    }

    /**
     * Notify all session event listeners that a particular event has occurred for this Session. The default implementation
     * performs this notification synchronously using the calling thread.
     *
     * @param type Event type
     * @param data Event data
     */
    private void fireSessionEvent(String type, Object data) {
        if (listeners.size() < 1)
            return;
        SessionEvent event = new SessionEvent(this, type, data);
        SessionListener[] list = new SessionListener[0];
        synchronized (listeners) {
            list = (SessionListener[]) listeners.toArray(list);
        }

        for (int i = 0; i < list.length; i++) {
            ((SessionListener) list[i]).sessionEvent(event);
        }

    }

    private void sessionMetadataDirty() {
        if (!sessionMetadataDirty && !isNew && log.isTraceEnabled())
            log.tracef("Marking session metadata dirty %s", id);
        sessionMetadataDirty = true;
    }

    /**
     * Advise our manager to remove this expired session.
     *
     * @param localOnly whether the rest of the cluster should be made aware of the removal
     */
    private void removeFromManager(boolean localOnly) {
        if (localOnly) {
            manager.removeLocal(this);
        } else {
            manager.remove(this);
        }
    }

    private void requireFullReplication() {
        this.fullReplicationRequired = true;
        this.fullReplicationWindow = System.currentTimeMillis() + FULL_REPLICATION_WINDOW_LENGTH;
    }

}
