/*
 * JBoss, the OpenSource WebOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.web.tomcat.tc5.sso;


import org.jboss.web.tomcat.tc5.Tomcat5;

import java.io.IOException;
import java.security.Principal;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;

import org.apache.catalina.LifecycleException;
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;


/**
 * A <strong>Valve</strong> that supports a "single sign on" user experience,
 * where the security identity of a user who successfully authenticates to one
 * web application is propogated to other web applications in the same
 * security domain.  For successful use, the following requirements must
 * be met:
 * <ul>
 * <li>This Valve must be configured on the Container that represents a
 * virtual host (typically an implementation of <code>Host</code>).</li>
 * <li>The <code>Realm</code> that contains the shared user and role
 * information must be configured on the same Container (or a higher
 * one), and not overridden at the web application level.</li>
 * <li>The web applications themselves must use one of the standard
 * Authenticators found in the
 * <code>org.apache.catalina.authenticator</code> package.</li>
 * </ul>
 *
 * @author Brian E. Stansberry based on the work of Craig R. McClanahan
 * @version $Revision: 1.4.2.1 $ $Date: 2005/01/28 12:30:49 $
 */
public class ClusteredSingleSignOn
   extends org.apache.catalina.authenticator.SingleSignOn
{

   // Override the superclass value
   static
   {
      info = ClusteredSingleSignOn.class.getName();
   }
    
   // ----------------------------------------------------- Instance Variables

 
   /**
    * Fully qualified name of a class implementing
    * {@link SSOClusterManager SSOClusterManager} that will be used
    * to manage SSOs across a cluster.
    */
   private String clusterManagerClass =
      TreeCacheSSOClusterManager.class.getName();

   /**
    * Object used to provide cross-cluster support for single sign on.
    */
   private SSOClusterManager ssoClusterManager = null;

   /**
    * Object name of the tree cache used by SSOClusterManager.
    * Only relevant if the SSOClusterManager implementation is
    * TreeCacheSSOClusterManager.
    */
   private String treeCacheName = Tomcat5.DEFAULT_CACHE_NAME;

   // ------------------------------------------------------------- Properties

   /**
    * Gets the object that provides SSO support across a cluster.
    *
    * @return the object provided cluster support, or <code>null</code> if
    *         no such object has been configured.
    */
   public SSOClusterManager getClusterManager()
   {
      return this.ssoClusterManager;
   }


   /**
    * Sets the object that provides SSO support across a cluster.
    *
    * @param clusterManager the object that provides SSO support.
    * @throws IllegalStateException if this method is invoked after this valve
    *                               has been started.
    */
   public void setClusterManager(SSOClusterManager clusterManager)
   {
      if (started && (clusterManager != ssoClusterManager))
      {
         throw new IllegalStateException("already started -- cannot set a " +
            "new SSOClusterManager");
      }

      this.ssoClusterManager = clusterManager;

      if (clusterManager != null)
      {
         clusterManagerClass = clusterManager.getClass().getName();
      }
   }


   /**
    * Gets the name of the class that will be used to provide SSO support
    * across a cluster.
    *
    * @return Fully qualified name of a class implementing
    *         {@link SSOClusterManager SSOClusterManager}
    *         that is being used to manage SSOs across a cluster.
    *         May return <code>null</code> (the default) if clustered
    *         SSO support is not configured.
    */
   public String getClusterManagerClass()
   {
      return clusterManagerClass;
   }


   /**
    * Sets the name of the class that will be used to provide SSO support
    * across a cluster.
    * <p><b>NOTE: </b>
    * If this Valve has already started, and no SSOClusterManager has been
    * configured for it, calling this method will
    *
    * @param managerClass Fully qualified name of a class implementing
    *                     {@link SSOClusterManager SSOClusterManager}
    *                     that will be used to manage SSOs across a cluster.
    *                     Class must declare a public no-arguments
    *                     constructor.  <code>null</code> is allowed.
    */
   public void setClusterManagerClass(String managerClass)
   {
      if (!started)
      {
         clusterManagerClass = managerClass;
      }
      else if (ssoClusterManager == null)
      {
         try
         {
            createClusterManager(managerClass);
         }
         catch (LifecycleException e)
         {
            getContainer().getLogger().error("Exception creating SSOClusterManager " +
               managerClass, e);
         }
      }
      else
      {
          getContainer().getLogger().error("Cannot set clusterManagerClass to " + managerClass +
            "; already started using " + clusterManagerClass);
      }
   }

   /**
    * Object name of the tree cache used by SSOClusterManager.
    * Only relevant if the SSOClusterManager implementation is
    * TreeCacheSSOClusterManager.
    */
   public String getTreeCacheName()
   {
      return treeCacheName;
   }

   /**
    * Sets the object name of the tree cache used by SSOClusterManager.
    * Only relevant if the SSOClusterManager implementation is
    * TreeCacheSSOClusterManager.
    */
   public void setTreeCacheName(String cacheName)
      throws Exception
   {
      this.treeCacheName = cacheName;
      if (ssoClusterManager != null
         && ssoClusterManager instanceof TreeCacheSSOClusterManager)
      {
         ((TreeCacheSSOClusterManager) ssoClusterManager).setCacheName(cacheName);
      }
   }


   // ------------------------------------------------------ Lifecycle Methods


   /**
    * Prepare for the beginning of active use of the public methods of this
    * component.  This method should be called after <code>configure()</code>,
    * and before any of the public methods of the component are utilized.
    *
    * @throws LifecycleException if this component detects a fatal error
    *                            that prevents this component from being used
    */
   public void start() throws LifecycleException
   {
      // Validate and update our current component state
      if (started)
      {
         throw new LifecycleException
            (sm.getString("authenticator.alreadyStarted"));
      }

      // Attempt to create an SSOClusterManager
      createClusterManager(getClusterManagerClass());

      lifecycle.fireLifecycleEvent(START_EVENT, null);
      started = true;

      if (ssoClusterManager != null)
      {
         ssoClusterManager.start();
      }

   }


   /**
    * 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.
    *
    * @throws LifecycleException if this component detects a fatal error
    *                            that needs to be reported
    */
   public void stop() throws LifecycleException
   {
      // Validate and update our current component state
      if (!started)
      {
         throw new LifecycleException
            (sm.getString("authenticator.notStarted"));
      }

      if (ssoClusterManager != null)
      {
         ssoClusterManager.stop();
      }

      lifecycle.fireLifecycleEvent(STOP_EVENT, null);
      started = false;

   }


   // ------------------------------------------------ SessionListener Methods


   /**
    * Updates the state of a single sign on session to reflect the destruction
    * of a standard HTTP session.
    * <p/>
    * If the given event is a {@link Session#SESSION_DESTROYED_EVENT
    * Session destroyed event}, checks whether the session was destroyed due
    * to timeout or user action (i.e. logout).  If due to timeout, disassociates
    * the Session from the single sign on session.  If due to logout, invokes
    * the {@link #logout} method.
    *
    * @param event SessionEvent that has occurred
    */
   public void sessionEvent(SessionEvent event)
   {
      // We only care about session destroyed events
      if (!Session.SESSION_DESTROYED_EVENT.equals(event.getType()))
         return;

      // Look up the single session id associated with this session (if any)
      Session session = event.getSession();
      if (getContainer().getLogger().isDebugEnabled())
          getContainer().getLogger().debug("Process session destroyed on " + session);

      String ssoId = null;
      synchronized (reverse)
      {
         ssoId = (String) reverse.get(session);
      }
      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))
      {
         removeSession(ssoId, session);
      }
      else
      {
         // The session was logged out.
         logout(ssoId);
      }

   }


   // ---------------------------------------------------------- Valve Methods


   /**
    * Perform single-sign-on support processing for this request.
    * <p/>
    * Overrides the superclass version by handling the fact that a
    * single sign on may have been originated on another cluster node and
    * thus may not have a <code>Principal</code> object associated with it
    * on this node.
    *
    * @param request  The servlet request we are processing
    * @param response The servlet response we are creating
    * @param context  The valve context used to invoke the next valve
    *                 in the current processing pipeline
    * @throws IOException      if an input/output error occurs
    * @throws ServletException if a servlet error occurs
    */
   public void invoke(Request request, Response response)
      throws IOException, ServletException
   {
      request.removeNote(Constants.REQ_SSOID_NOTE);

      // Has a valid user already been authenticated?
      if (getContainer().getLogger().isDebugEnabled())
         getContainer().getLogger().debug("Process request for '" + request.getRequestURI() + "'");
      if (request.getUserPrincipal() != null)
      {
         if (getContainer().getLogger().isDebugEnabled())
            getContainer().getLogger().debug(" Principal '" + request.getUserPrincipal().getName() +
               "' has already been authenticated");
         getNext().invoke(request, response);
         return;
      }

      // Check for the single sign on cookie
      Cookie cookie = null;
      Cookie cookies[] = request.getCookies();
      if (cookies == null)
         cookies = new Cookie[0];
      for (int i = 0; i < cookies.length; i++)
      {
         if (Constants.SINGLE_SIGN_ON_COOKIE.equals(cookies[i].getName()))
         {
            cookie = cookies[i];
            break;
         }
      }
      if (cookie == null)
      {
         if (getContainer().getLogger().isDebugEnabled())
            getContainer().getLogger().debug(" SSO cookie is not present");
         getNext().invoke(request, response);
         return;
      }

      // Look up the cached Principal associated with this cookie value
      if (getContainer().getLogger().isDebugEnabled())
          getContainer().getLogger().debug(" Checking for cached principal for " + cookie.getValue());
      SingleSignOnEntry entry = getSingleSignOnEntry(cookie.getValue());
      if (entry != null)
      {
         Principal ssoPrinc = entry.getPrincipal();
         // have to deal with the fact that the entry may not have an
         // associated Principal. SSO entries retrieved via a lookup from a 
         // cluster will not have a Principal, as Principal is not Serializable
         if (getContainer().getLogger().isDebugEnabled())
         {
             getContainer().getLogger().debug(" Found cached principal '" +
               (ssoPrinc == null ? "NULL" : ssoPrinc.getName()) +
               "' with auth type '" + entry.getAuthType() + "'");
         }
         request.setNote(Constants.REQ_SSOID_NOTE, cookie.getValue());
         // Only set security elements if per-request reauthentication is 
         // not required AND the SSO entry had a Principal.  
         if (!getRequireReauthentication() && ssoPrinc != null)
         {
            request.setAuthType(entry.getAuthType());
            request.setUserPrincipal(ssoPrinc);
         }
      }
      else
      {
         if (getContainer().getLogger().isDebugEnabled())
            getContainer().getLogger().debug(" No cached principal found, erasing SSO cookie");
         cookie.setMaxAge(0);
         response.addCookie(cookie);
      }

      // Invoke the next Valve in our pipeline
      getNext().invoke(request, response);
   }


   // ------------------------------------------------------ Protected Methods


   /**
    * Associate the specified single sign on identifier with the
    * specified Session.
    * <p/>
    * Differs from the superclass version in that it notifies the cluster
    * of any new association of SSO and Session.
    *
    * @param ssoId   Single sign on identifier
    * @param session Session to be associated
    */
   protected void associate(String ssoId, Session session)
   {
      if (getContainer().getLogger().isDebugEnabled())
          getContainer().getLogger().debug("Associate sso id " + ssoId + " with session " + session);

      SingleSignOnEntry sso = getSingleSignOnEntry(ssoId);
      boolean added = false;
      if (sso != null)
         added = sso.addSession(this, session);

      synchronized (reverse)
      {
         reverse.put(session, ssoId);
      }

      // If we made a change, notify any cluster
      if (added && ssoClusterManager != null)
         ssoClusterManager.addSession(ssoId, session);
   }


   /**
    * Deregister the specified session.  If it is the last session,
    * then also get rid of the single sign on identifier.
    * <p/>
    * Differs from the superclass version in that it notifies the cluster
    * of any disassociation of SSO and Session.
    *
    * @param ssoId   Single sign on identifier
    * @param session Session to be deregistered
    */
   protected void deregister(String ssoId, Session session)
   {
      synchronized (reverse)
      {
         reverse.remove(session);
      }

      SingleSignOnEntry sso = getSingleSignOnEntry(ssoId);
      if (sso == null)
         return;

      boolean removed = sso.removeSession(session);
      // If we changed anything, notify any cluster
      if (removed && ssoClusterManager != null)
      {
         ssoClusterManager.removeSession(ssoId, session);
      }

      // see if this was the last session on this node, 
      // if remove sso entry from our local cache
      if (sso.getSessionCount() == 0)
      {
         synchronized (cache)
         {
            sso = (SingleSignOnEntry) cache.remove(ssoId);
         }
      }
   }


   /**
    * Deregister the specified single sign on identifier, and invalidate
    * any associated sessions.
    *
    * @param ssoId Single sign on identifier to deregister
    */
   protected void deregister(String ssoId)
   {
      if (getContainer().getLogger().isDebugEnabled())
          getContainer().getLogger().debug("Deregistering sso id '" + ssoId + "'");

      // Look up and remove the corresponding SingleSignOnEntry
      SingleSignOnEntry sso = null;
      synchronized (cache)
      {
         sso = (SingleSignOnEntry) cache.remove(ssoId);
      }

      if (sso == null)
         return;

      // Expire any associated sessions
      Session sessions[] = sso.findSessions();
      for (int i = 0; i < sessions.length; i++)
      {
         if (getContainer().getLogger().isTraceEnabled())
             getContainer().getLogger().trace(" Invalidating session " + sessions[i]);
         // Remove from reverse cache first to avoid recursion
         synchronized (reverse)
         {
            reverse.remove(sessions[i]);
         }
         // Invalidate this session
         sessions[i].expire();
      }

      // NOTE:  Clients may still possess the old single sign on cookie,
      // but it will be removed on the next request since it is no longer
      // in the cache
   }


   /**
    * Deregister the given SSO, invalidating any associated sessions, then
    * notify any cluster of the logout.
    *
    * @param ssoId the id of the SSO session
    */
   protected void logout(String ssoId)
   {
      deregister(ssoId);
      //  broadcast logout to any cluster
      if (ssoClusterManager != null)
         ssoClusterManager.logout(ssoId);
   }


   /**
    * Look up and return the cached SingleSignOn entry associated with this
    * sso id value, if there is one; otherwise return <code>null</code>.
    *
    * @param ssoId Single sign on identifier to look up
    */
   protected SingleSignOnEntry getSingleSignOnEntry(String ssoId)
   {
      SingleSignOnEntry sso = localLookup(ssoId);
      // If we don't have one locally and there is a cluster,
      // query the cluster for the SSO
      if (sso == null && ssoClusterManager != null)
      {
         sso = ssoClusterManager.lookup(ssoId);
         if (sso != null)
         {
            // Store it locally
            synchronized (cache)
            {
               cache.put(ssoId, sso);
            }
         }
      }

      return sso;
   }


   /**
    * Attempts reauthentication to the given <code>Realm</code> using
    * the credentials associated with the single sign-on session
    * identified by argument <code>ssoId</code>.
    * <p/>
    * If reauthentication is successful, the <code>Principal</code> and
    * authorization type associated with the SSO session will be bound
    * to the given <code>HttpRequest</code> object via calls to
    * {@link HttpRequest#setAuthType HttpRequest.setAuthType()} and
    * {@link HttpRequest#setUserPrincipal HttpRequest.setUserPrincipal()}
    * </p>
    *
    * @param ssoId   identifier of SingleSignOn session with which the
    *                caller is associated
    * @param realm   Realm implementation against which the caller is to
    *                be authenticated
    * @param request the request that needs to be authenticated
    * @return <code>true</code> if reauthentication was successful,
    *         <code>false</code> otherwise.
    */
   protected boolean reauthenticate(String ssoId, Realm realm,
      Request request)
   {
      if (ssoId == null || realm == null)
         return false;

      boolean reauthenticated = false;

      SingleSignOnEntry entry = getSingleSignOnEntry(ssoId);
      if (entry != null && entry.getCanReauthenticate())
      {

         String username = entry.getUsername();
         if (username != null)
         {
            Principal reauthPrincipal =
               realm.authenticate(username, entry.getPassword());
            if (reauthPrincipal != null)
            {
               reauthenticated = true;                    
               // Bind the authorization credentials to the request
               request.setAuthType(entry.getAuthType());
               request.setUserPrincipal(reauthPrincipal);
            }
         }
      }

      return reauthenticated;
   }


   /**
    * Register the specified Principal as being associated with the specified
    * value for the single sign on identifier.
    * <p/>
    * Differs from the superclass version in that it notifies the cluster
    * of the registration.
    *
    * @param ssoId     Single sign on identifier to register
    * @param principal Associated user principal that is identified
    * @param authType  Authentication type used to authenticate this
    *                  user principal
    * @param username  Username used to authenticate this user
    * @param password  Password used to authenticate this user
    */
   protected void register(String ssoId, Principal principal, String authType,
      String username, String password)
   {
      registerLocal(ssoId, principal, authType, username, password);

      // broadcast change to any cluster
      if (ssoClusterManager != null)
         ssoClusterManager.register(ssoId, authType, username, password);
   }


   /**
    * Remove a single Session from a SingleSignOn.  Called when
    * a session is timed out and no longer active.
    * <p/>
    * Differs from the superclass version in that it notifies the cluster
    * of any disassociation of SSO and Session.
    *
    * @param ssoId   Single sign on identifier from which to remove the session.
    * @param session the session to be removed.
    */
   protected void removeSession(String ssoId, Session session)
   {
      if (getContainer().getLogger().isDebugEnabled())
          getContainer().getLogger().debug("Removing session " + session.toString() +
            " from sso id " + ssoId);

      // Get a reference to the SingleSignOn
      SingleSignOnEntry entry = getSingleSignOnEntry(ssoId);
      if (entry == null)
         return;

      // Remove the inactive session from SingleSignOnEntry
      boolean removed = entry.removeSession(session);
      // If we changed anything, notify any cluster
      if (removed && ssoClusterManager != null)
      {
         ssoClusterManager.removeSession(ssoId, session);
      }

      // Remove the inactive session from the 'reverse' Map.
      synchronized (reverse)
      {
         reverse.remove(session);
      }

      // If there are no sessions left in the SingleSignOnEntry,
      // deregister the entry.
      if (entry.getSessionCount() == 0)
      {
         deregister(ssoId);
      }
   }


   /**
    * Updates any <code>SingleSignOnEntry</code> found under key
    * <code>ssoId</code> with the given authentication data.
    * <p/>
    * The purpose of this method is to allow an SSO entry that was
    * established without a username/password combination (i.e. established
    * following DIGEST or CLIENT-CERT authentication) to be updated with
    * a username and password if one becomes available through a subsequent
    * BASIC or FORM authentication.  The SSO entry will then be usable for
    * reauthentication.
    * <p/>
    * <b>NOTE:</b> Only updates the SSO entry if a call to
    * <code>SingleSignOnEntry.getCanReauthenticate()</code> returns
    * <code>false</code>; otherwise, it is assumed that the SSO entry already
    * has sufficient information to allow reauthentication and that no update
    * is needed.
    * <p/>
    * Differs from the superclass version in that it notifies the cluster
    * of any update.
    *
    * @param ssoId     identifier of Single sign to be updated
    * @param principal the <code>Principal</code> returned by the latest
    *                  call to <code>Realm.authenticate</code>.
    * @param authType  the type of authenticator used (BASIC, CLIENT-CERT,
    *                  DIGEST or FORM)
    * @param username  the username (if any) used for the authentication
    * @param password  the password (if any) used for the authentication
    */
   protected void update(String ssoId, Principal principal, String authType,
      String username, String password)
   {
      boolean needToBroadcast = updateLocal(ssoId, principal, authType,
         username, password);                                    
                                    
      // if there was a change, broadcast it to any cluster
      if (needToBroadcast && ssoClusterManager != null)
      {
         ssoClusterManager.updateCredentials(ssoId, authType,
            username, password);
      }
   }

   //----------------------------------------------  Package-Protected Methods

   /**
    * Search in our local cache for an SSO entry.
    *
    * @param ssoId the id of the SSO session
    * @return any SingleSignOnEntry associated with the given id, or
    *         <code>null</code> if there is none.
    */
   SingleSignOnEntry localLookup(String ssoId)
   {
      synchronized (cache)
      {
         return ((SingleSignOnEntry) cache.get(ssoId));
      }

   }

   /**
    * Create a SingleSignOnEntry using the passed configuration parameters and
    * register it in the local cache, bound to the given id.
    *
    * @param ssoId     the id of the SSO session
    * @param principal the <code>Principal</code> returned by the latest
    *                  call to <code>Realm.authenticate</code>.
    * @param authType  the type of authenticator used (BASIC, CLIENT-CERT,
    *                  DIGEST or FORM)
    * @param username  the username (if any) used for the authentication
    * @param password  the password (if any) used for the authentication
    */
   void registerLocal(String ssoId, Principal principal, String authType,
      String username, String password)
   {
      if (getContainer().getLogger().isDebugEnabled())
      {
          getContainer().getLogger().debug("Registering sso id '" + ssoId + "' for user '" +
            principal.getName() + "' with auth type '" + authType + "'");
      }

      synchronized (cache)
      {
         cache.put(ssoId, new SingleSignOnEntry(principal, authType,
            username, password));
      }
   }

   /**
    * Updates any <code>SingleSignOnEntry</code> found under key
    * <code>ssoId</code> with the given authentication data.
    *
    * @param ssoId     identifier of Single sign to be updated
    * @param principal the <code>Principal</code> returned by the latest
    *                  call to <code>Realm.authenticate</code>.
    * @param authType  the type of authenticator used (BASIC, CLIENT-CERT,
    *                  DIGEST or FORM)
    * @param username  the username (if any) used for the authentication
    * @param password  the password (if any) used for the authentication
    * @return <code>true</code> if the update resulted in an actual change
    *         to the entry's authType, username or principal properties
    */
   boolean updateLocal(String ssoId, Principal principal, String authType,
      String username, String password)
   {
      boolean shouldBroadcast = false;

      SingleSignOnEntry sso = getSingleSignOnEntry(ssoId);
      // Only update if the entry is missing information
      if (sso != null)
      {
         if (sso.getCanReauthenticate() == false)
         {
            if (getContainer().getLogger().isDebugEnabled())
                getContainer().getLogger().debug("Update sso id " + ssoId + " to auth type " + authType);

            synchronized (sso)
            {
               shouldBroadcast = sso.updateCredentials(principal, authType,
                  username, password);
            }
         }
         else if (sso.getPrincipal() == null && principal != null)
         {
            if (getContainer().getLogger().isDebugEnabled())
                getContainer().getLogger().debug("Update sso id " + ssoId + " with principal " +
                  principal.getName());

            synchronized (sso)
            {
               sso.setPrincipal(principal);
               // No need to notify cluster; Principals don't replicate
            }
         }

      }

      return shouldBroadcast;

   }

   void remoteUpdate(String ssoId, String authType,
      String username, String password)
   {
      SingleSignOnEntry sso = getSingleSignOnEntry(ssoId);
      // Only update if the entry is missing information
      if (sso != null && sso.getCanReauthenticate() == false)
      {
         if (getContainer().getLogger().isDebugEnabled())
             getContainer().getLogger().debug("Update sso id " + ssoId + " to auth type " + authType);

         synchronized (sso)
         {
            // Use the existing principal
            Principal p = sso.getPrincipal();
            sso.updateCredentials(p, authType, username, password);
         }
      }

   }

   
   // -------------------------------------------------------  Private Methods

   
   /**
    * Instantiates an instance of the given class, making it this valve's
    * SSOClusterManager.
    * <p/>
    * If this valve has been started and the given class implements
    * <code>Lifecycle</code>, starts the new SSOClusterManager.
    *
    * @param className fully qualified class name of an implementation
    *                  of {@link SSOClusterManager SSOClusterManager}.
    * @throws LifecycleException if there is any problem instantiating or
    *                            starting the object, or if the created
    *                            object does not implement
    *                            <code>SSOClusterManger</code>
    */
   private void createClusterManager(String className)
      throws LifecycleException
   {
      if (ssoClusterManager != null)
         return;

      if (className != null)
      {
         SSOClusterManager mgr = null;
         try
         {
            ClassLoader tcl =
               Thread.currentThread().getContextClassLoader();
            Class clazz = tcl.loadClass(className);
            mgr = (SSOClusterManager) clazz.newInstance();
            mgr.setSingleSignOnValve(this);
            if (mgr instanceof TreeCacheSSOClusterManager)
            {
               ((TreeCacheSSOClusterManager) mgr).setCacheName(getTreeCacheName());
            }
            ssoClusterManager = mgr;
            clusterManagerClass = className;
         }
         catch (Throwable t)
         {
            throw new LifecycleException("Cannot create " +
               "SSOClusterManager using " +
               className, t);
         }

         if (started)
         {
            ssoClusterManager.start();
         }
      }
   }

}