/*
* Copyright (c) 2007 NTT DATA Corporation
*
* 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 jp.terasoluna.fw.message;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.support.AbstractMessageSource;
/**
* DAOから取得したメッセージリソースより、メッセージコード及びロケールをキー
* として、メッセージもしくはメッセージフォーマットを決定するクラス。
*
* <p>
* 本クラスはクラスロード時にDBを参照し、DB中のメッセージリソースからメッセージ
* もしくはメッセージフォーマットを決定するクラスである。
* また、国際化に対応しており、言語コード、国コード、バリアントコードによる
* ロケール判別が可能である。
* </p>
* <strong>使用方法</strong><br>
* このクラスを利用するにはアプリケーションコンテキスト起動時にMessageSource
* として設定し、またメッセージリソースを格納したDBとの接続をする
* DAOオブジェクトとして設定する必要がある。<br>
* <br>
* <strong>設定例</strong><br>
* Bean定義ファイルに以下の内容の記述をする。<br>
* DAOとしてDBMessageResourceDAOを利用した場合<br>
*
* <pre>
* <bean id = "messageSource"
* class = "jp.terasoluna.fw.message.DataSourceMessageSource">
* <property name = "DBMessageResourceDAO">
* <ref bean = "dBMessageResourceDAO"></ref>
* </property>
* </bean>
* </pre>
*
* <strong>解説</strong><br>
* <bean>要素のid属性に"messageSource" を指定することでMessageSource
* として認識される。<br>
* <bean>要素内<property>要素にはDAOの設定を記述する。<br>
* <br>
*
* <br>
* デフォルトロケールの変更<br>
* デフォルトロケールは、メッセージリソースのロケールが設定されていない場合、
* もしくは設定されていても正しくロケールが設定されていない場合に指定される
* ロケールである。<br>
* デフォルトロケールの初期設定は、クライアントのVMで使用されるロケールである。
* <br>
* デフォルトロケールは本クラス内に実装されているsetDefaultLocaleを利用する
* ことで変更することが出来る。 <br>
* <br>
* <strong>設定例</strong><br>
* Bean定義ファイル中に以下の内容の記述をする。<br>
* デフォルトロケールを日本語(言語コード「ja」)にする場合。<br>
* <br>
*
* <pre>
* <bean id = "messageSource"
* class = "jp.terasoluna.fw.message.DataSourceMessageSource">
* <property name = "DBMessageResourceDAO">
* <ref bean = "dBMessageResourceDAO"></ref>
* </property>
* <property name = "defaultLocale">
* <value>ja</value>
* </property>
* </bean>
* </pre>
*
* <strong>解説</strong><br>
* <bean>要素内<properities>要素のname属性にdefaultLocaleを指定し、
* value属性にて設定したい値を指定する。
*
* @see jp.terasoluna.fw.message.DBMessage
* @see jp.terasoluna.fw.message.DBMessageQuery
* @see jp.terasoluna.fw.message.DBMessageResourceDAO
* @see jp.terasoluna.fw.message.DBMessageResourceDAOImpl
*
*
*/
public class DataSourceMessageSource extends AbstractMessageSource implements
InitializingBean {
/**
* メッセージコード毎にロケールとメッセージフォーマットをマップで保持する。
* <br>
* Map <Code, Map <Locale, MessageFormat>>
*/
protected final Map<String, Map<Locale, MessageFormat>> cachedMessageFormats
= new HashMap<String, Map<Locale, MessageFormat>>();
/**
* ロケール毎にメッセージコードとメッセージをマップで保持する。
* <br/> Map <Locale, Properties>
*/
protected Map<Locale, Properties> cachedMergedProperties
= new HashMap<Locale, Properties>();
/**
* ログクラス。
*/
private static Log log = LogFactory.getLog(DataSourceMessageSource.class);
/**
* ロケールが指定されていない場合のデフォルトロケール。 メッセージリソース内
* でロケールが指定されていない場合、 このロケールが設定される。
* デフォルトではサーバー側JVMの言語コードのみをロケールとして使用する。
*/
protected Locale defaultLocale
= new Locale(Locale.getDefault().getLanguage());
/**
* メッセージリソースを取得するDAO。
*/
protected DBMessageResourceDAO dbMessageResourceDAO = null;
/**
* デフォルトロケールを設定する。設定しない場合はクライアントのVMのロケール
* が設定される。VMのロケールが認識できない場合は英語が設定される。
*
* @see #getMessageInternal
* @see java.util.Locale#getDefault
*
* @param defaultLocale
* デフォルトのロケール。
*/
public void setDefaultLocale(Locale defaultLocale) {
this.defaultLocale = defaultLocale;
}
/**
* DBMessageResourceDAOを設定する。
*
* @param dbMessageResourceDAO
* 全てのメッセージリソースを取得するDAO
*/
public synchronized void setDbMessageResourceDAO(
DBMessageResourceDAO dbMessageResourceDAO) {
this.dbMessageResourceDAO = dbMessageResourceDAO;
}
/**
* Webアプリケーションコンテキスト起動時に実行される。<br>
* メッセージリソースからメッセージコード、ロケール、メッセージ
* (メッセージフォーマット含む)の3項目で分類し、キャッシュに保持する。
*
* @see #cachedMergedProperties
*
*/
@Override
public void afterPropertiesSet() {
if (log.isDebugEnabled()) {
log.debug("afterPropertiesSet");
}
readMessagesFromDataSource();
}
/**
* メッセージリソースをリロードする。
* このメソッドを明示的に呼び出すことでDBから動的にメッセージリソースを
* リロードする。DBの更新があった場合、このメソッドを呼び出すことで
* メッセージリソースをリロードすることが可能。
*/
public synchronized void reloadDataSourceMessage() {
readMessagesFromDataSource();
}
/**
* DAOからメッセージリソースを取得し、整理する。メッセージリソースをロケール
* 別にまとめ、メッセージコードとメッセージ本体をセットにして格納する。
* 取得した全てのメッセージリソースに対して実施する。<br>
* メッセージリソースとは、メッセージコード、言語コード、国コード、
* バリアントコード、メッセージ本体である。
*/
protected synchronized void readMessagesFromDataSource() {
if (log.isDebugEnabled()) {
log.debug("readMessageFromDataSource");
}
cachedMergedProperties.clear();
cachedMessageFormats.clear();
// DAOからメッセージリソースを取得する
List<DBMessage> messages = dbMessageResourceDAO.findDBMessages();
//メッセージコードとメッセージ内容がnullではない場合、
//キャッシュに読み込む
for (DBMessage message : messages) {
if (message.code != null && message.message != null) {
mapMessage(message);
}
}
if (log.isDebugEnabled()) {
log.debug("get MessageResource from DAO.");
}
}
/**
* メッセージリソースをロケール別に整理し、メッセージコードとメッセージ本体
* をセットにして、ハッシュテーブルに格納する。
*
* @param message
* メッセージリソースを格納したDBMessageオブジェクト。
*/
protected void mapMessage(DBMessage message) {
// ロケールオブジェクトを言語コード、国コード、バリアントコードから
// 生成する。
Locale locale = createLocale(message);
// ロケールに対応する全てのメッセージを取得する。
Properties messages = getMessages(locale);
// 取得した全てのメッセージに新規メッセージを追加する。
messages.setProperty(message.getCode(), message.getMessage());
if (log.isDebugEnabled()) {
log.debug("add Message[" + message.getMessage() + "] (code["
+ message.getCode() + "], locale[" + locale + "])");
}
}
/**
* Localeオブジェクトを生成する。<br>
* 言語コード、国コード、バリアントコードからLocaleオブジェクトを生成する。
* 言語コードが与えられていない場合は、デフォルトロケールの言語コードのみ
* を格納し、Localeオブジェクトを生成する。
*
* @param message メッセージリソース
*
* @return
* 言語コード、国コード、バリアントコードを格納したLocaleオブジェクト。
*
* @throws IllegalArgumentException
* メッセージコード及びメッセージが存在するメッセージリソースに
* ロケールが設定されていない。かつ、デフォルトロケールも設定出来ない
* 場合のエラー。
*/
protected Locale createLocale(DBMessage message) {
if (message.getLanguage() == null) {
if (defaultLocale != null) {
return defaultLocale;
}
if (log.isErrorEnabled()) {
log.error("Can't resolve Locale.Define Locale"
+ " in MessageSource or Defaultlocale.");
}
throw new IllegalArgumentException("Can't resolve Locale."
+ "Define Locale in MessageSource or Defaultlocale.");
}
if (message.getCountry() == null) {
return new Locale(message.getLanguage());
}
if (message.getVariant() == null) {
return new Locale(message.getLanguage(), message.getCountry());
}
return new Locale(message.getLanguage(), message.getCountry(),
message.getVariant());
}
/**
* ロケールに対応する全てのメッセージを返却する。 指定されたロケールの
* メッセージが存在しない場合は新たに生成し、nullを返却しない。
*
* @param locale
* メッセージのロケール。
*
* @return ロケールに対応した全てのメッセージ。 メッセージコードと
* メッセージ本体が関連付けられ、格納されている。
*/
protected Properties getMessages(Locale locale) {
// ロケールをキーとし、全てのメッセージを取得する 。
Properties messages = cachedMergedProperties.get(locale);
// ロケールに対応した全てのメッセージが存在しなかった場合、
// 新たに作成し、cachedMergedProperties内に格納する。
if (messages == null) {
messages = new Properties();
cachedMergedProperties.put(locale, messages);
}
return messages;
}
/**
* 引数として渡されたメッセージコードとロケールからメッセージを決定し、
* メッセージを返却する。親クラスから呼び出されるメソッド。
* AbstractMessageSourceのメソッドをオーバーライドしている。
*
* @param code
* メッセージコード
* @param locale
* メッセージのロケール
*
* @return メッセージ本体
*/
@Override
protected synchronized String resolveCodeWithoutArguments(
String code,
Locale locale) {
String msg = internalResolveCodeWithoutArguments(code, locale);
if (msg == null) {
if (log.isDebugEnabled()) {
log.debug("could not resolve [" + code + "] for locale ["
+ locale + "]");
}
}
return msg;
}
/**
* メッセージコードとロケールからメッセージを決定する。 引数として与えられた
* ロケールでメッセージの決定が出来なかった場合、ロケールを変化させ、
* メッセージの取得を試みる。
* また、デフォルトロケールが与えられていた場合、デフォルトロケールでの
* メッセージの決定を最後に試みる。
*
* @param code
* メッセージコード
* @param locale
* メッセージのロケール
*
* @return メッセージ本体
*/
protected String internalResolveCodeWithoutArguments(
String code,
Locale locale) {
// メッセージコードとロケールに対応したメッセージ本体をmsgに格納する。
String msg = getMessages(locale).getProperty(code);
// メッセージ本体の取得が出来た場合、メッセージ本体を返却する。
if (msg != null) {
return msg;
}
// メッセージ本体の取得が出来なかった場合、ロケールを変化させて
// メッセージ本体の取得を試みる。
// ロケールオブジェクトのパターンの生成
List<Locale> locales = getAlternativeLocales(locale);
// メッセージコードと新たに生成したロケールに対応したメッセージを決定し、
// メッセージ本体を返却します。
for (int i = 0; i < locales.size(); i++) {
msg = getMessages(locales.get(i)).getProperty(code);
if (msg != null) {
return msg;
}
}
// メッセージが取得できなった場合はnullを返却する。
return null;
}
/**
* メッセージを決定する際のキーを生成する。 ロケールの値から
* ロケールオブジェクトを生成し、リストに格納、返却する。
* 1.引数localeの言語コード、国コードを持つもの。(バリアントコードを削除。)
* 2.引数localeの言語コードを持つもの。(国コード、バリアントコードを削除。)
* 3.デフォルトロケールの言語コード、国コード、バリアントコードを持つもの。
* 4.デフォルトロケールの言語コード、国コードを持つもの。
* 5.デフォルトロケールの言語コードを持つもの。
*
* @param locale
* ロケールオブジェクト
*
* @return メッセージ決定のキーとなるロケールオブジェクト
*/
protected List<Locale> getAlternativeLocales(Locale locale) {
List<Locale> locales = new ArrayList<Locale>();
// ロケール内にバリアントコードが存在する場合
if (locale.getVariant().length() > 0) {
// Locale(language,country,"")を設定
locales.add(new Locale(locale.getLanguage(), locale.getCountry()));
}
// ロケール内に国コードが存在する場合
if (locale.getCountry().length() > 0) {
// Locale(language,"","")を設定
locales.add(new Locale(locale.getLanguage()));
}
// デフォルトロケールが設定されている場合
if (defaultLocale != null && !locale.equals(defaultLocale)) {
if (defaultLocale.getVariant().length() > 0) {
// Locale(language,country,"")を設定
locales.add(defaultLocale);
}
if (defaultLocale.getCountry().length() > 0) {
// Locale(language,country,"")を設定
locales.add(new Locale(defaultLocale.getLanguage(),
defaultLocale.getCountry()));
}
// ロケール内に国コードが存在する場合
if (defaultLocale.getLanguage().length() > 0) {
// Locale(language,"","")を設定
locales.add(new Locale(defaultLocale.getLanguage()));
}
}
return locales;
}
/**
* 引数として渡されたメッセージコードとロケールからメッセージフォーマットを
* 決定し、メッセージフォーマットを返却する。
* 親クラスから呼び出されるメソッド。AbstractMessageSourceのメソッドを
* オーバーライドしている。
*
* @param code
* メッセージコード
* @param locale
* メッセージのロケール
*
* @return メッセージフォーマット
*/
@Override
protected synchronized MessageFormat resolveCode(
String code,
Locale locale) {
// メッセージコードとロケールに対応したメッセージ本体をmessageFormatに
// 格納する。
MessageFormat messageFormat = getMessageFormat(code, locale);
// メッセージ本体の取得が出来た場合、メッセージフォーマットを返却する
if (messageFormat != null) {
if (log.isDebugEnabled()) {
log.debug("resolved [" + code + "] for locale [" + locale
+ "] => [" + messageFormat + "]");
}
return messageFormat;
}
// メッセージフォーマットの取得が出来なかった場合、ロケールを変化させて
// メッセージフォーマットの取得を試みる。
// ロケールオブジェクトのパターンの生成
List<Locale> locales = getAlternativeLocales(locale);
// メッセージコードと新たに生成したロケールに対応した
// メッセージフォーマットを決定し、メッセージフォーマットを返却します。
for (int i = 0; i < locales.size(); i++) {
messageFormat = getMessageFormat(code, locales.get(i));
if (messageFormat != null) {
if (log.isDebugEnabled()) {
log.debug("resolved [" + code + "] for locale [" + locale
+ "] => [" + messageFormat + "]");
}
return messageFormat;
}
}
if (messageFormat == null) {
if (log.isDebugEnabled()) {
log.debug("could not resolve [" + code + "] for locale ["
+ locale + "]");
}
}
// メッセージフォーマットが取得出来なかった場合はnullを返却する。
return null;
}
/**
* 引数として渡されたメッセージコードとロケールからメッセージフォーマット
* を決定する。
*
* @param code
* メッセージコード
* @param locale
* メッセージのロケール
*
* @return 決定されたメッセージフォーマット
*/
protected MessageFormat getMessageFormat(String code, Locale locale) {
// メッセージコードに対応したロケールマップを取得する。
Map<Locale, MessageFormat> localeMap
= this.cachedMessageFormats.get(code);
// ロケールマップが存在した場合、ロケールマップよりロケールに対応する
// メッセージフォーマットを取得、返却する。
if (localeMap != null) {
MessageFormat result = localeMap.get(locale);
if (result != null) {
return result;
}
}
String msg = getMessages(locale).getProperty(code);
// メッセージが存在する場合
if (msg != null) {
// ロケールマップが存在しない場合、新たにロケールマップを生成し、
// メッセージフォーマットを返却する。
if (localeMap == null) {
localeMap = new HashMap<Locale, MessageFormat>();
this.cachedMessageFormats.put(code, localeMap);
}
// メッセージとロケールよりメッセージフォーマットを作成する。
MessageFormat result = createMessageFormat(msg, locale);
localeMap.put(locale, result);
return result;
}
// メッセージフォーマットが取得出来なかった場合はnullを返却する。
return null;
}
}