/*
* Copyright (c) 2002-2012 Alibaba Group Holding Limited.
* All rights reserved.
*
* 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.alibaba.citrus.util.i18n;
import static com.alibaba.citrus.util.BasicConstant.*;
import static com.alibaba.citrus.util.CollectionUtil.*;
import static com.alibaba.citrus.util.StringUtil.*;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import java.util.EventListener;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import com.alibaba.citrus.util.ClassLoaderUtil;
import com.alibaba.citrus.util.StringUtil;
import com.alibaba.citrus.util.io.StreamUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 用来处理地域和字符编码的工具类。
* <p>
* 由于系统locale和charset是不可靠的,不同的环境可能会有不同的系统设置,因此应用程序最好不要依赖这个系统值。
* <code>LocaleUtil</code>提供了一个方案,可以“修改”默认locale和charset。
* </p>
* <p>
* <code>LocaleUtil</code>提供了以下几个作用域的locale/charset设定:
* </p>
* <ul>
* <li>系统作用域:由JVM所运行的操作系统环境决定,在JVM生命期内不改变。可通过<code>LocaleUtil.getSystem()</code>
* 取得。</li>
* <li>默认作用域:在整个JVM中全局有效,可被改变。可通过<code>LocaleUtil.getDefault()</code>
* 取得。如未明确指定,则取“系统作用域”的值。</li>
* <li>线程作用域:在整个线程中全局有效,可被改变。可通过<code>LocaleUtil.getContext()</code>
* 取得。如未明确指定,则取“默认作用域”的值。每个线程都可以有自己的locale和charset设置,不会干扰其它线程。</li>
* </ul>
* <p>
* Util工具箱里的其它工具类,当需要时,将从<code>LocaleUtil.getContext()</code>
* 中取得当前的locale和charset设置。例如:<code>StringEscapeUtil.escapeURL(value)</code>
* ,如不指定charset
* ,将从context中取得charset。这样,框架往往可以修改context值,而所有线程中的方法调用将服从于框架的locale和charset设定。
* </p>
*
* @author Michael Zhou
*/
public class LocaleUtil {
private static final LocaleInfo systemLocaleInfo = new LocaleInfo();
private static LocaleInfo defaultLocalInfo = systemLocaleInfo;
private static final ThreadLocal<LocaleInfo> contextLocaleInfoHolder = new ThreadLocal<LocaleInfo>();
/**
* 判断locale是否被支持。
*
* @param locale 要检查的locale
*/
public static boolean isLocaleSupported(Locale locale) {
return locale != null && AvailableLocalesLoader.locales.AVAILABLE_LANGUAGES.contains(locale.getLanguage())
&& AvailableLocalesLoader.locales.AVAILABLE_COUNTRIES.contains(locale.getCountry());
}
/**
* 判断指定的charset是否被支持。
*
* @param charset 要检查的charset
*/
public static boolean isCharsetSupported(String charset) {
return charset != null && Charset.isSupported(charset);
}
/**
* 解析locale字符串。
* <p>
* Locale字符串是符合下列格式:<code>language_country_variant</code>。
* </p>
*
* @param localeString 要解析的字符串
* @return <code>Locale</code>对象,如果locale字符串为空,则返回<code>null</code>
*/
public static Locale parseLocale(String localeString) {
localeString = trimToNull(localeString);
if (localeString == null) {
return null;
}
String language = EMPTY_STRING;
String country = EMPTY_STRING;
String variant = EMPTY_STRING;
// language
int start = 0;
int index = localeString.indexOf("_");
if (index >= 0) {
language = localeString.substring(start, index).trim();
// country
start = index + 1;
index = localeString.indexOf("_", start);
if (index >= 0) {
country = localeString.substring(start, index).trim();
// variant
variant = localeString.substring(index + 1).trim();
} else {
country = localeString.substring(start).trim();
}
} else {
language = localeString.substring(start).trim();
}
return new Locale(language, country, variant);
}
/**
* 取得正规的字符集名称, 如果指定字符集不存在, 则抛出<code>UnsupportedEncodingException</code>.
*
* @param charset 字符集名称
* @return 正规的字符集名称
* @throws IllegalCharsetNameException 如果指定字符集名称非法
* @throws UnsupportedCharsetException 如果指定字符集不存在
*/
public static String getCanonicalCharset(String charset) {
return Charset.forName(charset).name();
}
/**
* 取得备选的resource bundle风格的名称列表。
* <p>
* 例如:
* <code>calculateBundleNames("hello.jsp", new Locale("zh", "CN", "variant"))</code>
* 将返回下面列表:
* <ol>
* <li>hello_zh_CN_variant.jsp</li>
* <li>hello_zh_CN.jsp</li>
* <li>hello_zh.jsp</li>
* <li>hello.jsp</li>
* </ol>
* </p>
*
* @param baseName bundle的基本名
* @param locale 区域设置
* @return 所有备选的bundle名
*/
public static List<String> calculateBundleNames(String baseName, Locale locale) {
return calculateBundleNames(baseName, locale, false);
}
/**
* 取得备选的resource bundle风格的名称列表。
* <p>
* 例如:
* <code>calculateBundleNames("hello.jsp", new Locale("zh", "CN", "variant"),
* false)</code>将返回下面列表:
* <ol>
* <li>hello_zh_CN_variant.jsp</li>
* <li>hello_zh_CN.jsp</li>
* <li>hello_zh.jsp</li>
* <li>hello.jsp</li>
* </ol>
* </p>
* <p>
* 当<code>noext</code>为<code>true</code>时,不计算后缀名,例如
* <code>calculateBundleNames("hello.world",
* new Locale("zh", "CN", "variant"), true)</code>将返回下面列表:
* <ol>
* <li>hello.world_zh_CN_variant</li>
* <li>hello.world_zh_CN</li>
* <li>hello.world_zh</li>
* <li>hello.world</li>
* </ol>
* </p>
*
* @param baseName bundle的基本名
* @param locale 区域设置
* @return 所有备选的bundle名
*/
public static List<String> calculateBundleNames(String baseName, Locale locale, boolean noext) {
baseName = StringUtil.trimToEmpty(baseName);
if (locale == null) {
locale = new Locale(EMPTY_STRING);
}
// 取后缀。
String ext = EMPTY_STRING;
int extLength = 0;
if (!noext) {
int extIndex = baseName.lastIndexOf(".");
if (extIndex != -1) {
ext = baseName.substring(extIndex, baseName.length());
extLength = ext.length();
baseName = baseName.substring(0, extIndex);
if (extLength == 1) {
ext = EMPTY_STRING;
extLength = 0;
}
}
}
// 计算locale后缀。
LinkedList<String> result = createLinkedList();
String language = locale.getLanguage();
int languageLength = language.length();
String country = locale.getCountry();
int countryLength = country.length();
String variant = locale.getVariant();
int variantLength = variant.length();
StringBuilder buffer = new StringBuilder(baseName);
buffer.append(ext);
result.addFirst(buffer.toString());
buffer.setLength(buffer.length() - extLength);
// 如果locale是("", "", "").
if (languageLength + countryLength + variantLength == 0) {
return result;
}
// 加入baseName_language,如果baseName为空,则不加下划线。
if (buffer.length() > 0) {
buffer.append('_');
}
buffer.append(language);
if (languageLength > 0) {
buffer.append(ext);
result.addFirst(buffer.toString());
buffer.setLength(buffer.length() - extLength);
}
if (countryLength + variantLength == 0) {
return result;
}
// 加入baseName_language_country
buffer.append('_').append(country);
if (countryLength > 0) {
buffer.append(ext);
result.addFirst(buffer.toString());
buffer.setLength(buffer.length() - extLength);
}
if (variantLength == 0) {
return result;
}
// 加入baseName_language_country_variant
buffer.append('_').append(variant);
buffer.append(ext);
result.addFirst(buffer.toString());
buffer.setLength(buffer.length() - extLength);
return result;
}
/**
* 取得操作系统默认的区域。
*
* @return 操作系统默认的区域
*/
public static LocaleInfo getSystem() {
return systemLocaleInfo;
}
/**
* 取得默认的区域。
*
* @return 默认的区域
*/
public static LocaleInfo getDefault() {
return defaultLocalInfo == null ? systemLocaleInfo : defaultLocalInfo;
}
/**
* 设置默认的区域。
*
* @param locale 区域
* @return 原来的默认区域
*/
public static LocaleInfo setDefault(Locale locale) {
LocaleInfo old = getDefault();
setDefaultAndNotify(new LocaleInfo(locale, null, systemLocaleInfo));
return old;
}
/**
* 设置默认的区域。
*
* @param locale 区域
* @param charset 编码字符集
* @return 原来的默认区域
*/
public static LocaleInfo setDefault(Locale locale, String charset) throws UnsupportedCharsetException {
LocaleInfo old = getDefault();
setDefaultAndNotify(new LocaleInfo(locale, charset, systemLocaleInfo));
return old;
}
/**
* 设置默认的区域。
*
* @param localeInfo 区域和编码字符集信息
* @return 原来的默认区域
*/
public static LocaleInfo setDefault(LocaleInfo localeInfo) throws UnsupportedCharsetException {
if (localeInfo == null) {
return setDefault(null, null);
} else {
LocaleInfo old = getDefault();
setDefaultAndNotify(localeInfo);
return old;
}
}
private static void setDefaultAndNotify(LocaleInfo localeInfo) throws UnsupportedCharsetException {
defaultLocalInfo = localeInfo.assertCharsetSupported();
for (Notifier notifier : notifiers) {
notifier.defaultChanged(localeInfo);
}
}
/** 复位默认的区域设置。 */
public static void resetDefault() {
defaultLocalInfo = systemLocaleInfo;
for (Notifier notifier : notifiers) {
notifier.defaultReset();
}
}
/**
* 取得当前thread默认的区域。
*
* @return 当前thread默认的区域
*/
public static LocaleInfo getContext() {
LocaleInfo contextLocaleInfo = contextLocaleInfoHolder.get();
return contextLocaleInfo == null ? getDefault() : contextLocaleInfo;
}
/**
* 设置当前thread默认的区域。
*
* @param locale 区域
* @return 原来的thread默认的区域
*/
public static LocaleInfo setContext(Locale locale) {
LocaleInfo old = getContext();
setContextAndNotify(new LocaleInfo(locale, null, defaultLocalInfo));
return old;
}
/**
* 设置当前thread默认的区域。
*
* @param locale 区域
* @param charset 编码字符集
* @return 原来的thread默认的区域
*/
public static LocaleInfo setContext(Locale locale, String charset) throws UnsupportedCharsetException {
LocaleInfo old = getContext();
setContextAndNotify(new LocaleInfo(locale, charset, defaultLocalInfo));
return old;
}
/**
* 设置当前thread默认的区域。
*
* @param localeInfo 区域和编码字符集信息
* @return 原来的thread默认的区域
*/
public static LocaleInfo setContext(LocaleInfo localeInfo) throws UnsupportedCharsetException {
if (localeInfo == null) {
return setContext(null, null);
} else {
LocaleInfo old = getContext();
setContextAndNotify(localeInfo);
return old;
}
}
private static void setContextAndNotify(LocaleInfo localeInfo) throws UnsupportedCharsetException {
contextLocaleInfoHolder.set(localeInfo.assertCharsetSupported());
for (Notifier notifier : notifiers) {
notifier.contextChanged(localeInfo);
}
}
/** 复位当前thread的区域设置。 */
public static void resetContext() {
contextLocaleInfoHolder.remove();
for (Notifier notifier : notifiers) {
notifier.contextReset();
}
}
private static Logger log = LoggerFactory.getLogger(LocaleUtil.class);
private static Notifier[] notifiers = getNotifiers();
private static Notifier[] getNotifiers() {
try {
URL[] files = ClassLoaderUtil.getResources("META-INF/services/localeNotifiers", ClassLoaderUtil.class);
List<Notifier> list = createLinkedList();
for (URL file : files) {
for (String className : StringUtil
.split(StreamUtil.readText(file.openStream(), "UTF-8", true), "\r\n ")) {
list.add(Notifier.class.cast(ClassLoaderUtil.newInstance(className, ClassLoaderUtil.class)));
}
}
return list.toArray(new Notifier[list.size()]);
} catch (Exception e) {
log.warn("Failure in LocaleUtil.getNotifiers()", e);
return new Notifier[0];
}
}
/** 当default或context locale被改变时,通知监听器。 */
public interface Notifier extends EventListener {
void defaultChanged(LocaleInfo newValue);
void defaultReset();
void contextChanged(LocaleInfo newValue);
void contextReset();
}
/** 延迟加载所有可用的国家和语言。 */
private static class AvailableLocalesLoader {
private static final AvailableLocales locales = new AvailableLocales();
}
private static class AvailableLocales {
private final Set<String> AVAILABLE_LANGUAGES = createHashSet();
private final Set<String> AVAILABLE_COUNTRIES = createHashSet();
private AvailableLocales() {
Locale[] availableLocales = Locale.getAvailableLocales();
for (Locale locale : availableLocales) {
AVAILABLE_LANGUAGES.add(locale.getLanguage());
AVAILABLE_COUNTRIES.add(locale.getCountry());
}
}
}
}