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

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.Serializable;
import java.util.Enumeration;
import java.util.Map;

import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

import org.apache.catalina.Context;
import org.apache.catalina.Manager;
import org.apache.catalina.Session;
import org.apache.catalina.session.StandardSession;
import org.apache.catalina.util.Enumerator;
import org.apache.catalina.util.StringManager;
import org.jboss.logging.Logger;

/**
 * Abstract base class for session clustering based on StandardSession. Different session
 * replication strategy can be implemented such as session- or attribute-based ones.
 *
 * @author Ben Wang
 * @version $Revision: 1.5.2.10.6.1 $
 */
abstract class ClusteredSession
   extends StandardSession 
   implements Externalizable
{
   private static final long serialVersionUID = -758573655613558722L;
   protected static Logger log = Logger.getLogger(ClusteredSession.class);

   // ----------------------------------------------------- Instance Variables
   /**
    * Descriptive information describing this Session implementation.
    */
   protected static final String info = "ClusteredSession/1.0";

   protected int invalidationPolicy;

   /**
    * If true, it means the in-memory session data is not in sync with that in the data store.
    */
   protected transient boolean isOutdated;

   /**
    * Version number to track cache invalidation. If any new version number is greater than this
    * one, it means the data it holds is newer than this one.
    */
   protected int version;

   /**
    * The string manager for this package.
    */
   protected static StringManager sm =
      StringManager.getManager("org.jboss.web.tomcat.session");

   public ClusteredSession(AbstractJBossManager manager)
   {
      super(manager);
      invalidationPolicy = ((AbstractJBossManager) this.manager).getInvalidateSessionPolicy();
      isOutdated = false;
      version = 0;
   }

   /**
    * Check to see if the session data is still valid. Outdated here means that the in-memory data is
    * not in sync with one in the data store.
    * @return
    */
   public boolean isOutdated()
   {
      return isOutdated;
   }

   public void setIsOutdated(boolean outdated)
   {
      isOutdated = outdated;
   }

   /**
    * This is called specifically for failover case using mod_jk where the new session has this node name in
    * there. As a result, it is safe to just replace the id since the backend store is using the "real" id
    * without the node name.
    * @param id
    */
   public void resetIdWithRouteInfo(String id)
   {
      this.id = id;
   }

   /**
    * Check to see if the input version number is greater than I am. If it is, it means we will
    * need to invalidate the in-memory cache.
    * @param version
    * @return
    */
   public boolean isNewData(int version)
   {
      return (this.version < version) ? true: false;
   }

   public int getVersion()
   {
      return version;
   }

   /**
    * There are couple ways to generate this version number. But we will stick with the simple one
    * for now.
    * @return
    */
   public int incrementVersion()
   {
      return version++;
   }

   /**
    * This is called after loading a session to initialize the transient values.
    *
    * @param manager
    */
   public abstract void initAfterLoad(AbstractJBossManager manager);

   /**
    * Set the Manager within which this Session is valid.
    *
    * @param manager The new Manager
    */
   public void setManager(Manager manager)
   {

      super.setManager(manager);

      this.manager = manager;
   }

   /**
    * Propogate session to the internal store.
    */
   public abstract void processSessionRepl();

   /**
    * Remove myself from the internal store.
    */
   public abstract void removeMyself();

   /**
    * Remove myself from the <t>local</t> internal store.
    */
   public abstract void removeMyselfLocal();

   //
   // ------------------------------------------------- Session Public Methods

   public Object getAttribute(String name)
   {

      if (!isValid())
         throw new IllegalStateException
            (sm.getString("clusteredSession.getAttribute.ise"));

      return getAttributeInternal(name);
   }

   public Enumeration getAttributeNames()
   {
      if (!isValid())
         throw new IllegalStateException
            (sm.getString("clusteredSession.getAttributeNames.ise"));

      return (new Enumerator(getAttributesInternal().keySet(), true));
   }

   public void setAttribute(String name, Object value)
   {
      // Name cannot be null
      if (name == null)
         throw new IllegalArgumentException
            (sm.getString("clusteredSession.setAttribute.namenull"));

      // Null value is the same as removeAttribute()
      if (value == null)
      {
         removeAttribute(name);
         return;
      }

      // Validate our current state
      if (!isValid())
         throw new IllegalStateException
            (sm.getString("clusteredSession.setAttribute.ise"));
      if ((manager != null) && manager.getDistributable() &&
         !(value instanceof Serializable))
         throw new IllegalArgumentException
            (sm.getString("clusteredSession.setAttribute.iae"));

      // Construct an event with the new value
      HttpSessionBindingEvent event = null;

      // Call the valueBound() method if necessary
      if (value instanceof HttpSessionBindingListener)
      {
         event = new HttpSessionBindingEvent(getSession(), name, value);
         try
         {
            ((HttpSessionBindingListener) value).valueBound(event);
         }
         catch (Throwable t)
         {
             manager.getContainer().getLogger().error(sm.getString("standardSession.bindingEvent"), t);
         }
      }

      // Replace or add this attribute
      Object unbound = setInternalAttribute(name, value);

      // Call the valueUnbound() method if necessary
      if ((unbound != null) && (unbound != value) &&
         (unbound instanceof HttpSessionBindingListener))
      {
         try
         {
            ((HttpSessionBindingListener) unbound).valueUnbound
               (new HttpSessionBindingEvent(getSession(), name));
         }
         catch (Throwable t)
         {
             manager.getContainer().getLogger().error(sm.getString("standardSession.bindingEvent"), t);
         }
      }

      // Notify interested application event listeners
      Context context = (Context) manager.getContainer();
      Object listeners[] = context.getApplicationEventListeners();
      if (listeners == null)
         return;
      for (int i = 0; i < listeners.length; i++)
      {
         if (!(listeners[i] instanceof HttpSessionAttributeListener))
            continue;
         HttpSessionAttributeListener listener =
            (HttpSessionAttributeListener) listeners[i];
         try
         {
            if (unbound != null)
            {
               fireContainerEvent(context,
                  "beforeSessionAttributeReplaced",
                  listener);
               if (event == null)
               {
                  event = new HttpSessionBindingEvent
                     (getSession(), name, unbound);
               }
               listener.attributeReplaced(event);
               fireContainerEvent(context,
                  "afterSessionAttributeReplaced",
                  listener);
            }
            else
            {
               fireContainerEvent(context,
                  "beforeSessionAttributeAdded",
                  listener);
               if (event == null)
               {
                  event = new HttpSessionBindingEvent
                     (getSession(), name, value);
               }
               listener.attributeAdded(event);
               fireContainerEvent(context,
                  "afterSessionAttributeAdded",
                  listener);
            }
         }
         catch (Throwable t)
         {
            try
            {
               if (unbound != null)
               {
                  fireContainerEvent(context,
                     "afterSessionAttributeReplaced",
                     listener);
               }
               else
               {
                  fireContainerEvent(context,
                     "afterSessionAttributeAdded",
                     listener);
               }
            }
            catch (Exception e)
            {
               ;
            }
            manager.getContainer().getLogger().error(sm.getString("standardSession.attributeEvent"), t);
         }
      }

   }


   /**
    * Invalidates this session and unbinds any objects bound to it.
    * Override here to remove across the cluster instead of just expiring.
    *
    * @exception IllegalStateException if this method is called on
    *  an invalidated session
    */
   public void invalidate()
   {
     if (!isValid())
        throw new IllegalStateException
           (sm.getString("clusteredSession.invalidate.ise"));

     // Cause this session to expire globally
     boolean notify = true;
     boolean expireLocally = false;
     expire(notify, expireLocally);
   }

  /**
   * Override here to reverse the order of manager session removal and
   * attribute removal.
   *
   * @param notify
   */
  public void expire(boolean notify)
  {
     boolean expireLocally = true;
     expire(notify, expireLocally);
  }

  protected void expire(boolean notify, boolean expireLocally)
  {
      if (log.isDebugEnabled())
      {
         log.debug("The session has expired with id: " + id + " is it local? " +expireLocally);
      }
      // Mark this session as "being expired" if needed
      if (expiring)
         return;

      synchronized (this)
      {

         if (manager == null)
            return;

         expiring = true;

         // Notify interested application event listeners
         // FIXME - Assumes we call listeners in reverse order
         Context context = (Context) manager.getContainer();
         Object listeners[] = context.getApplicationLifecycleListeners();
         if (notify && (listeners != null))
         {
            HttpSessionEvent event =
               new HttpSessionEvent(getSession());
            for (int i = 0; i < listeners.length; i++)
            {
               int j = (listeners.length - 1) - i;
               if (!(listeners[j] instanceof HttpSessionListener))
                  continue;
               HttpSessionListener listener =
                  (HttpSessionListener) listeners[j];
               try
               {
                  fireContainerEvent(context,
                     "beforeSessionDestroyed",
                     listener);
                  listener.sessionDestroyed(event);
                  fireContainerEvent(context,
                     "afterSessionDestroyed",
                     listener);
               }
               catch (Throwable t)
               {
                  try
                  {
                     fireContainerEvent(context,
                        "afterSessionDestroyed",
                        listener);
                  }
                  catch (Exception e)
                  {
                     ;
                  }
                  manager.getContainer().getLogger().error(sm.getString("standardSession.sessionEvent"), t);
               }
            }
         }

         // Notify interested session event listeners
         if (notify)
         {
            fireSessionEvent(Session.SESSION_DESTROYED_EVENT, null);
         }

         // Bug fix JBAS-1360
         // Unbind any objects associated with this session
         String keys[] = keys();
         for (int i = 0; i < keys.length; i++)
             removeAttributeInternal(keys[i], notify);

         // Remove this session from our manager's active sessions
         if (manager != null)
         {
            if(expireLocally)
            {
                ((AbstractJBossManager) manager).removeLocal(this);
            } else
            {
               ((AbstractJBossManager) manager).remove(this);
            }
         }

         // We have completed expire of this session
         accessCount = 0;
         setValid(false);
         expiring = false;

      }
   }

   public void passivate()
   {
      // TODO. We don't do this now.
   }

   public void activate()
   {
      // TODO. We don't do this now.
   }

   // ---------------------------------------------------------  Externalizable

   /**
    * Reads all non-transient state from the ObjectOutput <i>except
    * the attribute map</i>.  Subclasses that wish the attribute map
    * to be read should override this method and 
    * {@link #writeExternal(ObjectOutput) writeExternal()}.
    * 
    * @see java.io.Externalizable#readExternal(java.io.ObjectInput)
    */
   public void readExternal(ObjectInput in) 
      throws IOException, ClassNotFoundException
   {
      //    From StandardSession
      id                  = in.readUTF();
      creationTime        = in.readLong();
      lastAccessedTime    = in.readLong();
      maxInactiveInterval = in.readInt();
      isNew               = in.readBoolean();
      isValid             = in.readBoolean();
      thisAccessedTime    = in.readLong();
      
      // From ClusteredSession
      invalidationPolicy  = in.readInt();
      version             = in.readInt();
   }

   
   /**
    * Writes all non-transient state to the ObjectOutput <i>except
    * the attribute map</i>.  Subclasses that wish the attribute map
    * to be written should override this method and append it.
    *  
    * @see java.io.Externalizable#writeExternal(java.io.ObjectOutput)
    */
   public void writeExternal(ObjectOutput out) 
      throws IOException
   {
      synchronized (attributes)
      {
         // From StandardSession
         out.writeUTF(id);
         out.writeLong(creationTime);
         out.writeLong(lastAccessedTime);
         out.writeInt(maxInactiveInterval);
         out.writeBoolean(isNew);
         out.writeBoolean(isValid);
         out.writeLong(thisAccessedTime);
         
         // From ClusteredSession
         out.writeInt(invalidationPolicy);
         out.writeInt(version);
      }
   }
   
   

   // ------------------------------------------------ Internal protected method override

   /**
    * Method inherited from Tomcat. Return zero-length based string if not found.
    */
   protected String[] keys()
   {
      return ((String[]) getAttributesInternal().keySet().toArray(EMPTY_ARRAY));
   }

   protected void removeAttributeInternal(String name, boolean notify)
   {

      // Remove this attribute from our collection
      Object value = removeJBossInternalAttribute(name);

      // Do we need to do valueUnbound() and attributeRemoved() notification?
      if (!notify || (value == null))
      {
         return;
      }

      // Call the valueUnbound() method if necessary
      HttpSessionBindingEvent event = null;
      if (value instanceof HttpSessionBindingListener)
      {
         event = new HttpSessionBindingEvent(getSession(), name, value);
         ((HttpSessionBindingListener) value).valueUnbound(event);
      }

      // Notify interested application event listeners
      Context context = (Context) manager.getContainer();
      Object listeners[] = context.getApplicationEventListeners();
      if (listeners == null)
         return;
      for (int i = 0; i < listeners.length; i++)
      {
         if (!(listeners[i] instanceof HttpSessionAttributeListener))
            continue;
         HttpSessionAttributeListener listener =
            (HttpSessionAttributeListener) listeners[i];
         try
         {
            fireContainerEvent(context,
               "beforeSessionAttributeRemoved",
               listener);
            if (event == null)
            {
               event = new HttpSessionBindingEvent
                  (getSession(), name, value);
            }
            listener.attributeRemoved(event);
            fireContainerEvent(context,
               "afterSessionAttributeRemoved",
               listener);
         }
         catch (Throwable t)
         {
            try
            {
               fireContainerEvent(context,
                  "afterSessionAttributeRemoved",
                  listener);
            }
            catch (Exception e)
            {
               ;
            }
            manager.getContainer().getLogger().error(sm.getString("standardSession.attributeEvent"), t);
         }
      }

   }

   protected Object getAttributeInternal(String name)
   {
      return getJBossInternalAttribute(name);
   }

   protected Map getAttributesInternal()
   {
      return getJBossInternalAttributes();
   }

   protected Object setInternalAttribute(String name, Object value)
   {
      return setJBossInternalAttribute(name, value);
   }

   // ------------------------------------------------ JBoss internal abstract method
   protected abstract Object getJBossInternalAttribute(String name);

   protected abstract Object removeJBossInternalAttribute(String name);

   protected abstract Map getJBossInternalAttributes();

   protected abstract Object setJBossInternalAttribute(String name, Object value);

   // ------------------------------------------------ Session Package Methods
   protected void writeObject(java.io.ObjectOutputStream out)
      throws IOException
   {
      synchronized (attributes)
      {
         out.defaultWriteObject();
      }
   }

   protected void readObject(java.io.ObjectInputStream in)
      throws IOException, ClassNotFoundException
   {
      in.defaultReadObject();
   }

}
