/*
 * @(#)SessionCookieImpl.java
 *
 * Copyright 2001-2002 by Oracle Corporation,
 * 500 Oracle Parkway, Redwood Shores, California, 94065, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information
 * of Oracle Corporation.
 */

package oracle.jbo.common.ampool;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Date;
import java.util.Hashtable;
import com.sun.java.util.collections.HashMap;
import oracle.jbo.ApplicationModule;
import oracle.jbo.common.Diagnostic;
import oracle.jbo.common.JboEnvUtil;
import oracle.jbo.common.PropertyMetadata;
import oracle.jbo.common.PropertyConstants;
import java.security.Principal;
import oracle.jbo.JboContext;  
import oracle.jbo.Session;

/**
 * Default SessionCookie implementation.
 * <p>
 * Default session cookie instances should only be instantiated by an
 * application pool.  This is required because the default session cookie
 * uses the application pool framework to manage application state
 * and to share application module instances between requests.  Please see
 * {@link oracle.jbo.common.ampool.ApplicationPool#createSessionCookie(String, String, Properties)}
 * for more information about creating session cookies.
 * <p>
 * The default implementation uses a session cookie lock to prevent
 * access to the session cookie application module resource by multiple threads.
 * A lock is obtained when a thread invokes useApplicationModule.
 * The lock is released when the thread invokes releaseApplicationModule.
 * <p>
 * Please see {@link oracle.jbo.common.ampool.SessionCookie} for more
 * information about session cookies.
 */
public class SessionCookieImpl extends Object
   implements SessionCookie, Serializable
{
   static final long serialVersionUID = 2910740964920232094L;
   
   // Non-mutable values
   private final String                mApplicationId;
   private final String                mSessionId;
   private final Date                  mLastUpdate = new Date();
   
   private ApplicationPool             mApplicationPool;
   private EnvInfoProvider             mEnvInfoProvider          = null;

   private transient SessionCookieLock mMutex;

   // Cache the unique cookie identifer.
   private final int                   mCookieId;

   // Mutable values
   private int                             mPassivationId;
   private int                             mReservedPassivationId;
   private String                          mValue                    = null;
   private boolean                         mActivationRequired       = false;
   private boolean                         mUsed                     = false;
   private Hashtable                       mEnvironment              = null; 
   private String                          mSSOSubscriber            = null;
   private Hashtable                       mUserData                 = null;
   private boolean                         mIsSingleThreaded         = true;

   private transient SessionCookieListener mListener                 = null;
   private transient Date                  mClonedLastUpdate         = null;
   private transient HashMap               mThreadAccess             = null;
   private transient Hashtable             mClientEnvironment        = null; 

   private transient boolean               mInRelease                = false;

   static final char                   COOKIE_SEPARATOR              = '-';
   private static final long           DEFAULT_WAIT_TIMEOUT          = -1;
   private static final long           WAIT_TIME_INCREMENT           = 10000;

   /**
    * Constructor.
    * <p>
    * Applications should not invoke this constructor directly.  A session
    * cookie factory should be used instead.
    * <p>
    * The implementation requires that all three parameters be not null.
    * <p>
    * @param applicationId a unique identifier for the session application
    * @param sessionId a unique identifer for the session
    * @param pool an application pool instance
    * @param timeout the amount of time a thread should wait for an application module resource
    */
   public SessionCookieImpl(String applicationId, String sessionId, ApplicationPool pool, Principal userPrincipal)
   {
      if ((applicationId == null) || (sessionId == null) || (pool == null))
      {
         throw new ApplicationPoolException(
            AMPoolMessageBundle.class
            , AMPoolMessageBundle.EXC_AMPOOL_INVALID_COOKIE_PARAMS
            , new Object[] {applicationId, sessionId, (pool == null ? "null" : pool.getName())});
      }

      mApplicationId = applicationId;
      mSessionId = sessionId;
      mApplicationPool = pool;
      mEnvironment = (Hashtable)pool.getEnvironment().clone();

      StringBuffer sb = new StringBuffer(mSessionId);
      sb.append(COOKIE_SEPARATOR);
      sb.append(mApplicationId);
      mCookieId = sb.toString().hashCode();

      mPassivationId = SessionCookie.NULL_PASSIVATION_ID;
      mReservedPassivationId = SessionCookie.NULL_PASSIVATION_ID;

      if (userPrincipal != null)
      {
         String ssoUserName = userPrincipal.getName();
         mEnvironment.put(PropertyMetadata.USER_PRINCIPAL.pName, ssoUserName);
         mEnvironment.put(PropertyConstants.ENV_SECURITY_AUTHORIZED, PropertyConstants.TRUE);

         mSSOSubscriber = SecurityUtil.getRealmName(userPrincipal);
         Diagnostic.println("SessionCookieImpl SSOUSER " + ssoUserName);
         Diagnostic.println("SessionCookieImpl SSOSUBSCRIBER " + mSSOSubscriber);
         Diagnostic.println("SessionCookieImpl User Principal " + userPrincipal.getClass().getName());
      }
      
      initialize();
   }

   public SessionCookieImpl(String applicationId, String sessionId, ApplicationPool pool)
   {
      this(applicationId, sessionId, pool, null);
   }
             
   private void initialize()
   {
      // Initializes transient state.
      if (mMutex == null)
      {
         mMutex = new SessionCookieLock();
      }

      if (mUserData == null)
      {
         mUserData = new Hashtable();
      }

      if (mThreadAccess == null)
      {
         mThreadAccess = new HashMap();
      }

      if (mClientEnvironment == null)
      {
         mClientEnvironment = new Hashtable();
      }
   }

   public EnvInfoProvider getEnvInfoProvider()
   {
      return mEnvInfoProvider;
   }

   public void setEnvInfoProvider(EnvInfoProvider envInfo)
   {
      mEnvInfoProvider = envInfo;
   }

   public Date getLastUpdate()
   {
      // Cache the cloned value to prevent excessive object instantiation
      // on the critical path.
      if (mClonedLastUpdate == null)
      {
         mClonedLastUpdate = new Date();
      }
      mClonedLastUpdate.setTime(mLastUpdate.getTime());
      return mClonedLastUpdate;
   }

   // From Object
   public String toString()
   {
      synchronized(mMutex)
      {
         return getValue();
      }
   }

   public int hashCode()
   {
      return mCookieId;
   }

   /**
    * Session cookies are equal if their application and session identifiers
    * are equal
    */
   public boolean equals(Object obj)
   {
      if (obj != null && obj instanceof SessionCookie)
      {
         SessionCookie cookie = (SessionCookie)obj;
         if ((mSessionId.equals(cookie.getSessionId()))
            && (mApplicationId.equals(cookie.getApplicationId())))
         {
            return true;
         }
      }

      return false;
   }

   public long getPoolSignature()
   {
      return mApplicationPool.getSignature();
   }

   public String getApplicationId()
   {
      return mApplicationId;
   }

   public String getSessionId()
   {
      return mSessionId;
   }

   public String getValue()
   {
      synchronized(mMutex)
      {
         if (mValue == null)
         {
            // If the next passivation id is not null then append it to the
            // session id to generate the cookie value for this session.  If the
            // next passivation id is null and the passivation id is not null
            // then append the passivation id to the session id to generate the cookie
            // value for this session.  This algorithm guarantees that the "next"
            // cookie value is always generated for the session if it exists.
            mValue = ((mReservedPassivationId != SessionCookie.NULL_PASSIVATION_ID)
               ? Integer.toString(mReservedPassivationId)
               : Integer.toString(mPassivationId));
         }

         return mValue;
      }
   }

   public Hashtable getEnvironment()
   {
      synchronized(mMutex)
      {
         return (Hashtable)mEnvironment.clone();
      }
   }

   public Object getEnvironment(Object key)
   {
      synchronized(mMutex)
      {
         return mEnvironment.get(key);
      }
   }

   public Hashtable getClientEnvironment()
   {
      synchronized(mMutex)
      {
         return (Hashtable)mClientEnvironment.clone();
      }
   }

   public Object getClientEnvironment(Object key)
   {
      synchronized(mMutex)
      {
         return mClientEnvironment.get(key);
      }
   }

   public void setEnvironment(Object key, Object value)
   {
      synchronized(mMutex)
      {
         if (mUsed)
         {
            // Throw an exception.  You may not set the environment of an
            // active session cookie.
            throw new ApplicationPoolException(
               AMPoolMessageBundle.class
               , AMPoolMessageBundle.EXC_AMPOOL_COOKIE_ENV_IMMUTABLE
               , new Object[] {getSessionId(), getApplicationId()});
         }

         if (value == null)
         {
            mEnvironment.remove(key);
         }
         else
         {
            mEnvironment.put(key, value);
         }
      } 
   }

   public void setEnvironment(Hashtable environment)
   {
      synchronized(mMutex)
      {
         if (mUsed)
         {
            // Throw an exception.  You may not set the environment of an
            // active session cookie.
            throw new ApplicationPoolException(
               AMPoolMessageBundle.class
               , AMPoolMessageBundle.EXC_AMPOOL_COOKIE_ENV_IMMUTABLE
               , new Object[] {getSessionId(), getApplicationId()});
         }

         mEnvironment = (Hashtable)environment.clone();

         // store the applet context in a transient member.  the environment
         // must be serializable.  see bug 2188320.
         Object applet = mEnvironment.remove(JboContext.USE_APPLET);
         if (applet != null)
         {
            mClientEnvironment.put(JboContext.USE_APPLET, applet);
         }
      }
   }

   public boolean isApplicationModuleReserved()
   {
      synchronized(mMutex)
      {
         // Request an application module instance for the session without chekcking
         // out a new instance.  If the session does not reference a reserved instance
         // then the pool will return null.
         return (mApplicationPool.useApplicationModule(this, false /* checkout */) != null);
      }
   }

   public ApplicationModule useApplicationModule()
   {
      return useApplicationModule(false);
   }

   public ApplicationModule useApplicationModule(boolean lock)
   {
      return useApplicationModule(lock, DEFAULT_WAIT_TIMEOUT);
   }

   /**
    * The default implementation must obtain a session cookie lock before
    * checking an application module instance out from the pool.  This
    * will prevent multi-threaded use of an application module instance for
    * a given session.
    */
   public ApplicationModule useApplicationModule(boolean lock, long waitTimeout)
   {
      synchronized(mMutex)
      {
         ApplicationModule am = null;

         boolean hadLock = mMutex.hasLock();
         try
         {
            mMutex.lock(waitTimeout);

            am = mApplicationPool.useApplicationModule(this, true);

            incrementThreadRefCount();
            mUsed = true;

            am.getSession().getEnvironment().put(Session.JBO_SESSION_COOKIE, this);

         }
         catch (InterruptedException iex)
         {
            // Be sure to release the mutex lock.  The finally block may not
            // perform this below if a lock was requested or the current thread
            // held the lock before entering this method.
            if (mMutex.hasLock())
            {
               mMutex.unlock();
            }

            // What to do?
            Diagnostic.printStackTrace(iex);
         }
         catch (RuntimeException rex)
         {
            // Be sure to release the mutex lock.  The finally block may not
            // perform this below if a lock was requested or the current thread
            // held the lock before entering this method.
            if (mMutex.hasLock())
            {
               mMutex.unlock();
            }
            throw rex;
         }
         finally
         {
            // If a lock was not requested and the current thread did not hold
            // the lock when entering this method then release the lock immediately.
            // The lock method is still used above, regardless of the value of the
            // lock parameter, in order to ensure that the current thread is
            // blocked if another thread has requested the lock.
            // The hasLock method is invoked in order to ensure that the current
            // thread has actually acquired the lock above.  The thread may not
            // have acquired the lock if it was timed out or if an exception
            // occured while getting the application module resource.
            if (!lock && !hadLock && mMutex.hasLock())
            {
               mMutex.unlock();
            }
         }

         touch();
         return am;
      }
   }

   /**
    * Internal use only.
    */
   public void timeout()
   {
      // Implemented this here instead of HttpSessionCookieImpl because access
      // to the mMutex object was necessary to synchronize the
      // isApplicationModuleReserved and releaseApplicationModule invocations.
      synchronized(mMutex)
      {
         // If timing out then release all SessionCookie locks.  This assumes
         // that timeout can never occur while a request thread is actively
         // using a SessionCookie, which I think is reasonable.
         mMutex.clearLock();

         // Clear the ThreadAccess table.
         mThreadAccess.clear();

         // Check to see if the session cookie currently has an application
         // module checked out.
         if (isApplicationModuleReserved())
         {
            releaseApplicationModule(
               SHARED 
               , STATE_UNMANAGED); 
         }
         
         mApplicationPool.removeSessionCookie(this);
         mListener = null;
      }
   }

   /**
    * The default implementation will release the session cookie lock
    * after having checked in the session application module.  At this point
    * any other waiting session threads may be notified.
    */
   public void releaseApplicationModule(boolean checkin, boolean manageState)
   {
      releaseApplicationModule(checkin, manageState, DEFAULT_WAIT_TIMEOUT);
   }

   /**
    * The default implementation will release the session cookie lock
    * after having checked in the session application module.  At this point
    * any other waiting session threads may be notified.
    */
   public void releaseApplicationModule(boolean checkin, boolean manageState, long waitTimeout)
   {
      synchronized(mMutex)
      {
         int oldPassivationId = getPassivationId();
         try
         {
            // Attempt to acquire the lock.  This is performed again here in order
            // to handle the scenario where the current thread has not already
            // acquired a lock.  The thread should not be able to continue
            // if another thread has acquired a lock.
            mMutex.lock(waitTimeout);

            mInRelease = true;
            
            // bug 2372313.  prevent release from occuring if the releasing
            // thread is not the last referencing thread.
            decrementThreadRefCount();
            if (getThreadRefCount() > 0)
            {
               return;
            }
            
            boolean isReserved = isApplicationModuleReserved();
            ApplicationModule am = null;
            if (isReserved)
            {
               am = mApplicationPool.useApplicationModule(this, true);
            }
            
            if (checkin)
            {
               mApplicationPool.releaseApplicationModule(this, manageState);
            }
            // Checks if the application module is reserved.  In order to
            // maintain symmetry with the checkin condition above this logic
            // should throw if the application module is not reserved.
            // However, in order to maintain backwards compability with the
            // beta, the logic is performing a reserved check in 9.0.2 instead
            // of throwing.
            else if (manageState && isFailoverEnabled() && isReserved)
            {
               // Assumes that mApplicationPool is an instance of
               // ApplicationPoolImpl.  The pooling framework assumes
               // that custom pool implementations will extend the default
               // ApplicationPoolImpl.  Not checking the type here for
               // potential performance consideration (release is a 
               // potentially high traffic method).
               ((ApplicationPoolImpl)mApplicationPool).doFailover(useApplicationModule(), this);
            }
            
            if (am != null) 
            {
               am.getSession().getEnvironment().remove(Session.JBO_SESSION_COOKIE);
            }

         }
         catch (InterruptedException iex)
         {
            // What to do?
            Diagnostic.printStackTrace(iex);
         }
         finally
         {
            mInRelease = false;

            // Notify other waiting threads that the session has been unlocked.
            // Check if the current thread has the lock before unlocking
            // in order to prevent the current thread from unlocking a lock
            // that it does not own.  This may occur if the session timed out
            // while acquiring the lock above.
            if (mMutex.hasLock())
            {
               mMutex.unlock();
            }
         }

         // If the old passivation id is not equal to the current passivation
         // id then the release must have updated the passivation id.  Fire
         // the cookie update event.  This is performed here rather than in 
         // setPassivationId b/c cookie serialization requires that the cookie
         // not reference a reserved application module and that all cookie
         // locks have been released.
         if (oldPassivationId != getPassivationId())
         {
            fireCookieUpdated();
         }

         touch();
      }
   }

   public void writeValue(Object sink)
   {
   }

   public String readValue(Object source)
   {
      return null;
   }

   public Object getSyncLock()
   {
      // The object which is used to synchronize the session cookie is
      // exposed here because the pool must often synchronize with the session
      // cookie when it is bootstrapping cookies into the pool's internal
      // cookie table.  Applications should never use this method.
      return mMutex;
   }

   public boolean isActivationRequired()
   {
      synchronized(mMutex)
      {
         return mActivationRequired;
      }
   }

   public boolean isActivationRequired(ApplicationModule context)
   {
      synchronized(mMutex)
      {
         return isActivationRequired();
      }
   }

   public void setActivationRequired(boolean activationRequired)
   {
      synchronized(mMutex)
      {
         mActivationRequired = activationRequired;
      }
   }

   public int getPassivationId()
   {
      synchronized(mMutex)
      {
         return mPassivationId;
      }
   }

   public void setPassivationId(int passivationId)
   {
      synchronized(mMutex)
      {
         mPassivationId = passivationId;
         mValue = null;

         touch();
      }
   }

   public void reservePassivationId()
   {
      reservePassivationId(null);
   }

   public void reservePassivationId(Object sink)
   {
      // Only reserve a passivation id for the session application if one
      // has not already been reserved and if the session application 
      // has an application module reserved.
      synchronized(mMutex)
      {
         if ((getReservedPassivationId() == SessionCookie.NULL_PASSIVATION_ID)
            && isFailoverEnabled()
            && isApplicationModuleReserved())
         {
            int reservedPassivationId = useApplicationModule().reservePassivationId();
            setReservedPassivationId(reservedPassivationId);

            if (sink != null)
            {
               writeValue(sink);
            }
         }
         
      }
   }

   public int getReservedPassivationId()
   {
      synchronized(mMutex)
      {
         return mReservedPassivationId;
      }
   }

   public void setReservedPassivationId(int reservedPassivationId)
   {
      synchronized(mMutex)
      {
         mReservedPassivationId = reservedPassivationId;
         mValue = null;
      }
   }

   public Hashtable getUserData()
   {
      return mUserData;
   }

   public String getSSOUserName()
   {
       return (String)mEnvironment.get(PropertyMetadata.SECURITY_PRINCIPAL.pName);
   }
    
    
   public String getSSOSubscriber()
   {
       return mSSOSubscriber;
   }
    
   public void resetState()
   {
      synchronized(mMutex)
      {
         if (isApplicationModuleReserved())
         {
            ApplicationPoolException apex = new ApplicationPoolException(
               AMPoolMessageBundle.class
               , AMPoolMessageBundle.EXC_AMPOOL_CANNOT_RESET_STATE
               , new Object[] {getSessionId(), getApplicationId()});
            throw apex;
         }

         ((ApplicationPoolImpl)mApplicationPool).resetApplicationModule(this, true);
         resetStateInternal();
      }
   }
 
   public void resetStateInternal()
   {
      synchronized(mMutex)
      {
         // Reset all mutable data
         mPassivationId = SessionCookie.NULL_PASSIVATION_ID;
         mReservedPassivationId = SessionCookie.NULL_PASSIVATION_ID;
         mActivationRequired = false;
         mValue = null;
         mThreadAccess.clear();

         touch();
      }
   }

   public void copyInto(SessionCookie cookie)
   {
      synchronized(mMutex)
      {
         cookie.setPassivationId(mPassivationId);
         cookie.setReservedPassivationId(mReservedPassivationId);
         cookie.setActivationRequired(mActivationRequired);

         try
         {
            cookie.setEnvironment(mEnvironment);
         }
         catch(ApplicationPoolException apex)
         {
            // Ignore the exception.  The target cookie environment
            // must have already been initialized.
         }
         
         touch();
      }
   }

   public boolean isFailoverEnabled()
   {
      boolean rtn = false;

      String value = (String)mEnvironment.get(PropertyMetadata.ENV_DO_FAILOVER.pName);
      if (value != null)
      {
         rtn = Boolean.valueOf(value).booleanValue();
      }

      return rtn;
   }

   public boolean isConnectionPoolingEnabled()
   {
      return getProperty(
         PropertyMetadata.ENV_DO_CONNECTION_POOLING.pName
         , mEnvironment
         , Boolean.valueOf(PropertyMetadata.ENV_DO_CONNECTION_POOLING.pDefault)
            .booleanValue());
   }

   public boolean isResetNonTransactionalState()
   {
      return getProperty(
         PropertyMetadata.ENV_AMPOOL_RESET_NON_TRANSACTIONAL_STATE.pName
         , mEnvironment
         , Boolean.valueOf(PropertyMetadata.ENV_AMPOOL_RESET_NON_TRANSACTIONAL_STATE.pDefault)
            .booleanValue());
   }

   protected boolean getProperty(String name, Hashtable environment, boolean defaultValue)
   {
      boolean rtn = true;

      String value = (String)environment.get(name);
      if (value != null)
      {
         rtn = Boolean.valueOf(value).booleanValue();
      }
      else
      {
         rtn = JboEnvUtil.getPropertyAsBoolean(name, defaultValue);
      }

      return rtn;
   }

   public void setSessionCookieListener(SessionCookieListener listener)
   {
      synchronized(mMutex)
      {
         mListener = listener;
      }
   }

   private void fireCookieUpdated()
   {
      if (mListener != null)
      {
         mListener.cookieUpdated();
      }
   }

   public static String parseSessionId(String cookieValue)
   {
      String sessionId = cookieValue;

      int sepIndex = cookieValue.indexOf(COOKIE_SEPARATOR);
      if (sepIndex >= 0)
      {
         sessionId = cookieValue.substring(0, sepIndex);
      }

      return sessionId;
   }

   public static int parsePassivationId(String cookieValue)
   {
      int passivationId = SessionCookie.NULL_PASSIVATION_ID;

      int sepIndex = cookieValue.indexOf(COOKIE_SEPARATOR);
      if (sepIndex >= 0)
      {
         passivationId = new Integer(cookieValue.substring(sepIndex + 1)).intValue();
      }

      return passivationId;
   }

   private void writeObject(ObjectOutputStream out) throws IOException
   {
      // What happens if the session has an application module checked out?
      // If the serialization is occuring to load balance
      // a request vs. occuring to failover state to other cluster nodes then
      // a memory leak may occur because the application module is never
      // released to the pool for recycling.  Consider the following scenario:
      // A session which is referencing a checked out application module has
      // a request that is load balanced to another cluster node (non-sticky
      // load balancing).  The remainder of the session requests are forwarded
      // to the new destination VM.  Consequently, the session cookie in
      // the source VM is never signalled to release the application module to
      // the pool and a memory leak is created.
      //
      // Throw a not serializable exception to prevent this scenario.
      // Ultimately, this should be a development time exception.  A web
      // application developer may prevent this scenario by not using reserved
      // release mode.  A web server implementation may prevent this scenario
      // by implementing sticky requests.  The other issue is that the web
      // server may be serializing in order to provide failover support
      // between cluster nodes (uh-oh, the serialization will fail, will the
      // web server properly handle the exception?).
      //
      // Invoke useApplicationModule to test if the session cookie is
      // referencing a checked out application module.  Invoking
      // useApplicationModule with checkout equal to false will
      // return an application module if the session cookie has an application
      // module checked out.  Otherwise it will return null.
      //
      // JRS Too conservative.  This may prevent clustering from working
      // properly in asynchronous web containers which may failover session
      // state while requests are still active.
/*
      if (isApplicationModuleReserved())
      {
         ApplicationPoolException apex = new ApplicationPoolException(
            AMPoolMessageBundle.class
            , AMPoolMessageBundle.EXC_ILLEGAL_COOKIE_SERIALIZATION
            , new Object[] {getSessionId(), getApplicationId()});

         // Print the exception stack trace.  OC4J will eat this exception.
         apex.printStackTrace();
         throw apex;
      }

      // Validate that no cookie locks are currently held.  If so, throw a
      // not serializable exception.  This scenario may occur if the web
      // application has not properly released the cookie lock at the end
      // of every request.  An application developer may correct this scenario
      // by ensuring that cookie locks are released at the end of every request.
      if (mMutex.isLocked())
      {
         ApplicationPoolException apex = new ApplicationPoolException(
            AMPoolMessageBundle.class
            , AMPoolMessageBundle.EXC_ILLEGAL_COOKIE_SERIALIZATION
            , new Object[] {getSessionId(), getApplicationId()});

         // Print the exception stack trace.  OC4J will eat this exception.
         apex.printStackTrace();
         throw apex;
      }
*/

      out.defaultWriteObject();
   }

   private void readObject(ObjectInputStream in)
      throws IOException, ClassNotFoundException
   {
      try
      {
         in.defaultReadObject();
      }
      catch (java.io.NotActiveException naex)
      {
         // Only thrown if defaultReadObject is not invoked from readObject
      }

      // Re-initialize the transient session cookie state
      initialize();

      // What if the session cookie pool already exists on this VM?  We better
      // check and add the session cookie to the different pool instance if
      // necessary.   If the application pool object references are different
      // then:
      ApplicationPool pool = (ApplicationPool)PoolMgr.getInstance().getResourcePool(mApplicationPool.getName());
      if (pool != mApplicationPool)
      {
         // Update mApplicationPool with the pool instance resident in this
         // VM.  This may be necessary if the application pool has already
         // been instantiated in the target VM.
         mApplicationPool = pool;
      }

      // Add the cookie to the application pool instance.  This is
      // necessary to support the following scenario:
      //
      // The application pool instance has already been instantiated in
      // this VM.  However, this is the first time that the session has
      // accessed this VM (through web server load balancing).  Consequently,
      // it is possible that the session cookie has not been registered with
      // the application pool instance.  addSessionCookie will register
      // a session cookie with the target application pool.  If the cookie
      // is already registered (determined by cookie equality rules) then
      // add updates the existing instance with this instance.
      //
      // Please note that the last updated cookie is treated as the source
      // of truth.
      mApplicationPool.addSessionCookie(this);

   }

   private void touch()
   {
      mLastUpdate.setTime(System.currentTimeMillis());
   }

   public final int getThreadRefCount()
   {
      synchronized(mMutex)
      {
         return mThreadAccess.size();
      }
   }

   private void decrementThreadRefCount()
   {
      synchronized(mMutex)
      {
         if (!mIsSingleThreaded)
         {
            mThreadAccess.remove(Thread.currentThread());
         }
         else
         {
            // just in case
            mThreadAccess.clear();
         }
      }
   }

   private void incrementThreadRefCount()
   {
      synchronized(mMutex)
      {
         // don't increment the thread refcount if we are invoking
         // during a release cycle.  the refcount will never be
         // decremented!
         if (!mIsSingleThreaded && !mInRelease)
         {
            mThreadAccess.put(Thread.currentThread(), null);
         }
      }
   }

   /**
    * Advanced Use Only:
    * <p>
    * Setting this property will force the cookie to maintain a refcount
    * for all threads that have invoked useApplicationModule after the
    * property has been set.  Only after the last thread that has used
    * the application module releases the application will the application
    * module actually be returned to the pool.
    * <p>
    * The default value of this property for
    * {@link oracle.jbo.common.ampool.SessionCookieImpl} instances is true.
    * <p>
    * The default value of this property for
    * {@link oracle.jbo.http.HttpSessionCookieImpl} instances is false.
    */
   public void setSingleThreaded(boolean isSingleThreaded)
   {
      mIsSingleThreaded = isSingleThreaded;
   }

   class SessionCookieLock extends Object
   {
      private Thread  mCurrentThread  = null;
      private int     mWaiters        = 0;

      void lock(long waitTimeout) throws InterruptedException
      {
         // Only attempt to perform the lock if the current thread holding
         // the lock is not equal to the thread requesting the lock.  This is
         // important because a given thread may request an application module
         // resource many times, but will only release the application module
         // resource once.
         if (!hasLock())
         {
            // already synchronized
            mWaiters++;

            long waitTime = waitTimeout;

            // Wait for the specified timeout period
            while ((mCurrentThread != null) // handle a false wakeup
               && ((waitTimeout < 0) || ((waitTimeout >= 0) && (waitTime > 0)))) // these two conditions are paired together
            {
               StringBuffer sb = new StringBuffer();
               sb.append("WARNING:  The thread, ");
               sb.append(Thread.currentThread().getName());
               sb.append(", is waiting in oracle.jbo.common.ampool.SessionCookieImpl.useApplicationModule(boolean) for a shared application module resource");
               Diagnostic.println(sb.toString());

               long waitIncrement = WAIT_TIME_INCREMENT;
               if (waitTimeout >= 0)
               {
                  if (waitTime < WAIT_TIME_INCREMENT)
                  {
                     waitIncrement = waitTime;
                  }

                  waitTime = waitTime - WAIT_TIME_INCREMENT;
               }

               wait(waitIncrement);
            }

            mWaiters--;

            // If the current thread is still not null then we must have timed
            // out above
            if (mCurrentThread == null)
            {
               mCurrentThread = Thread.currentThread();
            }
            else
            {
               // We must have timed out.  Throw a time out exception
               throw new ApplicationPoolException(
                  AMPoolMessageBundle.class
                  , AMPoolMessageBundle.EXC_AMPOOL_SESSION_COOKIE_TIMEOUT
                  , new Object[] {Thread.currentThread().getName()});
            }
         }
      }

      void unlock()
      {
         // Prevent a thread that does not currently hold a lock from
         // unlocking the resource
         if (hasLock())
         {
            mCurrentThread = null;
            notifyAll();
         }
         // Throw an exception.  An thread should never release the session
         // cookie without first acquiring a lock.
         else
         {
            throw new ApplicationPoolException(
               AMPoolMessageBundle.class
               , AMPoolMessageBundle.EXC_AMPOOL_INVALID_LOCK_RELEASE
               , new Object[] {Thread.currentThread().getName()});
         }
      }

      void clearLock()
      {
         if (mWaiters > 0)
         {
            // should not happen.
            throw new RuntimeException(
               "SessionCookie timeout occured with threads waiting");
         }

         mCurrentThread = null;
      }

      boolean isLocked()
      {
         // Test if any thread currently holds the session cookie lock.
         return (mCurrentThread != null);
      }

      boolean hasLock()
      {
         // Test if the current thread holds the session cookie lock.
         return Thread.currentThread().equals(mCurrentThread);
      }
   }
}
