/* ResourceCache.java

	Purpose:
		
	Description:
		
	History:
		Fri Jun  3 08:59:12     2005, Created by tomyeh

Copyright (C) 2005 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
	This program is distributed under LGPL Version 2.1 in the hope that
	it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.util.resource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zkoss.lang.Library;
import org.zkoss.lang.PotentialDeadLockException;
import org.zkoss.lang.SystemException;
import org.zkoss.util.CacheMap;
import org.zkoss.util.WaitLock;

/**
 * Used to cache resources.
 * To use this class, you have to implement {@link Loader} and then
 * ResourceCache will use it to check whether a resource is gone,
 * modified and load the resource.
 *
 * <p>Unlike {@link CacheMap}, it is thread-safe.
 *
 * <p>The default check period depends on the library property called
 * org.zkoss.util.resource.checkPeriod (unit: second). If not specified, 5 seconds are assumed
 *
 * @author tomyeh
 */
public class ResourceCache<K, V> extends CacheMap<Object, Object> {
	private static final Logger log = LoggerFactory.getLogger(ResourceCache.class);

	/** The loader. */
	protected final Loader<K, V> _loader;
	/** unit=milliseconds. */
	private int _checkPeriod;

	/** Constructor.
	 * @param loader the loader to load resource
	 */
	public ResourceCache(Loader<K, V> loader) {
		if (loader == null)
			throw new NullPointerException();
		_loader = loader;
		_checkPeriod = getInitCheckPeriod();
	}
	/** Constructor.
	 * @param loader the loader to load resource
	 * @param initsz the initial size of the map
	 */
	public ResourceCache(Loader<K, V> loader, int initsz) {
		super(initsz);
		if (loader == null)
			throw new NullPointerException();
		_loader = loader;
		_checkPeriod = getInitCheckPeriod();
	}
	private static int getInitCheckPeriod() {
		final int v = Library.getIntProperty("org.zkoss.util.resource.checkPeriod", 5);
		return v > 0 ? v * 1000: v;
	}

	/** Returns the loader.
	 */
	public Loader<K, V> getLoader() {
		return _loader;
	}
	/** Returns how often to check (unit=milliseconds).
	 * <p>Default: 5000
	 */
	public int getCheckPeriod() {
		return _checkPeriod;
	}
	/** Sets how often to check (unit=milliseconds).
	 * @return this object
	 */
	public ResourceCache setCheckPeriod(int checkPeriod) {
		_checkPeriod = checkPeriod;
		return this;
	}
	
	//-- Map --//
	/** Returns the resource, or null if not found.
	 */
	@SuppressWarnings("unchecked")
	public V get(Object src) {
		WaitLock lock = null;
		for (;;) {
			Info ri = null;
			synchronized (this) {
				Object o = super.get(src);
				if (o instanceof WaitLock) {
					lock = (WaitLock)o;
				} else if (o != null) { //was loaded
					ri = (Info)o;
				} else {
					super.put(src, lock = new WaitLock());
					break; //then, load it
				}
			} //sync(this)

			//check whether cached is valid
			if (ri != null) {
				synchronized (ri) {
					if (ri.isValid())
						return ri.getResource(); //reuse cached
				}
				//invalid, so remove it (if not updated by others)
				synchronized (this) {
					if (super.get(src) == ri) super.remove(src);
				}
			} else if (!lock.waitUntilUnlock(300*1000)) { //5 minute
				final PotentialDeadLockException ex =
					new PotentialDeadLockException(
					"Unable to load from "+src+"\nCause: conflict too long.");
				log.warn("", ex); //very rare, possibly a bug
				throw ex;
			}
		} //for (;;)

		//load it
		try {
			boolean cache;
			final Info ri = new Info((K)src);
			V resource = ri.getResource();

			if (resource instanceof Loader.Resource) {
				final Loader.Resource lr = (Loader.Resource)resource;
				resource = (V)lr.resource;
				cache = lr.cacheable;
			} else
				cache = resource != null;
			
			synchronized (this) {
				if (cache) {
					super.put(src, ri);
				} else {
					super.remove(src); //remove lock
				}
			}

			return resource;
		} catch (Throwable ex) {
			synchronized (this) {
				super.remove(src); //remove lock
			}
			throw SystemException.Aide.wrap(ex);
		} finally {
			lock.unlock();
		}
	}
	/** Don't use it.
	 * @exception UnsupportedOperationException if called
	 */
	public Object put(Object src, Object val) {
		throw new UnsupportedOperationException("Used only internally");
	}
	/** It is OK to remove the resource if you don't want to cache it.
	 * It is thread safe.
	 */
	public Object remove(Object src) {
		synchronized (this) {
			return super.remove(src);
		}
	}
	/** It is OK to clear up all cached resources if you don't want to cache it.
	 * It is thread safe.
	 */
	public void clear() {
		synchronized (this) {
			super.clear();
		}
	}

	//-- private --//
	/** Providing info about a resource. */
	private class Info {
		/** The source. */
		private final K _src;
		/** The result resource. */
		private V _resource;
		private long _lastModified;
		/* When to check lastModified again. */
		private long _nextCheck;

		/**
		 * @param src the source
		 */
		public Info(K src) throws Exception {
			//if (log.isDebugEnabled()) log.debug("Loading from "+src);
			_src = src;
			load();
		}
		/** Returns the result resource.
		 */
		public final V getResource() {
			return _resource;
		}
		/** Quick check whether the page is still valid. */
		public boolean isValid() {
			final long now = System.currentTimeMillis();
			if (!_loader.shallCheck(_src, now - _nextCheck))
				return true;

			final long lastmod = _loader.getLastModified(_src);
			if (lastmod == -1) //removed or not support last-modified
				return false; //reload is required

			final boolean valid = lastmod == _lastModified;
			if (!valid)
				log.info("Source is changed: "+_src);
			//else if (log.finerable()) log.finer("Source not changed: "+_src);
			return valid;
		}
		/** Loads the file. */
		protected void load() throws Exception {
			_resource = _loader.load(_src);
			if (_resource != null) {
				_lastModified = _loader.getLastModified(_src);
				_nextCheck = System.currentTimeMillis() + _checkPeriod;
			}
		}
	}
}
