/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, 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.tc5.session;

import org.apache.catalina.Manager;
import org.apache.catalina.Context;
import org.apache.catalina.Session;

import java.io.IOException;
import java.io.Serializable;
import java.util.Enumeration;
import java.util.Map;

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

import javax.servlet.http.*;

/**
 * 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.1.2.17 $
 */
abstract class ClusteredSession
   extends StandardSession
{
   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;
   }

   /**
    * 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)
         {
             log(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)
         {
             log(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)
            {
               ;
            }
            log(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 the {@link StandardSession#isValid() superclass method}
    * to call {@ #isValid(boolean) isValid(true)}.
    */
   public boolean isValid()
   {
      return isValid(true);
   }
   
   /**
    * Returns whether the current session is still valid, but
    * only calls {@link #expire(boolean)} for timed-out sessions
    * if <code>expireIfInvalid</code> is <code>true</code>.
    * 
    * @param expireIfInvalid  <code>true</code> if sessions that have
    *                         been timed out should be expired
    */
   public boolean isValid(boolean expireIfInvalid)
   {
      if (this.expiring)
      {
         return true;
      }

      if (!this.isValid)
      {
         return false;
      }

      if (accessCount > 0)
      {
         return true;
      }

      if (maxInactiveInterval >= 0)
      {
         long timeNow = System.currentTimeMillis();
         int timeIdle = (int) ((timeNow - thisAccessedTime) / 1000L);
         if (timeIdle >= maxInactiveInterval)
         {
            if (expireIfInvalid)
               expire(true);
            else
               return false;
         }
      }

      return (this.isValid);
      
   }

   /**
    * 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 is locally? " +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)
                  {
                     ;
                  }
                  log(sm.getString("standardSession.sessionEvent"), t);
               }
            }
         }
         accessCount = 0;
         setValid(false);

         // 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
         expiring = false;
      }
   }

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

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

   // ------------------------------------------------ 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)
            {
               ;
            }
            log(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();
   }

}
