Package org.jboss.web.tomcat.service.sso.jbc

Source Code of org.jboss.web.tomcat.service.sso.jbc.JBossCacheSSOClusterManager$SSOCleanerTask

/*
* 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

TOP

Related Classes of org.jboss.web.tomcat.service.sso.jbc.JBossCacheSSOClusterManager$SSOCleanerTask

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