/********************************************************************
 * Copyright © 2004 VHA. All rights reserved
 ********************************************************************/
// Package
package gov.va.med.fw.cache;

// Java classes
import java.io.Serializable;

// Library classes
import org.apache.commons.lang.SerializationUtils;
import org.apache.commons.lang.Validate;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

// Framework classes
import gov.va.med.fw.service.AbstractComponent;
import gov.va.med.fw.model.AbstractEntity;
import gov.va.med.fw.model.EntityKey;

// ESR classes

/**
 * Implements template design pattern to intercept a method
 * invocation to check if an entity is present in a cache. If 
 * it is, uses an EntityCacheManager to return a cached value
 * from a thread-bound data storage  If not, proceed with a method
 * invocation.  This advise class is usually used in conjunction 
 * with a NamMatchMethodPointcutAdvisor to intercept the specific
 * method.
 *
 * Project: Framework</br>
 * Created on: 11:20:56 AM </br>
 *
 * @author DNS   LEV
 */
public abstract class AbstractEntityCacheAdvice extends AbstractComponent 
	implements MethodInterceptor, Serializable {

	/**
	 * An instance of serialVersionUID
	 */
	private static final long serialVersionUID = 3417988966012828069L;

	/**
	 * An instance of cacheManager
	 */
	private EntityCacheManager cacheManager = null;
	
	
	/*
	 * TODO: resultCloned and returnedCachedItem are currently implmented for the first caller
	 * to the cache.  However, they are not used if the item is already cached.  This could cause
	 * undesirable side affects as the cache is not guaranteed to be read-only.  Cloning the
	 * cached item (before returning it) should also be configurable for than just the initial caller.
	 */
	
	/**
	 * An instance of isResultCloned
	 */
	private boolean resultCloned = true;
   
   /**
    * A flag to determine whether to return a cached item
    * or a result from a method invocation. 
    */
   private boolean returnCachedItem = false;
	
	/**
	 * A default constructor
	 */
	protected AbstractEntityCacheAdvice() {
		super();
	}

   /**
    * Returns a flag to indicate a cloned result
    * @return boolean resultCloned.
    */
   public boolean isResultCloned() {
      return resultCloned;
   }
   
   /** 
    * Indicates whether or not a result object should be cloned 
    * @param isResultCloned A flag to clone a result object
    */
   public void setResultCloned(boolean isResultCloned) {
      this.resultCloned = isResultCloned;
   }

   /**
    * Returns a flag indicating to return a cached item
    * @return boolean returnCachedItem.
    */
   public boolean isReturnCachedItem() {
      return returnCachedItem;
   }

   /**
    * Indicates whether or not a cached object should be returned
    * @param returnCachedItem The returnCachedItem to set.
    */
   public void setReturnCachedItem(boolean returnCachedItem) {
      this.returnCachedItem = returnCachedItem;
   }

	/**
	 * @param cacheManager The cacheManager to set.
	 */
	public void setCacheManager(EntityCacheManager cacheManager) {
		this.cacheManager = cacheManager;
	}

   /**
    * Returns an instance of cacheManager
    * @return EntityCacheManager cacheManager.
    */
   public EntityCacheManager getCacheManager() {
      return cacheManager;
   }

   /**
    * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
    */
   public Object invoke( MethodInvocation invocation ) throws Throwable {
      return this.isCached( invocation ) ? this.getCachedItem( invocation ) : processInvocation( invocation );
   }

	/**
	 * @see gov.va.med.fw.service.AbstractComponent#afterPropertiesSet()
	 */
	public void afterPropertiesSet() throws Exception {
		super.afterPropertiesSet();
		// Check if a cache manager is configured
		Validate.notNull( this.cacheManager, "A cache manager is required");
	}

   /** Processes a method invocation. This method checks if a return
	 * value should be cloned.  A default implementation is to clone
	 * a result value.  To change a default behavior, set an isResultCloned
	 * flag to false. The return value is cached in thread-bound storage
	 * in an EntityCacheManager class.
	 * 
	 * @param invocation Information needed to invoke a method
	 * @return A result object either cloned or uncloned.
	 * @throws Throwable In case of errors in a method invocation
	 */
	protected Object processInvocation( MethodInvocation invocation ) throws Throwable {
		
		Object result = invocation.proceed();
      Object cached = cache( clone( result ), invocation );
		return returnCachedItem ? cached : result;
	}
   
   /**
    * Caches an item if it is of type AbstractKeyedEntity
    * @param item An object to cache if it is AbstractKeyedEntity
    * @return a cached item
    */
   protected Object cache( Object result, MethodInvocation invocation ) {

      // Only cache an entity at this level. 
      if( result instanceof AbstractEntity ) {
         this.cacheManager.storeItem( (AbstractEntity)result, getCachedKey( result, invocation ) );
      }
      return result;
   }
   
   protected Object getCachedKey( Object result, MethodInvocation invocation ) {
      
      Object key = null;
      Object[] args = invocation.getArguments();
      int size = args != null ? args.length : 0;

      // If there is only one input argument  
      // then use it as a key to cache
      if( size == 1 ) {
         key = args[0];
      }
      // If there are more than one args, call toString on the arg
      // then append all string info together to form a key
      else if( size > 1 ){
         StringBuffer info = new StringBuffer();
         for( int i=0; i<size; i++ ) {
            info.append( args[i].toString() );
         }
         key = info.toString();
      }
      else {
         // Can't really cache an entity without a key so log debug here
         if( logger.isDebugEnabled() ) {
            logger.debug( "A key is required to cache " + result );
         }
      }
      return key;
   }
   
   /**
    * Clones an object if it is of type Serializable and an 
    * isResultCloned flag is set to true.
    * 
    * @param item An object to clone
    * @return A cloned object
    */
   protected Object clone( Object item ) {
      
      Object cloned = item;
      if( this.isResultCloned() && item instanceof Serializable ) {
         cloned = SerializationUtils.clone( (Serializable)item );
      }
      return cloned;
   }
	
	/** Checks if the specific entity key is already in a cached.
	 * This method encapsulates a usage of an entity cache manager
	 * to allow a derived class to check for the specific entity
	 * key in a cache.
	 * 
	 * @param key An entity key to look for in a cache
	 * @return true if it is in a cache, false otherwise
	 */
	protected boolean isCached( EntityKey key ) {
		boolean isCached = false;
		if( key != null ) {
			isCached = (this.cacheManager.getItem( key ) != null);
		}
		return isCached;
	}

	/** Returns a cached AbstractKeyedEntity for the specific entity
	 * key.  This method encapsulates a usage of an entity cache 
	 * manager to allow a derived class to retrieve an AbstractKeyedEntity
	 * from a cache.
	 * 
	 * @param key An entity key to retrieve a cached entity
	 * @return A cached AbstractKeyedEntity
	 */
	protected AbstractEntity getCachedItem( EntityKey key ) {
		AbstractEntity entity = null;
		if( key != null ) {
			entity = this.cacheManager.getItem( key );
		}
		return entity;
	}
	
	/** A derived class should check to see if an entity is already in a 
	 * cache.  If it it, return true.  Otherwise, return false to allow
	 * a method invocation to proceed
	 * 
	 * @param invocation Encapsulates information needed to invoke a method
	 * @return true if an entity is in a cache. false otherwise
	 */
	protected abstract boolean isCached( MethodInvocation invocation );
	
	/** Returns a result item from a cache.  A method invocation is used
	 * to extract arguments into a method so that a correct cached entity
	 * can be retrieved.
	 * 
	 * @param invocation Encapsulates information needed to invoke a method
	 * @return A return value from a method's invocation
	 */
	protected abstract Object getCachedItem( MethodInvocation invocation );

}