/* * JBoss, Home of Professional Open Source. * Copyright 2008, Red Hat Middleware LLC, and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.undertow.session; import java.beans.PropertyChangeSupport; import java.io.Serializable; import java.lang.reflect.Method; import java.security.Principal; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import javax.servlet.http.HttpSessionActivationListener; import javax.servlet.http.HttpSessionBindingEvent; import io.undertow.server.HttpServerExchange; import io.undertow.server.session.Session; import io.undertow.server.session.SessionConfig; import org.jboss.as.clustering.web.DistributableSessionMetadata; import org.jboss.as.clustering.web.DistributedCacheManager; import org.jboss.as.clustering.web.IncomingDistributableSessionData; import org.jboss.as.clustering.web.OutgoingDistributableSessionData; import org.jboss.as.clustering.web.SessionOwnershipSupport; import org.jboss.as.undertow.session.notification.ClusteredSessionManagementStatus; import org.jboss.as.undertow.session.notification.ClusteredSessionNotificationCause; import org.jboss.as.undertow.session.notification.ClusteredSessionNotificationPolicy; import org.jboss.logging.Logger; import org.jboss.metadata.web.jboss.ReplicationTrigger; import static org.jboss.as.undertow.UndertowMessages.MESSAGES; /** * 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 Session { private static final boolean ACTIVITY_CHECK = false; /** * Set of attribute names which are not allowed to be replicated/persisted. */ protected static final String[] excludedAttributes = { /*Globals.SUBJECT_ATTR*/}; /** * Set containing all members of {@link #excludedAttributes}. */ protected static final Set<String> replicationExcludes = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(excludedAttributes))); protected static final Logger log = Logger.getLogger(ClusteredSession.class); /** * Length of time we do full replication as workaround to JBCACHE-1531 */ private static final long FULL_REPLICATION_WINDOW_LENGTH = 5000; // ----------------------------------------------------- Instance Variables /** * The collection of user data attributes associated with this Session. */ private final Map<String, Object> attributes = new ConcurrentHashMap<String, Object>(16, 0.75f, 2); /** * The authentication type used to authenticate our cached Principal, if any. NOTE: This value is not included in the * serialized version of this object. */ private transient String authType = null; /** * The <code>java.lang.Method</code> for the <code>fireContainerEvent()</code> method of the * <code>org.apache.catalina.core.StandardContext</code> method, if our Context implementation is of this class. This value * is computed dynamically the first time it is needed, or after a session reload (since it is declared transient). */ private transient Method containerEventMethod = null; /** * The time this session was created, in milliseconds since midnight, January 1, 1970 GMT. */ private long creationTime = 0L; /** * We are currently processing a session expiration, so bypass certain IllegalStateException tests. NOTE: This value is not * included in the serialized version of this object. */ private transient volatile boolean expiring = false; /** * The session identifier of this Session. */ private String id = null; /** * The last accessed time for this Session. */ private volatile long lastAccessedTime = creationTime; /** * The Manager with which this Session is associated. */ private transient DistributableSessionManager<O> manager = null; /** * Our proxy to the distributed cache. */ private transient DistributedCacheManager<O> distributedCacheManager; /** * The maximum time interval, in seconds, between client requests before the servlet container may invalidate this session. * A negative time indicates that the session should never time out. */ private int maxInactiveInterval = -1; /** * Flag indicating whether this session is new or not. */ private boolean isNew = false; /** * Flag indicating whether this session is valid or not. */ private volatile boolean isValid = false; /** * Internal notes associated with this session by Catalina components and event listeners. <b>IMPLEMENTATION NOTE:</b> This * object is <em>not</em> saved and restored across session serializations! */ private final transient Map<String, Object> notes = new Hashtable<String, Object>(); /** * The authenticated Principal associated with this session, if any. <b>IMPLEMENTATION NOTE:</b> This object is <i>not</i> * saved and restored across session serializations! */ private transient Principal principal = null; /** * The property change support for this component. NOTE: This value is not included in the serialized version of this * object. */ private transient PropertyChangeSupport support = new PropertyChangeSupport(this); /** * The current accessed time for this session. */ private volatile long thisAccessedTime = creationTime; /** * The access count for this session. */ private final transient AtomicInteger accessCount; /** * Policy controlling whether reading/writing attributes requires replication. */ private ReplicationTrigger invalidationPolicy; /** * If true, means the local in-memory session data contains metadata changes that have not been published to the distributed * cache. */ private transient boolean sessionMetadataDirty; /** * If true, means the local in-memory session data contains attribute changes that have not been published to the * distributed cache. */ private transient boolean sessionAttributesDirty; /** * Object wrapping thisAccessedTime. Create once and mutate so we can store it in JBoss Cache w/o concern that a transaction * rollback will revert the cached ref to an older object. */ private final transient AtomicLong timestamp = new AtomicLong(0); /** * Object wrapping other metadata for this session. Create once and mutate so we can store it in JBoss Cache w/o concern * that a transaction rollback will revert the cached ref to an older object. */ private transient volatile DistributableSessionMetadata metadata = new DistributableSessionMetadata(); /** * The last time {@link #setIsOutdated setIsOutdated(true)} was called or <code>0</code> if * <code>setIsOutdated(false)</code> was subsequently called. */ private transient volatile long outdatedTime; /** * Version number to track cache invalidation. If any new version number is greater than this one, it means the data it * holds is newer than this one. */ private final AtomicInteger version = new AtomicInteger(0); /** * The session's id with any jvmRoute removed. */ private transient String realId; /** * Timestamp when we were last replicated. */ private transient volatile long lastReplicated; /** * Maximum number of milliseconds this session should be allowed to go unreplicated if access to the session doesn't mark it * as dirty. */ private transient long maxUnreplicatedInterval; /** * True if maxUnreplicatedInterval is 0 or less than maxInactiveInterval */ private transient boolean alwaysReplicateTimestamp = true; /** * Whether any of this session's attributes implement HttpSessionActivationListener. */ private transient Boolean hasActivationListener; /** * Has this session only been accessed once? */ private transient boolean firstAccess; /** * Policy that drives whether we issue servlet spec notifications. */ private transient ClusteredSessionNotificationPolicy notificationPolicy; private transient ClusteredSessionManagementStatus clusterStatus; /** * True if a call to activate() is needed to offset a preceding passivate() call */ private transient boolean needsPostReplicateActivation; /** * True if a getOutgoingSessionData() should include metadata and all attributes no matter what. This is a workaround to * JBCACHE-1531. This flag ensures that at least one request gets full replication, whether or not in occurs before * this.fullReplicationWindow */ private transient boolean fullReplicationRequired = true; /** * End of period when we do full replication */ private transient long fullReplicationWindow = -1; /** * Coordinate updates from the cluster */ private transient Lock ownershipLock = new ReentrantLock(); // ------------------------------------------------------------ Constructors /** * Creates a new ClusteredSession. * * @param manager the manager for this session */ protected ClusteredSession(DistributableSessionManager<O> manager) { super(); // Initialize access count accessCount = ACTIVITY_CHECK ? new AtomicInteger() : null; this.firstAccess = true; setManager(manager); requireFullReplication(); } // ---------------------------------------------------------------- Session public String getAuthType() { return this.authType; } public void setAuthType(String authType) { String oldAuthType = this.authType; this.authType = authType; support.firePropertyChange("authType", oldAuthType, this.authType); } @Override public long getCreationTime() { if (!isValidInternal()) throw MESSAGES.expiredSession(); return (this.creationTime); } /** * Set the creation time for this session. This method is called by the Manager when an existing Session instance is reused. * * @param time The new creation time */ public void setCreationTime(long time) { this.creationTime = time; this.lastAccessedTime = time; this.thisAccessedTime = time; sessionMetadataDirty(); } @Override 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 MESSAGES.expiredSession(); } return this.lastAccessedTime; } public long getLastAccessedTimeInternal() { return (this.lastAccessedTime); } public SessionManager getManager() { return this.manager; } public void setManager(DistributableSessionManager manager) { @SuppressWarnings("unchecked") DistributableSessionManager<O> unchecked = (DistributableSessionManager<O>) manager; this.manager = unchecked; this.invalidationPolicy = this.manager.getReplicationTrigger(); int maxUnrep = this.manager.getMaxUnreplicatedInterval() * 1000; setMaxUnreplicatedInterval(maxUnrep); this.notificationPolicy = this.manager.getNotificationPolicy(); establishDistributedCacheManager(); } @Override public int getMaxInactiveInterval() { return this.maxInactiveInterval; } /** * Overrides the superclass to calculate {@link #getMaxUnreplicatedInterval() maxUnreplicatedInterval}. */ @Override public void setMaxInactiveInterval(int interval) { this.maxInactiveInterval = interval; checkAlwaysReplicateTimestamp(); sessionMetadataDirty(); } public Principal getPrincipal() { return this.principal; } @Override public String changeSessionId(final HttpServerExchange exchange, final SessionConfig config) { throw new RuntimeException("NYI"); } @Override public io.undertow.server.session.SessionManager getSessionManager() { return manager; } @Override public void invalidate(final HttpServerExchange exchange) { invalidate(); } /** * 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() { try { this.acquireSessionOwnership(); } catch (TimeoutException e) { // May be contending with the session expiration thread of another node, so retry once. try { this.acquireSessionOwnership(); } catch (TimeoutException te) { throw MESSAGES.failAcquiringOwnership(realId, te); } } 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() throws TimeoutException { SessionOwnershipSupport support = this.distributedCacheManager.getSessionOwnershipSupport(); if (support != null) { try { this.ownershipLock.lockInterruptibly(); try { if (support.acquireSessionOwnership(this.realId, needNewLock()) == SessionOwnershipSupport.LockResult.ACQUIRED_FROM_CLUSTER) { IncomingDistributableSessionData data = this.distributedCacheManager.getSessionData(this.realId, false); if (data != null) { // We may be out of date re: the distributed cache update(data); } } } finally { this.ownershipLock.unlock(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw MESSAGES.interruptedAcquiringOwnership(realId, e); } } } private boolean needNewLock() { return firstAccess && isNew; } public void requestDone(final HttpServerExchange exchange) { 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 MESSAGES.expiredSession(); 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 org.apache.catalina.session.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. * * @throws IllegalStateException if this method is called on an invalidated session */ public void invalidate() { if (!isValid()) throw MESSAGES.expiredSession(); // Cause this session to expire globally boolean notify = true; boolean localCall = true; boolean localOnly = false; expire(notify, localCall, localOnly, ClusteredSessionNotificationCause.INVALIDATE); // Preemptively relinquish ownership that was acquired in access() - don't wait for endAccess() this.relinquishSessionOwnership(false); } public void expire() { boolean notify = true; boolean localCall = true; boolean localOnly = false; expire(notify, localCall, localOnly, ClusteredSessionNotificationCause.INVALIDATE); } 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); } // ------------------------------------------------------------ HttpSession @Override public Object getAttribute(String name) { if (!isValid()) throw MESSAGES.expiredSession(); return getAttributeInternal(name); } @Override @SuppressWarnings("unchecked") public Set<String> getAttributeNames() { if (!isValid()) throw MESSAGES.expiredSession(); return getAttributesInternal().keySet(); } @Override public Object setAttribute(String name, Object value) { // Name cannot be null if (name == null) throw MESSAGES.expiredSession(); // Null value is the same as removeAttribute() if (value == null) { return removeAttribute(name); } // Validate our current state if (!isValidInternal()) { throw MESSAGES.expiredSession(); } if (canAttributeBeReplicated(value) == false) { throw MESSAGES.failToReplicateAttribute(name, value.getClass().getCanonicalName()); } // Construct an event with the new value HttpSessionBindingEvent event = null; if (value instanceof HttpSessionActivationListener) hasActivationListener = Boolean.TRUE; // Replace or add this attribute return setAttributeInternal(name, value); } @Override public Object removeAttribute(String name) { // Validate our current state if (!isValidInternal()) throw MESSAGES.expiredSession(); final boolean localCall = true; final boolean localOnly = false; final boolean notify = true; return removeAttributeInternal(name, localCall, localOnly, notify, ClusteredSessionNotificationCause.MODIFY); } // ---------------------------------------------------- DistributableSession /** * Gets the session id with any appended jvmRoute info removed. * * @see #getUseJK() */ public String getRealId() { return realId; } public boolean getMustReplicateTimestamp() { // If the access times are the same, access() was never called // on the session boolean touched = this.thisAccessedTime != this.lastAccessedTime; boolean exceeds = alwaysReplicateTimestamp && touched; if (!exceeds && touched && maxUnreplicatedInterval > 0) // -1 means ignore { long unrepl = System.currentTimeMillis() - lastReplicated; exceeds = (unrepl >= maxUnreplicatedInterval); } return exceeds; } /** * {@inheritDoc} */ public void update(IncomingDistributableSessionData sessionData) { assert sessionData != null : MESSAGES.nullParamter("sessionData"); this.version.set(sessionData.getVersion()); long ts = sessionData.getTimestamp(); this.lastAccessedTime = this.thisAccessedTime = ts; this.timestamp.set(ts); DistributableSessionMetadata md = sessionData.getMetadata(); // TODO -- get rid of these field and delegate to metadata this.id = md.getId(); this.creationTime = md.getCreationTime(); this.maxInactiveInterval = md.getMaxInactiveInterval(); this.isNew = md.isNew(); this.isValid = md.isValid(); this.metadata = md; // Get our id without any jvmRoute appended parseRealId(id); // We no longer know if we have an activationListener hasActivationListener = null; // If the session has been replicated, any subsequent // access cannot be the first. this.firstAccess = false; // We don't know when we last replicated our timestamp. We may be // getting called due to activation, not deserialization after // replication, so this.timestamp may be after the last replication. // So use the creation time as a conservative guesstimate. Only downside // is we may replicate a timestamp earlier than we need to, which is not // a heavy cost. this.lastReplicated = this.creationTime; this.clusterStatus = new ClusteredSessionManagementStatus(this.realId, true, null, null); checkAlwaysReplicateTimestamp(); populateAttributes(sessionData.getSessionAttributes()); // TODO uncomment when work on JBAS-1900 is completed // // Session notes -- for FORM auth apps, allow replicated session // // to be used without requiring a new login // // We use the superclass set/removeNote calls here to bypass // // the custom logic we've added // String username = (String) in.readObject(); // if (username != null) // { // super.setNote(Constants.SESS_USERNAME_NOTE, username); // } // else // { // super.removeNote(Constants.SESS_USERNAME_NOTE); // } // String password = (String) in.readObject(); // if (password != null) // { // super.setNote(Constants.SESS_PASSWORD_NOTE, password); // } // else // { // super.removeNote(Constants.SESS_PASSWORD_NOTE); // } // We are no longer outdated vis a vis distributed cache this.outdatedTime = 0; // Requests must publish our full state back to the cluster in case anything got dropped. this.requireFullReplication(); } // ------------------------------------------------------------------ Public /** * Increment our version and propagate ourself to the distributed cache. */ public synchronized void processSessionReplication() { // Replicate the session. if (log.isTraceEnabled()) { log.tracef("processSessionReplication(): session is dirty. Will increment version from: %s and replicate.", getVersion()); } version.incrementAndGet(); O outgoingData = getOutgoingSessionData(); distributedCacheManager.storeSessionData(outgoingData); sessionAttributesDirty = false; sessionMetadataDirty = false; lastReplicated = System.currentTimeMillis(); this.fullReplicationRequired = false; if (this.fullReplicationWindow > 0 && System.currentTimeMillis() > this.fullReplicationWindow) { this.fullReplicationWindow = -1; } } /** * Remove myself from the distributed cache. */ public void removeMyself() { getDistributedCacheManager().removeSession(getRealId()); } /** * Remove myself from the <t>local</t> instance of the distributed cache. */ public void removeMyselfLocal() { getDistributedCacheManager().removeSessionLocal(getRealId()); } /** * Gets the sessions creation time, skipping any validity check. * * @return the creation time */ public long getCreationTimeInternal() { return creationTime; } /** * Gets the time {@link #processSessionReplication()} was last called, or <code>0</code> if it has never been called. */ public long getLastReplicated() { return lastReplicated; } /** * Gets the maximum period in ms after which a request accessing this session will trigger replication of its timestamp, * even if the request doesn't otherwise modify the session. A value of -1 means no limit. */ public long getMaxUnreplicatedInterval() { return maxUnreplicatedInterval; } /** * Sets the maximum period in ms after which a request accessing this session will trigger replication of its timestamp, * even if the request doesn't otherwise modify the session. A value of -1 means no limit. */ public void setMaxUnreplicatedInterval(long interval) { this.maxUnreplicatedInterval = Math.max(interval, -1); checkAlwaysReplicateTimestamp(); } /** * This is called specifically for failover case using mod_jk where the new session has this node name in there. As a * result, it is safe to just replace the id since the backend store is using the "real" id without the node name. * * @param id */ public void resetIdWithRouteInfo(String id) { this.id = id; parseRealId(id); } /** * Update our version due to changes in the distributed cache. * * @param version the distributed cache version * @return <code>true</code> */ public boolean setVersionFromDistributedCache(int version) { boolean outdated = getVersion() < version; if (outdated) { outdatedTime = System.currentTimeMillis(); } return outdated; } /** * Check to see if the session data is still valid. Outdated here means that the in-memory data is not in sync with one in * the data store. * * @return */ public boolean isOutdated() { // if creationTime == 0 we've neither been synced with the // distributed cache nor had creation time set (i.e. brand new session) return thisAccessedTime < outdatedTime || this.creationTime == 0; } public boolean isSessionDirty() { return sessionAttributesDirty || sessionMetadataDirty; } /** * Inform any HttpSessionListener of the creation of this session */ public void tellNew(ClusteredSessionNotificationCause cause) { // Notify interested session event listeners //fireSessionEvent(Session.SESSION_CREATED_EVENT, null); //TODO: notifications } /** * Returns whether the current session is still valid, but only calls <code>expire</code> for timed-out sessions if * <code>expireIfInvalid</code> is <code>true</code>. * * @param expireIfInvalid <code>true</code> if sessions that have been timed out should be expired */ public boolean isValid(boolean expireIfInvalid) { if (this.expiring) { return true; } if (!this.isValid) { return false; } if (ACTIVITY_CHECK && accessCount.get() > 0) { return true; } if (maxInactiveInterval > 0) { long timeNow = System.currentTimeMillis(); int timeIdle = (int) ((timeNow - thisAccessedTime) / 1000L); if (timeIdle >= maxInactiveInterval) { if (expireIfInvalid) { boolean notify = true; boolean localCall = true; boolean localOnly = true; expire(notify, localCall, localOnly, ClusteredSessionNotificationCause.TIMEOUT); } else { return false; } } } return (this.isValid); } /** * Expires the session, notifying listeners and possibly the manager. * <p> * <strong>NOTE:</strong> The manager will only be notified of the expiration if <code>localCall</code> is <code>true</code> * ; otherwise it is the responsibility of the caller to notify the manager that the session is expired. (In the case of * JBossCacheManager, it is the manager itself that makes such a call, so it of course is aware). * </p> * * @param notify whether servlet spec listeners should be notified * @param localCall <code>true</code> if this call originated due to local activity (such as a session invalidation in user * code or an expiration by the local background processing thread); <code>false</code> if the expiration originated * due to some kind of event notification from the cluster. * @param localOnly <code>true</code> if the expiration should not be announced to the cluster, <code>false</code> if other * cluster nodes should be made aware of the expiration. Only meaningful if <code>localCall</code> is * <code>true</code>. * @param cause the cause of the expiration */ public void expire(boolean notify, boolean localCall, boolean localOnly, ClusteredSessionNotificationCause cause) { if (log.isTraceEnabled()) { log.tracef("The session has expired with id: %s -- is expiration local? %s", id, localOnly); } // If another thread is already doing this, stop if (expiring) return; synchronized (this) { // If we had a race to this sync block, another thread may // have already completed expiration. If so, don't do it again if (!isValid) return; if (manager == null) return; expiring = true; // SRV.10.6 (2.5) 11.6 (3.0) Propagate listener exceptions RuntimeException listenerException = null; final boolean requireOwnershipLock = localCall && !localOnly; if (requireOwnershipLock) { try { this.acquireSessionOwnership(); } catch (TimeoutException e) { this.expiring = false; throw MESSAGES.failAcquiringOwnership(realId, e); } } try { //TODO: listeners if (ACTIVITY_CHECK) { accessCount.set(0); } // 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 (requireOwnershipLock) { 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 //TODO: listeners } /** * 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; } } /** * Gets whether the session needs to notify HttpSessionActivationListeners that it has been activated following replication. */ public boolean getNeedsPostReplicateActivation() { return needsPostReplicateActivation; } @Override public String toString() { return new StringBuilder(getClass().getSimpleName()).append('[').append("id: ").append(id) .append(" lastAccessedTime: ").append(lastAccessedTime).append(" version: ").append(version) .append(" lastOutdated: ").append(outdatedTime).append(']').toString(); } // ----------------------------------------------------- Protected Methods protected abstract Object setAttributeInternal(String name, Object value); protected abstract Object removeAttributeInternal(String name, boolean localCall, boolean localOnly); protected abstract O getOutgoingSessionData(); protected Object getAttributeInternal(String name) { Object result = getAttributesInternal().get(name); // Do dirty check even if result is null, as w/ SET_AND_GET null // still makes us dirty (ensures timely replication w/o using ACCESS) if (isGetDirty(result)) { sessionAttributesDirty(); } return result; } /** * Extension point for subclasses to load the attribute map from the distributed cache. */ protected void populateAttributes(Map<String, Object> distributedCacheAttributes) { Map<String, Object> existing = getAttributesInternal(); Map<String, Object> excluded = removeExcludedAttributes(existing); existing.clear(); existing.putAll(distributedCacheAttributes); if (excluded != null) existing.putAll(excluded); } protected final Map<String, Object> getAttributesInternal() { return attributes; } protected final ClusteredSessionManager<O> getManagerInternal() { return manager; } protected final DistributedCacheManager<O> getDistributedCacheManager() { return distributedCacheManager; } protected final void setDistributedCacheManager(DistributedCacheManager<O> distributedCacheManager) { this.distributedCacheManager = distributedCacheManager; } /** * Returns whether the attribute's type is one that can be replicated. * * @param attribute the attribute * @return <code>true</code> if <code>attribute</code> is <code>null</code>, <code>Serializable</code> or an array of * primitives. */ protected boolean canAttributeBeReplicated(Object attribute) { if (attribute instanceof Serializable || attribute == null) return true; Class<?> clazz = attribute.getClass().getComponentType(); return (clazz != null && clazz.isPrimitive()); } /** * Removes any attribute whose name is found in {@link #excludedAttributes} from <code>attributes</code> and returns a Map * of all such attributes. * * @param attributes source map from which excluded attributes are to be removed. * @return Map that contains any attributes removed from <code>attributes</code>, or <code>null</code> if no attributes were * removed. */ protected final Map<String, Object> removeExcludedAttributes(Map<String, Object> attributes) { Map<String, Object> excluded = null; for (int i = 0; i < excludedAttributes.length; i++) { Object attr = attributes.remove(excludedAttributes[i]); if (attr != null) { if (log.isTraceEnabled()) { log.tracef("Excluding attribute %s from replication", excludedAttributes[i]); } if (excluded == null) { excluded = new HashMap<String, Object>(); } excluded.put(excludedAttributes[i], attr); } } return excluded; } protected final boolean isGetDirty(Object attribute) { boolean result = false; switch (invalidationPolicy) { case SET_AND_GET: result = true; break; case SET_AND_NON_PRIMITIVE_GET: result = isMutable(attribute); break; default: // result is false } return result; } protected boolean isMutable(Object attribute) { return attribute != null && !(attribute instanceof String || attribute instanceof Number || attribute instanceof Character || attribute instanceof Boolean); } /** * Gets a reference to the JBossCacheService. */ protected void establishDistributedCacheManager() { if (distributedCacheManager == null) { distributedCacheManager = getManagerInternal().getDistributedCacheManager(); // still null??? if (distributedCacheManager == null) { throw MESSAGES.nullParamter("distributedCacheManager"); } } } protected final void sessionAttributesDirty() { if (!sessionAttributesDirty && log.isTraceEnabled()) log.tracef("Marking session attributes dirty %s", id); sessionAttributesDirty = true; } protected final void setHasActivationListener(boolean hasListener) { this.hasActivationListener = Boolean.valueOf(hasListener); } protected int getVersion() { return version.get(); } protected long getSessionTimestamp() { this.timestamp.set(this.thisAccessedTime); return this.timestamp.get(); } protected boolean isSessionMetadataDirty() { return sessionMetadataDirty; } protected DistributableSessionMetadata getSessionMetadata() { this.metadata.setId(id); this.metadata.setCreationTime(creationTime); this.metadata.setMaxInactiveInterval(maxInactiveInterval); this.metadata.setNew(isNew); this.metadata.setValid(isValid); return this.metadata; } protected boolean isSessionAttributeMapDirty() { return sessionAttributesDirty || isFullReplicationNeeded(); } protected boolean isFullReplicationNeeded() { if (fullReplicationRequired) { return true; } return fullReplicationRequired || (fullReplicationWindow > 0 && System.currentTimeMillis() < fullReplicationWindow); } // ----------------------------------------------------------------- Private private void checkAlwaysReplicateTimestamp() { this.alwaysReplicateTimestamp = (maxUnreplicatedInterval == 0 || (maxUnreplicatedInterval > 0 && maxInactiveInterval >= 0 && maxUnreplicatedInterval > (maxInactiveInterval * 1000))); } private void parseRealId(String sessionId) { String newId = this.manager.parse(sessionId).getKey(); // realId is used in a lot of map lookups, so only replace it // if the new id is actually different -- preserve object identity if (!newId.equals(realId)) { realId = newId; } } /** * Remove the attribute from the local cache and possibly the distributed cache, plus notify any listeners * * @param name the attribute name * @param localCall <code>true</code> if this call originated from local activity (e.g. a removeAttribute() in the webapp or * a local session invalidation/expiration), <code>false</code> if it originated due to an remote event in the * distributed cache. * @param localOnly <code>true</code> if the removal should not be replicated around the cluster * @param notify <code>true</code> if listeners should be notified * @param cause the cause of the removal */ private Object removeAttributeInternal(String name, boolean localCall, boolean localOnly, boolean notify, ClusteredSessionNotificationCause cause) { // Remove this attribute from our collection Object value = removeAttributeInternal(name, localCall, localOnly); return value; } 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; } private void sessionMetadataDirty() { if (!sessionMetadataDirty && !isNew && log.isTraceEnabled()) log.tracef("Marking session metadata dirty %s", id); sessionMetadataDirty = true; } /** * Advise our manager to remove this expired session. * * @param localOnly whether the rest of the cluster should be made aware of the removal */ private void removeFromManager(boolean localOnly) { if (localOnly) { manager.removeLocal(this); } else { manager.remove(this); } } private void requireFullReplication() { this.fullReplicationRequired = true; this.fullReplicationWindow = System.currentTimeMillis() + FULL_REPLICATION_WINDOW_LENGTH; } }