/* Charsets.java

	Purpose:
		
	Description:
		
	History:
		Wed Jan  5 13:55:30     2005, Created by tomyeh

Copyright (C) 2004 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.web.servlet;

import java.util.Locale;

import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.zkoss.lang.Exceptions;
import org.zkoss.lang.Library;
import org.zkoss.lang.Objects;
import org.zkoss.util.Locales;
import org.zkoss.web.Attributes;

/**
 * Utilities to handle characters
 *
 * @author tomyeh
 */
public class Charsets {
	private static final Logger log = LoggerFactory.getLogger(Charsets.class);
	private static final String ATTR_SETUP = "org.zkoss.web.charset.setup";

	private static final String _uriCharset;

	static {
		String cs = Library.getProperty("org.zkoss.web.uri.charset");
		if (cs == null || cs.length() == 0)
			cs = "UTF-8"; //Default: UTF-8
		_uriCharset = cs;
	}

	/** Returns the charset used to encode URI and query string.
	 */
	public static final String getURICharset() {
		return _uriCharset;
	}

	/** Sets up the charset for the request and response based on
	 * {@link #getPreferredLocale(HttpSession,ServletRequest)}. After setting up, you shall invoke
	 * {@link #cleanup} before exiting.
	 *
	 * <pre><code> final Object old = setup(request, response, null);
	 * try {
	 *	  ....
	 * } finally {
	 *    cleanup(request, old);
	 * }
	 *
	 * <p>It is OK to call this method multiple time, since it is smart
	 * enough to ignore redundant calls.
	 *
	 * <p>{@link CharsetFilter} actually use this method to setup
	 * the proper charset and locale. By mapping {@link CharsetFilter} to
	 * all servlets, the encoding charset could be prepared correctly.
	 * However, if you are writing libraries to be as independent of
	 * web.xml as possible, you might choose to invoke this method directly.
	 *
	 * @param sess the session to look for the preferred locale. Ignored if null.
	 * @param charset the response's charset. If null or empty,
	 * response.setCharacterEncoding won't be called, i.e., the container's
	 * default is used.
	 * @return an object that must be passed to {@link #cleanup}
	 */
	public static final Object setup(HttpSession sess, ServletRequest request, ServletResponse response,
			String charset) {
		if (hasSetup(request)) //processed before?
			return Objects.UNKNOWN;

		final Locale locale = getPreferredLocale(sess, request);
		response.setLocale(locale);
		if (charset != null && charset.length() > 0) {
			try {
				if (Servlets.isServlet24()) {
					response.setCharacterEncoding(charset);
				} else {
					//don't access 2.4 API: setCharacterEncoding, getContentType
					response.setContentType(";charset=" + charset);
				}
			} catch (Throwable ex) {
				try {
					final String v = response.getCharacterEncoding();
					if (!Objects.equals(v, charset))
						log.warn("Unable to set response's charset: " + charset + " (current=" + v + ')', ex);
				} catch (Throwable t) { //just in case
				}
			}
		}

		if (request.getCharacterEncoding() == null) {
			charset = response.getCharacterEncoding();
			try {
				request.setCharacterEncoding(charset);
			} catch (Throwable ex) {
				final String v = request.getCharacterEncoding();
				if (!Objects.equals(v, charset))
					log.warn("Unable to set request's charset: " + charset + " (current=" + v + "): "
							+ Exceptions.getMessage(ex));
			}
		}

		markSetup(request, true);
		return Locales.setThreadLocal(locale);
	}

	/** Sets up the charset for the request and response based on
	 * {@link #getPreferredLocale(HttpSession,ServletRequest)}.
	 * It is the same as setup(request.getSession(false), request, response, charset);
	 */
	public static final Object setup(ServletRequest request, ServletResponse response, String charset) {
		return setup(getSession(request), request, response, charset);
	}

	private static final HttpSession getSession(ServletRequest request) {
		return request instanceof HttpServletRequest ? ((HttpServletRequest) request).getSession(false) : null;
	}

	/** Cleans up what has been set in {@link #setup}.
	 * Some invocation are not undo-able, so this method only does the basic
	 * cleanups.
	 *
	 * @param old the value must be the one returned by the last call to
	 * {@link #setup}.
	 */
	public static final void cleanup(ServletRequest request, Object old) {
		if (old != Objects.UNKNOWN) {
			Locales.setThreadLocal((Locale) old);
			markSetup(request, false);
		}
	}

	/** Returns whether the specified request has been set up, i.e.,
	 * {@link #setup} is called
	 *
	 * <p>It is rarely needed to call this method, because it is called
	 * automatically by {@link #setup}.
	 */
	public static final boolean hasSetup(ServletRequest request) {
		return request.getAttribute(ATTR_SETUP) != null; //processed before?
	}

	/** Marks the specified request whether it has been set up, i.e.,
	 * {@link #setup} is called.
	 *
	 * <p>It is rarely needed to call this method, because it is called
	 * automatically by {@link #setup}.
	 */
	public static final void markSetup(ServletRequest request, boolean setup) {
		if (setup)
			request.setAttribute(ATTR_SETUP, Boolean.TRUE);
		else
			request.removeAttribute(ATTR_SETUP);
	}

	/** Returns the preferred locale of the specified request.
	 * You rarely need to invoke this method directly, because it is done
	 * automatically by {@link #setup}.
	 *
	 * <ol>
	 * <li>It checks whether any attribute stored in HttpSession called
	 * {@link Attributes#PREFERRED_LOCALE}. If so, return it.</li>
	 * <li>If not found, it checks if the servlet context has the
	 * attribute called {@link Attributes#PREFERRED_LOCALE}. If so, return it.</li>
	 * <li>If not found, it checks if the library property
	 * called {@link Attributes#PREFERRED_LOCALE} is defined. If so, return it.</li>
	 * <li>Otherwise, use ServletRequest.getLocale().</li>
	 * </ol>
	 *
	 * @param sess the session to look for the preferred locale. Ignored if null.
	 */
	public static final Locale getPreferredLocale(HttpSession sess, ServletRequest request) {
		final String s = Library.getProperty(Attributes.PREFERRED_LOCALE);
		final Locale l = (s != null) ? Locales.getLocale(s) : request.getLocale();
		if (sess != null) {
			Object v = sess.getAttribute(Attributes.PREFERRED_LOCALE);
			if (v == null)
				v = sess.getAttribute(PX_PREFERRED_LOCALE); //backward compatible (prior to 5.0.3)
			if (v != null) {
				if (v instanceof Locale)
					return (Locale) v;
				logLocaleError(v);
			}

			v = sess.getServletContext().getAttribute(Attributes.PREFERRED_LOCALE);
			if (v == null)
				v = sess.getServletContext().getAttribute(PX_PREFERRED_LOCALE); //backward compatible (prior to 5.0.3)
			if (v != null) {
				if (v instanceof Locale)
					return (Locale) v;
				logLocaleError(v);
			}

			if (s != null) {
				return l;
			}
		}
		// B65-ZK-1916: convert zh_HANS-XX and zh_HANT-XX to zh_XX
		return l != null ? fixZhLocale(l) : Locale.getDefault();
	}
	
	/** Returns the preferred locale of the specified request.
	 * It is the same as getPreferredLocale(request.getSession(false), request).
	 */
	public static final Locale getPreferredLocale(ServletRequest request) {
		return getPreferredLocale(getSession(request), request);
	}

	/* Maps zh_HANS-XX to zh_HANT-XX to zh_XX 
	 * 
	 * Mapping rules:
	 * 
	 *     zh_HANS     =>  zh_CN
	 *     zh_HANS-CN  =>  zh_CN
	 *     zh_HANS-SG  =>  zh_SG
	 *     
	 *     zh_HANT     =>  zh_TW
	 *     zh_HANT-TW  =>  zh_TW
	 *     zh_HANT-HK  =>  zh_HK
	 *     zh_HANT-MO  =>  zh_MO
	 *     zh_HANS-CN  =>  zh_CN
	 * 
	 * This function should be deprecated or modified in JDK 1.7 environment or later.
	 */
	private static Locale fixZhLocale(Locale locale) {
		final String country = locale.getCountry();
		if (new Locale("zh").getLanguage().equals(locale.getLanguage()) && !country.isEmpty()) {
			if (country.startsWith("HANS")) {
				if (country.endsWith("SG"))
					return new Locale("zh", "SG");
				else
					return Locale.SIMPLIFIED_CHINESE;
			} else if (country.startsWith("HANT")) {
				if (country.endsWith("TW"))
					return Locale.TAIWAN;
				else if (country.endsWith("HK"))
					return new Locale("zh", "HK");
				else if (country.endsWith("MO"))
					return new Locale("zh", "MO");
				else
					return Locale.TRADITIONAL_CHINESE;
			}
		}
		return locale;
	}

	/** The previous attribute name (backward compatible prior to 5.0.3. */
	private static final String PX_PREFERRED_LOCALE = "px_preferred_locale";

	private static void logLocaleError(Object v) {
		log.warn(Attributes.PREFERRED_LOCALE + " ignored. Locale is required, not " + v.getClass());
	}

	/** Sets the preferred locale for the specified session.
	 * It is the default locale for the whole Web session.
	 * <p>Default: null (no preferred locale -- depending on browser's setting).
	 * @param locale the preferred Locale. If null, it means no preferred locale
	 * @see #getPreferredLocale(HttpSession,ServletRequest)
	 * @since 3.6.3
	 */
	public static final void setPreferredLocale(HttpSession hsess, Locale locale) {
		if (locale != null) {
			hsess.setAttribute(Attributes.PREFERRED_LOCALE, locale);
		} else {
			hsess.removeAttribute(Attributes.PREFERRED_LOCALE);
			hsess.removeAttribute(PX_PREFERRED_LOCALE);
		}
	}

	/** Sets the preferred locale for the specified servlet context.
	 * It is the default locale for the whole Web application.
	 * <p>Default: null (no preferred locale -- depending on browser's setting).
	 * @param locale the preferred Locale. If null, it means no preferred locale
	 * @see #getPreferredLocale(HttpSession,ServletRequest)
	 * @since 3.6.3
	 */
	public static final void setPreferredLocale(ServletContext ctx, Locale locale) {
		if (locale != null) {
			ctx.setAttribute(Attributes.PREFERRED_LOCALE, locale);
		} else {
			ctx.removeAttribute(Attributes.PREFERRED_LOCALE);
			ctx.removeAttribute(PX_PREFERRED_LOCALE);
		}
	}
}
