[jboss-cvs] JBossAS SVN: r58021 - in branches/JBoss_4_0_3_SP1_JBAS-3808/tomcat/src/main/org/jboss/web/tomcat/tc5: session sso
jboss-cvs-commits at lists.jboss.org
jboss-cvs-commits at lists.jboss.org
Thu Nov 2 15:46:27 EST 2006
Author: bstansberry at jboss.com
Date: 2006-11-02 15:46:25 -0500 (Thu, 02 Nov 2006)
New Revision: 58021
Modified:
branches/JBoss_4_0_3_SP1_JBAS-3808/tomcat/src/main/org/jboss/web/tomcat/tc5/session/JBossManager.java
branches/JBoss_4_0_3_SP1_JBAS-3808/tomcat/src/main/org/jboss/web/tomcat/tc5/sso/ClusteredSingleSignOn.java
branches/JBoss_4_0_3_SP1_JBAS-3808/tomcat/src/main/org/jboss/web/tomcat/tc5/sso/TreeCacheSSOClusterManager.java
branches/JBoss_4_0_3_SP1_JBAS-3808/tomcat/src/main/org/jboss/web/tomcat/tc5/sso/mbeans-descriptors.xml
Log:
[JBAS-3808] Prevent clustered SSO invalidation on webapp undeploy or server shutdown
Modified: branches/JBoss_4_0_3_SP1_JBAS-3808/tomcat/src/main/org/jboss/web/tomcat/tc5/session/JBossManager.java
===================================================================
--- branches/JBoss_4_0_3_SP1_JBAS-3808/tomcat/src/main/org/jboss/web/tomcat/tc5/session/JBossManager.java 2006-11-02 20:44:41 UTC (rev 58020)
+++ branches/JBoss_4_0_3_SP1_JBAS-3808/tomcat/src/main/org/jboss/web/tomcat/tc5/session/JBossManager.java 2006-11-02 20:46:25 UTC (rev 58021)
@@ -208,6 +208,15 @@
return sessionIDGenerator_.getSessionId();
}
+ /**
+ * Gets the JMX <code>ObjectName</code> under
+ * which our <code>TreeCache</code> is registered.
+ */
+ public ObjectName getObjectName()
+ {
+ return objectName_;
+ }
+
public boolean isUseLocalCache()
{
return useLocalCache_;
Modified: branches/JBoss_4_0_3_SP1_JBAS-3808/tomcat/src/main/org/jboss/web/tomcat/tc5/sso/ClusteredSingleSignOn.java
===================================================================
--- branches/JBoss_4_0_3_SP1_JBAS-3808/tomcat/src/main/org/jboss/web/tomcat/tc5/sso/ClusteredSingleSignOn.java 2006-11-02 20:44:41 UTC (rev 58020)
+++ branches/JBoss_4_0_3_SP1_JBAS-3808/tomcat/src/main/org/jboss/web/tomcat/tc5/sso/ClusteredSingleSignOn.java 2006-11-02 20:46:25 UTC (rev 58021)
@@ -8,22 +8,36 @@
import org.jboss.web.tomcat.tc5.Tomcat5;
+import org.jboss.web.tomcat.tc5.session.JBossManager;
import java.io.IOException;
import java.security.Principal;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import javax.management.ObjectName;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
+import org.apache.catalina.Lifecycle;
+import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleException;
+import org.apache.catalina.LifecycleListener;
+import org.apache.catalina.Manager;
import org.apache.catalina.Session;
import org.apache.catalina.Realm;
import org.apache.catalina.SessionEvent;
import org.apache.catalina.authenticator.Constants;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
+import org.apache.catalina.session.ManagerBase;
+import EDU.oswego.cs.dl.util.concurrent.ConcurrentHashMap;
+
/**
* A <strong>Valve</strong> that supports a "single sign on" user experience,
* where the security identity of a user who successfully authenticates to one
@@ -46,8 +60,13 @@
*/
public class ClusteredSingleSignOn
extends org.apache.catalina.authenticator.SingleSignOn
+ implements LifecycleListener
{
-
+ /** By default we process expired SSOs no more often than once per minute */
+ public static final int DEFAULT_PROCESS_EXPIRES_INTERVAL = 60;
+ /** By default we let SSOs without active sessions live for 30 mins */
+ public static final int DEFAULT_MAX_EMPTY_LIFE = 1800;
+
// Override the superclass value
static
{
@@ -77,6 +96,30 @@
*/
private String treeCacheName = Tomcat5.DEFAULT_CACHE_NAME;
+ /** Currently started Managers that have associated as session with an SSO */
+ private Set activeManagers = Collections.synchronizedSet(new HashSet());
+
+ /** Max number of ms an SSO with no active sessions will be usable by a request */
+ private int maxEmptyLife = DEFAULT_MAX_EMPTY_LIFE * 1000;
+
+ /**
+ * Minimum number of ms since the last processExpires() run
+ * before a new run is allowed.
+ */
+ private int processExpiresInterval = DEFAULT_PROCESS_EXPIRES_INTERVAL * 1000;
+
+ /** Timestamp of the last processExpires() run */
+ private long lastProcessExpires = System.currentTimeMillis();
+
+ /**
+ * Map<String, Long> containing the ids of SSOs with no active sessions
+ * and the time at which they entered that state
+ */
+ private Map emptySSOs = new ConcurrentHashMap();
+
+ /** Used for sync locking of processExpires runs */
+ private final Object mutex = new Object();
+
// ------------------------------------------------------------- Properties
/**
@@ -195,7 +238,105 @@
}
}
+ /**
+ * Gets the max number of seconds an SSO with no active sessions will be
+ * usable by a request.
+ *
+ * @return a non-negative number
+ *
+ * @see #DEFAULT_MAX_EMPTY_LIFE *
+ * @see #setMaxEmptyLife()
+ */
+ public int getMaxEmptyLife()
+ {
+ return (maxEmptyLife / 1000);
+ }
+
+ /**
+ * Sets the maximum number of seconds an SSO with no active sessions will be
+ * usable by a request.
+ * <p>
+ * A positive value for this property allows a user to continue to use an SSO
+ * even after all the sessions associated with it have been expired. It does not
+ * keep an SSO alive if a session associated with it has been invalidated due to
+ * an <code>HttpSession.invalidate()</code> call.
+ * </p>
+ * <p>
+ * The primary purpose of this property is to avoid the situation where a server
+ * on which all of an SSO's sessions lives is shutdown, thus expiring all the
+ * sessions and causing the invalidation of the SSO. A positive value for this
+ * property would give the user an opportunity to fail over to another server
+ * and maintain the SSO.
+ * </p>
+ *
+ * @param maxEmptyLife a non-negative number
+ *
+ * @throws IllegalArgumentException if <code>maxEmptyLife < 0</code>
+ */
+ public void setMaxEmptyLife(int maxEmptyLife)
+ {
+ if (maxEmptyLife < 0)
+ throw new IllegalArgumentException("maxEmptyLife must be >= 0");
+
+ this.maxEmptyLife = maxEmptyLife * 1000;
+ }
+
+
+ /**
+ * Gets the minimum number of seconds since the start of the last check for overaged
+ * SSO's with no active sessions before a new run is allowed.
+ *
+ * @return a positive number
+ *
+ * @see #DEFAULT_PROCESS_EXPIRES_INTERVAL
+ * @see #setMaxEmptyLife()
+ * @see #setProcessExpiresInterval(int)
+ */
+ public int getProcessExpiresInterval()
+ {
+ return processExpiresInterval / 1000;
+ }
+
+ /**
+ * Sets the minimum number of seconds since the start of the last check for overaged
+ * SSO's with no active sessions before a new run is allowed. During this check,
+ * any such overaged SSOs will be invalidated.
+ * <p>
+ * Note that setting this value does not imply that a check will be performed
+ * every <code>processExpiresInterval</code> seconds, only that it will not
+ * be performed more often than that.
+ * </p>
+ *
+ * @param processExpiresInterval a non-negative number. <code>0</code> means
+ * the overage check can be performed whenever
+ * the container wishes to.
+ *
+ * @throws IllegalArgumentException if <code>processExpiresInterval < 1</code>
+ *
+ * @see #setMaxEmptyLife()
+ */
+ public void setProcessExpiresInterval(int processExpiresInterval)
+ {
+ if (processExpiresInterval < 0)
+ throw new IllegalArgumentException("processExpiresInterval must be >= 0");
+
+ this.processExpiresInterval = processExpiresInterval * 1000;
+ }
+
+
+ /**
+ * Gets the timestamp of the start of the last check for overaged
+ * SSO's with no active sessions.
+ *
+ * @see #setProcessExpiresInterval(int)
+ */
+ public long getLastProcessExpires()
+ {
+ return lastProcessExpires;
+ }
+
+
// ------------------------------------------------------ Lifecycle Methods
@@ -292,25 +433,112 @@
if (ssoId == null)
return;
- // Was the session destroyed as the result of a timeout?
- // If so, we'll just remove the expired session from the
- // SSO. If the session was logged out, we'll log out
- // of all sessions associated with the SSO.
- if ((session.getMaxInactiveInterval() > 0)
- && (System.currentTimeMillis() - session.getLastAccessedTime() >=
- session.getMaxInactiveInterval() * 1000))
+ try
{
- removeSession(ssoId, session);
+ // Was the session destroyed as the result of a timeout or
+ // the undeployment of the containing webapp?
+ // If so, we'll just remove the expired session from the
+ // SSO. If the session was logged out, we'll log out
+ // of all sessions associated with the SSO.
+ if (isSessionTimedOut(session) || isManagerStopped(session))
+ {
+ removeSession(ssoId, session);
+
+ // Quite poor. We hijack the caller thread (the Tomcat background thread)
+ // to do our cleanup of expired sessions
+ processExpires();
+ }
+ else
+ {
+ // The session was logged out.
+ logout(ssoId);
+ }
}
- else
+ catch (Exception e)
{
- // The session was logged out.
- logout(ssoId);
+ // Don't propagate back to the webapp; we don't want to disrupt
+ // the session expiration process
+ getContainer().getLogger().error("Caught exception updating SSO " + ssoId +
+ " following destruction of session " +
+ session.getId(), e);
}
+ }
+ private boolean isSessionTimedOut(Session session)
+ {
+ return (session.getMaxInactiveInterval() > 0)
+ && (System.currentTimeMillis() - session.getLastAccessedTime() >=
+ session.getMaxInactiveInterval() * 1000);
}
+ private boolean isManagerStopped(Session session)
+ {
+ boolean stopped = false;
+
+ Manager manager = session.getManager();
+
+ if (manager instanceof ManagerBase)
+ {
+ ObjectName mgrName = ((ManagerBase)manager).getObjectName();
+ stopped = (!activeManagers.contains(mgrName));
+ }
+ else if (manager instanceof JBossManager)
+ {
+ ObjectName mgrName = ((JBossManager)manager).getObjectName();
+ stopped = (!activeManagers.contains(mgrName));
+ }
+ else if (manager instanceof Lifecycle)
+ {
+ stopped = (!activeManagers.contains(manager));
+ }
+ // else we have no way to tell, so assume not
+
+ return stopped;
+ }
+ // ---------------------------------------------- LifecycleListener Methods
+
+
+ public void lifecycleEvent(LifecycleEvent event)
+ {
+ String type = event.getType();
+ if (Lifecycle.BEFORE_STOP_EVENT.equals(type)
+ || Lifecycle.STOP_EVENT.equals(type)
+ || Lifecycle.AFTER_STOP_EVENT.equals(type))
+ {
+ Lifecycle source = event.getLifecycle();
+ boolean removed;
+ if (source instanceof ManagerBase)
+ {
+ removed = activeManagers.remove(((ManagerBase)source).getObjectName());
+ }
+ else if (source instanceof JBossManager)
+ {
+ removed = activeManagers.remove(((JBossManager)source).getObjectName());
+ }
+ else
+ {
+ removed = activeManagers.remove(source);
+ }
+
+ if (removed)
+ {
+ source.removeLifecycleListener(this);
+
+ if (getContainer().getLogger().isDebugEnabled())
+ {
+ getContainer().getLogger().debug("ClusteredSSO: removed " +
+ "stopped manager " + source.toString());
+ }
+ }
+
+ // TODO consider getting the sessions and removing any from our sso's
+ // Idea is to cleanup after managers that don't destroy sessions
+
+ }
+ }
+
+
// ---------------------------------------------------------- Valve Methods
@@ -368,10 +596,11 @@
}
// Look up the cached Principal associated with this cookie value
+ String ssoId = cookie.getValue();
if (getContainer().getLogger().isDebugEnabled())
- getContainer().getLogger().debug(" Checking for cached principal for " + cookie.getValue());
+ getContainer().getLogger().debug(" Checking for cached principal for " + ssoId);
SingleSignOnEntry entry = getSingleSignOnEntry(cookie.getValue());
- if (entry != null)
+ if (entry != null && isValid(ssoId, entry))
{
Principal ssoPrinc = entry.getPrincipal();
// have to deal with the fact that the entry may not have an
@@ -433,9 +662,48 @@
reverse.put(session, ssoId);
}
- // If we made a change, notify any cluster
- if (added && ssoClusterManager != null)
- ssoClusterManager.addSession(ssoId, session);
+ // If we made a change, track the manager and notify any cluster
+ if (added)
+ {
+ Manager manager = session.getManager();
+
+ // Prefer to cache an ObjectName to avoid risk of leaking a manager,
+ // so if the manager exposes one, use it
+ Object mgrKey = null;
+ if (manager instanceof ManagerBase)
+ {
+ mgrKey = ((ManagerBase)manager).getObjectName();
+ }
+ else if (manager instanceof JBossManager)
+ {
+ mgrKey = ((JBossManager)manager).getObjectName();
+ }
+ else if (manager instanceof Lifecycle)
+ {
+ mgrKey = manager;
+ }
+ else {
+ getContainer().getLogger().warn("Manager for session " +
+ session.getId() +
+ " does not implement Lifecycle; web app shutdown may " +
+ " lead to incorrect SSO invalidations");
+ }
+
+ if (mgrKey != null)
+ {
+ synchronized (activeManagers)
+ {
+ if (!activeManagers.contains(mgrKey))
+ {
+ activeManagers.add(mgrKey);
+ ((Lifecycle) manager).addLifecycleListener(this);
+ }
+ }
+ }
+
+ if (ssoClusterManager != null)
+ ssoClusterManager.addSession(ssoId, session);
+ }
}
@@ -490,6 +758,11 @@
if (getContainer().getLogger().isDebugEnabled())
getContainer().getLogger().debug("Deregistering sso id '" + ssoId + "'");
+
+ // It's possible we don't have the SSO locally but it's in
+ // the emptySSOs map; if so remove it
+ emptySSOs.remove(ssoId);
+
// Look up and remove the corresponding SingleSignOnEntry
SingleSignOnEntry sso = null;
synchronized (cache)
@@ -504,8 +777,8 @@
Session sessions[] = sso.findSessions();
for (int i = 0; i < sessions.length; i++)
{
- if (getContainer().getLogger().isTraceEnabled())
- getContainer().getLogger().trace(" Invalidating session " + sessions[i]);
+ if (getContainer().getLogger().isDebugEnabled())
+ getContainer().getLogger().debug(" Invalidating session " + sessions[i]);
// Remove from reverse cache first to avoid recursion
synchronized (reverse)
{
@@ -677,12 +950,12 @@
reverse.remove(session);
}
- // If there are no sessions left in the SingleSignOnEntry,
- // deregister the entry.
- if (entry.getSessionCount() == 0)
- {
- deregister(ssoId);
- }
+// // If there are no sessions left in the SingleSignOnEntry,
+// // deregister the entry.
+// if (entry.getSessionCount() == 0)
+// {
+// deregister(ssoId);
+// }
}
@@ -846,6 +1119,35 @@
}
}
+
+ /**
+ * Callback from the SSOManager when it detects an SSO without
+ * any active sessions across the cluster
+ */
+ void notifySSOEmpty(String ssoId)
+ {
+ Object obj = emptySSOs.put(ssoId, new Long(System.currentTimeMillis()));
+
+ if (obj == null && getContainer().getLogger().isDebugEnabled())
+ {
+ getContainer().getLogger().debug("Notified that SSO " + ssoId + " is empty");
+ }
+ }
+
+ /**
+ * Callback from the SSOManager when it detects an SSO that
+ * has active sessions across the cluster
+ */
+ void notifySSONotEmpty(String ssoId)
+ {
+ Object obj = emptySSOs.remove(ssoId);
+
+ if (obj != null && getContainer().getLogger().isDebugEnabled())
+ {
+ getContainer().getLogger().debug("Notified that SSO " + ssoId +
+ " is no longer empty");
+ }
+ }
// ------------------------------------------------------- Private Methods
@@ -901,5 +1203,66 @@
}
}
}
+
+
+ private void processExpires()
+ {
+ long now = 0L;
+ synchronized (mutex)
+ {
+ now = System.currentTimeMillis();
+
+ if (now - lastProcessExpires > processExpiresInterval)
+ {
+ lastProcessExpires = now;
+ }
+ else
+ {
+ return;
+ }
+ }
+
+ clearExpiredSSOs(now);
+ }
+
+ private synchronized void clearExpiredSSOs(long now)
+ {
+ for (Iterator iter = emptySSOs.entrySet().iterator(); iter.hasNext();)
+ {
+ Map.Entry entry = (Map.Entry) iter.next();
+ if ( (now - ((Long) entry.getValue()).longValue()) > maxEmptyLife)
+ {
+ String ssoId = (String) entry.getKey();
+ if (getContainer().getLogger().isDebugEnabled())
+ {
+ getContainer().getLogger().debug("Invalidating expired SSO " + ssoId);
+ }
+ logout(ssoId);
+ }
+ }
+ }
+
+ private boolean isValid(String ssoId, SingleSignOnEntry entry)
+ {
+ boolean valid = true;
+ if (entry.getSessionCount() == 0)
+ {
+ Long expired = (Long) emptySSOs.get(ssoId);
+ if (expired != null
+ && (System.currentTimeMillis() - expired.longValue()) > maxEmptyLife)
+ {
+ valid = false;
+
+ if (getContainer().getLogger().isDebugEnabled())
+ {
+ getContainer().getLogger().debug("Invalidating expired SSO " + ssoId);
+ }
+
+ logout(ssoId);
+ }
+ }
+
+ return valid;
+ }
}
\ No newline at end of file
Modified: branches/JBoss_4_0_3_SP1_JBAS-3808/tomcat/src/main/org/jboss/web/tomcat/tc5/sso/TreeCacheSSOClusterManager.java
===================================================================
--- branches/JBoss_4_0_3_SP1_JBAS-3808/tomcat/src/main/org/jboss/web/tomcat/tc5/sso/TreeCacheSSOClusterManager.java 2006-11-02 20:44:41 UTC (rev 58020)
+++ branches/JBoss_4_0_3_SP1_JBAS-3808/tomcat/src/main/org/jboss/web/tomcat/tc5/sso/TreeCacheSSOClusterManager.java 2006-11-02 20:46:25 UTC (rev 58021)
@@ -1,15 +1,29 @@
/*
- * JBoss, the OpenSource WebOS
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2006, 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.
*
- * Distributable under LGPL license.
- * See terms of license at gnu.org.
+ * 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.tc5.sso;
import java.io.Serializable;
import java.security.Principal;
import java.util.HashSet;
-import java.util.LinkedList;
import java.util.Set;
import javax.management.MBeanServer;
@@ -91,20 +105,19 @@
// ------------------------------------------------------- Instance Fields
/**
- * List of SSO ids which this object is currently storing to the cache
+ * SSO id which the thread is currently storing to the cache
*/
- private LinkedList beingLocallyAdded = new LinkedList();
+ private ThreadLocal beingLocallyAdded = new ThreadLocal();
/**
- * List of SSO ids which this object is currently removing from the cache
+ * SSO id which a thread is currently removing from the cache
*/
- private LinkedList beingLocallyRemoved = new LinkedList();
+ private ThreadLocal beingLocallyRemoved = new ThreadLocal();
/**
- * List of SSO ids which are being deregistered due to removal on another
- * node
+ * SSO id which the thread is deregistering due to removal on another node
*/
- private LinkedList beingRemotelyRemoved = new LinkedList();
+ private ThreadLocal beingRemotelyRemoved = new ThreadLocal();
/**
* ObjectName of the TreeCache
@@ -117,12 +130,6 @@
private String cacheName = null;
/**
- * CredentialUpdater used to allow asynchronous updates of
- * SSO credentials
- */
- private CredentialUpdater credentialUpdater = null;
-
- /**
* Transaction Manager
*/
private TransactionManager tm = null;
@@ -167,6 +174,11 @@
*/
private boolean missingCacheErrorLogged = false;
+ /**
+ * Our node's address in the cluster.
+ */
+ private Serializable localAddress = null;
+
// ---------------------------------------------------------- Constructors
@@ -177,9 +189,21 @@
{
// Find our MBeanServer
server = MBeanServerLocator.locateJBoss();
+ if (server == null)
+ server = MBeanServerLocator.locate();
}
+
+ /**
+ * Creates a new TreeCacheSSOClusterManager that works with the given
+ * MBeanServer. This constructor is only intended for use in unit testing.
+ */
+ public TreeCacheSSOClusterManager(MBeanServer server)
+ {
+ this.server = server;
+ }
+
// ------------------------------------------------------------ Properties
public String getCacheName()
@@ -216,7 +240,7 @@
return;
}
- removeAsTreeCacheListener(cacheObjectName);
+ removeAsTreeCacheListener();
this.tm = null;
this.cacheObjectName = objectName;
@@ -238,16 +262,11 @@
"before ClusteredSingleSignOn can handle requests");
}
}
- else if (started)
- {
- // Confirm the cache has a transaction manager
- // and fail if not found
- findTransactionManager();
- }
}
// ----------------------------------------------------- SSOClusterManager
+
/**
* Notify the cluster of the addition of a Session to an SSO session.
*
@@ -261,9 +280,8 @@
return;
}
- if (false == isTreeCacheAvailable(false))
+ if (!checkTreeCacheAvailable())
{
- logMissingCacheError();
return;
}
@@ -277,6 +295,11 @@
boolean doTx = false;
try
{
+ // Confirm we have a transaction manager; if not get it from TreeCache
+ // failure to find will throw an IllegalStateException
+ if (tm == null)
+ configureFromCache();
+
// Don't do anything if there is already a transaction
// context associated with this thread.
if(tm.getTransaction() == null)
@@ -286,7 +309,7 @@
tm.begin();
Set sessions = getSessionSet(fqn, true);
- sessions.add(session.getId());
+ sessions.add(new SessionAddress(session.getId(), localAddress));
putInTreeCache(fqn, sessions);
}
catch (Exception e)
@@ -296,7 +319,7 @@
if(doTx)
tm.setRollbackOnly();
}
- catch (Exception x)
+ catch (Exception ignored)
{
}
String sessId = (session == null ? "NULL" : session.getId());
@@ -345,23 +368,20 @@
*/
public void logout(String ssoId)
{
- if (false == isTreeCacheAvailable(false))
+ if (!checkTreeCacheAvailable())
{
- logMissingCacheError();
return;
}
// Check whether we are already handling this removal
- //synchronized (beingLocallyRemoved)
+ if (ssoId.equals(beingLocallyRemoved.get()))
{
- if (beingLocallyRemoved.contains(ssoId))
- {
- return;
- }
- // Add this SSO to our list of in-process local removals so
- // this.nodeRemoved() will ignore the removal
- beingLocallyRemoved.add(ssoId);
- }
+ return;
+ }
+
+ // Add this SSO to our list of in-process local removals so
+ // this.nodeRemoved() will ignore the removal
+ beingLocallyRemoved.set(ssoId);
if (log.isTraceEnabled())
{
@@ -382,10 +402,7 @@
}
finally
{
- //synchronized (beingLocallyRemoved)
- {
- beingLocallyRemoved.remove(ssoId);
- }
+ beingLocallyRemoved.set(null);
}
}
@@ -401,20 +418,16 @@
*/
public SingleSignOnEntry lookup(String ssoId)
{
- if (false == isTreeCacheAvailable(false))
+ if (!checkTreeCacheAvailable())
{
- logMissingCacheError();
return null;
}
SingleSignOnEntry entry = null;
// Find the latest credential info from the cluster
Fqn fqn = getCredentialsFqn(ssoId);
- //UserTransaction tx = null;
try
{
- //tx = getNewTransaction();
- //tx.begin();
SSOCredentials data = (SSOCredentials) getFromTreeCache(fqn);
if (data != null)
{
@@ -423,20 +436,9 @@
data.getUsername(),
data.getPassword());
}
- //tx.commit();
}
catch (Exception e)
{
- /*
- if (tx != null)
- {
- try
- {
- tx.rollback();
- }
- catch (Exception x) {}
- }
- */
log.error("caught exception looking up SSOCredentials for SSO id " +
ssoId, e);
}
@@ -456,9 +458,8 @@
public void register(String ssoId, String authType,
String username, String password)
{
- if (false == isTreeCacheAvailable(false))
+ if (!checkTreeCacheAvailable())
{
- logMissingCacheError();
return;
}
@@ -479,33 +480,32 @@
*/
public void removeSession(String ssoId, Session session)
{
- if (false == isTreeCacheAvailable(false))
+ if (ssoId == null || session == null)
{
- logMissingCacheError();
return;
}
+ if (!checkTreeCacheAvailable())
+ {
+ return;
+ }
+
// Check that this session removal is not due to our own deregistration
// of an SSO following receipt of a nodeRemoved() call
- //synchronized(beingRemotelyRemoved)
+ if (ssoId.equals(beingRemotelyRemoved.get()))
{
- if (beingRemotelyRemoved.contains(ssoId))
- {
- return;
- }
+ return;
}
-
- if (log.isTraceEnabled())
- {
- log.trace("removeSession(): removing Session " + session.getId() +
- " from cached session set for SSO " + ssoId);
- }
-
+
Fqn fqn = getSessionsFqn(ssoId);
boolean doTx = false;
- boolean removing = false;
+// boolean removing = false;
try
{
+ // Confirm we have a transaction manager; if not get it from TreeCache
+ // failure to find will throw an IllegalStateException
+ if (tm == null)
+ configureFromCache();
// Don't do anything if there is already a transaction
// context associated with this thread.
@@ -518,23 +518,29 @@
Set sessions = getSessionSet(fqn, false);
if (sessions != null)
{
- sessions.remove(session.getId());
- if (sessions.size() == 0)
+ sessions.remove(new SessionAddress(session.getId(), localAddress));
+
+ if (log.isTraceEnabled())
{
- // Add this SSO to our list of in-process local removals so
- // this.nodeRemoved() will ignore the removal
- //synchronized (beingLocallyRemoved)
- {
- beingLocallyRemoved.add(ssoId);
- }
- removing = true;
- // No sessions left; remove node
- removeFromTreeCache(getSingleSignOnFqn(ssoId));
+ log.trace("removed session " + session.getId() +
+ " from cached session set for SSO " + ssoId + " -- " +
+ sessions.size() + " sessions remain");
}
- else
- {
+
+// if (sessions.size() == 0)
+// {
+// // No sessions left; remove node
+//
+// // Add this SSO to our list of in-process local removals so
+// // this.nodeRemoved() will ignore the removal
+// removing = true;
+// beingLocallyRemoved.set(ssoId);
+// removeFromTreeCache(getSingleSignOnFqn(ssoId));
+// }
+// else
+// {
putInTreeCache(fqn, sessions);
- }
+// }
}
}
catch (Exception e)
@@ -554,21 +560,18 @@
}
finally
{
- try
- {
- if (removing)
- {
- //synchronized (beingLocallyRemoved)
- {
- beingLocallyRemoved.remove(ssoId);
- }
- }
- }
- finally
- {
+// try
+// {
+// if (removing)
+// {
+// beingLocallyRemoved.set(null);
+// }
+// }
+// finally
+// {
if (doTx)
endTransaction();
- }
+// }
}
}
@@ -586,9 +589,8 @@
public void updateCredentials(String ssoId, String authType,
String username, String password)
{
- if (false == isTreeCacheAvailable(false))
+ if (!checkTreeCacheAvailable())
{
- logMissingCacheError();
return;
}
@@ -660,20 +662,17 @@
public void nodeRemoved(Fqn fqn)
{
String ssoId = getIdFromFqn(fqn);
+
+ if (ssoId == null)
+ return;
// Ignore messages generated by our own activity
- //synchronized(beingLocallyRemoved)
+ if (ssoId.equals(beingLocallyRemoved.get()))
{
- if (beingLocallyRemoved.contains(ssoId))
- {
- return;
- }
+ return;
}
- //synchronized (beingRemotelyRemoved)
- {
- beingRemotelyRemoved.add(ssoId);
- }
+ beingRemotelyRemoved.set(ssoId);
try
{
@@ -686,10 +685,7 @@
}
finally
{
- //synchronized(beingRemotelyRemoved)
- {
- beingRemotelyRemoved.remove(ssoId);
- }
+ beingRemotelyRemoved.set(null);
}
}
@@ -712,21 +708,30 @@
*/
public void nodeModified(Fqn fqn)
{
- // We are only interested in changes to the CREDENTIALS node
- if (CREDENTIALS.equals(getTypeFromFqn(fqn)) == false)
+ String type = getTypeFromFqn(fqn);
+ if (CREDENTIALS.equals(type))
{
- return;
+ handleCredentialUpdate(fqn);
}
+ else if (SESSIONS.equals(type))
+ {
+ handleSessionSetChange(fqn);
+ }
+ }
- String ssoId = getIdFromFqn(fqn);
+ /**
+ *
+ * @param fqn an Fqn that points to the CREDENTIALS node of an SSO
+ */
+ private void handleCredentialUpdate(Fqn fqn)
+ {
+ String ssoId = getIdFromFqn(fqn); // won't be null per the API contract
+
// Ignore invocations that come as a result of our additions
- //synchronized(beingLocallyAdded)
+ if (ssoId.equals(beingLocallyAdded.get()))
{
- if (beingLocallyAdded.contains(ssoId))
- {
- return;
- }
+ return;
}
SingleSignOnEntry sso = ssoValve.localLookup(ssoId);
@@ -742,11 +747,72 @@
}
// Put this SSO in the queue of those to be updated
- credentialUpdater.enqueue(sso, ssoId);
- }
+// credentialUpdater.enqueue(sso, ssoId);
+ try
+ {
+ SSOCredentials data = (SSOCredentials) getFromTreeCache(fqn);
+ if (data != null)
+ {
+ // We want to release our read lock quickly, so get the needed
+ // data from the cache, commit the tx, and then use the data
+ String authType = data.getAuthType();
+ String username = data.getUsername();
+ String password = data.getPassword();
+ if (log.isTraceEnabled())
+ {
+ log.trace("CredentialUpdater: Updating credentials for SSO " + sso);
+ }
+ synchronized (sso)
+ {
+ // Use the existing principal
+ Principal p = sso.getPrincipal();
+ sso.updateCredentials(p, authType, username, password);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ log.error("failed to update credentials for SSO " + ssoId, e);
+ }
+ }
+
/**
+ *
+ * @param fqn an Fqn that points to the SESSIONS node of an SSO
+ */
+ private void handleSessionSetChange(Fqn fqn)
+ {
+ // Here we *want* to handle local activity as well as remote,
+ // as the distributed cache set of sessions is the only
+ // complete representation
+
+ String ssoId = getIdFromFqn(fqn);
+ try
+ {
+ Set sessions = (Set) getFromTreeCache(fqn);
+ if (sessions == null)
+ {
+ // don't think we'd get this event;
+ // in any case we don't deal with this
+ }
+ else if (sessions.size() == 0)
+ {
+ ssoValve.notifySSOEmpty(ssoId);
+ }
+ else
+ {
+ ssoValve.notifySSONotEmpty(ssoId);
+ }
+ }
+ catch (Exception e)
+ {
+ log.error("failed in check for empty SSO " + ssoId, e);
+ }
+ }
+
+ /**
* Does nothing
*/
public void viewChange(View new_view)
@@ -818,15 +884,12 @@
throw new LifecycleException
("TreeCacheSSOClusterManager already Started");
}
-
- // Start the thread we use to clear nodeModified events
- credentialUpdater = new CredentialUpdater();
try
{
- if (tm == null && isTreeCacheAvailable(true))
+ if (isTreeCacheAvailable(true))
{
- findTransactionManager();
+ integrateWithCache();
}
}
catch (Exception e)
@@ -859,9 +922,7 @@
throw new LifecycleException
("TreeCacheSSOClusterManager not Started");
}
-
- credentialUpdater.stop();
-
+
started = false;
// Notify our interested LifecycleListeners
@@ -903,7 +964,12 @@
*/
private String getIdFromFqn(Fqn fqn)
{
- return (String) fqn.get(1);
+ String id = null;
+ if (fqn.size() > 1 && SSO.equals(fqn.get(0)))
+ {
+ id = (String) fqn.get(1);
+ }
+ return id;
}
private Set getSessionSet(Fqn fqn, boolean create)
@@ -922,25 +988,30 @@
* object.
*
* @param fqn the Fully Qualified Name used by TreeCache
- * @return the last element in the Fqn -- either
- * {@link #CREDENTIALS CREDENTIALS} or {@link #SESSIONS SESSIONS}.
+ * @return the 3rd in the Fqn -- either
+ * {@link #CREDENTIALS CREDENTIALS} or {@link #SESSIONS SESSIONS},
+ * or <code>null</code> if <code>fqn</code> is not for an SSO.
*/
private String getTypeFromFqn(Fqn fqn)
{
- return (String) fqn.get(fqn.size() - 1);
+ String type = null;
+ if (fqn.size() > 2 && SSO.equals(fqn.get(0)))
+ type = (String) fqn.get(2);
+ return type;
}
/**
+ * Obtains needed configuration information from the tree cache.
* Invokes "getTransactionManager" on the tree cache, caching the
* result or throwing an IllegalStateException if one is not found.
+ * Also get our cluster-wide unique local address from the cache.
*
* @throws Exception
*/
- private void findTransactionManager() throws Exception
- {
- tm = (TransactionManager) server.invoke(getCacheObjectName(),
- "getTransactionManager",
- null, new String[] {});
+ private void configureFromCache() throws Exception
+ {
+ tm = (TransactionManager) server.getAttribute(getCacheObjectName(),
+ "TransactionManager");
if (tm == null)
{
@@ -949,6 +1020,15 @@
"configure a valid " +
"TransactionManagerLookupClass");
}
+
+ // Find out our address
+ Object address = server.getAttribute(cacheObjectName, "LocalAddress");
+ // In reality this is a JGroups IpAddress, but the API says
+ // "Object" so we have to be sure its Serializable
+ if (address instanceof Serializable)
+ localAddress = (Serializable) address;
+ else
+ localAddress = address.toString();
}
@@ -994,13 +1074,19 @@
{
try
{
- registerAsTreeCacheListener(cacheObjectName);
+ // If Tomcat5 overrides the default cache name, it will do so
+ // after we are started. So we need to configure ourself here
+ // and throw an exception if there is a problem. Having this
+ // here also allows us to recover if our cache is started
+ // after we are
+ if (started)
+ integrateWithCache();
setMissingCacheErrorLogged(false);
}
catch (Exception e)
{
- log.error("Caught exception registering as listener to " +
- cacheObjectName, e);
+ log.error("Caught exception configuring from cache " +
+ cacheObjectName, e);
available = false;
}
}
@@ -1009,6 +1095,14 @@
}
return treeCacheAvailable;
}
+
+ private boolean checkTreeCacheAvailable()
+ {
+ boolean avail = isTreeCacheAvailable(false);
+ if (!avail)
+ logMissingCacheError();
+ return avail;
+ }
private void putInTreeCache(Fqn fqn, Object data) throws Exception
{
@@ -1016,16 +1110,26 @@
server.invoke(getCacheObjectName(), "put", args, PUT_SIGNATURE);
}
+ private void integrateWithCache() throws Exception
+ {
+ // Ensure we have a transaction manager and a cluster-wide unique address
+ configureFromCache();
+
+ registerAsTreeCacheListener();
+
+ log.debug("Successfully integrated with cache service " + cacheObjectName);
+ }
+
+
/**
* Invokes an operation on the JMX server to register ourself as a
* listener on the TreeCache service.
*
* @throws Exception
*/
- private void registerAsTreeCacheListener(ObjectName listenTo)
- throws Exception
+ private void registerAsTreeCacheListener() throws Exception
{
- server.invoke(listenTo, "addTreeCacheListener",
+ server.invoke(cacheObjectName, "addTreeCacheListener",
new Object[]{this},
new String[]{TreeCacheListener.class.getName()});
registeredAsListener = true;
@@ -1038,12 +1142,11 @@
*
* @throws Exception
*/
- private void removeAsTreeCacheListener(ObjectName removeFrom)
- throws Exception
+ private void removeAsTreeCacheListener() throws Exception
{
- if (registeredAsListener && removeFrom != null)
+ if (registeredAsListener && cacheObjectName != null)
{
- server.invoke(removeFrom, "removeTreeCacheListener",
+ server.invoke(cacheObjectName, "removeTreeCacheListener",
new Object[]{this},
new String[]{TreeCacheListener.class.getName()});
}
@@ -1076,41 +1179,23 @@
String password)
{
SSOCredentials data = new SSOCredentials(authType, username, password);
+
// Add this SSO to our list of in-process local adds so
// this.nodeModified() will ignore the addition
- //synchronized (beingLocallyAdded)
- {
- beingLocallyAdded.add(ssoId);
- }
- //UserTransaction tx = null;
+ beingLocallyAdded.set(ssoId);
+
try
{
- //tx = getNewTransaction();
- //tx.begin();
putInTreeCache(getCredentialsFqn(ssoId), data);
- //tx.commit();
}
catch (Exception e)
{
- /*
- if (tx != null)
- {
- try
- {
- tx.rollback();
- }
- catch (Exception x) {}
- }
- */
log.error("Exception attempting to add TreeCache nodes for SSO " +
ssoId, e);
}
finally
{
- //synchronized (beingLocallyAdded)
- {
- beingLocallyAdded.remove(ssoId);
- }
+ beingLocallyAdded.set(null);
}
}
@@ -1144,219 +1229,6 @@
}
}
- // --------------------------------------------------------- Inner Classes
-
- /**
- * Spawns a thread to handle updates of credentials
- *
- * TODO This was added in the early days of JBossCache when invoking
- * a get() on a node in the middle of a TreeCacheListener
- * notification would cause a deadlock. Should not be necessary now.
- */
- private class CredentialUpdater
- implements Runnable
- {
- private HashSet awaitingUpdate = new HashSet();
- private Thread updateThread;
- private boolean updateThreadSleeping = false;
- private boolean queueEmpty = true;
- private boolean stopped = false;
-
- private CredentialUpdater()
- {
- updateThread =
- new Thread(this, "SSOClusterManager.CredentialUpdater");
- updateThread.setDaemon(true);
- updateThread.start();
- }
-
- // ------------------------------------------------------ Runnable
-
- public void run()
- {
- while (!stopped)
- {
- // Ensure that no runtime exceptions kill this thread
- try
- {
- updateThreadSleeping = false;
- // Get the current list of ids awaiting processing
- SSOWrapper[] ssos = null;
- synchronized (awaitingUpdate)
- {
- ssos = new SSOWrapper[awaitingUpdate.size()];
- ssos = (SSOWrapper[]) awaitingUpdate.toArray(ssos);
- awaitingUpdate.clear();
- queueEmpty = true;
- }
-
- // Handle the credential update
- for (int i = 0; i < ssos.length; i++)
- {
- processUpdate(ssos[i]);
- }
-
- // Wait for another invocation of enqueue(). But,
- // first have to check in case it was invoked while we
- // were processing the previous bunch
- if (queueEmpty)
- {
- try
- {
- // There is a slight possibility here of a race condition
- // between the above check for queueEmpty and another
- // thread accessing enqueue()'s check of
- // updateThreadSleeping. If this happens, the update
- // will not be processed by the local node until the
- // updateThread wakes up (30 secs) or is interrupted by
- // another update. This situation is quite unlikely,
- // as updates only happen 1) in odd configurations where
- // CLIENT-CERT authentication is used for some apps and
- // FORM or BASIC are used for others and 2) the user has
- // first logged in to a CLIENT-CERT app and later logs in
- // to a FORM/BASIC app. If such a race condition were to
- // occur, the only downside would be that if the user
- // accessed a FORM/BASIC app on this node before the local
- // update is processed, they would have to log in again.
- updateThreadSleeping = true;
- updateThread.sleep(30000);
- }
- catch (InterruptedException e)
- {
- if (log.isTraceEnabled())
- {
- log.trace("CredentialUpdater: interrupted");
- }
- // process the next bunch
- }
- }
- else if (log.isTraceEnabled())
- {
- log.trace("CredentialUpdater: more updates added while " +
- "handling existing updates");
- }
- }
- catch (Exception e)
- {
- log.error("CredentialUpdater thread caught an exception", e);
- }
- }
- }
-
- // ------------------------------------------------- Private Methods
-
- /**
- * Adds an SSO id to the set of those awaiting credential updating, and
- * interrupts the update handler thread to notify it of the addition.
- *
- * @param sso the id of the SSO session whose local credentials
- * are to be updated
- */
- private void enqueue(SingleSignOnEntry sso, String ssoId)
- {
- synchronized (awaitingUpdate)
- {
- awaitingUpdate.add(new SSOWrapper(sso, ssoId));
- queueEmpty = false;
- }
- // Interrupt the update thread so it wakes up to process
- // the enqueued update. Only do this if its "sleeping" flag
- // is set so we don't inadvertently interrupt it while its
- // blocked waiting for a TreeCache lock to clear
- if (updateThreadSleeping)
- {
- updateThread.interrupt();
- }
- }
-
- private void processUpdate(SSOWrapper wrapper)
- {
- if (wrapper.sso.getCanReauthenticate())
- {
- // No need to update
- return;
- }
-
- Fqn fqn = getCredentialsFqn(wrapper.id);
- //UserTransaction tx = null;
- try
- {
- //tx = getNewTransaction();
- //tx.begin();
- SSOCredentials data = (SSOCredentials) getFromTreeCache(fqn);
- if (data != null)
- {
- // We want to release our read lock quickly, so get the needed
- // data from the cache, commit the tx, and then use the data
- String authType = data.getAuthType();
- String username = data.getUsername();
- String password = data.getPassword();
- //tx.commit();
-
- if (log.isTraceEnabled())
- {
- log.trace("CredentialUpdater: Updating credentials for SSO " +
- wrapper.sso);
- }
-
- synchronized (wrapper.sso)
- {
- // Use the existing principal
- Principal p = wrapper.sso.getPrincipal();
- wrapper.sso.updateCredentials(p, authType, username, password);
- }
- }
- /*
- else
- {
- tx.commit();
- }
- */
-
- }
- catch (Exception e)
- {
- /*
- if (tx != null)
- {
- try
- {
- tx.rollback();
- }
- catch (Exception x) {}
- }
- */
- log.error("Exception attempting to get SSOCredentials from " +
- "TreeCache node " + fqn.toString(), e);
- }
- }
-
- /**
- * Stops the update handler thread.
- */
- private void stop()
- {
- stopped = true;
- }
-
- } // end CredentialUpdater
-
-
- /**
- * Wrapper class that holds a SingleSignOnEntry and its id
- */
- private class SSOWrapper
- {
- private SingleSignOnEntry sso = null;
- private String id = null;
-
- private SSOWrapper(SingleSignOnEntry entry, String ssoId)
- {
- this.sso = entry;
- this.id = ssoId;
- }
- }
-
// --------------------------------------------------------- Outer Classes
/**
@@ -1421,6 +1293,135 @@
}
} // end SSOCredentials
+
+ static class SessionAddress implements Serializable
+ {
+ /** The serialVersionUID */
+ private static final long serialVersionUID = -3702932999380140004L;
+
+ Serializable address;
+ String sessionId;
+
+ SessionAddress(String sessionId, Serializable address)
+ {
+ this.sessionId = sessionId;
+ this.address = address;
+ }
+ public boolean equals(Object obj)
+ {
+ if (this == obj)
+ return true;
+
+ if (!(obj instanceof SessionAddress))
+ return false;
+
+ SessionAddress other = (SessionAddress) obj;
+
+ return (sessionId.equals(other.sessionId)
+ && address.equals(other.address));
+ }
+
+ public int hashCode()
+ {
+ int total = (19 * 43) + sessionId.hashCode();
+ return ((total * 43) + address.hashCode());
+ }
+
+
+ }
+
+// DISABLED UNTIL 4.0.6.CR1
+// /**
+// * Runnable that's run when the removal of a node from the cluster has been detected.
+// * Removes any SessionAddress objects associated with dead members from the
+// * session set of each SSO. Operates locally only so each node can independently clean
+// * its SSOs without concern about replication lock conflicts.
+// */
+// private class DeadMemberCleaner implements Runnable
+// {
+// public void run()
+// {
+// synchronized (cleanupMutex)
+// {
+// try
+// {
+// // Ensure we have a TransactionManager
+// if (tm == null)
+// configureFromCache();
+//
+// Set ids = getSSOIds();
+//
+// for (Iterator iter = ids.iterator(); iter.hasNext();)
+// {
+// cleanDeadMembersFromSSO((String) iter.next());
+//
+// }
+// }
+// catch (Exception e)
+// {
+// log.error("Caught exception cleaning sessions from dead cluster members from SSOs ", e);
+// }
+// }
+// }
+//
+// private void cleanDeadMembersFromSSO(String ssoId)
+// {
+// Fqn fqn = getSessionsFqn(ssoId);
+// boolean doTx = false;
+// try
+// {
+// // Don't start tx if there is already one associated with this thread.
+// if(tm.getTransaction() == null)
+// doTx = true;
+//
+// if(doTx)
+// tm.begin();
+//
+// Set sessions = getSessionSet(fqn, false, true);
+// if (sessions != null && sessions.size() > 0)
+// {
+// boolean changed = false;
+// for (Iterator iter = sessions.iterator(); iter.hasNext();)
+// {
+// SessionAddress session = (SessionAddress) iter.next();
+// if (!currentView.containsMember((Address) session.address))
+// {
+// iter.remove();
+// changed = true;
+// }
+// }
+//
+// if (changed)
+// {
+// if (sessions.size() == 0)
+// {
+// ssoValve.notifySSOEmpty(ssoId);
+// }
+//
+// putInTreeCache(fqn, sessions, true);
+// }
+// }
+// }
+// catch (Exception e)
+// {
+// try
+// {
+// if(doTx)
+// tm.setRollbackOnly();
+// }
+// catch (Exception ignored)
+// {
+// }
+// log.error("caught exception cleaning dead members from SSO " + ssoId, e);
+// }
+// finally
+// {
+// if (doTx)
+// endTransaction();
+// }
+// }
+// }
+
} // end TreeCacheSSOClusterManager
Modified: branches/JBoss_4_0_3_SP1_JBAS-3808/tomcat/src/main/org/jboss/web/tomcat/tc5/sso/mbeans-descriptors.xml
===================================================================
--- branches/JBoss_4_0_3_SP1_JBAS-3808/tomcat/src/main/org/jboss/web/tomcat/tc5/sso/mbeans-descriptors.xml 2006-11-02 20:44:41 UTC (rev 58020)
+++ branches/JBoss_4_0_3_SP1_JBAS-3808/tomcat/src/main/org/jboss/web/tomcat/tc5/sso/mbeans-descriptors.xml 2006-11-02 20:46:25 UTC (rev 58021)
@@ -20,6 +20,19 @@
description="Should we attempt to reauthenticate each request against the security Realm?"
type="boolean"/>
+ <attribute name="maxEmptyLife"
+ description="The maximum number of seconds an SSO with no active sessions will be usable by a request"
+ type="int"/>
+
+ <attribute name="processExpiresInterval"
+ description="The maximum number of seconds an SSO with no active sessions will be usable by a request"
+ type="int"/>
+
+ <attribute name="lastProcessExpires"
+ writeable="false"
+ description="The timestamp of the start of the last check for overaged SSO's with no active sessions."
+ type="long"/>
+
<attribute name="clusterManager"
description="SSOClusterManager to use for cluster support"
type="org.jboss.web.tomcat.tc5.sso.SSOClusterManager"/>
More information about the jboss-cvs-commits
mailing list