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

import org.jboss.metadata.WebMetaData;

import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.Serializable;
import java.security.Principal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

/**
 * Implementation of a clustered session for the JBossCacheManager. The replication granularity
 * level is session based; that is, we replicate per whole session object.
 * We use JBossCache for our internal replicated data store.
 * The internal structure is like in JBossCache:
 * <pre>
 * /JSESSION
 *    /web_app_path    (path + session id is unique)
 *        /id    Map(id, session)
 *                  (VERSION_KEY, version)  // Used for version tracking. version is an Integer.
 * </pre>
 * <p/>
 * Note that the isolation level of the cache dictates the
 * concurrency behavior.</p>
 *
 * @author Ben Wang
 * @version $Revision: 1.3.2.7 $
 */
class SessionBasedClusteredSession
   extends ClusteredSession implements Serializable
{
   static final long serialVersionUID = 3200976125245487256L;
   /**
    * Descriptive information describing this Session implementation.
    */
   protected static final String info = "SessionBasedClusteredSession/1.0";

   private transient boolean isSessionModifiedSinceLastSave;
   private transient JBossCacheService proxy_;

   public SessionBasedClusteredSession(AbstractJBossManager manager)
   {
      super(manager);
      initAfterLoad(manager);
   }

   /**
    * Initialize fields marked as transient after loading this session
    * from the distributed store
    *
    * @param manager the manager for this session
    */
   public void initAfterLoad(AbstractJBossManager manager)
   {
      // Use proxy to determine if this is first time session retrieval.
      if (this.proxy_ == null)
      {
         if (log.isDebugEnabled())
         {
            log.debug("initAfterLoad(): initialize the transient variables ...");
         }
         setManager(manager);
         listeners = new ArrayList();
         notes = new HashMap();
         support = new PropertyChangeSupport(this);
         expiring = false;
         isSessionModifiedSinceLastSave = false;

         proxy_ = ((JBossCacheManager) manager).getCacheService();

         // still null???
         if (proxy_ == null)
         {
            throw new RuntimeException("SessionBasedClusteredSession: Cache service is null.");
         }

         // Notify all attributes of type HttpSessionActivationListener (SRV 7.7.2)
         this.activate();
      }

      // cache invalidate purpose
      isOutdated = false;

   }

   // ----------------------------------------------------- Session Properties
   /**
    * Set the creation time for this session.  This method is called by the
    * Manager when an existing Session instance is reused.
    *
    * @param time The new creation time
    */
   public void setCreationTime(long time)
   {
      super.setCreationTime(time);
      sessionIsDirty();
   }

   /**
    * Set the authenticated Principal that is associated with this Session.
    * This provides an <code>Authenticator</code> with a means to cache a
    * previously authenticated Principal, and avoid potentially expensive
    * <code>Realm.authenticate()</code> calls on every request.
    *
    * @param principal The new Principal, or <code>null</code> if none
    */
   public void setPrincipal(Principal principal)
   {

      Principal oldPrincipal = this.principal;
      this.principal = principal;
      support.firePropertyChange("principal", oldPrincipal, this.principal);

      if ((oldPrincipal != null && !oldPrincipal.equals(principal)) ||
         (oldPrincipal == null && principal != null))
         sessionIsDirty();

   }

   // ------------------------------------------------- Session Public Methods
   /**
    * Return a string representation of this object.
    */
   public String toString()
   {
      StringBuffer sb = new StringBuffer();
      sb.append("SessionBasedClusteredSession[");
      sb.append(id);
      sb.append("]");
      return (sb.toString());

   }

   /**
    * Start to process my local attribute changes to the replication layer.
    */
   public synchronized void processSessionRepl()
   {
      if (!isSessionDirty())
      {
         if (log.isDebugEnabled())
         {
            log.debug("processSessionRepl(): session is not dirty. No need to replicate.");
         }
         return;
      }
      // Replicate this first. Note this will be lightweight since many of the attributes are transient.
      // And also without attributes
      if (log.isDebugEnabled())
      {
         log.debug("processSessionRepl(): session is dirty. Will increment version from: " +
                 getVersion() + " and replicate.");
      }
      this.incrementVersion();   // cache invalidation versioning
      proxy_.putSession(id, this);

      isSessionModifiedSinceLastSave = false;
   }

   public void removeMyself()
   {
      proxy_.removeSession(id);
   }

   public void removeMyselfLocal()
   {
      proxy_.removeSessionLocal(id);
   }

   // ----------------------------------------------HttpSession Public Methods

   /**
    * Update the accessed time information for this session.  This method
    * should be called by the context when a request comes in for a particular
    * session, even if the application does not reference it.
    */
   public void access()
   {
      super.access();
      // If we do not use the local cache the session is dirty
      // after every access.
      if (invalidationPolicy == WebMetaData.SESSION_INVALIDATE_ACCESS)
      {
         this.sessionIsDirty();
      }
   }

   protected Object getJBossInternalAttribute(String name)
   {

      Object result = attributes.get(name);

      if (result != null)
      {
         if (invalidationPolicy == WebMetaData.SESSION_INVALIDATE_SET_AND_GET)
         {
            sessionIsDirty();
         }
         else if (invalidationPolicy == WebMetaData.SESSION_INVALIDATE_SET_AND_NON_PRIMITIVE_GET)
         {
            if (!(result instanceof String ||
               result instanceof Integer ||
               result instanceof Long ||
               result instanceof Byte ||
               result instanceof Short ||
               result instanceof Float ||
               result instanceof Double ||
               result instanceof Character ||
               result instanceof Boolean)
            )
            {
               sessionIsDirty();
            }
         }
      }

      return result;

   }

   protected Object removeJBossInternalAttribute(String name)
   {
      sessionIsDirty();
      return attributes.remove(name);
   }

   protected Map getJBossInternalAttributes()
   {
      // TODO Add it to the doc
      // No matter what, get all attributes is considered dirty.
      sessionIsDirty();
      return attributes;
   }

   protected Object setJBossInternalAttribute(String name, Object value)
   {
      sessionIsDirty();
      return attributes.put(name, value);
   }

   protected void sessionIsDirty()
   {
      // Session is dirty
      isSessionModifiedSinceLastSave = true;
   }

   public boolean isSessionDirty()
   {
      return isSessionModifiedSinceLastSave;
   }

   /**
    * Overrides the superclass version by additionally reading the 
    * attributes map.
    * 
    * @see org.jboss.web.tomcat.tc5.session.ClusteredSession#readExternal(java.io.ObjectInput)
    */
   public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
   {
      // Let superclass read in everything but the attribute map
      super.readExternal(in);
      
      attributes = (HashMap) in.readObject();      
   }

   /**
    * Overrides the superclass version by appending the attributes map.
    * 
    * @see org.jboss.web.tomcat.tc5.session.ClusteredSession#writeExternal(java.io.ObjectOutput)
    */
   public void writeExternal(ObjectOutput out) throws IOException
   {
      synchronized (attributes)
      {
         // Let superclass write out everything but the attribute map
         super.writeExternal(out);
      
         out.writeObject(attributes);
      }
   }
   
   

}
