Skip to content

Instantly share code, notes, and snippets.

@monzou
Created August 11, 2013 05:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save monzou/6203570 to your computer and use it in GitHub Desktop.
Save monzou/6203570 to your computer and use it in GitHub Desktop.
UTF-8 based ResourceBundle.
package com.usopla.gist;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.net.URLConnection;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.ResourceBundle.Control;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import com.usopla.gist.LocaleChangeListener;
import com.usopla.gist.LocaleHolder;
import com.google.common.base.Function;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.common.io.Closer;
/**
* リソースバンドルを取り扱うユーティリティです。
* <p>
* {@link LocaleHolder} に対応した国際化対応を行います。<br />
* 現在の {@link Locale} に対応するリソースバンドルが存在しない場合, フォールバック処理を行いません。 但し
* <code>@default</code> という接尾辞のバンドルがある場合, そのバンドルをフォールバック先として採用します。
* <p>
* properties ファイルは UTF-8 を想定しています。native2ascii で変換する必要はありません。
*
* @author monzou
*/
public final class I18nBundle implements Function<String, String> {
/** リソースが見つからなかった場合にスローされる例外 */
@SuppressWarnings("serial")
public static class ResourceNotFoundException extends RuntimeException {
ResourceNotFoundException(String message) {
super(message);
}
}
private static final String DEFAULT_SUFFIX = "@default";
private static final String CHARSET_NAME = "UTF-8";
private static final Map<Locale, Map<String, I18nBundle>> REPOSITORY = Maps.newHashMap();
/**
* {@link I18nBundle} を取得します。
*
* @param clazz クラス
* @return {@link I18nBundle}
*/
public static I18nBundle getBundle(Class<?> clazz) {
return getBundle(clazz, null);
}
/**
* {@link I18nBundle} を取得します。
*
* @param clazz クラス
* @param parent 親 {@link I18nBundle}
* @return {@link I18nBundle}
*/
public static I18nBundle getBundle(Class<?> clazz, @Nullable I18nBundle parent) {
return getBundle(clazz, parent, null);
}
/**
* {@link I18nBundle} を取得します。
*
* @param clazz クラス
* @param parent 親 {@link I18nBundle}
* @param locale {@link Locale}
* @return {@link I18nBundle}
*/
public static I18nBundle getBundle(Class<?> clazz, @Nullable I18nBundle parent, @Nullable Locale locale) {
return getBundle(clazz.getName(), parent, locale);
}
/**
* {@link I18nBundle} を取得します。
*
* @param pkg パッケージ
* @param baseName リソースバンドル名称
* @return {@link I18nBundle}
*/
public static I18nBundle getBundle(Package pkg, String baseName) {
return getBundle(pkg, baseName, null);
}
/**
* {@link I18nBundle} を取得します。
*
* @param pkg パッケージ
* @param baseName リソースバンドル名称
* @param parent 親 {@link I18nBundle}
* @return {@link I18nBundle}
*/
public static I18nBundle getBundle(Package pkg, String baseName, @Nullable I18nBundle parent) {
return getBundle(pkg, baseName, parent, null);
}
/**
* {@link I18nBundle} を取得します。
*
* @param pkg パッケージ
* @param baseName リソースバンドル名称
* @param parent 親 {@link I18nBundle}
* @param locale {@link Locale}
* @return {@link I18nBundle}
*/
public static I18nBundle getBundle(Package pkg, String baseName, @Nullable I18nBundle parent, @Nullable Locale locale) {
return getBundle(String.format("%s.%s", pkg.getName(), baseName), parent, locale);
}
/**
* {@link I18nBundle} を取得します。
*
* @param baseName リソースバンドル名称
* @return {@link I18nBundle}
*/
public static I18nBundle getBundle(String baseName) {
return getBundle(baseName, null);
}
/**
* {@link I18nBundle} を取得します。
*
* @param baseName リソースバンドル名称
* @param parent 親 {@link I18nBundle}
* @return {@link I18nBundle}
*/
public static I18nBundle getBundle(String baseName, @Nullable I18nBundle parent) {
return getBundle(baseName, parent, null);
}
/**
* {@link I18nBundle} を取得します。
*
* @param baseName リソースバンドル名称
* @param parent 親 {@link I18nBundle}
* @param locale {@link Locale}
* @return {@link I18nBundle}
*/
public static I18nBundle getBundle(String baseName, @Nullable I18nBundle parent, @Nullable Locale locale) {
Locale bundleLocale = locale == null ? LocaleHolder.getInstance().get() : locale;
synchronized (REPOSITORY) {
Map<String, I18nBundle> cache = REPOSITORY.get(bundleLocale);
if (cache == null) {
cache = Maps.newHashMap();
REPOSITORY.put(bundleLocale, cache);
}
I18nBundle bundle = cache.get(baseName);
if (bundle == null) {
bundle = new I18nBundle(baseName, parent, bundleLocale);
cache.put(baseName, bundle);
}
return bundle;
}
}
/**
* キャッシュを破棄します。
*/
static void clearCache() {
synchronized (REPOSITORY) {
REPOSITORY.clear();
}
}
private volatile I18nBundle parent;
private volatile ResourceBundle defaultBundle;
private volatile ResourceBundle bundle;
private I18nBundle(final String baseName, I18nBundle parent, Locale locale) {
this.parent = parent;
LocaleChangeListener localeChangeHandler = new LocaleChangeListener() {
@Override
public void localeChanged(Locale oldLocale, Locale newLocale) {
refresh(baseName, oldLocale, newLocale);
}
};
try {
LocaleHolder.getInstance().addLocaleChangeListener(localeChangeHandler);
refresh(baseName, null, locale);
} catch (RuntimeException e) {
LocaleHolder.getInstance().removeLocaleChangeListener(localeChangeHandler);
throw e;
}
}
private void refresh(String baseName, @Nullable Locale oldLocale, @Nullable Locale newLocale) {
synchronized (REPOSITORY) {
bundle = innerRefresh(baseName, oldLocale, newLocale);
defaultBundle = innerRefresh(String.format("%s%s", baseName, DEFAULT_SUFFIX), oldLocale, newLocale);
if (bundle == null && defaultBundle == null) {
throw new ResourceNotFoundException(String.format("the bundle does not exist: baseName=%s, locale=%s", //
baseName, newLocale));
}
}
}
@Nullable
@CheckForNull
private ResourceBundle innerRefresh(String baseName, @Nullable Locale oldLocale, @Nullable Locale newLocale) {
ResourceBundle bundle = null;
if (oldLocale != null) {
Map<String, I18nBundle> cache = REPOSITORY.get(oldLocale);
if (cache != null) {
cache.remove(baseName);
}
}
try {
bundle = ResourceBundle.getBundle(baseName, newLocale, new NoFallbackControl());
Map<String, I18nBundle> cache = REPOSITORY.get(newLocale);
if (cache == null) {
cache = Maps.newHashMap();
REPOSITORY.put(newLocale, cache);
}
cache.put(baseName, this);
} catch (MissingResourceException e) {
return null;
}
return bundle;
}
/**
* 対応する {@link Locale} のバンドルが存在しない場合にフォールバックせず,
* {@link I18nBundle#CHARSET_NAME} に従いリソースバンドルを読み込む {@link Control} の実装です。
*
* @author monzou
*/
private static class NoFallbackControl extends Control {
/** {@inheritDoc} */
@Override
public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
throws IllegalAccessException, InstantiationException, IOException {
if ("java.properties".equals(format)) {
String bundleName = toBundleName(baseName, locale);
String resourceName = toResourceName(bundleName, "properties");
Closer closer = Closer.create();
try {
InputStream stream = closer.register(openStream(loader, resourceName, reload));
if (stream != null) {
Reader reader = closer.register(new BufferedReader(new InputStreamReader(stream, CHARSET_NAME)));
return new PropertyResourceBundle(reader);
}
} catch (Throwable t) {
closer.rethrow(t);
} finally {
closer.close();
}
}
return super.newBundle(baseName, locale, format, loader, reload);
}
private InputStream openStream(final ClassLoader classLoader, final String resourceName, final boolean reload) throws IOException {
try {
return AccessController.doPrivileged(new PrivilegedExceptionAction<InputStream>() {
@Override
public InputStream run() throws IOException {
InputStream in = null;
if (reload) {
URL url = classLoader.getResource(resourceName);
if (url != null) {
URLConnection connection = url.openConnection();
if (connection != null) {
connection.setUseCaches(false);
in = connection.getInputStream();
}
}
} else {
in = classLoader.getResourceAsStream(resourceName);
}
return in;
}
});
} catch (PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
/** {@inheritDoc} */
@Override
public List<String> getFormats(String baseName) {
return ResourceBundle.Control.FORMAT_DEFAULT;
}
/** {@inheritDoc} */
@Override
public Locale getFallbackLocale(String baseName, Locale locale) {
return null;
}
}
/**
* 国際化対応された文字列を取得します。
* <p>
* キーに対応するパターンが定義されていなかった場合, 空文字が返されます。
*
* @param key キー
* @param args リソースバンドルのオプション
* @return 文字列
*/
public String get(String key, @Nullable Object... args) {
String pattern = Strings.nullToEmpty(getPattern(key));
return MessageFormat.format(pattern, args);
}
/**
* 国際化対応された文字列をフォーマットするためのパターンを取得します。
* <p>
* 同じキーに対するパターンが定義されていた場合, 子が優先されます。
*
* @param key キー
* @return 文字列パターン
*/
@CheckForNull
@Nullable
public String getPattern(String key) {
String pattern = null;
if (bundle != null) {
try {
pattern = bundle.getString(key);
} catch (RuntimeException ignore) { // SUPPRESS CHECKSTYLE
}
}
if (pattern == null && defaultBundle != null) {
try {
pattern = defaultBundle.getString(key);
} catch (RuntimeException ignore) { // SUPPRESS CHECKSTYLE
}
}
if (pattern == null && parent != null) {
pattern = parent.getPattern(key);
}
return pattern;
}
/**
* キーの一覧を取得します。
* <p>
* 重複したキーが存在する場合, 子のキーが優先されます。
*
* @return キーの一覧
*/
public List<String> getKeys() {
List<String> keys = Collections.list(bundle.getKeys());
if (defaultBundle != null) {
addIfNotExist(keys, Collections.list(defaultBundle.getKeys()));
}
if (parent != null) {
addIfNotExist(keys, parent.getKeys());
}
return ImmutableList.copyOf(keys);
}
private <E> void addIfNotExist(List<E> destination, List<E> list) {
for (E element : list) {
if (destination.contains(element)) {
continue;
}
destination.add(element);
}
}
/**
* {@link ResourceBundle} に変換したインスタンスを取得します。
* <p>
* 通常は使用しないで下さい。
*
* @return {@link ResourceBundle}
*/
public ResourceBundle asResourceBundle() {
return new ResourceBundle() {
@Override
protected Object handleGetObject(String key) {
return I18nBundle.this.getPattern(key);
}
@Override
public Enumeration<String> getKeys() {
return Collections.enumeration(I18nBundle.this.getKeys());
}
};
}
/** {@inheritDoc} */
@Override
public String apply(String key) {
return key == null ? null : getPattern(key);
}
}
package com.usopla.gist;
import java.util.Locale;
/**
* アプリケーションの {@link Locale} の変更を監視するリスナです。
*
* @author monzou
*/
public interface LocaleChangeListener {
/**
* {@link Locale} 変化時のコールバックです。
*
* @param oldLocale 変更前の {@link Locale}
* @param newLocale 変更後の {@link Locale}
*/
void localeChanged(Locale oldLocale, Locale newLocale);
}
package com.usopla.gist;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import com.usopla.gist.Tuple;
import com.usopla.gist.Tuple.Tuple3;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* アプリケーションの {@link Locale} を保持するクラスです。
*
* @author monzou
*/
public final class LocaleHolder {
private static final Logger LOGGER = LoggerFactory.getLogger(LocaleHolder.class);
private static final LocaleHolder INSTANCE;
static {
INSTANCE = new LocaleHolder();
INSTANCE.reset();
}
/**
* シングルトン・インスタンスを取得します。
*
* @return シングルトン・インスタンス
*/
public static LocaleHolder getInstance() {
return INSTANCE;
}
private Locale locale = Locale.getDefault();
private final Lock lock;
private final List<LocaleChangeListener> listeners = new CopyOnWriteArrayList<>();
/**
* {@link Locale} を取得します。
*
* @return {@link Locale}
*/
public Locale get() {
lock.lock();
try {
return locale;
} finally {
lock.unlock();
}
}
/**
* {@link Locale} を初期化します。
*/
public void reset() {
Locale locale = Locale.getDefault();
String language = System.getProperty("user.language", locale.getLanguage());
String country = System.getProperty("user.country", locale.getCountry());
String variant = System.getProperty("user.variant", locale.getVariant());
Tuple3<String, String, String> t1 = Tuple.of(locale.getLanguage(), locale.getCountry(), locale.getVariant());
Tuple3<String, String, String> t2 = Tuple.of(language, country, variant);
set(t1.equals(t2) ? locale : new Locale(language, country, variant));
}
/**
* {@link Locale} を設定します。
*
* @param locale {@link Locale}
*/
public void set(Locale locale) {
lock.lock();
try {
Locale old = this.locale;
if (old != locale) {
this.locale = locale;
for (LocaleChangeListener listener : listeners) {
listener.localeChanged(old, this.locale);
}
LOGGER.info("Locale changed: {}", locale);
}
} finally {
lock.unlock();
}
}
/**
* {@link LocaleChangeListener} を追加します。
*
* @param listener {@link LocaleChangeListener}
*/
public void addLocaleChangeListener(LocaleChangeListener listener) {
lock.lock();
try {
if (listeners.contains(listener)) {
return;
}
listeners.add(listener);
} finally {
lock.unlock();
}
}
/**
* {@link LocaleChangeListener} を削除します。
*
* @param listener {@link LocaleChangeListener}
*/
public void removeLocaleChangeListener(LocaleChangeListener listener) {
lock.lock();
try {
listeners.remove(listener);
} finally {
lock.unlock();
}
}
/**
* 全ての {@link LocaleChangeListener} を削除します。
*/
public void removeLocaleChangeListeners() {
lock.lock();
try {
listeners.clear();
} finally {
lock.unlock();
}
}
private LocaleHolder() {
lock = new ReentrantLock(false);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment