/*
 *  Copyright 2008 biaoping.yin
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.  
 */
package com.frameworkset.orm.cache;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Properties;
import java.util.Set;

import org.apache.log4j.Logger;



public class CacheModel {
	
	private static final Logger log = Logger.getLogger(CacheModel.class);

	  private static final int MAX_OBJECT_LOG_SIZE = 200;

	  /**
	   * This is used to represent null objects that are returned from the cache so
	   * that they can be cached, too.
	   */
	  public static final Object NULL_OBJECT = new String("SERIALIZABLE_NULL_OBJECT");
	  private int requests = 0;
	  private int hits = 0;

	  /**
	   * Constant to turn off periodic cache flushes
	   */
	  private static final long NO_FLUSH_INTERVAL = -99999;

	  private String id;

	  private boolean readOnly;
	  private boolean serialize;

	  private long lastFlush;
	  private long flushInterval;
	  private long flushIntervalSeconds;
	  private Set flushTriggerStatements;

	  private CacheControl controller;

	  private String resource;

	  /**
	   * Default constructor
	   */
	  public CacheModel() {
	    this.flushInterval = NO_FLUSH_INTERVAL;
	    this.flushIntervalSeconds = NO_FLUSH_INTERVAL;
	    this.lastFlush = System.currentTimeMillis();
	    this.flushTriggerStatements = new HashSet();
	  }

	  /**
	   * Getter for the cache model's id
	   *
	   * @return the id
	   */
	  public String getId() {
	    return id;
	  }

	  /**
	   * Setter for the cache model's id
	   *
	   * @param id - the new id
	   */
	  public void setId(String id) {
	    this.id = id;
	  }

	  /**
	   * Getter for read-only property
	   *
	   * @return true if a read-only model
	   */
	  public boolean isReadOnly() {
	    return readOnly;
	  }

	  /**
	   * Setter for read-only property
	   *
	   * @param readOnly - the new setting
	   */
	  public void setReadOnly(boolean readOnly) {
	    this.readOnly = readOnly;
	  }

	  /**
	   * Getter to tell if the cache serializes
	   *
	   * @return true if the cache model serializes objects
	   */
	  public boolean isSerialize() {
	    return serialize;
	  }

	  /**
	   * Setter to tell the cache to serialize objects
	   *
	   * @param serialize - if the cache model is to serialize objects
	   */
	  public void setSerialize(boolean serialize) {
	    this.serialize = serialize;
	  }

	  /**
	   * Getter for resource property
	   *
	   * @return the value of the resource property
	   */
	  public String getResource() {
	    return resource;
	  }

	  /**
	   * Setter for resource property
	   *
	   * @param resource - the new value
	   */
	  public void setResource(String resource) {
	    this.resource = resource;
	  }

	  /**
	   * Sets up the controller for the cache model
	   *
	   * @throws ClassNotFoundException - if the class cannot be found
	   * @throws InstantiationException - if the class cannot be instantiated
	   * @throws IllegalAccessException - if the classes constructor is not accessible
	   */
	  public void setCacheController(CacheControl controller)
	      throws ClassNotFoundException, InstantiationException, IllegalAccessException {
	    this.controller = controller;
	  }

	  /**
	   * Getter for flushInterval property
	   *
	   * @return The flushInterval (in milliseconds)
	   */
	  public long getFlushInterval() {
	    return flushInterval;
	  }

	  /**
	   * Getter for flushInterval property
	   *
	   * @return The flushInterval (in milliseconds)
	   */
	  public long getFlushIntervalSeconds() {
	    return flushIntervalSeconds;
	  }

	  /**
	   * Setter for flushInterval property
	   *
	   * @param flushInterval The new flushInterval (in milliseconds)
	   */
	  public void setFlushInterval(long flushInterval) {
	    this.flushInterval = flushInterval;
	    this.flushIntervalSeconds = flushInterval / 1000;
	  }

	  /**
	   * Adds a flushTriggerStatment. When a flushTriggerStatment is executed, the
	   * cache is flushed (cleared).
	   *
	   * @param statementName The statement to add.
	   */
	  public void addFlushTriggerStatement(String statementName) {
	    flushTriggerStatements.add(statementName);
	  }

	  /**
	   * Gets an Iterator containing all flushTriggerStatment objects for this cache.
	   *
	   * @return The Iterator
	   */
	  public Iterator getFlushTriggerStatementNames() {
	    return flushTriggerStatements.iterator();
	  }

	  /**
	   * ExecuteListener event.  This will be called by a MappedStatement
	   * for which this cache is registered as a ExecuteListener.  It will
	   * be called each time an executeXXXXXX method is called.  In the
	   * case of the Cache class, it is registered in order to flush the
	   * cache whenever a certain statement is executed.
	   * (i.e. the flushOnExecute cache policy)
	   *
	   * @param statement The statement to execute
	   */
	  public void onExecuteStatement(MappedStatement statement) {
	    flush();
	  }


	  /**
	   * Returns statistical information about the cache.
	   *
	   * @return the number of cache hits divided by the total requests
	   */
	  public double getHitRatio() {
	    return (double) hits / (double) requests;
	  }

	  /**
	   * Configures the cache
	   *
	   * @param props
	   */
	  public void configure(Properties props) {
	    controller.setProperties(props);
	  }

	  /**
	   * Clears the cache
	   */
	  public void flush() {
	  	synchronized (this)  {
	      controller.flush(this);
	      lastFlush = System.currentTimeMillis();
	      if ( log.isDebugEnabled() )  {
	        log("flushed", false, null);
	      }
	    }
	  }

	  /**
	   * Get an object out of the cache.
	   * A side effect of this method is that is may clear the cache if it has not been
	   * cleared in the flushInterval.
	   *
	   * @param key The key of the object to be returned
	   * @return The cached object (or null)
	   */
	  public Object getObject(CacheKey key) {
	  	Object value = null;
	    synchronized (this) {
	      if (flushInterval != NO_FLUSH_INTERVAL
	          && System.currentTimeMillis() - lastFlush > flushInterval) {
	        flush();
	      }

	      value = controller.getObject(this, key);
	      if (serialize && !readOnly &&
	       	    (value != NULL_OBJECT && value != null)) {
	        try {
	          ByteArrayInputStream bis = new ByteArrayInputStream((byte[]) value);
	          ObjectInputStream ois = new ObjectInputStream(bis);
	          value = ois.readObject();
	          ois.close();
	        } catch (Exception e) {
	          throw new RuntimeException("Error caching serializable object.  Be sure you're not attempting to use " +
	                                           "a serialized cache for an object that may be taking advantage of lazy loading.  Cause: " + e, e);
	        }
	      }
	      requests++;
	      if (value != null) {
	        hits++;
	      }
	      if ( log.isDebugEnabled() )  {
	      	if ( value != null )  {
	            log("retrieved object", true, value);
	      	}
	      	else  {
	      		log("cache miss", false, null);
	      	}
	      }
	    }
	    return value;
	  }

	  /**
	   * Add an object to the cache
	   *
	   * @param key   The key of the object to be cached
	   * @param value The object to be cached
	   */
	  public void putObject(CacheKey key, Object value) {
	  	if (null == value) value = NULL_OBJECT;
	  	synchronized ( this )  {
	      if (serialize && !readOnly && value != NULL_OBJECT) {
	        try {
	          ByteArrayOutputStream bos = new ByteArrayOutputStream();
	          ObjectOutputStream oos = new ObjectOutputStream(bos);
	          oos.writeObject(value);
	          oos.flush();
	          oos.close();
	          value = bos.toByteArray();
	        } catch (IOException e) {
	          throw new RuntimeException("Error caching serializable object.  Cause: " + e, e);
	        }
	      }
	      controller.putObject(this, key, value);
	      if ( log.isDebugEnabled() )  {
	        log("stored object", true, value);
	      }
	    }
	  }

	  /**
	   * Get the maximum size of an object in the log output.
	   *
	   * @return Maximum size of a logged object in the output
	   */
	  protected int getMaxObjectLogSize()  {
	      return MAX_OBJECT_LOG_SIZE;
	  }

	  /**
	   * Log a cache action. Since this method is pretty heavy
	   * weight, it's best to enclose it with a log.isDebugEnabled()
	   * when called.
	   *
	   * @param action String to output
	   * @param addValue Add the value being cached to the log
	   * @param cacheValue The value being logged
	   */
	  protected void log(String action, boolean addValue,
	  		             Object cacheValue)  {
	    StringBuffer output = new StringBuffer("Cache '");
	    output.append(getId());
	    output.append("': ");
	    output.append(action);
	    if ( addValue )  {
	      String cacheObjectStr = (cacheValue == null ? "null" : cacheValue.toString());
	      output.append(" '");
	      if ( cacheObjectStr.length() < getMaxObjectLogSize() )  {
	      	output.append(cacheObjectStr.toString());
	      }
	      else  {
	      	output.append(cacheObjectStr.substring(1,
	      			                 getMaxObjectLogSize()));
	      	output.append("...");
	      }
	      output.append("'");
	    }
	    log.debug(output.toString());
	  }

	  public void setControllerProperties(Properties cacheProps) {
	    controller.setProperties(cacheProps);
	  }

}
