/* PropertyBundle.java


	Purpose: 
	Description: 
	History:
	90/12/06 20:09:36, Create, Tom M. Yeh.

Copyright (C) 2001 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 java.io.InputStream;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zkoss.lang.Objects;
import org.zkoss.lang.SystemException;
import org.zkoss.util.Cache;
import org.zkoss.util.CacheMap;
import org.zkoss.util.Maps;

/**
 * The property bundle.
 *
 * <p>It is similar to java.util.ResourceBundle, but they differ as follows.
 *
 * <ul>
 *  <li>It uses {@link Maps#load(Map, InputStream)} to load the properties.
 *  Thus, It is capable to handle UTF-16 and UTF-8 (but not ISO-8859-1).</li>
 *  <li>The locator could be any object as long as it implements
 *  <code>InputStream getResourceAsStream(String)</code>.</li>
 *  <li>It supports only property files.</li>
 *  <li>The getBundle method returns null if the resource not found,
 *  while ResourceBundle throws MissingResourceException.</li>
 * </ul>
 *
 * <p>Instances of PropertyBundle are cached, so the performance is good.
 * However, it implies the property file mapped by the giving
 * class loader, name and locale is immutable. In other words, if you
 * update the content of a property file, it might not be reflected to
 * getString unless it is cleared out of the cache.
 *
 * <p>Thread safe.
 *
 * @author tomyeh
 */
public class PropertyBundle {
	private static final Logger log = LoggerFactory.getLogger(PropertyBundle.class);

	/** The cache to hold bundles (Key, PropertyBundle). */
	private static final Cache<Key, PropertyBundle> _cache;
	static {
		_cache = new CacheMap<Key, PropertyBundle>();
		_cache.setMaxSize(100);
	}

	/** The map of properties. */
	private final Map<String, String> _map;
	/** The locale of the bundle. */
	private final Locale _locale;

	/** The key used to look up the cache. */
	private static class Key {
		private String baseName;
		private Locale locale;
		private Locator locator;
		private boolean caseInsensitive;

		private Key(String baseName, Locale locale, Locator locator,
		boolean caseInsensitive) {
			this.baseName = baseName;
			this.locale = locale;
			this.locator = locator;
			this.caseInsensitive = caseInsensitive;
		}

		//-- Object --//
		public int hashCode() {
			return baseName.hashCode()
				^ (locale != null ? locale.hashCode(): 0) ^ locator.hashCode();
		}
		public boolean equals(Object o) {
			if (this == o) return true;
			if (!(o instanceof Key))
				return false;
			Key k = (Key)o;
			return k.baseName.equals(baseName)
			&& Objects.equals(k.locale, locale) && k.locator.equals(locator)
			&& k.caseInsensitive == caseInsensitive;
		}
	}
	/**
	 * Gets a resource bundle using the specified
	 * base name, locale, and locator.
	 *
	 * @param locator the locator (never null). See {@link Locators#getDefault}.
	 * @param caseInsensitive whether the key used to access the map
	 * is case-insensitive. If true, all keys are converted to lower cases.
	 * @return the bundle; null if not found
	 */
	public static final PropertyBundle getBundle(
	String baseName, Locale locale, Locator locator, boolean caseInsensitive) {
		if (baseName == null || locator == null)
			throw new IllegalArgumentException();

		//We don't lock the whole method, so it is possible that
		//more than one thread are loading the same property file.
		//However, it is OK since any result is correct.
		//On the other hand, it is more likely that two or more
		//threads are asking different property files at the same
		//time, so we avoid the big lock.

		final Key key = new Key(baseName, locale, locator, caseInsensitive);
		synchronized(_cache) {
			PropertyBundle bundle = _cache.get(key);
			if (bundle != null)
				return bundle;
		}

		final PropertyBundle bundle =
			new PropertyBundle(baseName, locale, locator, caseInsensitive);
		if (bundle._map == null) //failed
			return null;

		synchronized(_cache) {
			_cache.put(key, bundle);
		}
		return bundle;
	}
	/**
	 * Gets a resource bundle using the specified
	 * base name, locale, and locator.
	 */
	public static final PropertyBundle
	getBundle(String baseName, Locale locale, Locator locator) {
		return getBundle(baseName, locale, locator, false);
	}
	/**
	 * Gets a resource bundle using the specified
	 * base name, locale, and the default locator, {@link Locators#getDefault}.
	 *
	 * @param caseInsensitive whether the key used to access the map
	 * is case-insensitive. If true, all keys are converted to lower cases.
	 * @return the bundle; null if not found
	 */
	public static final PropertyBundle
	getBundle(String baseName, Locale locale, boolean caseInsensitive) {
		return getBundle(baseName, locale, Locators.getDefault(), caseInsensitive);
	}
	/**
	 * Gets a resource bundle using the specified
	 * base name, locale, and the default locator, {@link Locators#getDefault}.
	 */
	public static final PropertyBundle
	getBundle(String baseName, Locale locale) {
		return getBundle(baseName, locale, false);
	}

	/**
	 * Constructor.
	 *
	 * @param caseInsensitive whether the key used to access the map
	 * is case-insensitive. If true, all keys are converted to lower cases.
	 */
	protected PropertyBundle(String baseName, Locale locale, Locator locator,
	boolean caseInsensitive) {
		try {
			final Locators.StreamLocation loc =
			Locators.locateAsStream(baseName + ".properties", locale, locator);
			if (loc != null) {
				_map = new HashMap<String, String>(32);
				Maps.load(_map, loc.stream, caseInsensitive);
				_locale = loc.locale;
			} else {
				_map = null; //we use _map to denote failure
				_locale = null;
			}
		}catch(RuntimeException ex) {
			throw ex;
		}catch(Exception ex) {
			throw SystemException.Aide.wrap(ex, "Unable to load " + baseName + ".properties");
		}
	}

	/** Returns the property for the given key from this resource bundle
	 * or one of its parents.
	 */
	public final String getProperty(String key) {
		//It is OK not to sync because _map is immutable
		return _map.get(key);
	}
	/** Returns a map of all properties, (String key , String value).
	 */
	public final Map<String, String> getProperties() {
		return _map;
	}
	/** Returns the locale of the bundle, or null if it is the default.
	 * Note: it is value might not be the same as the locale being passed
	 * to the constructor, because the constructor will do some fallback.
	 */
	public final Locale getLocale() {
		return _locale;
	}
}
