/*
* JBoss, Home of Professional Open Source
* Copyright 2006, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt 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.sso.jbc;
import java.io.Serializable;
import java.security.AccessController;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanInfo;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.transaction.Status;
import javax.transaction.TransactionManager;
import org.jboss.cache.Cache;
import org.jboss.cache.CacheManager;
import org.jboss.cache.CacheStatus;
import org.jboss.cache.Fqn;
import org.jboss.cache.InvocationContext;
import org.jboss.cache.Node;
import org.jboss.cache.Region;
import org.jboss.cache.RegionNotEmptyException;
import org.jboss.cache.config.Option;
import org.jboss.cache.config.Configuration.CacheMode;
import org.jboss.cache.notifications.annotation.CacheListener;
import org.jboss.cache.notifications.annotation.NodeModified;
import org.jboss.cache.notifications.annotation.NodeRemoved;
import org.jboss.cache.notifications.annotation.ViewChanged;
import org.jboss.cache.notifications.event.NodeModifiedEvent;
import org.jboss.cache.notifications.event.NodeRemovedEvent;
import org.jboss.cache.notifications.event.ViewChangedEvent;
import org.jboss.cache.pojo.PojoCache;
import org.jboss.ha.framework.server.CacheManagerLocator;
import org.jboss.logging.Logger;
import org.jboss.util.NestedRuntimeException;
import org.jboss.util.loading.ContextClassLoaderSwitcher;
import org.jboss.util.threadpool.ThreadPool;
import org.jboss.web.tomcat.service.sso.spi.FullyQualifiedSessionId;
import org.jboss.web.tomcat.service.sso.spi.SSOClusterManager;
import org.jboss.web.tomcat.service.sso.spi.SSOCredentials;
import org.jboss.web.tomcat.service.sso.spi.SSOLocalManager;
import org.jgroups.Address;
/**
* An implementation of SSOClusterManager that uses a TreeCache
* to share SSO information between cluster nodes.
*
* @author Brian E. Stansberry
* @version $Revision: 59567 $ $Date: 2007-01-12 03:39:24 +0100 (ven., 12 janv. 2007) $
*/
@CacheListener
public final class JBossCacheSSOClusterManager
implements SSOClusterManager
{
// ------------------------------------------------------------- Constants
/**
* Final segment of any FQN that names a TreeCache node storing
* SSO credential information.
*/
private static final String CREDENTIALS = "credentials";
/**
* First segment of any FQN that names a TreeCache node associated
* with an SSO
*/
private static final String SSO = "SSO";
/**
* Final segment of any FQN that names a TreeCache node storing
* the set of Sessions associated with an SSO.
*/
private static final String SESSIONS = "sessions";
/**
* Key under which data is stored to the TreeCache.
*/
private static final String KEY = "key";
/**
* Default global value for the threadPoolName property
*/
public static final String DEFAULT_THREAD_POOL_NAME =
"jboss.system:service=ThreadPool";
/** The default JBoss Cache to use for storing SSO entries */
public static final String DEFAULT_CACHE_NAME = "clustered-sso";
/** The legacy name of the JBoss Cache to use for storing SSO entries */
public static final String LEGACY_CACHE_NAME = "jboss.cache:service=TomcatClusteringCache";
// ------------------------------------------------------- Instance Fields
/**
* SSO id which the thread is currently storing to the cache
*/
private ThreadLocal<String> beingLocallyAdded = new ThreadLocal<String>();
/**
* SSO id which a thread is currently removing from the cache
*/
private ThreadLocal<String> beingLocallyRemoved = new ThreadLocal<String>();
/**
* SSO id which the thread is deregistering due to removal on another node
*/
private ThreadLocal<String> beingRemotelyRemoved = new ThreadLocal<String>();
/**
* String name to use to access the TreeCache
*/
private String cacheName = DEFAULT_CACHE_NAME;
/**
* ObjectName of the TreeCache if legacy JMX integration is used
*/
private ObjectName cacheObjectName = null;
/**
* The cache itself.
*/
private Cache<Object, Object> cache = null;
/**
* Transaction Manager
*/
private TransactionManager tm = null;
private String threadPoolName = DEFAULT_THREAD_POOL_NAME;
private ThreadPool threadPool;
/**
* The Log-object for this class
*/
private Logger log = Logger.getLogger(getClass().getName());;
/**
* Whether we are registered as a TreeCacheListener anywhere
*/
private boolean registeredAsListener = false;
/**
* The MBean server we use to access our TreeCache
*/
private MBeanServer server = null;
/**
* The SingleSignOn for which we are providing cluster support
*/
private SSOLocalManager ssoValve = null;
/**
* Whether we have been started
*/
private boolean started = false;
/**
* Whether a valid TreeCache is available for use
*/
private boolean treeCacheAvailable = false;
/**
* Whether we have logged an error due to not having a valid cache
*/
private boolean missingCacheErrorLogged = false;
/**
* Our node's address in the cluster.
*/
private Serializable localAddress = null;
/** The members of the last view passed to viewChange() */
private Set<Object> currentView = new HashSet<Object>();;
/** Mutex lock to ensure only one view change at a time is being processed */
private Object cleanupMutex = new Object();
// ---------------------------------------------------------- Constructors
/**
* Creates a new JBossCacheSSOClusterManager
*/
public JBossCacheSSOClusterManager() {}
/**
* Creates a new JBossCacheSSOClusterManager that works with the given
* MBeanServer. This constructor is only intended for use in unit testing.
*/
public JBossCacheSSOClusterManager(MBeanServer server)
{
this.server = server;
}
// ------------------------------------------------------------ Properties
public String getCacheName()
{
return cacheName;
}
public String getThreadPoolName()
{
return threadPoolName;
}
public boolean isUsingThreadPool()
{
return threadPool != null;
}
// ----------------------------------------------------- SSOClusterManager
/**
* Notify the cluster of the addition of a Session to an SSO session.
*
* @param ssoId the id of the SSO session
* @param sessionId id of the Session that has been added
*/
public void addSession(String ssoId, FullyQualifiedSessionId sessionId)
{
if (ssoId == null || sessionId == null)
{
return;
}
if (!checkTreeCacheAvailable())
{
return;
}
if (log.isTraceEnabled())
{
log.trace("addSession(): adding Session " + sessionId.getSessionId() +
" to cached session set for SSO " + ssoId);
}
Fqn<Serializable> fqn = getSessionsFqn(ssoId);
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)
doTx = true;
if(doTx)
tm.begin();
putInTreeCache(fqn, sessionId, null);
}
catch (Exception e)
{
try
{
if(doTx)
tm.setRollbackOnly();
}
catch (Exception ignored)
{
}
log.error("caught exception adding session " + sessionId.getSessionId() +
" to SSO id " + ssoId, e);
}
finally
{
if (doTx)
endTransaction();
}
}
/**
* Gets the SingleSignOn valve for which this object is handling
* cluster communications.
*
* @return the <code>SingleSignOn</code> valve.
*/
public SSOLocalManager getSSOLocalManager()
{
return ssoValve;
}
/**
* Sets the SingleSignOn valve for which this object is handling
* cluster communications.
* <p><b>NOTE:</b> This method must be called before calls can be
* made to the other methods of this interface.
*
* @param localManager a <code>SingleSignOn</code> valve.
*/
public void setSSOLocalManager(SSOLocalManager localManager)
{
ssoValve = localManager;
if (ssoValve != null)
{
if (server == null)
{
server = ssoValve.getMBeanServer();
}
String config = ssoValve.getCacheConfig();
if (config != null)
{
cacheName = config;
}
String poolName = ssoValve.getThreadPoolName();
if (poolName != null)
{
threadPoolName = poolName;
}
}
}
/**
* Notifies the cluster that a single sign on session has been terminated
* due to a user logout.
*
* @param ssoId
*/
public void logout(String ssoId)
{
if (!checkTreeCacheAvailable())
{
return;
}
// Check whether we are already handling this removal
if (ssoId.equals(beingLocallyRemoved.get()))
{
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())
{
log.trace("Registering logout of SSO " + ssoId +
" in clustered cache");
}
Fqn<String> fqn = getSingleSignOnFqn(ssoId);
try
{
removeFromTreeCache(fqn, false);
}
catch (Exception e)
{
log.error("Exception attempting to remove node " +
fqn.toString() + " from TreeCache", e);
}
finally
{
beingLocallyRemoved.set(null);
}
}
public SSOCredentials lookup(String ssoId)
{
if (!checkTreeCacheAvailable())
{
return null;
}
SSOCredentials credentials = null;
// Find the latest credential info from the cluster
Fqn<String> fqn = getCredentialsFqn(ssoId);
try
{
credentials = (SSOCredentials) getFromTreeCache(fqn, KEY);
}
catch (Exception e)
{
log.error("caught exception looking up SSOCredentials for SSO id " +
ssoId, e);
}
return credentials;
}
/**
* Notifies the cluster of the creation of a new SSO entry.
*
* @param ssoId the id of the SSO session
* @param authType the type of authenticator (BASIC, CLIENT-CERT, DIGEST
* or FORM) used to authenticate the SSO.
* @param username the username (if any) used for the authentication
* @param password the password (if any) used for the authentication
*/
public void register(String ssoId, String authType,
String username, String password)
{
if (!checkTreeCacheAvailable())
{
return;
}
if (log.isTraceEnabled())
{
log.trace("Registering SSO " + ssoId + " in clustered cache");
}
storeSSOData(ssoId, authType, username, password);
}
/**
* Notify the cluster of the removal of a Session from an SSO session.
*
* @param ssoId the id of the SSO session
* @param sessionId id of the Session that has been removed
*/
public void removeSession(String ssoId, FullyQualifiedSessionId sessionId)
{
if (ssoId == null || sessionId == null)
{
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
if (ssoId.equals(beingRemotelyRemoved.get()))
{
return;
}
if (log.isTraceEnabled())
{
log.trace("removeSession(): removing Session " + sessionId.getSessionId() +
" from cached session set for SSO " + ssoId);
}
Fqn<Serializable> fqn = getSessionsFqn(ssoId);
boolean doTx = 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.
if(tm.getTransaction() == null)
doTx = true;
if(doTx)
tm.begin();
Set<Object> keys = getSessionKeys(ssoId);
if (keys.contains(sessionId))
{
if (keys.size() == 1)
{
// This is our last session locally; remove our node (which,
// via nodeRemoved callback also marks the sso empty if it's
// also the last session globally
removeFromTreeCache(fqn, false);
}
else
{
// Simple removal of one our local sessions
removeFromTreeCache(fqn, sessionId);
}
}
}
catch (Exception e)
{
try
{
if(doTx)
tm.setRollbackOnly();
}
catch (Exception x)
{
}
log.error("caught exception removing session " + sessionId.getSessionId() +
" from SSO id " + ssoId, e);
}
finally
{
try
{
if (removing)
{
beingLocallyRemoved.set(null);
}
}
finally
{
if (doTx)
endTransaction();
}
}
}
/**
* Notifies the cluster of an update of the security credentials
* associated with an SSO session.
*
* @param ssoId the id of the SSO session
* @param authType the type of authenticator (BASIC, CLIENT-CERT, DIGEST
* or FORM) used to authenticate the SSO.
* @param username the username (if any) used for the authentication
* @param password the password (if any) used for the authentication
*/
public void updateCredentials(String ssoId, String authType,
String username, String password)
{
if (!checkTreeCacheAvailable())
{
return;
}
if (log.isTraceEnabled())
{
log.trace("Updating credentials for SSO " + ssoId +
" in clustered cache");
}
storeSSOData(ssoId, authType, username, password);
}
// ------------------------------------------------------ CacheListener
/**
* Extracts an SSO session id from the Fqn and uses it in an invocation of
* {@link ClusteredSingleSignOn#deregister(String) ClusteredSingleSignOn.deregister(String)}.
* <p/>
* Ignores invocations resulting from TreeCache changes originated by
* this object.
*
* @param fqn the fully-qualified name of the node that was removed
*/
@NodeRemoved
public void nodeRemoved(NodeRemovedEvent event)
{
if (event.isPre())
return;
@SuppressWarnings("unchecked")
Fqn<Serializable> fqn = event.getFqn();
String ssoId = getIdFromFqn(fqn);
if (ssoId == null)
return;
if (fqn.size() == 2)
{
// Entire SSO is being removed; i.e. an invalidation
// Ignore messages generated by our own logout activity
if (!ssoId.equals(beingLocallyRemoved.get()))
{
handleRemoteInvalidation(ssoId);
}
}
else if (fqn.size() == 4)
{
// A peer is gone
handlePeerRemoval(ssoId);
}
}
/**
* If any nodes have been removed from the view, asynchronously scans
* all SSOs looking for and removing sessions owned by the removed node.
* Notifies the SSO valve if as a result any SSOs no longer have active
* sessions. If the removed node is the one associated with this object,
* does nothing.
*/
@ViewChanged
public synchronized void viewChange(ViewChangedEvent event)
{
if (event.isPre())
return;
log.debug("Received ViewChangedEvent " + event);
Set<Object> oldMembers = new HashSet<Object>(currentView);
synchronized (currentView)
{
currentView.clear();
currentView.addAll(event.getNewView().getMembers());
// If we're not in the view, just exit
if (localAddress == null || !currentView.contains(localAddress))
return;
// Remove all the current members from the old set; any left
// are the dead members
oldMembers.removeAll(currentView);
}
if (oldMembers.size() > 0)
{
log.debug("Members have been removed; will launch cleanup task. Dead members: " + oldMembers);
launchSSOCleaner(false);
}
}
/**
* Instantiates a DeadMemberCleaner and assigns a thread
* to execute the cleanup task.
* @param notifyIfEmpty TODO
*/
private void launchSSOCleaner(boolean notifyIfEmpty)
{
SSOCleanerTask cleaner = new SSOCleanerTask();
cleaner.setCheckForEmpty(notifyIfEmpty);
if (threadPool != null)
{
threadPool.run(cleaner);
}
else
{
Thread t = new Thread(cleaner, "ClusteredSSOCleaner");
t.setDaemon(true);
t.start();
}
}
/**
* Handles the notification that an entire SSO has been removed remotely
*
* @param ssoId id of the removed SSO
*/
private void handleRemoteInvalidation(String ssoId)
{
beingRemotelyRemoved.set(ssoId);
try
{
if (log.isTraceEnabled())
{
log.trace("received a node removed message for SSO " + ssoId);
}
ssoValve.deregister(ssoId);
}
finally
{
beingRemotelyRemoved.set(null);
}
}
/**
* Checks whether any peers remain for the given SSO; if not
* notifies the valve that the SSO is empty.
*
* @param ssoId
*/
private void handlePeerRemoval(String ssoId)
{
try
{
Set<Object> peers = getSSOPeers(ssoId);
if (peers.size() == 0)
{
ssoValve.notifySSOEmpty(ssoId);
}
}
catch (Exception e)
{
log.error("Caught exception checking if " + ssoId + " is empty", e);
}
}
/**
* Extracts an SSO session id from the Fqn and uses it in an invocation of
* {@link ClusteredSingleSignOn#update ClusteredSingleSignOn.update()}.
* <p/>
* Only responds to modifications of nodes whose FQN's final segment is
* "credentials".
* <p/>
* Ignores invocations resulting from TreeCache changes originated by
* this object.
* <p/>
* Ignores invocations for SSO session id's that are not registered
* with the local SingleSignOn valve.
*
* @param fqn the fully-qualified name of the node that was modified
*/
@NodeModified
public void nodeModified(NodeModifiedEvent event)
{
if (event.isPre() || event.isOriginLocal())
return;
@SuppressWarnings("unchecked")
Fqn<Serializable> fqn = event.getFqn();
String type = getTypeFromFqn(fqn);
if (CREDENTIALS.equals(type))
{
@SuppressWarnings("unchecked")
Map<Object, Object> nodeData = event.getData();
handleCredentialUpdate(getIdFromFqn(fqn), nodeData);
}
else if (SESSIONS.equals(type))
{
handleSessionSetChange(fqn);
}
}
/**
* @param ssoId the id of the sso
* @param nodeData JBC data map assoicated with the update
*/
private void handleCredentialUpdate(String ssoId, Map<Object, Object> nodeData)
{
// Ignore invocations that come as a result of our additions
if (ssoId.equals(beingLocallyAdded.get()))
{
return;
}
if (log.isTraceEnabled())
{
log.trace("received a credentials modified message for SSO " + ssoId);
}
try
{
SSOCredentials data = (SSOCredentials) nodeData.get(KEY);
if (data != null)
{
ssoValve.remoteUpdate(ssoId, data);
}
}
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 or lower
*/
private void handleSessionSetChange(Fqn<Serializable> fqn)
{
// Ignore anything not for a peer's session node
if (fqn.size() != 4)
return;
// Peers remove their entire node when it's empty, so any
// other modification means it's not empty
ssoValve.notifySSONotEmpty(getIdFromFqn(fqn));
}
/**
* Prepare for the beginning of active use of the public methods of this
* component. This method should be called before any of the public
* methods of this component are utilized. It should also send a
* LifecycleEvent of type START_EVENT to any registered listeners.
*
* @throws LifecycleException if this component detects a fatal error
* that prevents this component from being used
*/
public void start() throws Exception
{
// Validate and update our current component state
if (started)
{
throw new IllegalStateException("JBossCacheSSOClusterManager already Started");
}
initThreadPool();
if (isCacheAvailable(true))
{
integrateWithCache();
}
started = true;
}
/**
* Gracefully terminate the active use of the public methods of this
* component. This method should be the last one called on a given
* instance of this component. It should also send a LifecycleEvent
* of type STOP_EVENT to any registered listeners.
*
* @throws LifecycleException if this component detects a fatal error
* that needs to be reported
*/
public void stop() throws Exception
{
// Validate and update our current component state
if (!started)
{
throw new IllegalStateException("JBossCacheSSOClusterManager not Started");
}
removeAsCacheListener();
started = false;
}
// ------------------------------------------------------- Public Methods
/**
* Gets the number of sessions associated with the given SSO. The same
* session active on more than one node will count twice.
*/
public int getSessionCount(String ssoId) throws Exception
{
int count = 0;
Set<Object> peers = getSSOPeers(ssoId);
for (Object peer : peers)
{
Set<Object> ids = getSessionKeys(ssoId, (Serializable) peer);
count += ids.size();
}
return count;
}
// ------------------------------------------------------- Private Methods
private Object getFromTreeCache(Fqn<?> fqn, String key) throws Exception
{
return cache.get(fqn, key);
}
private Set<String> getSSOIds() throws Exception
{
Fqn<String> ssoRootFqn = Fqn.fromElements(SSO);
@SuppressWarnings("unchecked")
Node ssoRoot = cache.getRoot().getChild(ssoRootFqn);
@SuppressWarnings("unchecked")
Set<String> result = ssoRoot == null ? new HashSet<String>() : ssoRoot.getChildrenNames();
return result;
}
private Set<Object> getSSOPeers(String ssoId) throws Exception
{
Fqn<String> fqn = getSessionRootFqn(ssoId);
Node<Object, Object> ssoRoot = cache.getRoot().getChild(fqn);
Set<Object> result = ssoRoot == null ? new HashSet<Object>() : ssoRoot.getChildrenNames();
return result;
}
private Fqn<String> getCredentialsFqn(String ssoid)
{
return Fqn.fromElements(SSO, ssoid, CREDENTIALS);
}
private Fqn<String> getSessionRootFqn(String ssoId)
{
return Fqn.fromElements(SSO, ssoId, SESSIONS);
}
private Fqn<Serializable> getSessionsFqn(String ssoid)
{
return getSessionsFqn(ssoid, localAddress);
}
private Fqn<Serializable> getSessionsFqn(String ssoid, Serializable address)
{
return Fqn.fromElements(SSO, ssoid, SESSIONS, address);
}
private Fqn<String> getSingleSignOnFqn(String ssoid)
{
return Fqn.fromElements(SSO, ssoid);
}
/**
* Extracts an SSO session id from a fully qualified name object.
*
* @param fqn the Fully Qualified Name used by TreeCache
* @return the second element in the Fqn -- the SSO session id
*/
private String getIdFromFqn(Fqn<Serializable> fqn)
{
String id = null;
if (fqn.size() > 1 && SSO.equals(fqn.get(0)))
{
id = (String) fqn.get(1);
}
return id;
}
/**
* Extracts the SSO tree cache node type from a fully qualified name
* object.
*
* @param fqn the Fully Qualified Name used by TreeCache
* @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<Serializable> fqn)
{
String type = null;
if (fqn.size() > 2 && SSO.equals(fqn.get(0)))
type = (String) fqn.get(2);
return type;
}
private Set<Object> getSessionKeys(String ssoId)
{
return getSessionKeys(ssoId, localAddress);
}
private Set<Object> getSessionKeys(String ssoId, Serializable peer)
{
Fqn<Serializable> fqn = getSessionsFqn(ssoId, peer);
Set<Object> keys = null;
Node<Object, Object> sessions = cache.getRoot().getChild(fqn);
if (sessions != null)
{
keys = sessions.getKeys();
}
else
{
keys = new HashSet<Object>();
}
return keys;
}
/**
* 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 gets our cluster-wide unique local address from the cache.
*
* @throws Exception
*/
private void configureFromCache() throws Exception
{
tm = cache.getConfiguration().getRuntimeConfig().getTransactionManager();
if (tm == null)
{
throw new IllegalStateException("Cache does not have a " +
"transaction manager; please " +
"configure a valid " +
"TransactionManagerLookupClass");
}
// We no longer rule out buddy replication, as it can be valid if
// all activity for the SSO is meant to pinned to one server (i.e.
// only one session, or all sessions share the same session id cookie)
/*
if (cache.getConfiguration().getBuddyReplicationConfig() != null
&& cache.getConfiguration().getBuddyReplicationConfig().isEnabled())
{
throw new IllegalStateException("Underlying cache is configured for " +
"buddy replication; use of buddy " +
"replication with ClusteredSingleSignOn " +
"is not supported");
}
*/
// Find out our address
Object address = cache.getLocalAddress();
// 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 if (address != null)
localAddress = address.toString();
else if (CacheMode.LOCAL == cache.getConfiguration().getCacheMode())
localAddress = "LOCAL";
else
throw new IllegalStateException("Cannot get local address from cache");
log.debug("Local address is " + localAddress);
// Get the currentView
synchronized (currentView)
{
currentView.clear();
List<Address> members = cache.getMembers();
if (members != null)
{
currentView.addAll(members);
log.debug("Current view is " + currentView);
}
}
}
private void endTransaction()
{
try
{
if(tm.getTransaction().getStatus() != Status.STATUS_MARKED_ROLLBACK)
{
tm.commit();
}
else
{
tm.rollback();
}
}
catch (Exception e)
{
log.error(e);
throw new NestedRuntimeException("JBossCacheSSOClusterManager.endTransaction(): ", e);
}
}
private MBeanServer getMBeanServer()
{
if (server == null && ssoValve != null)
{
server = ssoValve.getMBeanServer();
}
return server;
}
/**
* Checks whether an MBean is registered under the value of property
* "cacheObjectName".
*
* @param forceCheck check for availability whether or not it has already
* been positively established
* @return <code>true</code> if property <code>cacheName</code> has been
* set and points to a registered MBean.
*/
private synchronized boolean isCacheAvailable(boolean forceCheck)
{
if (forceCheck || treeCacheAvailable == false)
{
boolean available = (cacheName != null);
if (available)
{
try
{
CacheManager cm = CacheManagerLocator.getCacheManagerLocator().getCacheManager(null);
available = cm.getConfigurationNames().contains(cacheName);
}
catch (IllegalStateException ise)
{
log.debug("No CacheManager available");
available = false;
}
if (!available && getMBeanServer() != null)
{
// See if there is a legacy JMX binding
String onameStr = cacheName;
if (DEFAULT_CACHE_NAME.equals(cacheName))
onameStr = LEGACY_CACHE_NAME;
try
{
ObjectName oname = new ObjectName(onameStr);
@SuppressWarnings("unchecked")
Set s = getMBeanServer().queryMBeans(oname, null);
if (s.size() > 0)
{
available = true;
// Save the object name to tell integrateWithCache to use JMX
cacheObjectName = oname;
cacheName = onameStr;
}
}
catch (Exception e)
{
// no jmx
}
}
if (available)
{
try
{
// If Tomcat6 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 configuring from cache " +
cacheName, e);
available = false;
}
}
}
treeCacheAvailable = available;
}
return treeCacheAvailable;
}
private boolean checkTreeCacheAvailable()
{
boolean avail = isCacheAvailable(false);
if (!avail)
logMissingCacheError();
return avail;
}
private void putInTreeCache(Fqn<?> fqn, Object key, Object data) throws Exception
{
cache.put(fqn, key, data);
}
private void integrateWithCache() throws Exception
{
if (cache == null)
{
// We are likely going to cause creation and start of a cache here;
// we don't want to leak the TCCL to cache/jgroups threads, so
// we switch it to our classloader
@SuppressWarnings("unchecked")
ContextClassLoaderSwitcher switcher = (ContextClassLoaderSwitcher) AccessController.doPrivileged(ContextClassLoaderSwitcher.INSTANTIATOR);
ContextClassLoaderSwitcher.SwitchContext switchContext = switcher.getSwitchContext(getClass().getClassLoader());
try
{
// Determine if our cache is a PojoCache or a plain Cache
if (cacheObjectName == null)
{
CacheManager cm = CacheManagerLocator.getCacheManagerLocator().getCacheManager(null);
cache = cm.getCache(cacheName, true);
}
else if (server != null)
{
// Look in JMX
MBeanInfo info = server.getMBeanInfo(cacheObjectName);
MBeanAttributeInfo[] attrs = info.getAttributes();
for (MBeanAttributeInfo attr : attrs)
{
if ("PojoCache".equals(attr.getName()))
{
cache = ((PojoCache) getMBeanServer().getAttribute(cacheObjectName, "PojoCache")).getCache();
break;
}
else if ("Cache".equals(attr.getName()))
{
@SuppressWarnings("unchecked")
Cache<Object, Object> unchecked = (Cache<Object, Object>) getMBeanServer().getAttribute(cacheObjectName, "Cache");
cache = unchecked;
break;
}
}
}
else
{
// Shouldn't be possible or isTreeCacheAvailable would return false
throw new IllegalStateException("No JBoss Cache available under name " + cacheName);
}
if (cache.getCacheStatus() != CacheStatus.STARTED)
cache.start();
}
finally
{
// Restore the TCCL
switchContext.reset();
}
// Ensure we have a transaction manager and a cluster-wide unique address
configureFromCache();
// If the SSO region is inactive, activate it
activateCacheRegion();
registerAsCacheListener();
// Scan for any SSOs with no entries; mark them for expiration
launchSSOCleaner(true);
log.debug("Successfully integrated with cache service " + cacheName);
}
}
/**
* If we are sharing a cache with HttpSession replication, the SSO
* region may not be active, so here we ensure it is.
*
* @throws Exception
*/
private void activateCacheRegion() throws Exception
{
if (cache.getConfiguration().isInactiveOnStartup())
{
if (cache.getConfiguration().isUseRegionBasedMarshalling())
{
Region region =cache.getRegion(Fqn.fromElements(SSO), true);
try
{
region.activate();
}
catch (RegionNotEmptyException e)
{
log.debug(SSO + " region already active", e);
}
}
}
}
/**
* Invokes an operation on the JMX server to register ourself as a
* listener on the TreeCache service.
*
* @throws Exception
*/
private void registerAsCacheListener() throws Exception
{
cache.addCacheListener(this);
registeredAsListener = true;
}
/**
* Invokes an operation on the JMX server to register ourself as a
* listener on the TreeCache service.
*
* @throws Exception
*/
private void removeAsCacheListener() throws Exception
{
if (registeredAsListener && cache != null)
{
cache.removeCacheListener(this);
registeredAsListener = false;
}
}
private void removeFromTreeCache(Fqn<?> fqn, boolean localOnly) throws Exception
{
if (localOnly)
{
InvocationContext ctx = cache.getInvocationContext();
Option option = new Option();
option.setCacheModeLocal(true);
ctx.setOptionOverrides(option);
}
cache.removeNode(fqn);
}
private void removeFromTreeCache(Fqn<Serializable> fqn, Object key) throws Exception
{
cache.remove(fqn, key);
}
/**
* Stores the given data to the clustered cache in a tree branch whose FQN
* is the given SSO id. Stores the given credential data in a child node
* named "credentials". If parameter <code>storeSessions</code> is
* <code>true</code>, also stores an empty HashSet in a sibling node
* named "sessions". This HashSet will later be used to hold session ids
* associated with the SSO.
* <p/>
* Any items stored are stored under the key "key".
*
* @param ssoId the id of the SSO session
* @param authType the type of authenticator (BASIC, CLIENT-CERT, DIGEST
* or FORM) used to authenticate the SSO.
* @param username the username (if any) used for the authentication
* @param password the password (if any) used for the authentication
*/
private void storeSSOData(String ssoId, String authType, String username,
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
beingLocallyAdded.set(ssoId);
try
{
putInTreeCache(getCredentialsFqn(ssoId), KEY, data);
}
catch (Exception e)
{
log.error("Exception attempting to add TreeCache nodes for SSO " +
ssoId, e);
}
finally
{
beingLocallyAdded.set(null);
}
}
private void initThreadPool()
{
if (threadPoolName != null && getMBeanServer() != null)
{
try
{
ObjectName on = new ObjectName(threadPoolName);
threadPool = (ThreadPool) server.getAttribute(on, "Instance");
log.debug("Using ThreadPool at " + threadPoolName + " to clean dead members");
}
catch (Exception e)
{
log.info("Unable to access ThreadPool at " + threadPoolName +
" -- will use individual threads for cleanup work");
log.debug("Failure to access ThreadPool due to: " + e);
}
}
else
{
log.debug("No ThreadPool configured -- will use individual threads for cleanup work");
}
}
private boolean isMissingCacheErrorLogged()
{
return missingCacheErrorLogged;
}
private void setMissingCacheErrorLogged(boolean missingCacheErrorLogged)
{
this.missingCacheErrorLogged = missingCacheErrorLogged;
}
private void logMissingCacheError()
{
StringBuffer msg = new StringBuffer("Cannot find TreeCache using ");
msg.append(getCacheName());
msg.append(" -- TreeCache must be started before ClusteredSingleSignOn ");
msg.append("can handle requests");
if (isMissingCacheErrorLogged())
{
// Just log it as a warning
log.warn(msg);
}
else
{
log.error(msg);
// Set a flag so we don't relog this error over and over
setMissingCacheErrorLogged(true);
}
}
// --------------------------------------------------------- Outer Classes
/**
* 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 SSOCleanerTask implements Runnable
{
boolean checkForEmpty = false;
boolean getCheckForEmpty()
{
return checkForEmpty;
}
void setCheckForEmpty(boolean checkForEmpty)
{
this.checkForEmpty = checkForEmpty;
}
public void run()
{
synchronized (cleanupMutex)
{
try
{
// Ensure we have a TransactionManager
if (tm == null)
configureFromCache();
Set<String> ids = getSSOIds();
for (String sso : ids)
{
cleanSSO(sso);
}
}
catch (Exception e)
{
log.error("Caught exception cleaning sessions from dead cluster members from SSOs ", e);
}
}
}
private void cleanSSO(String 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<Object> peers = getSSOPeers(ssoId);
if (peers != null && peers.size() > 0)
{
for (Object peer : peers)
{
boolean alive = true;
synchronized (currentView)
{
alive = currentView.contains(peer);
}
if (!alive)
{
if (log.isTraceEnabled())
{
log.trace("Removing peer " + peer + " from SSO " + ssoId);
}
Fqn<Serializable> fqn = getSessionsFqn(ssoId, (Serializable) peer);
// Remove the peer node, but local-only
// Each cache is responsible for cleaning itself
removeFromTreeCache(fqn, true);
}
}
}
else if (checkForEmpty)
{
// SSO has no peers; notify our valve so we can expire it
ssoValve.notifySSOEmpty(ssoId);
}
}
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 JBossCacheSSOClusterManager