//$Id: StandardQueryCache.java,v 1.15 2005/08/10 05:06:39 oneovthafew Exp $
package org.hibernate.cache;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.hibernate.HibernateException;
import org.hibernate.UnresolvableObjectException;
import org.hibernate.cfg.Settings;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.type.Type;
import org.hibernate.type.TypeFactory;

/**
 * The standard implementation of the Hibernate QueryCache interface.  This
 * implementation is very good at recognizing stale query results and
 * and re-running queries when it detects this condition, recaching the new
 * results.
 *
 * @author Gavin King
 */
public class StandardQueryCache implements QueryCache {

	private static final Log log = LogFactory.getLog(StandardQueryCache.class);

	private Cache queryCache;
	private UpdateTimestampsCache updateTimestampsCache;
	private final String regionName;

	public void clear() throws CacheException {
		queryCache.clear();
	}

	public StandardQueryCache(
			final Settings settings, 
			final Properties props, 
			final UpdateTimestampsCache updateTimestampsCache, 
			String regionName)
	throws HibernateException {
		
		if (regionName==null) regionName = StandardQueryCache.class.getName();
		String prefix = settings.getCacheRegionPrefix();
		if (prefix!=null) regionName = prefix + '.' + regionName;
		
		log.info("starting query cache at region: " + regionName);
		
		this.queryCache = settings.getCacheProvider().buildCache(regionName, props);
		this.updateTimestampsCache = updateTimestampsCache;
		this.regionName = regionName;
	}

	public boolean put(QueryKey key, Type[] returnTypes, List result, boolean isNaturalKeyLookup, SessionImplementor session) 
	throws HibernateException {
		
		if ( isNaturalKeyLookup && result.size()==0 ) {
			return false;
		}
		else {
			
			if ( log.isDebugEnabled() ) {
				log.debug("caching query results in region: " + regionName);
			}
			
			List cacheable = new ArrayList( result.size()+1 );
			cacheable.add( new Long( session.getTimestamp() ) );
			for ( int i=0; i<result.size(); i++ ) {
				if ( returnTypes.length==1 ) {
					cacheable.add( returnTypes[0].disassemble( result.get(i), session, null ) );
				}
				else {
					cacheable.add( TypeFactory.disassemble( (Object[]) result.get(i), returnTypes, null, session, null ) );
				}
			}
			
			queryCache.put(key, cacheable);
			
			return true;
			
		}

	}

	public List get(QueryKey key, Type[] returnTypes, boolean isNaturalKeyLookup, Set spaces, SessionImplementor session) 
	throws HibernateException {
		
		if ( log.isDebugEnabled() ) {
			log.debug("checking cached query results in region: " + regionName);
		}
		
		List cacheable = (List) queryCache.get(key);
		if (cacheable==null) {
			log.debug("query results were not found in cache");
			return null;
		}
		
		Long timestamp = (Long) cacheable.get(0);
		if ( !isNaturalKeyLookup && !isUpToDate(spaces, timestamp) ) {
			log.debug("cached query results were not up to date");
			return null;
		}
		
		log.debug("returning cached query results");
		for ( int i=1; i<cacheable.size(); i++ ) {
			if ( returnTypes.length==1 ) {
				returnTypes[0].beforeAssemble( (Serializable) cacheable.get(i), session );
			}
			else {
				TypeFactory.beforeAssemble( (Serializable[]) cacheable.get(i), returnTypes, session );
			}
		}
		List result = new ArrayList( cacheable.size()-1 );
		for ( int i=1; i<cacheable.size(); i++ ) {
			try {
				if ( returnTypes.length==1 ) {
					result.add( returnTypes[0].assemble( (Serializable) cacheable.get(i), session, null ) );
				}
				else {
					result.add( TypeFactory.assemble( (Serializable[]) cacheable.get(i), returnTypes, session, null ) );
				}
			}
			catch (UnresolvableObjectException uoe) {
				if (isNaturalKeyLookup) {
					//TODO: not really completely correct, since
					//      the uoe could occur while resolving
					//      associations, leaving the PC in an
					//      inconsistent state
					log.debug("could not reassemble cached result set");
					queryCache.remove(key);
					return null;
				}
				else {
					throw uoe;
				}
			}
		}
		return result;
	}

	protected boolean isUpToDate(Set spaces, Long timestamp) {
		if ( log.isDebugEnabled() ) {
			log.debug("Checking query spaces for up-to-dateness: " + spaces);
		}
		return updateTimestampsCache.isUpToDate(spaces, timestamp);
	}

	public void destroy() {
		try {
			queryCache.destroy();
		}
		catch (Exception e) {
			log.warn("could not destroy query cache: " + regionName, e);
		}
	}
	
	public Cache getCache() {
		return queryCache;
	}
	
	public String getRegionName() {
		return regionName;
	}
	
	public String toString() {
		return "StandardQueryCache(" + regionName + ')';
	}

}
