MessageResolver.java

package com.github.mygreen.cellformatter.lang;

import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.concurrent.ConcurrentHashMap;

/**
 * メッセージソースを管理するクラス。
 * <p>ロケールを指定した場合、そのロケールで存在しないキーがあるときに、標準の値を返す。
 *
 * @version 0.10
 * @since 0.5
 * @author T.TSUCHIE
 *
 */
public class MessageResolver {

    /**
     * 解決するリソース名。
     * <p>リソースバンドル名の形式。
     */
    private final String resourceName;

    /**
     * デフォルトのメッセージがない場合を許可するかどうか。
     */
    private final boolean allowedNoDefault;

    /**
     * クラスパスのルートにあるユーザ定義のメッセージソースも読み込むかどうか指定します。
     */
    private final boolean appendUserResource;

    /**
     * デフォルトのメッセージ情報
     */
    private MessageResource defaultResource;


    /**
     * ロケールごとのメッセージリソースの取得
     */
    private Map<Locale, MessageResource> resources;

    /**
     * リソース名を指定してインスタンスを生成する。
     * @param resourceName リソース名。形式は、{@link ResourceBundle}の名称。
     */
    public MessageResolver(final String resourceName) {
        this(resourceName, false, false);

    }

    /**
     * リソース名を指定してインスタンスを生成する。
     * @param resourceName リソース名。形式は、{@link ResourceBundle}の名称。
     * @param allowedNoDefault デフォルトのメッセージがない場合を許可するかどうか。
     * @param appendUserResouce クラスパスのルートにあるユーザ定義のメッセージソースも読み込むかどうか指定します。
     *      引数resourceNameの値が {@literal sample.SampleMessages}のとき、クラスパスのルート上にある「SampleMessages」を読み込みます。
     */
    public MessageResolver(final String resourceName, final boolean allowedNoDefault, final boolean appendUserResouce) {
        this.resourceName = resourceName;
        this.allowedNoDefault = allowedNoDefault;
        this.appendUserResource = appendUserResouce;
        this.defaultResource = loadDefaultResource(allowedNoDefault, appendUserResouce);
        this.resources = new ConcurrentHashMap<>();
    }

    private String getPropertyPath() {

        final String baseName = getResourceName().replaceAll("\\.", "/");
        String path = new StringBuilder()
                .append("/")
                .append(baseName)
                .append(".properties")
                .toString();

        return path;
    }

    private String[] getPropertyPath(final Locale locale) {

        final List<String> list = new ArrayList<>();

        final String baseName = getResourceName().replaceAll("\\.", "/");
        if(Utils.isNotEmpty(locale.getLanguage())) {
            String path = new StringBuilder()
                    .append("/")
                    .append(baseName)
                    .append("_").append(locale.getLanguage())
                    .append(".properties")
                    .toString();
            list.add(path);
        }

        if(Utils.isNotEmpty(locale.getLanguage()) && Utils.isNotEmpty(locale.getCountry())) {
            String path = new StringBuilder()
                    .append("/")
                    .append(baseName)
                    .append("_").append(locale.getLanguage())
                    .append("_").append(locale.getCountry())
                    .append(".properties")
                    .toString();
            list.add(path);
        }

        Collections.reverse(list);

        return list.toArray(new String[list.size()]);
    }

    /**
     * プロパティファイルから取得する。
     * <p>プロパティ名を補完する。
     * @param path プロパティファイルのパス名
     * @param allowedNoDefault デフォルトのメッセージがない場合を許可するかどうか。
     * @param appendUserResource クラスパスのルートにあるユーザ定義のメッセージソースも読み込むかどうか指定します。
     * @return
     */
    private MessageResource loadDefaultResource(final boolean allowedNoDefault, final boolean appendUserResource) {

        final String path = getPropertyPath();
        Properties props = new Properties();
        try {
            props.load(new InputStreamReader(MessageResolver.class.getResourceAsStream(path), "UTF-8"));
        } catch (NullPointerException | IOException e) {
            if(allowedNoDefault) {
                return MessageResource.NULL_OBJECT;
            } else {
                throw new RuntimeException("fail default properties. :" + path, e);
            }
        }

        final MessageResource resource = new MessageResource();

        final Enumeration<?> keys = props.propertyNames();
        while(keys.hasMoreElements()) {
            String key = (String) keys.nextElement();
            String value = props.getProperty(key);
            resource.addMessage(key, value);
        }

        // ユーザのリソースの読み込み
        if(appendUserResource) {
            resource.addMessage(loadUserResource(path));
        }

        return resource;

    }

    /**
     * 指定したロケールのリソースを取得する。
     * <p>該当するロケールのリソースが存在しない場合は、デフォルトのリソースを返す。
     * @param locale ロケールがnullの場合は、デフォルトのリソースを返す。
     * @return
     */
    MessageResource loadResource(final Locale locale) {

        if(locale == null) {
            return defaultResource;
        }

        if(resources.containsKey(locale)) {
            return resources.get(locale);
        }

        MessageResource localeResource = null;
        synchronized (resources) {
            for(String path : getPropertyPath(locale)) {

                try {
                    final Properties props = new Properties();
                    props.load(new InputStreamReader(MessageResolver.class.getResourceAsStream(path), "UTF-8"));

                    localeResource = new MessageResource();

                    final Enumeration<?> keys = props.propertyNames();
                    while(keys.hasMoreElements()) {
                        String key = (String) keys.nextElement();
                        String value = props.getProperty(key);
                        localeResource.addMessage(key, value);
                    }

                    // ユーザのリソースの読み込み
                    if(appendUserResource) {
                        localeResource.addMessage(loadUserResource(path));
                    }

                    break;

                } catch(NullPointerException | IOException e) {
                    continue;
                }

            }

            if(localeResource == null) {
                // 該当するリソースが見つからない場合は、デフォルトの値を返す。
                localeResource = defaultResource;
            }

            resources.put(locale, localeResource);
        }

        return localeResource;

    }

    /**
     *
     * @param basePath 基準となるプロパティファイルパス。「/」区切りで、拡張子、ロケール名が付いている。
     * @return 読み込んだメッセージソース。存在しない場合は、{@link MessageResource#NULL_OBJECT}を返す。
     */
    private MessageResource loadUserResource(final String basePath) {

        // ファイル名の切り出し
        final int index = basePath.lastIndexOf("/");
        if(index <= 0) {
            return MessageResource.NULL_OBJECT;
        }

        final String userPropertyPath = "/" + basePath.substring(index+1);
        try {
            Properties props = new Properties();
            props.load(new InputStreamReader(MessageResolver.class.getResourceAsStream(userPropertyPath), "UTF-8"));

            final MessageResource resource = new MessageResource();
            final Enumeration<?> keys = props.propertyNames();
            while(keys.hasMoreElements()) {
                String key = (String) keys.nextElement();
                String value = props.getProperty(key);
                resource.addMessage(key, value);
            }

            return resource;

        } catch(NullPointerException | IOException e) {
            return MessageResource.NULL_OBJECT;
        }

    }

    /**
     * リソース名の取得。
     * @return
     */
    public String getResourceName() {
        return resourceName;
    }

    /**
     * デフォルトのメッセージ情報がない場合を許可するかどうか。
     * @return
     */
    public boolean isAllowedNoDefault() {
        return allowedNoDefault;
    }

    /**
     * キーを指定してメッセージを取得する。
     * @param key メッセージキー
     * @return 存在しないキーの場合、nullを返す。
     */
    public String getMessage(final String key) {
        return defaultResource.getMessage(key);
    }

    /**
     * ロケールとキーを指定してメッセージを取得する。
     * <p>ロケールに該当する値を取得する。
     * @param locale ロケール
     * @param key メッセージキー
     * @return 該当するロケールのメッセージが見つからない場合は、デフォルトのリソースから取得する。
     */
    public String getMessage(final MSLocale locale, final String key) {
        if(locale == null) {
            return loadResource(null).getMessage(key);

        } else {
            return loadResource(locale.getLocale()).getMessage(key);
        }
    }

    /**
     * ロケールとキーを指定してメッセージを取得する。
     * @param locale ロケール
     * @param key メッセージキー
     * @param defaultValue
     * @return 該当するロケールのメッセージが見つからない場合は、引数で指定した'defaultValue'の値を返す。
     */
    public String getMessage(final MSLocale locale, final String key, final String defaultValue) {
        String message = getMessage(locale, key);
        return message == null ? defaultValue : message;
    }

    /**
     * ロケールとキーを指定してメッセージを取得する。
     * <p>ロケールに該当する値を取得する。
     * @param locale ロケール
     * @param key メッセージキー
     * @return 該当するロケールのメッセージが見つからない場合は、デフォルトのリソースから取得する。
     */
    public String getMessage(final Locale locale, final String key) {
        return loadResource(locale).getMessage(key);
    }

    /**
     * 値がnullの場合、defaultValueの値を返す。
     * @param locale ロケール
     * @param key メッセージキー
     * @param defaultValue
     * @return 該当するロケールのメッセージが見つからない場合は、引数で指定した'defaultValue'の値を返す。
     */
    public String getMessage(final Locale locale, final String key, final String defaultValue) {
        String message = getMessage(locale, key);
        return message == null ? defaultValue : message;
    }

    /**
     * 書式のロケールを優先して、キーに対するメッセージを取得する。
     * formatLocaleがnullのとき、runtimeLocaleから値を取得する。
     *
     * @since 0.10
     * @param formatLocale 書式に指定されているロケール。nullの場合がある。
     * @param runtimeLocale 実行時のロケール。
     * @param key メッセージキー
     * @return 該当するロケールのメッセージが見つからない場合は、デフォルトのリソースから取得する。
     */
    public String getMessage(final MSLocale formatLocale, Locale runtimeLocale, final String key) {

        if(formatLocale != null) {
            return getMessage(formatLocale, key);
        } else {
            return getMessage(runtimeLocale, key);
        }

    }

    /**
     * 書式のロケールを優先して、キーに対するメッセージを取得する。
     * formatLocaleがnullのとき、runtimeLocaleから値を取得する。
     *
     * @since 0.10
     * @param formatLocale 書式に指定されているロケール。nullの場合がある。
     * @param runtimeLocale 実行時のロケール。
     * @param key メッセージキー
     * @param defaultValue
     * @return 該当するロケールのメッセージが見つからない場合は、引数で指定した'defaultValue'の値を返す。
     */
    public String getMessage(final MSLocale formatLocale, Locale runtimeLocale, final String key, final String defaultValue) {

        if(formatLocale != null) {
            return getMessage(formatLocale, key, defaultValue);
        } else {
            return getMessage(runtimeLocale, key, defaultValue);
        }

    }


}