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

// Java classes
import java.io.Serializable;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.lang.Validate;

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

// 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) {
			StringBuilder info = new StringBuilder();
			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
	 * @throws CloneNotSupportedException
	 */
	protected Object clone(Object item) throws CloneNotSupportedException {

		Object cloned = item;
		if (this.isResultCloned() && item instanceof AbstractEntity) {
			cloned = ((AbstractEntity) item).clone();
		}
		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);

}