Package org.jboss.web.tomcat.service.session

Source Code of org.jboss.web.tomcat.service.session.ClusteredSession

/*
* 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 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.ha.framework.server.lock.TimeoutException;
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.distributedcache.spi.SessionOwnershipSupport;
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: 109139 $
*/
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()
   {
      @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 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;
  
   /** 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(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<O>) 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.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)
               {
                  // We may be out of date re: the distributed cache
                  update(this.distributedCacheManager.getSessionData(this.realId, false));
               }
            }
            catch (TimeoutException e)
            {
               throw new RuntimeException("Caught " + e.getClass().getSimpleName() + " acquiring ownership of " + this.realId, e);
            }
            finally
            {
               this.ownershipLock.unlock();
            }
         }
         catch (InterruptedException e)
         {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Interrupted while acquiring ownership of " + this.realId, e);
         }
      }
   }
  
   private boolean needNewLock()
   {
      return firstAccess && isNew;
   }

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

   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);
      // Preemptively relinquish ownership that was acquired in access() - don't wait for endAccess()
      this.relinquishSessionOwnership(false);
   }

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

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

   public Iterator<String> 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;
            StandardSessionFacade ssf = AccessController.doPrivileged(new PrivilegedAction<StandardSessionFacade>()
            {
               public StandardSessionFacade 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;
        
         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)
                     {
                        ;
                     }
                     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;
             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
                            (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;     
   }

}
TOP

Related Classes of org.jboss.web.tomcat.service.session.ClusteredSession

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.