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

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

複数のResourceBundleを参照できるようにする

はじめに

そもそもResourceBundle#getBundleに対して複数のリソースを読み込ませたいという要求自体があまりないのかもしれません。

もしそれをしたいのであれば、ListなりにResourceBundleを保持しておいて検索すれば事足りると思います。

ですが、BeanValidationでメッセージ出力をしているときに遭遇したのはResourceBundleをインターフェースとしたクラスを引数として渡さないといけないというケースでした。

色々なやり方があるとは思いますが、私はnewBundleを拡張するやり方でやってみました。

具体的には

vermeer.hatenablog.jp

で自作したライブラリの拡張です。

実装

方式としては、参照資産を全て1つのstreamに結合して読み込ませる、というだけです。

今回の実験で得た知識はSequenceInputStreamです。

なんと、JDK1.0からのクラスです。知りませんでした。

それをリソースファイル分、再帰処理で結合していきました(#concateInputStrem)。

ちょっとした工夫としては、リソースファイルが改行で終わっていないケースを考慮して 結合時に改行情報を差し込むようにしたところでしょうか。

protected ResourceBundle newBundleProperties(String baseName, Locale locale, ClassLoader loader, boolean reload)
        throws IllegalAccessException, InstantiationException, IOException {
    InputStream stream = readBaseNames(baseName, locale, loader, reload);
    ResourceBundle bundle = null;
    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;
}

InputStream readBaseNames(String baseName, Locale locale, ClassLoader loader, boolean reload) throws IOException {
    String[] _baseNames = baseName.split(",");
    List<InputStream> propertiesInputStreams = new ArrayList<>();
    for (String _baseName : _baseNames) {
        InputStream propertiesInputStream = readProperties(_baseName.trim(), locale, loader, reload);
        /* propertiesの結合時に使用する改行文字 */
        InputStream lineSeparator = new ByteArrayInputStream(System.lineSeparator().getBytes(StandardCharsets.UTF_8));
        if (propertiesInputStream != null) {
            InputStream inputStreamWithLineSeparator = new SequenceInputStream(propertiesInputStream, lineSeparator);
            propertiesInputStreams.add(inputStreamWithLineSeparator);
        }
    }

    if (propertiesInputStreams.isEmpty()) {
        return null;
    }

    Iterator<InputStream> inputStreamsIterator = propertiesInputStreams.iterator();
    InputStream inputStream = inputStreamsIterator.next();
    if (propertiesInputStreams.size() == 1) {
        return inputStream;
    }
    return concateInputStrem(inputStreamsIterator, inputStream);
}

InputStream readProperties(String baseName, Locale locale, ClassLoader loader, boolean reload) throws IOException {
    String bundleName = toBundleName(baseName, locale);
    final String resourceName = (bundleName.contains("://"))
                                ? null
                                : toResourceName(bundleName, "properties");
    if (resourceName == null) {
        return null;
    }
    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();
    }
    return stream;
}

/**
 * InputStreamを結合して返却します.
 *
 * @param inputStreamsIterator 参照資産のイテレーター
 * @param concatedInputStream 結合先となるInputStream
 * @return 結合したInputStream
 */
InputStream concateInputStrem(Iterator<InputStream> inputStreamsIterator, InputStream concatedInputStream) throws IOException {
    if (inputStreamsIterator.hasNext() == false) {
        return concatedInputStream;
    }
    InputStream inputStream = inputStreamsIterator.next();
    SequenceInputStream sequenceInputStream = new SequenceInputStream(concatedInputStream, inputStream);
    return concateInputStrem(inputStreamsIterator, sequenceInputStream);
}

Code

Bitbucket

さいごに

ResoueceBundleそのものを拡張する、というやり方も試したりしたのですが、思ったようにできませんでした。

このやり方であれば特殊なことはしていないので(単純に読み込むstreamを結合しただけ)、コードのボリュームの割には単純なやり方なように思います。

ちょっと迷ったのは、Controlに追加したいPropertyFileを指定する、というやり方です。 こちらの案だったら、引数の型を配列なり、リストなりにして「複数指定していますよ」ということをメソッドで表現できます。

最終的に本方式にしたのは「ファイルの指定箇所が分散すると3日後の自分が忘れそうだ」というのが大きく、もう1つはControlをResourceBundleの生成都度置き換えるという実装思想が、しっくり来なかったからです。