システム開発で思うところ

Javaで主にシステム開発をしながら思うところをツラツラを綴る。主に自分向けのメモ。EE関連の情報が少なく自分自身がそういう情報があったら良いなぁということで他の人の参考になれば幸い

全部入りなResourceBundleを考える

恥ずかしながら、これまで小さくローカル環境のシステムなので国際化も文字コードも考慮することがありませんでした。 これまで参画したプロジェクトではフレームワークなりで準備されたものを使っていたので、あまり意識していませんでした。

調べて見ると色々なことが分かりました。そして調べていくと良くあることなのですが「単純にやるならこれで十分。だけど本来ならこういうことも考えておきたい」という衝動です。
それだけでなく「全部入りで汎用なクラスがあったら後々便利かも!」という更なる衝動です。 YAGNI*1ということも考えるのですが、必要になったときに再び調べ直すのも面倒と思ったりしつつ、悶々としながら落としどころを勉強するくらいなら、いっそ自分なりの「これなら大体の事は大丈夫だろうクラス」を作ることにしました。

ということで、自分なりの全部入りResourseBundleというのを整理してみました。多分、これで当分はResourseBundleのことを自分としては忘れて良くなると思います。というか、そうなりたいです。

ちなみに、文字コードですが、自動で判別するような仕組みも考えようと思ったのですが、こちらについては良い案がありませんでした。個人的に色々と実験をしましたが、万能薬とまではいかないことと、自分が準備するファイルなのに毎回判定をするのは妥当なのかな?と考えた結果、文字コードは判別ではなく指定するやり方にしました。ということで、こちらはYAGNIと判断しました。

前置きが長くなりました。それ以上に長いコードと実行結果の貼り付けが以下に続きます。

実装

ResourceBundleのControlの拡張

/*
 *
 * 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.
 *
 *  Copyright © 2016 Yamashita,
 */
package com.mycompany.samples.resourse;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.FileSystemNotFoundException;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.ResourceBundle.Control;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.Singular;

/**
 * 通常のCotrolに加えて、文字コードを指定してResourseを取得する拡張をしたResourceBundle.Controlクラスを生成するBuilder<br>
 * 冗長的ではあるが拡張メソッドを把握するため、およびその用途メモを記すために、拡張対象メソッドを全てOverrideする.<br>
 *
 * @author Yamashita,
 */
@Builder
public class CustomControl extends Control {

    /**
     * formatに使用するXML用の定数
     */
    public static final List<String> FORMAT_XML = Collections.unmodifiableList(Arrays.asList("xml"));

    /* propertiesファイルの読み込み時のエンコードに使用する文字コード(例:UTF-8,SJIS etc) */
    private final String charCode;

    /* ResourceBundleの有効期限(0またはキャッシュ格納時刻からの正のミリ秒オフセット)、有効期限制御を無効にする場合はTTL_NO_EXPIRATION_CONTROL、キャッシュを無効にする場合はTTL_DONT_CACHE  */
    private final Long timeToLive;

    /* 読み込み対象とするformat */
    @Singular
    private final List<String> formats;

    /* リソースを取得する優先度のLocaleペアのリスト */
    @Singular
    private final List<TargetCandidateLocalePair> targetCandidateLocalePairs;

    /* fallbackが再帰的に呼び込まれている状態 = 無限ループ */
    @Getter(AccessLevel.PRIVATE)
    private boolean isFallBackInfiniteLoop = false;

    /**
     * 新しいResourceBundleを生成する.<br>
     * propertiesは、charCodeで指定した文字コードでエンコードしながら読み込む.<br>
     *
     * @see java.util.ResourceBundle.Control#newBundle(java.lang.String, java.util.Locale,
     * java.lang.String,java.lang.ClassLoader, boolean)
     *
     * @return 生成したResourceBundle
     * @throws java.lang.IllegalAccessException 配列以外のインスタンス作成、フィールドの設定または取得、メソッドの呼び出しを試みた場合の例外
     * @throws java.lang.InstantiationException 指定されたクラスオブジェクトのインスタンスを生成できない場合の例外
     * @throws java.io.IOException resourceファイルの取得時に発生時の例外
     */
    @Override
    public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
            throws IllegalAccessException, InstantiationException, IOException {

        this.isFallBackInfiniteLoop = false;
        if (CustomControl.FORMAT_CLASS.contains(format)) {
            return super.newBundle(baseName, locale, format, loader, reload);
        }

        if (CustomControl.FORMAT_PROPERTIES.contains(format)) {
            return this.newBundleProperties(baseName, locale, format, loader, reload);
        }

        if (CustomControl.FORMAT_XML.contains(format)) {
            return this.newBundleXML(baseName, locale, format, loader, reload);
        }

        throw new IllegalArgumentException("unknown format: " + format);
    }

    /**
     * propertiesファイルの読み込みResourceBundleを生成する.
     *
     * @param baseName
     * @param locale
     * @param format
     * @param loader
     * @param reload
     * @return 生成したResourceBundle
     * @throws IllegalAccessException
     * @throws InstantiationException
     * @throws IOException
     */
    private ResourceBundle newBundleProperties(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
            throws IllegalAccessException, InstantiationException, IOException {

        String bundleName = toBundleName(baseName, locale);
        ResourceBundle bundle = null;
        final String resourceName = (bundleName.contains("://"))
                                    ? null
                                    : toResourceName(bundleName, "properties");
        if (resourceName == null) {
            return bundle;
        }
        final ClassLoader classLoader = loader;
        final boolean reloadFlag = reload;
        InputStream stream = null;
        try {
            stream = AccessController.doPrivileged((PrivilegedExceptionAction<InputStream>) () -> {
                InputStream is = null;
                if (reloadFlag) {
                    URL url = classLoader.getResource(resourceName);
                    if (url != null) {
                        URLConnection connection = url.openConnection();
                        if (connection != null) {
                            // Disable caches to get fresh data for reloading.
                            connection.setUseCaches(false);
                            is = connection.getInputStream();
                        }
                    }
                } else {
                    is = classLoader.getResourceAsStream(resourceName);
                }
                return is;
            });
        } catch (PrivilegedActionException e) {
            throw (IOException) e.getException();
        }
        if (stream != null) {
            try {
                if (this.charCode != null) {
                    bundle = new PropertyResourceBundle(new InputStreamReader(stream, this.charCode));
                } else {
                    bundle = new PropertyResourceBundle(stream);
                }
            } finally {
                stream.close();
            }
        }
        return bundle;
    }

    /**
     * XMLファイルの読み込みResourceBundleを生成する.
     *
     * @param baseName
     * @param locale
     * @param format
     * @param loader
     * @param reload
     * @return 生成したResourceBundle
     * @throws IllegalAccessException
     * @throws InstantiationException
     * @throws IOException
     */
    private ResourceBundle newBundleXML(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
            throws IllegalAccessException, InstantiationException, IOException {

        ResourceBundle bundle = null;

        String bundleName = toBundleName(baseName, locale);
        final String resourceName = (bundleName.contains("://"))
                                    ? null
                                    : toResourceName(bundleName, "xml");

        InputStream stream = null;
        if (reload) {
            URL url = loader.getResource(resourceName);
            if (url != null) {
                URLConnection connection = url.openConnection();
                if (connection != null) {
                    // Disable caches to get fresh data for reloading.
                    connection.setUseCaches(false);
                    stream = connection.getInputStream();
                }
            }
        } else {
            stream = loader.getResourceAsStream(resourceName);
        }
        if (stream != null) {
            try (BufferedInputStream bis = new BufferedInputStream(stream)) {
                bundle = new XMLResourceBundle(bis);
            }
        }
        return bundle;
    }

    /**
     * リソースを取得する優先度を指定する.<br>
     *
     * 直接拡張をする場合の実装例(LocaleがJapaneseの時、なぜか英語を優先したい例).<br>
     * <pre>
     * {@code
     * if (locale.equals(Locale.JAPAN)) {
     *       return Arrays.asList(Locale.ENGLISH,
     *       locale,
     *       Locale.JAPANESE,
     *       Locale.ROOT);
     * } else {
     *       return super.getCandidateLocales(baseName, locale);
     * }
     * }
     * </pre>
     *
     * @see java.util.ResourceBundle.Control#getCandidateLocales(java.lang.String, java.util.Locale)
     *
     * @return 優先度順のロケールリスト
     */
    @Override
    public List<Locale> getCandidateLocales(String baseName, Locale locale) {

        Optional<List<Locale>> candidateLocales = targetCandidateLocalePairs.stream()
                .filter(pair -> pair.getTargetLocale().equals(locale))
                .map(TargetCandidateLocalePair::getCandidateLocales)
                .findAny();

        if (candidateLocales.isPresent() == false) {
            return super.getCandidateLocales(baseName, locale);
        }

        List<Locale> localeSetedCandidateLocales = new ArrayList<>();
        candidateLocales.get().stream()
                .forEachOrdered(candidateLocale -> {
                    localeSetedCandidateLocales.add(candidateLocale == null ? locale : candidateLocale);
                });
        return localeSetedCandidateLocales;
    }

    /**
     * デフォルトリソースを取得する.<br>
     * リソースバンドルの検索時、指定したロケールに対応したリソースバンドルが存在しない場合、デフォルトリソースではなく、デフォルトロケールに対応したリソースバンドルを検索してしまう.<br>
     * 本対応をしないと意図したリソースではないデフォルトロケールを取得してしまい国際化対応が正しく行われない.<br>
     *
     * @see java.util.ResourceBundle.Control#getFallbackLocale(java.lang.String, java.util.Locale)
     * @return デフォルトリソースのロケール
     */
    @Override
    public Locale getFallbackLocale(String baseName, Locale locale) {
        if (this.isFallBackInfiniteLoop) {
            throw new MissingResourceException("you set baseName is  [" + baseName + "]. fallback locale, but does not exist baseName resource file. check ResourceBundle.getBundle param 'baseName' and resource file name.", baseName, "");
        }
        this.isFallBackInfiniteLoop = true;
        return Locale.ROOT;
    }

    /**
     * ResourceBundleとして読み込み対象とする分類を返却する.<br>
     * デフォルトの優先度は高い順に、{@literal class > properties > xml}.<br>
     * 特定のフォーマットを指定した場合は、そちらを採用する.<br>
     * baseNameが同じで拡張子が異なるファイルが存在する場合、フォーマットを指定することで正しく処理できるようになる.<br>
     *
     * @see java.util.ResourceBundle.Control#getFormats(java.lang.String)
     * @return ResourceBundleとして読み込み対象とする分類リスト
     */
    @Override
    public List<String> getFormats(String baseName) {
        return this.formats.isEmpty()
               ? Collections.unmodifiableList(Arrays.asList("java.class",
                                                            "java.properties",
                                                            FORMAT_XML.get(0)))
               : Collections.unmodifiableList(this.formats);
    }

    /**
     * キャッシュ内のロード済みバンドルの有効期限を取得する.<br>
     * キャッシュ内のロード済みバンドルに有効期限を設ける場合はその時間(0またはキャッシュ格納時刻からの正のミリ秒オフセット)、有効期限制御を無効にする場合はTTL_NO_EXPIRATION_CONTROL、キャッシュを無効にする場合はTTL_DONT_CACHE。
     * <br>
     *
     * @see java.util.ResourceBundle.Control#getTimeToLive(java.lang.String, java.util.Locale)
     * @return バンドルの有効期限
     */
    @Override
    public long getTimeToLive(String baseName, Locale locale) {
        return this.timeToLive == null
               ? super.getTimeToLive(baseName, locale)
               : this.timeToLive;
    }

    /**
     * キャッシュ再ロード判定.<br>
     * キャッシュ内で有効期限の切れたbundleを再ロードする必要があるかどうかを、loadTimeに指定されたロード時刻やその他のいくつかの条件に基づいて判定する(継承元クラスのコメント抜粋).<br>
     * (拡張仕様が無いので継承元の操作をそのまま行う)<br>
     *
     * @see java.util.ResourceBundle.Control#needsReload(java.lang.String, java.util.Locale, java.lang.String,
     * java.lang.ClassLoader, java.util.ResourceBundle, long)
     * @return キャッシュ内で有効期限の切れたbundleを再ロード要否
     */
    @Override
    public boolean needsReload(String baseName, Locale locale, String format, ClassLoader loader, ResourceBundle bundle, long loadTime) {
        // ここを実装
        return super.needsReload(baseName, locale, format, loader, bundle, loadTime);
    }

    /**
     * BaseNameとlocaleの組み合わせで取得対象となるプロパティファイル名を編集する.<br>
     * (拡張仕様が無いので継承元の操作をそのまま行う)<br>
     *
     * @see java.util.ResourceBundle.Control#toBundleName(java.lang.String, java.util.Locale)
     * @return 取得対象のプロパティファイル名
     */
    @Override
    public String toBundleName(String baseName, Locale locale) {
        // ここを実装
        return super.toBundleName(baseName, locale);
    }

}
/*
 *
 * 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.
 *
 *  Copyright © 2016 Yamashita,
 */
package com.mycompany.samples.resourse;

import java.util.List;
import java.util.Locale;
import lombok.Builder;
import lombok.Getter;
import lombok.Singular;

/**
 * CustomControlで使用するCandidateLocaleのペア.<br>
 * candidateLocaleにnullを指定した場合、{@link com.mycompany.samples.resourse.CustomControl#getCandidateLocales(java.lang.String, java.util.Locale)
 * }getCandidateLocalesで候補となるLocaleを取得する際、引数として指定したlocaleを処理対象として置き換えて使用する.
 *
 * @author Yamashita,
 */
@Builder @Getter
public class TargetCandidateLocalePair {

    private final Locale targetLocale;

    @Singular
    private final List<Locale> candidateLocales;

}
/*
 *
 * 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.
 *
 *  Copyright © 2016 Yamashita,
 */
package com.mycompany.samples.resourse;

import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Properties;
import java.util.ResourceBundle;

/**
 * XML形式のResourceBundleクラス.<br>
 *
 * XMLのフォーマット形式(http://java.sun.com/dtd/properties.dtd)
 *
 * <pre>
 * {@code
 * <!--
 * Copyright 2006 Sun Microsystems, Inc.  All rights reserved.
 * -->
 *
 * <!-- DTD for properties -->
 *
 * <!ELEMENT properties ( comment?, entry* ) >
 *
 * <!ATTLIST properties version CDATA #FIXED "1.0">
 *
 * <!ELEMENT comment (#PCDATA) >
 *
 * <!ELEMENT entry (#PCDATA) >
 *
 * <!ATTLIST entry key CDATA #REQUIRED>
 *
 * }
 * </pre>
 *
 * @author Yamashita,
 */
public class XMLResourceBundle extends ResourceBundle {

    private final Properties properties;

    /**
     * XML形式のResourceBundleのコンストラクタ<br>
     * Propertiesクラスを使用してXMLファイルを読み込む
     *
     * @param stream プロパティファイルのInputStream
     * @throws IOException InputStreamの入出力時に発生した例外
     */
    public XMLResourceBundle(InputStream stream) throws IOException {
        properties = new Properties();
        properties.loadFromXML(stream);
    }

    @Override
    public Object handleGetObject(String key) {
        if (key == null) {
            throw new NullPointerException();
        }
        return properties.get(key);
    }

    @Override
    public Enumeration<String> getKeys() {
        return (Enumeration<String>) properties.propertyNames();
    }
}

ポイントになりそうなところは、コメントに極力書いたつもりです。
大きいところは、fallback制御のところと、文字コード指定のところでしょうか。参考にさせていただいた資料を元についでにXMLも処理できるようにしました。

実行クラス(テストケースもどき)

/*
 *  Copyright © 2016 Yamashita,Takahiro
 */
package com.mycompany.samples.resourse.runner;

import com.mycompany.samples.resourse.CustomControl;
import com.mycompany.samples.resourse.TargetCandidateLocalePair;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.Locale;
import java.util.ResourceBundle;

/**
 *
 * @author Yamashita,Takahiro
 */
public class ResourseTest {

    public void execute() {
        this.useAsciiProperties();
        this.useUTF8PropertiesNoSetLocaleIsDefaultLocale();
        this.useUTF8PropertiesNoExsistLocaleIsDefaultResource();
        this.useSJISXML();
        this.useSJISSameNamePropertiesXMLnotSetFormat();
        this.useSJISSameNamePropertiesXMLsetSimpleFormat();
        this.useSJISSameNamePropertiesXMLsetMultiFormat();
        this.useUTF8candidateLocalesMatchTargetLocale();
        this.useUTF8candidateLocalesNotMatchTargetLocale();
        this.useUTF8candidateLocalesNotExistTargetLocale();
        this.useUTF8candidateLocalesExistsNullTargetLocale();
        this.useUTF8candidateLocalesExistsTargetLocaleButNotSetLocale();
        this.useNoCache();
        this.useUTF8Cache1();
        this.useUTF8Cache2();
        this.useUTF8SleepTimeLtInterval();
        this.useUTF8IntervalLtSleepTime();

    }

    private void printLog(ResourceBundle bundle) {
        Enumeration<String> enums = bundle.getKeys();
        while (enums.hasMoreElements()) {
            String key = enums.nextElement();
            System.out.println("key= " + key + ":value= " + bundle.getString(key));
            System.out.println();
        }
    }

    /**
     * 標準のASCIIコードのpropertiesを参照する
     */
    private void useAsciiProperties() {
        CustomControl control = CustomControl.builder().build();
        ResourceBundle bundle = ResourceBundle.getBundle("message", control);
        System.out.println("useAsciiProperties select message.properties");
        this.printLog(bundle);
    }

    /**
     * 文字コードにUTF8を指定。ロケールの指定をしない場合は、デフォルトロケール(ロケールがja_JP)のpropertiesを参照する
     */
    private void useUTF8PropertiesNoSetLocaleIsDefaultLocale() {
        CustomControl control = CustomControl.builder().charCode(StandardCharsets.UTF_8.toString()).build();
        ResourceBundle bundle = ResourceBundle.getBundle("utf8", control);
        System.out.println("useUTF8PropertiesNoSetLocaleIsDefaultLocale is default locale. select utf8_ja_JP.properties");
        this.printLog(bundle);
    }

    /**
     * 文字コードにUTF8を指定。指定ロケールのpropertiesが存在しない場合は、デフォルトリソース(ロケール無)のpropertiesを参照する
     */
    private void useUTF8PropertiesNoExsistLocaleIsDefaultResource() {
        CustomControl control = CustomControl.builder()
                .charCode(StandardCharsets.UTF_8.toString()).build();
        ResourceBundle bundle = ResourceBundle.getBundle("utf8", Locale.CHINESE, control);
        System.out.println("useUTF8PropertiesNoExsistLocaleIsDefaultResource is default recourse. select nolabel utf8.properties");
        this.printLog(bundle);
    }

    /**
     * xmlのリソースを参照する.XMLの場合は文字コードの指定は不要
     */
    private void useSJISXML() {
        //XML no need charCode set (set ignore)
        CustomControl control = CustomControl.builder().build();
        ResourceBundle bundle = ResourceBundle.getBundle("SJIS", control);
        System.out.println("useSJISXML is default recourse. select nolabel SJIS.xml");
        this.printLog(bundle);
    }

    /**
     * xmlとpropertiesに同名のbaseNameの資産が存在する場合は、getFormats()の優先度(Class-Properties-XML)に従ってリソースを参照する.<br>
     * 本ケースはpropertiesが参照されるが、文字コードの指定が無いため文字化けが発生する
     */
    private void useSJISSameNamePropertiesXMLnotSetFormat() {
        CustomControl control = CustomControl.builder().build();
        ResourceBundle bundle = ResourceBundle.getBundle("SJIS-SAME", control);
        System.out.println("useSJISSameNamePropertiesXMLnotSetFormat prioritize properties. select SJIS-SAME.properties garbled characters (T^T)");
        this.printLog(bundle);
    }

    /**
     * xmlとpropertiesに同名のbaseNameの資産が存在するが、formatsにXMLを指定しているためXMLを参照し、文字化けも発生しない
     */
    private void useSJISSameNamePropertiesXMLsetSimpleFormat() {
        CustomControl control = CustomControl.builder().formats(CustomControl.FORMAT_XML).build();
        ResourceBundle bundle = ResourceBundle.getBundle("SJIS-SAME2", control);
        System.out.println("useSJISSameNamePropertiesXMLsetSingleFormat select xml. select SJIS-SAME2.xml no garbled characters");
        this.printLog(bundle);
    }

    /**
     * xmlとpropertiesに同名のbaseNameの資産が存在する.<br>
     * しかし、formatsの優先度に従ってpropertiesリソースを参照し、かつ文字コード指定に従って変換する
     */
    private void useSJISSameNamePropertiesXMLsetMultiFormat() {
        CustomControl control = CustomControl.builder()
                .charCode("SJIS")
                .formats(CustomControl.FORMAT_DEFAULT)
                .formats(CustomControl.FORMAT_XML)
                .build();
        ResourceBundle bundle = ResourceBundle.getBundle("SJIS-SAME3", control);
        System.out.println("useSJISSameNamePropertiesXMLsetMultiFormat select properties. select SJIS-SAME3.properties no garbled characters");
        this.printLog(bundle);
    }

    /**
     * ロケールがJAPANESEの時の候補Localeの第一優先がUS(日本語じゃなくて、英語のメッセージが出力される
     */
    private void useUTF8candidateLocalesMatchTargetLocale() {
        CustomControl control = CustomControl.builder()
                .charCode("UTF-8")
                .targetCandidateLocalePair(
                        TargetCandidateLocalePair.builder()
                        .targetLocale(Locale.JAPANESE)
                        .candidateLocale(Locale.US)
                        .candidateLocale(Locale.JAPAN)
                        .candidateLocale(Locale.ROOT)
                        .build())
                .build();

        ResourceBundle bundle = ResourceBundle.getBundle("utf8", Locale.JAPANESE, control);
        System.out.println("useUTF8candidateLocalesMatchTargetLocale select properties. Locale US");
        this.printLog(bundle);
    }

    /**
     * 優先度指定をしたロケール以外の場合は、対象ロケール本来の優先度に従って参照する.<br>
     * 本ケースの場合、USを指定しているので、優先度指定は無視される
     */
    private void useUTF8candidateLocalesNotMatchTargetLocale() {
        CustomControl control = CustomControl.builder()
                .charCode("UTF-8")
                .targetCandidateLocalePair(
                        TargetCandidateLocalePair.builder()
                        .targetLocale(Locale.JAPANESE)
                        .candidateLocale(Locale.JAPAN)
                        .candidateLocale(Locale.ENGLISH)
                        .candidateLocale(Locale.US)
                        .candidateLocale(Locale.ROOT)
                        .build())
                .build();

        ResourceBundle bundle = ResourceBundle.getBundle("utf8", Locale.US, control);
        System.out.println("useUTF8candidateLocalesNotMatchTargetLocale select properties. Locale US");
        this.printLog(bundle);
    }

    /**
     * 優先度指定をしたロケール以外の場合は、対象ロケール本来の優先度に従って参照する.<br>
     * 本ケースの場合、CHINAを指定しているので、優先度も無視されるし、デフォルトリソースが使用される
     */
    private void useUTF8candidateLocalesNotExistTargetLocale() {
        CustomControl control = CustomControl.builder()
                .charCode("UTF-8")
                .targetCandidateLocalePair(
                        TargetCandidateLocalePair.builder()
                        .targetLocale(Locale.JAPANESE)
                        .candidateLocale(Locale.JAPAN)
                        .candidateLocale(Locale.ROOT)
                        .build())
                .build();

        ResourceBundle bundle = ResourceBundle.getBundle("utf8", Locale.CHINA, control);
        System.out.println("useUTF8candidateLocalesNotExistTargetLocale select properties. Locale default");
        this.printLog(bundle);
    }

    /**
     * 優先度指定をしたロケールにNullを指定した場合は、パラメータで指定したロケールに置き換えて参照する.<br>
     * 本ケースの場合、優先度の順で判断した場合、一番始めヒットするのはJAPANESEのリソースであり、それが参照される.
     */
    private void useUTF8candidateLocalesExistsNullTargetLocale() {
        CustomControl control = CustomControl.builder()
                .charCode("UTF-8")
                .targetCandidateLocalePair(
                        TargetCandidateLocalePair.builder()
                        .targetLocale(Locale.JAPAN)
                        .candidateLocale(Locale.FRANCE)
                        .candidateLocale(null)
                        .build())
                .build();

        ResourceBundle bundle = ResourceBundle.getBundle("utf8", Locale.JAPAN, control);
        System.out.println("useUTF8candidateLocalesExistsNullTargetLocale select properties. Locale null = JAPAN");
        this.printLog(bundle);
    }

    /**
     * ロケールの指定をしない場合は、デフォルトロケール(ロケールがja_JP)のpropertiesを参照して、優先度指定は無視する.<br>
     * ※デフォルトロケールで優先度を参照もしないし、デフォルトリソースを参照することもしない.
     */
    private void useUTF8candidateLocalesExistsTargetLocaleButNotSetLocale() {
        CustomControl control = CustomControl.builder()
                .charCode("UTF-8")
                .targetCandidateLocalePair(
                        TargetCandidateLocalePair.builder()
                        .targetLocale(Locale.JAPANESE)
                        .candidateLocale(Locale.ROOT)
                        .build())
                .targetCandidateLocalePair(
                        TargetCandidateLocalePair.builder()
                        .targetLocale(Locale.JAPAN)
                        .candidateLocale(Locale.ROOT)
                        .build())
                .build();

        ResourceBundle bundle = ResourceBundle.getBundle("utf8", control);
        System.out.println("useUTF8candidateLocalesExistsTargetLocaleButNotSetLocale select properties. Locale ja_JP");
        this.printLog(bundle);
    }

    /**
     * データをキャッシュしない(CustomControl.TTL_DONT_CACHE).<br>
     * Controlの文字コードを変更すると"utf8NoCache"で同名のResourceBundleも参照文字コードも書き換わる
     */
    private void useNoCache() {
        CustomControl control = CustomControl.builder().timeToLive(CustomControl.TTL_DONT_CACHE).build();
        ResourceBundle bundle = ResourceBundle.getBundle("utf8NoCache", control);
        System.out.println("useNoCache before properties garbled characters. because setting charCode is default(ASCII)");
        this.printLog(bundle);

        control = CustomControl.builder().charCode("UTF-8").timeToLive(CustomControl.TTL_DONT_CACHE).build();
        bundle = ResourceBundle.getBundle("utf8NoCache", control);
        System.out.println("useNoCache after properties. no garbled characters. because setting charCode is UTF-8");
        this.printLog(bundle);
    }

    /**
     * データをキャッシュする(CustomControl.TTL_NO_EXPIRATION_CONTROL).<br>
     * Controlの文字コードを変更しても"utf8Cache1"で同名のキャッシュが書き換わらないので文字が化けない
     */
    private void useUTF8Cache1() {
        CustomControl control = CustomControl.builder().charCode("UTF-8").timeToLive(CustomControl.TTL_NO_EXPIRATION_CONTROL).build();
        ResourceBundle bundle = ResourceBundle.getBundle("utf8Cache1", control);
        System.out.println("useNoCache before properties no garbled characters. because setting charCode is UTF-8");
        this.printLog(bundle);

        control = CustomControl.builder().charCode("SJIS").timeToLive(CustomControl.TTL_NO_EXPIRATION_CONTROL).build();
        bundle = ResourceBundle.getBundle("utf8Cache1", control);
        System.out.println("useNoCache after properties. no garbled characters. because use cash. setting charCode is UTF-8");
        this.printLog(bundle);
    }

    /**
     * データをキャッシュする(CustomControl.TTL_NO_EXPIRATION_CONTROL).<br>
     * Controlの文字コードを変更しても"utf8Cache1"で同名のキャッシュが書き換わらないので文字が化けない
     */
    private void useUTF8Cache2() {
        CustomControl control = CustomControl.builder().timeToLive(CustomControl.TTL_NO_EXPIRATION_CONTROL).build();
        ResourceBundle bundle = ResourceBundle.getBundle("utf8Cache2", control);
        System.out.println("useCache2 before properties garbled characters. because setting charCode is default(ASCII)");
        this.printLog(bundle);

        control = CustomControl.builder().charCode("UTF-8").timeToLive(CustomControl.TTL_NO_EXPIRATION_CONTROL).build();
        bundle = ResourceBundle.getBundle("utf8Cache2", control);
        System.out.println("useCache2 after properties. garbled characters. because use cash. setting charCode is default(ASCII)");
        this.printLog(bundle);
    }

    /**
     * timeToLiveが停止時間よりも大きいのでキャッシュされた"utf8IntervalCache1"を参照する
     */
    private void useUTF8SleepTimeLtInterval() {
        CustomControl control = CustomControl.builder().charCode("UTF-8").timeToLive(1000L).build();
        ResourceBundle bundle = ResourceBundle.getBundle("utf8IntervalCache1", control);
        System.out.println("useUTF8SleepTimeLtInterval before properties UTF-8");
        this.printLog(bundle);

        try {
            System.err.println("sleeptime < timeToLive");
            Thread.sleep(1 * 1);
        } catch (InterruptedException e) {
            System.err.println(e);
        }

        control = CustomControl.builder().charCode("SJIS").build();
        bundle = ResourceBundle.getBundle("utf8IntervalCache1", control);
        System.out.println("useUTF8SleepTimeLtInterval after properties UTF-8. no garbled characters. because use cashe");
        this.printLog(bundle);
    }

    /**
     * 停止時間がtimeToLiveよりも大きいので改めて"utf8IntervalCache1"を参照したときに、ファイルが更新されていたらキャッシュを参照せず、再取得する
     */
    private void useUTF8IntervalLtSleepTime() {
        CustomControl control = CustomControl.builder().charCode("UTF-8").timeToLive(1L).build();
        ResourceBundle bundle = ResourceBundle.getBundle("utf8IntervalCache2", control);
        System.out.println("useUTF8IntervalLtSleepTime before properties UTF-8");
        this.printLog(bundle);

        try {
            System.err.println("timeToLive < sleeptime");
            System.err.println("edit utf8IntervalCache2.properties while sleeping !!");
            Thread.sleep(1 * 10000L);
        } catch (InterruptedException e) {
            System.err.println(e);
        }

        control = CustomControl.builder().charCode("SJIS").build();
        bundle = ResourceBundle.getBundle("utf8IntervalCache2", control);
        System.out.println("useUTF8IntervalLtSleepTime after properties UTF-8. garbled characters. because no use cashe");
        this.printLog(bundle);
    }
}

英語は実行結果で、ざっと分かれば良いレベルの適当なやっつけです。
最後のインターバルのテストは手作業が入っています。 おおよその使い方は、この実行クラスの例を見てもらえれば分かると思います。

参照するproperties

記事上は全て読めるようにしていますがmessage.propertiesがASCIIコードであることを除き、その他のファイルはプリフィックスに記した文字コードで作成しています。

ファイル名と内容

message.properties

test=ascii(default)


SJIS.xml

<?xml version="1.0" encoding="Shift_JIS" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
  <comment />
  <entry key="sjis.xml">XML読み込み(SJIS)</entry>
</properties>


SJIS-SAME.properties

test=properties:同名XMLあり1(SJIS)


SJIS-SAME.xml

<?xml version="1.0" encoding="Shift_JIS" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
  <comment />
  <entry key="sjis1.xml">xml:同名propertiesあり1(SJIS)</entry>
</properties>


SJIS-SAME2.properties

test=properties:同名XMLあり2(SJIS)


SJIS-SAME2.xml

<?xml version="1.0" encoding="Shift_JIS" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
  <comment />
  <entry key="sjis2.xml">xml:同名propertiesあり2(SJIS)</entry>
</properties>


SJIS-SAME3.properties

test=properties:同名XMLあり3(SJIS)


SJIS-SAME3.xml

<?xml version="1.0" encoding="Shift_JIS" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
  <comment />
  <entry key="sjis3.xml">xml:同名propertiesあり3(SJIS)</entry>
</properties>


utf8.properties

test=UTF8のテスト(default)


utf8_en_US.properties

test=UTF8english


utf8_ja_JP.properties

test=UTF8のテスト(JP)


utf8Cache1.properties

test=UTF8のテスト(cache1)


utf8Cache2.properties

test=UTF8のテスト(cache2)


utf8IntervalCache1.properties

test=UTF8のテスト(IntervalCache1)


utf8IntervalCache2.properties

test=UTF8のテスト(IntervalCache2)


utf8NoCache.properties

test=UTF8のテスト(NoCache)


実行結果

--- exec-maven-plugin:1.2.1:exec (default-cli) @ samples ---
useAsciiProperties select message.properties
key= test:value= ascii(default)

useUTF8PropertiesNoSetLocaleIsDefaultLocale is default locale. select utf8_ja_JP.properties
key= test:value= UTF8のテスト(JP)

useUTF8PropertiesNoExsistLocaleIsDefaultResource is default recourse. select nolabel utf8.properties
key= test:value= UTF8のテスト(default)

useSJISXML is default recourse. select nolabel SJIS.xml
key= sjis.xml:value= XML読み込み(SJIS)

useSJISSameNamePropertiesXMLnotSetFormat prioritize properties. select SJIS-SAME.properties garbled characters (T^T)
key= test:value= properties:? ̄??XML????1(SJIS)

useSJISSameNamePropertiesXMLsetSingleFormat select xml. select SJIS-SAME2.xml no garbled characters
key= sjis2.xml:value= xml:同名propertiesあり2(SJIS)

useSJISSameNamePropertiesXMLsetMultiFormat select properties. select SJIS-SAME3.properties no garbled characters
key= test:value= properties:同名XMLあり3(SJIS)

useUTF8candidateLocalesMatchTargetLocale select properties. Locale US
key= test:value= UTF8english

useUTF8candidateLocalesNotMatchTargetLocale select properties. Locale US
key= test:value= UTF8english

useUTF8candidateLocalesNotExistTargetLocale select properties. Locale default
key= test:value= UTF8のテスト(default)

useUTF8candidateLocalesExistsNullTargetLocale select properties. Locale null = JAPAN
key= test:value= UTF8のテスト(JP)

useUTF8candidateLocalesExistsTargetLocaleButNotSetLocale select properties. Locale ja_JP
key= test:value= UTF8のテスト(JP)

useNoCache before properties garbled characters. because setting charCode is default(ASCII)
key= test:value= UTF8????????????(NoCache)

useNoCache after properties. no garbled characters. because setting charCode is UTF-8
key= test:value= UTF8のテスト(NoCache)

useNoCache before properties no garbled characters. because setting charCode is UTF-8
key= test:value= UTF8のテスト(cache1)

useNoCache after properties. no garbled characters. because use cash. setting charCode is UTF-8
key= test:value= UTF8のテスト(cache1)

useCache2 before properties garbled characters. because setting charCode is default(ASCII)
key= test:value= UTF8????????????(cache2)

useCache2 after properties. garbled characters. because use cash. setting charCode is default(ASCII)
key= test:value= UTF8????????????(cache2)

useUTF8SleepTimeLtInterval before properties UTF-8
key= test:value= UTF8のテスト(IntervalCache1)

sleeptime < timeToLive
useUTF8SleepTimeLtInterval after properties UTF-8. no garbled characters. because use cashe
key= test:value= UTF8のテスト(IntervalCache1)

useUTF8IntervalLtSleepTime before properties UTF-8
key= test:value= UTF8のテスト(IntervalCache2)

timeToLive < sleeptime
edit utf8IntervalCache2.properties while sleeping !!
useUTF8IntervalLtSleepTime after properties UTF-8. garbled characters. because no use cashe
key= test:value= UTF8縺ョ繝?繧ケ繝?(IntervalCache2edit)

最後のテストケースはスリープ中に手作業で直接ファイルを更新
更新前
f:id:vermeer-1977:20161228135327p:plain

更新後
f:id:vermeer-1977:20161228135328p:plain

参考リンク

ResourseBundle

国際化対応や、ResourseBundleそのものの詳細は「Java SE 6完全攻略」から目を通すのが一番良いと思います。

文字コード変換

考慮はしませんでしたが調査足跡という意味でリンクだけ残しておこうと思います。ひょっとしたらファイルアップロード関連のチェックで改めて検討をする可能性もあるので。

著作権

本題とは関係ないですが、外部ライブラリを使用するにあたって調べたライセンス関連のリンク。


さいごに

やってみたものの、正直な感想は、こういう拡張は作法として良いのだろうか?という思いです。*2
いずれにせよ 全部入りなResourceBundleを とりあえず作れて個人的には満足です。
今後、今回のようなボリュームのあるものを記事にするときのためにGitHubについても勉強をした方が良いのかもしれないと新たな課題も見つかりました。

追記:2017/1/9

GitHubでプロジェクトを作成しました。
GitHub - vermeer-1977-blog/resource-bundle: 全部入りのResourceBundle

*1:"You ain't gonna need it"

*2:むしろ、どなたかに、これはダメだよ、と指摘していただけると非常にありがたいです。