/*
 * 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.Serializable;
import java.security.Principal;
import java.util.*;

/**
 * Implementation of a clustered session for the JBossCacheManager. The replication granularity
 * level is attribute based; that is, we replicate only the dirty attributes.
 * We use JBossCache for our internal, deplicated 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.
 *           /ATTRIBUTE    Map(attr_key, value)
 * </pre>
 * <p/>
 * Note that the isolation level of the cache dictates the
 * concurrency behavior. Also note that session and its associated attribtues are stored in different nodes.
 * This will be ok since cache will take care of concurrency. When replicating, we will need to replicate both
 * session and its attributes.</p>
 *
 * @author Ben Wang
 * @version $Revision: 1.3.2.5 $
 */
class AttributeBasedClusteredSession
   extends ClusteredSession implements Serializable
{
   static final long serialVersionUID = -5625209785550936713L;
   /**
    * Descriptive information describing this Session implementation.
    */
   protected static final String info = "AttributeBasedClusteredSession/1.0";

   private transient boolean isSessionModifiedSinceLastSave_;
   private transient JBossCacheService proxy_;
   // Transient map to store attr changes for replication.
   private transient Map attrModifiedMap_;
   // Note that the removed attr is intentionally stored in a map instead of set so it is faster to lookup and remove.
   private transient Map attrRemovedMap_;
   private static int REMOVE = 0;   // Used to track attribute changes
   private static int MODIFY = 1;
   private transient Map attributes_;

   public AttributeBasedClusteredSession(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)
      {
         setManager(manager);
         listeners = new ArrayList();
         notes = new HashMap();
         support = new PropertyChangeSupport(this);
         expiring = false;
         attributes_ = Collections.synchronizedMap(new HashMap());
         attrModifiedMap_ = new HashMap();
         attrRemovedMap_ = new HashMap();

         // cache invalidate purpose
         isOutdated = 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();
      }
      // Since attribute maps are transient, we will need to populate it from the underlying store.
      populateAttributes();
   }

   /**
    * Populate the attributes stored in the distributed store to local transient ones.
    */
   protected void populateAttributes()
   {
      Map map = proxy_.getAttributes(id);
      if (map.size() != 0) attributes_ = map;
   }

   // ----------------------------------------------------- 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("AttributeBasedClusteredSession[");
      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();
      proxy_.putSession(id, this);

      // Go thru the attribute change list
      // Go thru the remove attr list first
      {
         Set set = attrModifiedMap_.keySet();
         Iterator it = set.iterator();
         while (it.hasNext())
         {
            Object key = it.next();
            proxy_.putAttribute(id, (String) key, attrModifiedMap_.get(key));
         }
      }

      // Go thru the remove attr list
      {
         Set set = attrRemovedMap_.keySet();
         Iterator it = set.iterator();
         while (it.hasNext())
         {
            Object key = it.next();
            proxy_.removeAttribute(id, (String) key);
         }
      }

      clearAttrChangedMap();
      isSessionModifiedSinceLastSave_ = false;
   }

   public void removeMyself()
   {
      // This is a shortcut to remove session and it's child attributes.
      proxy_.removeSession(id);
      if (attributes_ != null)
         attributes_.clear();
   }

   public void removeMyselfLocal()
   {
      // Need to evict attribute first before session to clean up everything.
      proxy_.removeAttributeLocal(id);
      proxy_.removeSessionLocal(id);
      if (attributes_ != null)
         attributes_.clear();
   }

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

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

   // ------------------------------------------------ JBoss internal abstract method
   protected Object getJBossInternalAttribute(String name)
   {
      // Check the accumulate change maps first.
      Object result = null;
      // TODO Need to check if underlying store is dirty. This will be done with listener in the future.
      result = attributes_.get(name);

      if (result != null)
      {
         int invalidationPolicy = ((AbstractJBossManager) this.manager).getInvalidateSessionPolicy();

         if (invalidationPolicy == WebMetaData.SESSION_INVALIDATE_SET_AND_GET)
         {
            attributeChanged(name, result, MODIFY);
         }
         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)
            )
            {
               attributeChanged(name, result, MODIFY);
            }
         }
      }
      return result;
   }

   protected Object removeJBossInternalAttribute(String name)
   {
      Object result = attributes_.remove(name);
      attributeChanged(name, result, REMOVE);
      return result;
   }

   protected Map getJBossInternalAttributes()
   {
      return attributes_;
   }

   protected Set getJBossInternalKeys()
   {
      return attributes_.keySet();
   }

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

   protected Object setJBossInternalAttribute(String key, Object value)
   {
      attributes_.put(key, value);
      attributeChanged(key, value, MODIFY);
      return value;
   }

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

   public boolean isSessionDirty()
   {
      // Need to check if the attr change map is empty as well??
      return isSessionModifiedSinceLastSave_;
   }

   protected synchronized void attributeChanged(Object key, Object value, int op)
   {
      if (op == MODIFY)
      {
         if (attrRemovedMap_.containsKey(key))
         {
            attrRemovedMap_.remove(key);
         }
         attrModifiedMap_.put(key, value);
      }
      else if (op == REMOVE)
      {
         if (attrModifiedMap_.containsKey(key))
         {
            attrModifiedMap_.remove(key);
         }
         attrRemovedMap_.put(key, value);
      }
      sessionIsDirty();
   }

   protected synchronized void clearAttrChangedMap()
   {
      attrRemovedMap_.clear();
      attrModifiedMap_.clear();
   }
}