/*
 * 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.web.tomcat.service.session;

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.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.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

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.logging.Logger;
import org.jboss.metadata.web.jboss.ReplicationTrigger;
import org.jboss.web.tomcat.service.session.distributedcache.spi.DistributableSessionMetadata;
import org.jboss.web.tomcat.service.session.distributedcache.spi.DistributedCacheManager;
import org.jboss.web.tomcat.service.session.distributedcache.spi.IncomingDistributableSessionData;
import org.jboss.web.tomcat.service.session.distributedcache.spi.OutgoingDistributableSessionData;
import org.jboss.web.tomcat.service.session.notification.ClusteredSessionManagementStatus;
import org.jboss.web.tomcat.service.session.notification.ClusteredSessionNotificationCause;
import org.jboss.web.tomcat.service.session.notification.ClusteredSessionNotificationPolicy;

/**
 * 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: 105104 $
 */
public abstract class ClusteredSession<O extends OutgoingDistributableSessionData>
   implements HttpSession, Session
{
   // --------------------------------------------------------------- Constants
   
   private static final long serialVersionUID = -758573655613558722L;

   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;
   static
   {
      Set<String> set = new HashSet<String>();
      for (int i = 0; i < excludedAttributes.length; i++)
      {
         set.add(excludedAttributes[i]);
      }
      replicationExcludes = Collections.unmodifiableSet(set);
   }


   /**
    * 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()
   {
      @SuppressWarnings("unchecked")
      private final Map empty = Collections.EMPTY_MAP;

      @SuppressWarnings("unchecked")
      public Enumeration getIds()
      {
         return new Enumerator(empty);
      }

      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 volatile transient 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 ClusteredManager<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 volatile transient 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 volatile transient 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;
   
   /**
    * Whether JK is being used, in which case our realId will
    * not match our id
    */
   private transient boolean useJK;
   
   /**
    * Timestamp when we were last replicated.
    */
   private volatile transient 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;
   
   // ------------------------------------------------------------ Constructors

   /**
    * Creates a new ClusteredSession.
    * 
    * @param manager the manager for this session
    */
   protected ClusteredSession(ClusteredManager<O> manager)
   {
      super();
      
      // Initialize access count
      accessCount = ACTIVITY_CHECK ? new AtomicInteger() : null;
      this.firstAccess = true;
      
      setManager(manager);
      requireFullReplication();
   }
   
   // ---------------------------------------------------------------- Session

   public abstract String getInfo();
   
   public String getAuthType()
   {
      return (this.authType);
   }

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

   public long getCreationTime()
   {
      if (!isValidInternal())
         throw new IllegalStateException
             (sm.getString("clusteredSession.getCreationTime.ise"));

     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
    */
   public void setCreationTime(long time)
   {
      this.creationTime = time;
      this.lastAccessedTime = time;
      this.thisAccessedTime = time;
      sessionMetadataDirty();
   }

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

   public String getIdInternal()
   {
      return (this.id);
   }
   
   /**
    * Overrides the superclass method to also set the
    * {@link #getRealId() realId} property.
    */
   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);
   }

   public long getLastAccessedTime()
   {
      if (!isValidInternal()) {
         throw new IllegalStateException
             (sm.getString("clusteredSession.getLastAccessedTime.ise"));
     }

     return (this.lastAccessedTime);
   }

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

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

   public void setManager(Manager manager)
   {
      if ((manager instanceof ClusteredManager<?>) == false)
         throw new IllegalArgumentException("manager must implement ClusteredManager");
      @SuppressWarnings("unchecked")
      ClusteredManager<O> unchecked = (ClusteredManager) manager;
      this.manager = unchecked;

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

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

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

   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
    */
   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();
   }

   public void access()
   {
      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);
      }      
   }
   
   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;
      }
   }

   public boolean isNew()
   {
      if (!isValidInternal())
         throw new IllegalStateException
             (sm.getString("clusteredSession.isNew.ise"));

     return (this.isNew);
   }
   
   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)}.
    */
   public boolean isValid()
   {
      return isValid(true);
   }
   
   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
    */
   public void invalidate()
   {
      if (!isValid())
         throw new IllegalStateException(sm.getString("clusteredSession.invalidate.ise"));

      // Cause this session to expire globally
      boolean notify = true;
      boolean localCall = true;
      boolean localOnly = false;
      expire(notify, localCall, localOnly, ClusteredSessionNotificationCause.INVALIDATE);
   }

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

   /**
    * Expires the session, but in such a way that other cluster nodes
    * are unaware of the expiration.
    *
    * @param notify true if notifications should be to sent to registered listeners; 
    *               false otherwise
    * 
    * @deprecated Will be removed in AS 6
    */
   public void expire(boolean notify)
   {
      boolean localCall = true;
      boolean localOnly = true;
      expire(notify, localCall, localOnly, ClusteredSessionNotificationCause.TIMEOUT);
   }

   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;
      useJK = false;
      version.set(0);
      hasActivationListener = null;
      lastReplicated = 0;
      maxUnreplicatedInterval = 0;
      alwaysReplicateTimestamp = true;
      this.notificationPolicy = null;
      this.clusterStatus = null;
      
      distributedCacheManager = null;
   }
   
   public void addSessionListener(SessionListener listener)
   {
      listeners.add(listener);
   }

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

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

   @SuppressWarnings("unchecked")
   public Iterator getNoteNames()
   {
      return (notes.keySet().iterator());
   }

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

   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();
//      }
//   }

   public HttpSession getSession()
   {
      if (facade == null)
      {
         if (SecurityUtil.isPackageProtectionEnabled())
         {
            final HttpSession fsession = this;
            @SuppressWarnings("unchecked")
            StandardSessionFacade ssf = (StandardSessionFacade)AccessController.doPrivileged(new PrivilegedAction()
            {
               public Object run()
               {
                  return new StandardSessionFacade(fsession);
               }
            });
            this.facade = ssf;
         } 
         else 
         {
            facade = new StandardSessionFacade(this);
         }
      }
      return (facade);
   }
   
   // ------------------------------------------------------------ HttpSession

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

   public Object getAttribute(String name)
   {
      if (!isValid())
         throw new IllegalStateException
            (sm.getString("clusteredSession.getAttribute.ise"));

      return getAttributeInternal(name);
   }

   @SuppressWarnings("unchecked")
   public Enumeration<String> getAttributeNames()
   {
      if (!isValid())
         throw new IllegalStateException
            (sm.getString("clusteredSession.getAttributeNames.ise"));

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

   public void setAttribute(String name, Object value)
   {
      // Name cannot be null
      if (name == null)
         throw new IllegalArgumentException
            (sm.getString("clusteredSession.setAttribute.namenull"));

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

      // Validate our current state
      if (!isValidInternal())
      {
         throw new IllegalStateException
            (sm.getString("clusteredSession.setAttribute.ise"));
      }
      
      if (canAttributeBeReplicated(value) == false)
      {
         throw new IllegalArgumentException
            (sm.getString("clusteredSession.setAttribute.iae"));
      }
      
      // 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(sm.getString("clusteredSession.bindingEvent"), 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(sm.getString("clusteredSession.bindingEvent"), 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)
               {
                  ;
               }
               manager.getContainer().getLogger().error(sm.getString("clusteredSession.attributeEvent"), t);
            }
         }
      }
   }

   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);
   }

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

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

   public String[] getValueNames()
   {
      if (!isValidInternal())
         throw new IllegalStateException
             (sm.getString("clusteredSession.getValueNames.ise"));

     return (keys());
   }

   public void putValue(String name, Object value)
   {
      setAttribute(name, value);
   }
   
   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 : "sessionData is null";
      
      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.trace("processSessionReplication(): session is dirty. Will increment " +
                   "version from: " + getVersion() + " and replicate.");
      }
      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);
   }

   /**
    * Gets whether the session expects to be accessible via mod_jk, mod_proxy, 
    * mod_cluster or other such AJP-based load balancers.
    */
   public boolean getUseJK()
   {
      return useJK;
   }
   
   /**
    * 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)
                  {
                     ;
                  }
                  manager.getContainer().getLogger().error(sm.getString("clusteredSession.sessionEvent"), 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.trace("The session has expired with id: " + id + 
                   " -- is expiration local? " + 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;
         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)
	                  {
	                     ;
	                  }
	                  manager.getContainer().getLogger().error(sm.getString("clusteredSession.sessionEvent"), 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;        	 
         }
      }
   }

   /**
    * 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
                            (sm.getString("clusteredSession.attributeEvent"), 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
                            (sm.getString("clusteredSession.attributeEvent"), 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 ClusteredManager<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.trace("Excluding attribute " + excludedAttributes[i] + 
                         " from replication");
            }
            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 new RuntimeException("DistributedCacheManager is null.");
         }
      }
   }

   protected final void sessionAttributesDirty()
   {
      if (!sessionAttributesDirty && log.isTraceEnabled())
         log.trace("Marking session attributes dirty " + 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 = null;
      if (useJK)
         newId = Util.getRealId(sessionId);
      else
         newId = sessionId;
      
      // 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)
                  {
                     ;
                  }
                  manager.getContainer().getLogger().error(sm.getString("clusteredSession.attributeEvent"), 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.
    */
   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.trace("Marking session metadata dirty " + 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;      
   }

}
