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

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:むしろ、どなたかに、これはダメだよ、と指摘していただけると非常にありがたいです。

JSFのSelectItemにEnum以外の値を指定する

過去の記事への追記です。

vermeer.hatenablog.jp

セレクトボックスにEnum以外の「選択無し」という値を指定したい場合があると思います。

コードの貼り付けだけになりますが、誰かのご参考になれば
やっていることは静的ページもAjaxを使った動的ページも同じなのでAjaxのコードだけです。

ポイント

xhtml

"h:selectOneMenuoptionに選択無しの要素を追加します
<option jsfc="f:selectItem" itemLabel="#{msg['noselect']}"></option>

コード

codeがnullの場合の振舞いを追加実装

実装例

xhtml

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:h="http://xmlns.jcp.org/jsf/html">

    <h:head>
        <meta charset="UTF-8" />
        <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
        <title>JSF SELECT ITEM</title>
    </h:head>
    <body>
        <form jsfc="h:form">
            <h1>JSF SELECT ITEM PATTERN Ajax</h1>
            <dir>
                <h2>Enum has code and property </h2>
                <select jsfc="h:selectOneMenu" value="#{selectItemAjax.selectItemValueEnumCodeProperty}">
                    <option jsfc="f:selectItem" itemLabel="#{msg['noselect']}"></option>
                    <option jsfc="f:selectItems" value="#{selectItemAjax.enumCodeProperty.values}"></option>
                    <f:ajax listener="#{selectItemAjax.changeItem}" render="map" event="change"/>
                </select>
            </dir>
            <hr />
            <dir>
                <h2>Map</h2>
                <select id="map" jsfc="h:selectOneMenu"  value="#{selectItemAjax.selectItemValueMap}">
                    <option jsfc="f:selectItems" value="#{selectItemAjax.selectItemMap.values}"></option>
                </select>
            </dir>
            <hr />
            <dir>
                <input type="submit" jsfc="h:commandButton" value="更新" action="#{selectItemAjax.submit()}"/>
            </dir>
        </form>
    </body>
</html>

Controller(ManagedBean)

package com.mycompany.samplejsf.domain.selectitem;

@Named(value = "selectItemAjax")
@SessionScoped
@NoArgsConstructor @Getter
public class SelectItemAjaxController implements Serializable {

    private static final long serialVersionUID = 1L;

    @Setter
    private Integer selectItemValueEnumCodeProperty;

    @Setter
    private Integer selectItemValueMap;

    private JsfSelectItem enumCodeProperty;
    private JsfSelectItem selectItemMap;

    private Map<Integer, String> dummuyMap;

    @PostConstruct
    public void init() {
        this.selectItemValueEnumCodeProperty = Gender.FEMALE.getCode();
        this.enumCodeProperty = JsfSelectItem.of(Gender.class);
        this.replaceSelectItemMap();
        this.printLog("init");
    }

    public void submit() {
        this.replaceSelectItemMap();
        this.printLog("submit");
    }

    public void changeItem(AjaxBehaviorEvent event) {
        this.selectItemValueMap = (Integer) JsfAjaxBehaviorEvent.of(event).getValue();
        this.replaceSelectItemMap();
        this.printLog("changeItem");
    }

    private void replaceSelectItemMap() {
        try {
            switch (EnumCodeProperty.codeOf(Gender.class, this.selectItemValueEnumCodeProperty)) {
                case MALE:
                    this.maleSnack();
                    break;
                case FEMALE:
                    this.femaleSnack();
                    break;
                default:
                    this.noSnack();
            }
        } catch (NullPointerException | IllegalArgumentException ex) {
            this.noSnack();
        }
    }

    private void maleSnack() {
        Map<Integer, String> map = new LinkedHashMap<>();
        map.put(1, "大福");
        map.put(2, "おはぎ");
        map.put(3, "みたらしだんご");
        map.put(4, "せんべい");
        this.selectItemValueMap = map.keySet().iterator().next();
        this.selectItemMap = JsfSelectItem.of(map);
        this.dummuyMap = map;
    }

    private void femaleSnack() {
        Map<Integer, String> map = new LinkedHashMap<>();
        map.put(5, "チョコ");
        map.put(6, "クッキー");
        map.put(7, "プリン");
        map.put(8, "ゼリー");
        this.selectItemValueMap = map.keySet().iterator().next();
        this.selectItemMap = JsfSelectItem.of(map);
        this.dummuyMap = map;
    }

    private void noSnack() {
        Map<Integer, String> map = new LinkedHashMap<>();
        map.put(0, "");
        this.selectItemValueMap = map.keySet().iterator().next();
        this.selectItemMap = JsfSelectItem.of(map);
        this.dummuyMap = map;
    }

    private void printLog(String label) {
        System.out.println("label = " + label);
        System.out.println("selectItemValueEnumCodeProperty = " + selectItemValueEnumCodeProperty);
        if (selectItemValueEnumCodeProperty != null) {
            System.out.println("selectItemValueEnumCodeProperty codeOf = " + EnumCodeProperty.codeOf(Gender.class, selectItemValueEnumCodeProperty));
        }
        System.out.println("selectItemValueMap = " + this.selectItemValueMap);
        System.out.println("selectItemValueMap value = " + this.dummuyMap.get(this.selectItemValueMap));
    }
}

message.properties

noselect=選択無し
gender_male=男
gender_female=女

実行結果

f:id:vermeer-1977:20161222155819p:plain
「選択無し」を選択したら、お菓子リストがクリアされました。
今回は初期リストは空ですが、何かしら意味のあるリストを設定しても良いと思います。

Code

2018/4/17 追加

Bitbucket

JSFでTableのCheckboxをAjaxで更新

前回は、同一行の情報書き換えをAjaxで行いました。

今回は、明細行の全選択という一覧型の更新ページで良く登場するパターンをAjaxで実装してみたいと思います。

いくつか参考を探してみたのですがrenderの値を表示行ごとに生成するというやり方で実装をしたいと思います。*1

コードは前回との差分のみを掲載しています。

JSFJavaScriptを使うための準備

実装例を記したいところですが、その前にやっておくことがあります。
JSFJavaScriptなどHTML側の人からすると違和感を感じると思われるところがidの扱いです。*2

セパレーター文字列を変更する

JSFで生成されるidはxhtmlで表記している状態と違い、ルートからのパスを構造的に表現しています。

xhtmlで記述したid
id="isselect"

生成されたid
id="f:rep:0:isselect"

セパレーター文字をきちんと意識しておかないとrenderで正しくidを指定する事が出来ません。
デフォルトは:です。
JSFしか使わないのであれば問題はないと思いますが、JavaScriptのライブラリーによっては:だと具合が悪い事があります。今回は-にします。
やり方はweb.xmlに以下を追加するだけです。<param-value>-がセパレータ文字になります。

<context-param>
    <param-name>javax.faces.SEPARATOR_CHAR</param-name>
    <param-value>-</param-value>
</context-param>


変更が反映されたid
id="f-rep-0-isselect"


idのあれこれは、kikutaro777さんの説明が分かりやすいです。*3
JSFで生成されるidあれこれ - Challenge Java EE !

テーブル明細の全選択制御

前置きが長くなりました。
では、具体例を挙げます。

想定ケース

テーブルのタイトルのチェックボックスをONにしたら明細行全てのチェックボックスをONにする。

これだけであればJavaScriptでもできますが、ここでの全選択の要件が表示されていない分も含めて全ての明細だったらどうでしょうか?
例えば、チェックをONにした情報の一覧をcsvにしてダウンロードしたい場合、どうしましょう?

そういうケースを想定した実装例です。

xhtml

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://xmlns.jcp.org/jsf/html">

    <style type="text/css">
        td, th { border: 1px #000000 solid; }
    </style>
    <h:head>
        <meta charset="UTF-8" />
        <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
        <title>JSF SELECT ITEM</title>
    </h:head>
    <body>
        <form jsfc="h:form" id="f">
            <h1>JSF SELECT ITEM PATTERN Table + Ajax All Select</h1>
            <span>
                ページ
                <input type="text" jsfc="h:inputText"  value="#{selectItemTableRowSelect.table.startViewPagePos}">
                    <f:ajax listener="#{selectItemTableRowSelect.changePagePos}" render="f-list"/>
                </input>
                /3
            </span>

            <h:panelGroup id="list">
                <table>
                    <thead>
                        <tr>
                            <th >
                                <input type="checkbox" id="allselect" jsfc="h:selectBooleanCheckbox"  value="#{selectItemTableRowSelect.table.isSelect}">
                                    <f:ajax event="change" listener="#{selectItemTableRowSelect.checkAllSelect}"   render="#{selectItemTableRowSelect.table.allSelectRenderAttrIds}" />
                                </input>
                            </th>
                            <th>SEQ</th>
                            <th>適当な文字列</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr jsfc="ui:repeat" value="#{selectItemTableRowSelect.table.viewForms}" var="row" varStatus="varStatus" id="rep">
                            <td>
                                <input type="checkbox" id="isselect" jsfc="h:selectBooleanCheckbox"  value="#{row.isSelect}" >
                                    <f:ajax event="change" listener="#{selectItemTableRowSelect.checkDetailRowSelect}"   render="f-allselect" />
                                </input>
                            </td>
                            <td>#{row.seq}</td>
                            <td>固定文字ああああ</td>
                        </tr>
                    </tbody>
                </table>
            </h:panelGroup>
            <dir>
                <input type="submit" jsfc="h:commandButton" value="状態取得" action="#{selectItemTableRowSelect.submit()}"/>
            </dir>
        </form>
    </body>
</html>


明細行(1行)Form

package com.mycompany.samplejsf.domain.selectitem.selectform;

@Builder @Data
public class SelectForm {

    private Boolean isSelect;
    private Integer seq;

}


明細行(全明細)Form

package com.mycompany.samplejsf.domain.selectitem.selectform;

@Data
public class SelectForms {

    private static final int MAX_ROW_COUNT = 10;
    private Integer startViewPagePos;
    private String allSelectRenderAttrIds;

    private Boolean isSelect;

    private List<SelectForm> forms;

    private SelectForms() {
    }

    public static SelectForms of(List<SelectForm> forms) {
        SelectForms selectForms = new SelectForms();
        selectForms.setIsSelect(false);
        selectForms.setForms(forms);
        selectForms.replaceAllSelectRenderAttrIds(1);
        return selectForms;
    }

    public List<SelectForm> getViewForms() {
        List<SelectForm> viewForms;
        Integer startIndex = (this.startViewPagePos - 1) * MAX_ROW_COUNT;
        Integer endIndex = startIndex + toMaxIndex(this.startViewPagePos) + 1;
        viewForms = this.forms.subList(startIndex, endIndex);
        return viewForms;
    }

    public void setFormsIsSelect(Boolean isSelect) {
        this.isSelect = isSelect;
        this.forms.stream().forEach(form -> {
            form.setIsSelect(isSelect);
        });
    }

    public void setFormIsSelect(JsfAjaxBehaviorEvent jsfEvent) {
        this.isSelect = false;
        this.getForms().get(jsfEvent.uiRepeatRowIndex()).setIsSelect((Boolean) jsfEvent.getValue());
    }

    public void replaceAllSelectRenderAttrIds(Integer pagePos) {
        this.startViewPagePos = pagePos;
        this.allSelectRenderAttrIds = JsfContextUtil.editRepaetTargetRender("isselect", this.toMaxIndex(pagePos), "f", "rep");
    }

    private Integer toMaxIndex(Integer pagePos) {
        Integer maxPageCnt = this.forms.size() / MAX_ROW_COUNT;
        Integer rowCountMod = this.forms.size() % MAX_ROW_COUNT;
        maxPageCnt = rowCountMod < (MAX_ROW_COUNT - 1) ? maxPageCnt + 1 : maxPageCnt;
        return Objects.equals(pagePos, maxPageCnt) ? rowCountMod - 1 : MAX_ROW_COUNT - 1;
    }

}


Controller(ManagedBean)

package com.mycompany.samplejsf.domain.selectitem.selectform;

@Named(value = "selectItemTableRowSelect")
@SessionScoped
@NoArgsConstructor
public class SelectItemTableRowSelectController implements Serializable {

    private static final long serialVersionUID = 1L;

    @Getter @Setter
    private SelectForms table;

    @PostConstruct
    public void init() {
        List<SelectForm> forms = new ArrayList<>();
        for (int i = 0; i < 28; i++) {
            SelectForm form = SelectForm.builder().isSelect(false).seq(i + 1).build();
            forms.add(form);
        }

        this.table = SelectForms.of(forms);
        this.printLog("init");
    }

    public void submit() {
        this.printLog("submit");
    }

    public void checkAllSelect(AjaxBehaviorEvent event) {
        this.table.setFormsIsSelect((Boolean) JsfAjaxBehaviorEvent.of(event).getValue());
        this.printLog("ajax all select");
    }

    public void changePagePos(AjaxBehaviorEvent event) {
        this.table.replaceAllSelectRenderAttrIds((Integer) JsfAjaxBehaviorEvent.of(event).getValue());
        this.printLog("ajax change page position");
    }

    public void checkDetailRowSelect(AjaxBehaviorEvent event) {
        this.table.setFormIsSelect(JsfAjaxBehaviorEvent.of(event));
        this.printLog("ajax " + JsfAjaxBehaviorEvent.of(event).getValue());
    }

    private void printLog(String label) {
        System.out.println("label = " + label + "::allSelect:" + table.getIsSelect());
        this.getTable().getForms().stream()
                .forEachOrdered(form -> {
                    System.out.println(":rowSelect:" + form.getIsSelect() + ":seq:" + form.getSeq());
                });
    }
}


部品クラス

package com.mycompany.samplejsf.infrastructure.util.jsf;

import javax.faces.context.FacesContext;

public class JsfContextUtil {

    private JsfContextUtil() {
    }

    public static String getNamingContainerSeparatorChar() {
        return String.valueOf(FacesContext.getCurrentInstance().getNamingContainerSeparatorChar());
    }

    public static String editRepaetTargetRender(String targetAttrId, Integer maxIndex, String... prefixAttrsIds) {
        Integer prefixAttrsLength = prefixAttrsIds.length;
        String separator = JsfContextUtil.getNamingContainerSeparatorChar();
        String[] renderAttrIds = new String[maxIndex + 1];
        for (int i = 0; i <= maxIndex; i++) {
            String[] render = new String[prefixAttrsLength + 2];
            System.arraycopy(prefixAttrsIds, 0, render, 0, prefixAttrsLength);
            render[prefixAttrsLength] = String.valueOf(i);
            render[prefixAttrsLength + 1] = targetAttrId;
            String renderAttr = String.join(separator, render);
            renderAttrIds[i] = renderAttr;
        }
        return String.join(" ", renderAttrIds);
    }

}


実行結果

全明細チェック前

f:id:vermeer-1977:20161222140147p:plain


全明細チェック後

f:id:vermeer-1977:20161222140146p:plain
全明細にチェックが入ります

ページの値を変更

f:id:vermeer-1977:20161222140145p:plain


明細の表示が切り替わります。そしてチェックボックスは意図通りONになっています。


実際のシステムではもっと検証ロジックなど必要だと思いますが、表示切替の事例という事で手を抜いています。*4

ポイント

ポイントはrenderの指定です。
1つ目は、全明細チェック部分の
render="#{selectItemTableRowSelect.table.allSelectRenderAttrIds}"です。
Ajaxのeventをトリガーにして明細行分のrenderの文字列を編集して動的に更新対象を指定しています。

2つ目は、明細行側のチェック部分のrender="f-allselect"です。
生成された後のルートからの階層化されたidを指定してください。
セパレーター-は上述で指定した値です。

3つ目は、明細行のページ切り替え時の更新部分のrender="f-list"<h:panelGroup id="list">です。
xhtmltableタグにidを付与しても、JSF側では認識してくれません。<h:panelGroup>で更新対象をグループにまとめてidを付与してください。そうするとJSFが認識できるidを持った<span>タグが生成されます。

なお前回のような同じ行のAjax操作におけるrenderはidだけの指定で問題ありません。良い感じに接頭文字を編集してくれています。

ちょっとした事

idについては具体的に起動後のソースを確認をするのが一番分かりやすいです。
ちなみに、AjaxJavaScriptも使わないのであれば、Web系エンジニアやデザイナーの人たちには怒られるかもしれませんがxhtmlidを指定しなくても問題はありません。*5

Code

2018/4/17 追加

Bitbucket

さいごに

なお、画面表示しているものだけを対象とした全選択であれば、Ajaxを使うことなくJavaScriptで沢山のTipsがありますので、そちらを採用した方が楽です。状態の反映はsubmitしたときに全体として反映されるので問題も起きません。

Ajaxでの更新対象の指定ですが@formもあります。これを使えば今回のような細かい実装は不要です。そういう意味では便利です。ただform全体を通信することは忘れないでください。レスポンスが悪化する可能性があります。

*1:UIComponentを直接更新するというような方法もあるようですが、私の技術不足&UIComponentを直接更新するやり方はデザインの所在が分かりにくくなるような気がするので避けたい、という考えから、今回のやり方に私は落ち着きました

*2:ちなみに私はハマりました

*3:私がJSFの調べ物をすると良くたどり着くブログです。いつもお世話になっています。

*4:表示ページ値のValidationをすべきですがやっていません。また表示行数取得部分も最低限の処理しかしていません。例えば保持データ量が0件のケースなど考慮していません

*5:今回の事例でも手抜きが散見されます。問題が無い事と正しい事は違いますので、そこは適宜対応してください。

JSFのSelectItem(Table)をAjaxで更新

前回vermeer.hatenablog.jp

は単体のセレクトボックスの更新でした。

今回はテーブル構造のリストの一部を更新したケースです。 Ajaxでは繰り返し行の一部だけを更新しています。*1

今回はControllerに変換に使用するクラスを保持するというやり方をしていますが、実際の開発ではServiceを経由してリストのデータを取得するというやり方もあると思います。
その場合は、初期表示時のリストについては、Service側もしくはDTOからFormに変換する際に設定しておくというやり方になると思います。

明細行(1行)Form

package com.mycompany.samplejsf.domain.selectitem;

@Builder @Data
public class ItemForm {

    private Integer seq;
    private Integer genderCode;
    private JsfSelectItem genderEnum;

    private Integer snackCode;
    private JsfSelectItem snackList;

}

明細行(全明細)Form

package com.mycompany.samplejsf.domain.selectitem;

@Builder @Data
public class ItemForms {

    @Singular
    private List<ItemForm> forms;

    public ItemForm get(int rowIndex) {
        return this.forms.get(rowIndex);
    }

}

Builderパターンの実装も@Builder(lombok)で簡単にできます。またlombokの@Builder+@SingularはListへの初期化メソッドに単数と複数の両方を自動生成してくれるので便利なのでお勧めです。

ManagedBean

package com.mycompany.samplejsf.domain.selectitem;

@Named(value = "selectItemTableAjax")
@SessionScoped
@NoArgsConstructor
public class SelectItemTableController implements Serializable {

    private static final long serialVersionUID = 1L;

    @Getter @Setter
    private ItemForms genderSnackTable;

    private Map<Gender, Map<Integer, String>> genderShackMap;

    @PostConstruct
    public void init() {
        Map<Gender, Map<Integer, String>> enumMap = new EnumMap<>(Gender.class);
        Map<Integer, String> maleSnack = this.maleSnack();
        enumMap.put(Gender.MALE, maleSnack);
        Map<Integer, String> femaleSnack = this.femaleSnack();
        enumMap.put(Gender.FEMALE, femaleSnack);
        this.genderShackMap = enumMap;

        Integer defaultMaleSnackCode = this.genderShackMap.get(Gender.MALE).entrySet().iterator().next().getKey();
        Integer defaultFemaleSnackCode = this.genderShackMap.get(Gender.FEMALE).entrySet().iterator().next().getKey();
        this.genderSnackTable = ItemForms.builder()
                .form(ItemForm.builder()
                        .seq(1)
                        .genderCode(Gender.MALE.getCode())
                        .genderEnum(JsfSelectItem.of(Gender.class))
                        .snackCode(defaultMaleSnackCode)
                        .snackList(JsfSelectItem.of(this.genderShackMap.get(Gender.MALE)))
                        .build())
                .form(ItemForm.builder()
                        .seq(2)
                        .genderCode(Gender.FEMALE.getCode())
                        .genderEnum(JsfSelectItem.of(Gender.class))
                        .snackCode(defaultFemaleSnackCode)
                        .snackList(JsfSelectItem.of(this.genderShackMap.get(Gender.FEMALE)))
                        .build())
                .build();
        this.printLog("init");
    }

    public void submit() {
        this.printLog("submit");
    }

    public void changeRowGender(AjaxBehaviorEvent event) {
        int index = JsfAjaxBehaviorEvent.of(event).uiRepeatRowIndex();
        ItemForm form = genderSnackTable.get(index);
        Gender gender = EnumCodeProperty.codeOf(Gender.class, form.getGenderCode());
        form.setSnackCode(this.genderShackMap.get(gender).entrySet().iterator().next().getKey());
        form.setSnackList(JsfSelectItem.of(this.genderShackMap.get(gender)));
        this.printLog("ajax " + index);
    }

    private Map<Integer, String> maleSnack() {
        Map<Integer, String> map = new LinkedHashMap<>();
        map.put(1, "大福");
        map.put(2, "おはぎ");
        map.put(3, "みたらしだんご");
        map.put(4, "せんべい");
        return map;
    }

    private Map<Integer, String> femaleSnack() {
        Map<Integer, String> map = new LinkedHashMap<>();
        map.put(5, "チョコ");
        map.put(6, "クッキー");
        map.put(7, "プリン");
        map.put(8, "ゼリー");
        return map;
    }

    private void printLog(String label) {
        System.out.println("label = " + label);

        this.getGenderSnackTable().getForms().stream()
                .forEachOrdered(form -> {
                    Gender gender = EnumCodeProperty.codeOf(Gender.class, form.getGenderCode());
                    String snackName = this.genderShackMap.get(gender).get(form.getSnackCode());

                    System.out.println("SEQ:" + form.getSeq() + ":GenderCode:" + form.getGenderCode()
                                       + ":GenderValue:" + gender.getProperty() + ":SnackCode:" + form.getSnackCode() + ":SnackName:" + snackName);

                });
    }
}


JSFの選択行情報を取得する部品クラス

package com.mycompany.samplejsf.infrastructure.part.jsf;

@RequiredArgsConstructor(staticName = "of") @EqualsAndHashCode
public class JsfAjaxBehaviorEvent {

    private final AjaxBehaviorEvent event;

    public int uiRepeatRowIndex() {
        String clientId = this.event.getComponent().getParent().getClientId();
        Pattern pat = Pattern.compile("[0-9]*$");
        Matcher mat = pat.matcher(clientId);
        if (mat.find() == false) {
            throw new IllegalArgumentException("this method must call ui:repeat ajax event");
        }
        return Integer.parseInt(mat.group());
    }
}

過去記事の同名クラスへの差分のみ記載しています。
this.event.getComponent().getParent()indexを取得できれば良かったのですが、私には出来ませんでした。。正規表現は苦肉の策です。


xhtml

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://xmlns.jcp.org/jsf/html">

    <style type="text/css">
        td, th { border: 1px #000000 solid; }
    </style>

    <h:head>
        <meta charset="UTF-8" />
        <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
        <title>JSF SELECT ITEM</title>
    </h:head>
    <body>
        <form jsfc="h:form">
            <h1>JSF SELECT ITEM PATTERN Table + Ajax</h1>
            <table>
                <thead>
                    <tr>
                        <th>SEQ</th>
                        <th>性別</th>
                        <th>お菓子</th>
                    </tr>
                </thead>
                <tbody>
                    <tr jsfc="ui:repeat" value="#{selectItemTableAjax.genderSnackTable.forms}" var="row" varStatus="varStatus" id="rep">
                        <td>
                            #{row.seq}
                        </td>

                        <td>
                            <select jsfc="h:selectOneMenu" value="#{row.genderCode}">
                                <option jsfc="f:selectItems" value="#{row.genderEnum.values}" />
                                <f:ajax listener="#{selectItemTableAjax.changeRowGender}" render="snack" event="change" />
                            </select>
                        </td>

                        <td>
                            <select id="snack" jsfc="h:selectOneMenu" value="#{row.snackCode}">
                                <option jsfc="f:selectItems" value="#{row.snackList.values}" />
                            </select>
                        </td>
                    </tr>
                </tbody>
            </table>
            <dir>
                <input type="submit" jsfc="h:commandButton" value="状態取得" action="#{selectItemTableAjax.submit()}"/>
            </dir>
        </form>
    </body>
</html>

デザインをHtmlという前提の場合、繰り返し部分は ui:repeat を使います。
CSSは手を抜いています。)

実行結果

操作前

f:id:vermeer-1977:20161220180536p:plain

操作後

f:id:vermeer-1977:20161220180535p:plain

コンソールログ
label = init
SEQ:1:GenderCode:1:GenderValue:gender_male:SnackCode:1:SnackName:大福
SEQ:2:GenderCode:2:GenderValue:gender_female:SnackCode:5:SnackName:チョコ
label = ajax 1
SEQ:1:GenderCode:1:GenderValue:gender_male:SnackCode:1:SnackName:大福
SEQ:2:GenderCode:1:GenderValue:gender_male:SnackCode:1:SnackName:大福

Code

2018/4/17 追加

Bitbucket

さいごに

表題とは関係ありませんが、明細全体をファーストクラスコレクションにしておくことで明細行全体としての関心事を明確に凝集させています。 今回は明細全体での振舞いは実装していませんが、全選択時の振舞いなど実際の開発ではあると思います。 あとから凝集させることももちろんできますが、事前にファーストクラスコレクションとしてまとめておく癖をつけておけばリファクタリング時に「関心事」の塊が明確なのでControllerにロジックが漏れにくいと思います。 プロジェクトによっては凝集のために新たにクラスを作成しようとすると、それだけでも仰々しいと思われて抵抗されるケースがあります。
始めからクラスになっていれば、そういうことにはなりにくいです。*2
意味があるのかな?何を実装するのかな?このくらいなら別に良いだろう、と思われる気がするファーストクラスコレクションですが、地味に効いてくる作法だと思います。

*1:行位置の取得の部分は試行錯誤したところなので、正しいやり方かどうか分かりません。

*2:「凝集したクラスを作るだけ」と思っても「聖域としてのコード」を優先されることは多々あります。これは正しい間違っているというのとは違います。

JSFのSelectItemをAjaxで更新

前回の続き

vermeer.hatenablog.jp

submitで画面を更新しても良いですが、Ajaxの部分更新の方が操作性が良いです。

流れとしては

  • xhtmlajaxタグを追記
  • ManagedBeanにajaxイベントのListenerを実装

これだけです。

以下の実装でやっていることは、Enum has code and propertyの性別を変更したら、すぐにMapのお菓子リストが更新されます。 (前回の実装では【更新】ボタンを押下して更新)

xhtml

追加タグ

<f:ajax listener="#{selectItemAjax.changeItem}" render="map" event="change"/>

更新トリガーとなる領域の内側に、リスナーとなるメソッドを記述します。 render="xx" は、リスナーを受け取った後、更新先の領域のHtmlのidです。

selectItemAjax.xhtml

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:h="http://xmlns.jcp.org/jsf/html">

    <h:head>
        <meta charset="UTF-8" />
        <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
        <title>JSF SELECT ITEM</title>
    </h:head>
    <body>
        <form jsfc="h:form">
            <h1>JSF SELECT ITEM PATTERN Ajax</h1>
            <dir>
                <h2>Enum has code and property </h2>
                <select jsfc="h:selectOneMenu" value="#{selectItemAjax.selectItemValueEnumCodeProperty}">
                    <option jsfc="f:selectItems" value="#{selectItemAjax.enumCodeProperty.values}"></option>
                    <f:ajax listener="#{selectItemAjax.changeItem}" render="map" event="change"/>
                </select>
            </dir>
            <hr />
            <dir>
                <h2>Map</h2>
                <select id="map" jsfc="h:selectOneMenu"  value="#{selectItemAjax.selectItemValueMap}">
                    <option jsfc="f:selectItems" value="#{selectItemAjax.selectItemMap.values}"></option>
                </select>
            </dir>
            <hr />
            <dir>
                <input type="submit" jsfc="h:commandButton" value="更新" action="#{selectItemAjax.submit()}"/>
            </dir>
        </form>
    </body>
</html>

ManagedBean

追加したメソッド

    public void changeItem(AjaxBehaviorEvent event) {
        this.selectItemValueMap = JsfAjaxBehaviorEvent.of(event).getValue();
        ・・・
    }

クラス全体

package com.mycompany.samplejsf.domain.selectitem;

@Named(value = "selectItemAjax")
@SessionScoped
@NoArgsConstructor @Getter
public class SelectItemAjaxController implements Serializable {

    private static final long serialVersionUID = 1L;

    @Setter
    private Integer selectItemValueEnumCodeProperty;

    @Setter
    private Object selectItemValueMap;

    private JsfSelectItem enumCodeProperty;
    private JsfSelectItem selectItemMap;

    private Map<Object, String> dummuyMap;

    @PostConstruct
    public void init() {
        this.selectItemValueEnumCodeProperty = Gender.FEMALE.getCode();
        this.enumCodeProperty = JsfSelectItem.of(Gender.class);
        this.replaceSelectItemMap();
        this.printLog("init");
    }

    public void submit() {
        this.replaceSelectItemMap();
        this.printLog("submit");
    }

    public void changeItem(AjaxBehaviorEvent event) {
        this.selectItemValueMap = JsfAjaxBehaviorEvent.of(event).getValue();
        this.replaceSelectItemMap();
        this.printLog("changeItem");
    }

    private void replaceSelectItemMap() {
        if (this.selectItemValueEnumCodeProperty.equals(Gender.MALE.getCode())) {
            this.maleSnack();
            return;
        }
        this.femaleSnack();
    }

    private void maleSnack() {
        Map<Object, String> map = new LinkedHashMap<>();
        map.put(1, "大福");
        map.put(2, "おはぎ");
        map.put(3, "みたらしだんご");
        map.put(4, "せんべい");
        this.selectItemValueMap = map.keySet().iterator().next();
        this.selectItemMap = JsfSelectItem.of(map);
        this.dummuyMap = map;
    }

    private void femaleSnack() {
        Map<Object, String> map = new LinkedHashMap<>();
        map.put(5, "チョコ");
        map.put(6, "クッキー");
        map.put(7, "プリン");
        map.put(8, "ゼリー");
        this.selectItemValueMap = map.keySet().iterator().next();
        this.selectItemMap = JsfSelectItem.of(map);
        this.dummuyMap = map;
    }

    private void printLog(String label) {
        System.out.println("label = " + label);
        System.out.println("selectItemValueEnumCodeProperty = " + selectItemValueEnumCodeProperty);
        System.out.println("selectItemValueEnumCodeProperty codeOf = " + EnumCodeProperty.codeOf(Gender.class, selectItemValueEnumCodeProperty));
        System.out.println("selectItemValueMap = " + this.selectItemValueMap);
        System.out.println("selectItemValueMap value = " + this.dummuyMap.get(this.selectItemValueMap));
    }
}

部品クラス

たいした処理ではないですが類似実装をシンプルにするためにあえてラッパークラスを設けました。 *1

package com.mycompany.samplejsf.infrastructure.part.jsf;

@RequiredArgsConstructor(staticName = "of") @EqualsAndHashCode
public class JsfAjaxBehaviorEvent {

    private final AjaxBehaviorEvent event;

    public Object getValue() {
        UIInput uiInput = (UIInput) this.event.getComponent();
        return uiInput.getValue();
    }
}

実行結果

f:id:vermeer-1977:20161219192213p:plain

Code

2018/4/17 追加

Bitbucket

さいごに

以前、実装したものはValueChangeListenerを使ったけど、今回の整理をしている中で同じことをやろうとしたけど上手くいかなかったです。 ただ、以前の実装よりも今回のものがスッキリしているので結果として良かったように思います。

JSFのSelectItemを拡張したEnumで実装する

前回、Enumを拡張しました
vermeer.hatenablog.jp

これを使ってJSFのSelectItemを実装します。

ちなみに、Enumだけでも十分対応は出来るようです。
【メモ】JSFでenumのオブジェクトをselectOneMenuに使う方法 - mike-neckのブログ
JSFのConverterで見た闇の話 - mike-neckのブログ

固定値であればEnumが良いですが、例えば何かしらの一覧をリストから選択したい場合、コード値とプロパティのペアを動的に設定したいことは良くあります。
今回、Enumに加えてMapも同様に扱うようにしました。

中途半端ではありますが、ResourceBundleも扱ってみました。*1*2

propertyはmessage.propertiesで設定にして表示文字を外部に持たせてます。
Map側はRDBに保存している値を使用する想定なので国際化対応はしない想定です。*3

以下の実装では、Enum has code and propertyの性別を変更して【更新】ボタンを押下したら、Mapのお菓子リストが更新されます。

実行対象のクラス・インターフェース・メッセージプロパティ

Enum
@AllArgsConstructor @Getter
public enum Gender implements EnumCodePropertyInterface {
    MALE(1, "gender_male"), FEMALE(2, "gender_female");

    private final Integer code;
    private final String property;

}
Interface
public interface EnumCodePropertyInterface {

    public Object getCode();

    public String getProperty();

}
Enum(拡張)のFactory
public class EnumCodeProperty {

    public static <E extends Enum<E> & EnumCodePropertyInterface> E codeOf(Class<E> enumType, @NonNull Object code) {
	for (E type : enumType.getEnumConstants()) {
	    if (type.getCode().equals(code)) {
		return type;
	    }
	}
	throw new IllegalArgumentException();
    }

    public static <E extends Enum<E> & EnumCodePropertyInterface> E propertyOf(Class<E> enumType, @NonNull String property) {
	for (E type : enumType.getEnumConstants()) {
	    if (type.getProperty().equals(property)) {
		return type;
	    }
	}
	throw new IllegalArgumentException();
    }
}
message.properties
noselect=選択無し
gender_male=男
gender_female=女
JsfのSelectItemのFactory

Enum も Map も同じメソッド名で使えるようにオーバーロードしています。

package com.mycompany.samplejsf.infrastructure.part.jsf;

@RequiredArgsConstructor(staticName = "of") @Value
public class JsfSelectItem {

    private final List<SelectItem> values;

    public static <E extends Enum<E> & EnumCodePropertyInterface> JsfSelectItem of(Class<E> enumClass) {
	List<SelectItem> items = new ArrayList<>();
	ResourceBundle bundle = ResourceBundle.getBundle("message");
	for (E item : enumClass.getEnumConstants()) {
	    String value = bundle.containsKey(item.getProperty())
			   ? bundle.getString(item.getProperty())
			   : item.getProperty();
	    SelectItem selectItem = new SelectItem(item.getCode(), value);
	    items.add(selectItem);
	}
	return JsfSelectItem.of(items);
    }

    public static JsfSelectItem of(Map<?, String> itemMap) {
	List<SelectItem> items = new ArrayList<>();
	itemMap.entrySet().stream()
		.forEachOrdered(map -> {
		    SelectItem item = new SelectItem(map.getKey(), map.getValue());
		    items.add(item);
		});
	return JsfSelectItem.of(items);
    }
}

実行資産

ManagedBean
package com.mycompany.samplejsf.domain.selectitem;

@Named(value = "selectItem")
@SessionScoped
@NoArgsConstructor @Getter
public class SelectItemController implements Serializable {

    private static final long serialVersionUID = 1L;
    @Setter
    private Integer selectItemValueDirectWrite;

    @Setter
    private Integer selectItemValueEnumCodeProperty;

    @Setter
    private Integer selectItemValueMap;

    private JsfSelectItem enumCodeProperty;
    private JsfSelectItem selectItemMap;

    private Map<Integer, String> dummuyMap;

    @PostConstruct
    public void init() {
	this.selectItemValueDirectWrite = GenderEnumOnly.MALE.ordinal();

	this.selectItemValueEnumCodeProperty = Gender.FEMALE.getCode();
	this.enumCodeProperty = JsfSelectItem.of(Gender.class);

	this.replaceSelectItemMap();
	this.printLog("init");
    }

    public void submit() {
	this.replaceSelectItemMap();
	this.printLog("submit");
    }

    private void replaceSelectItemMap() {
	if (this.selectItemValueEnumCodeProperty.equals(Gender.MALE.getCode())) {
	    this.maleSnack();
	    return;
	}
	this.femaleSnack();
    }

    private void maleSnack() {
	Map<Integer, String> map = new LinkedHashMap<>();
	map.put(1, "大福");
	map.put(2, "おはぎ");
	map.put(3, "みたらしだんご");
	map.put(4, "せんべい");
	this.selectItemValueMap = map.keySet().iterator().next();
	this.selectItemMap = JsfSelectItem.of(map);
	this.dummuyMap = map;
    }

    private void femaleSnack() {
	Map<Integer, String> map = new LinkedHashMap<>();
	map.put(5, "チョコ");
	map.put(6, "クッキー");
	map.put(7, "プリン");
	map.put(8, "ゼリー");
	this.selectItemValueMap = map.keySet().iterator().next();
	this.selectItemMap = JsfSelectItem.of(map);
	this.dummuyMap = map;
    }

    private void printLog(String label) {
	System.out.println("label = " + label);
	System.out.println("selectItemValueDirectWrite = " + selectItemValueDirectWrite);
	System.out.println("selectItemValueEnumCodeProperty = " + selectItemValueEnumCodeProperty);
	System.out.println("selectItemValueEnumCodeProperty codeOf = " + EnumCodeProperty.codeOf(Gender.class, selectItemValueEnumCodeProperty));
	System.out.println("selectItemValueMap = " + selectItemValueMap);
	System.out.println("selectItemValueMap value = " + this.dummuyMap.get(this.selectItemValueMap));
    }
}
xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:p="http://primefaces.org/ui">
    <head>
        <title>JSF SELECT ITEM</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></meta>
    </head>
    <body>
        <form jsfc="h:form" enctype="multipart/form-data">
            <h1>JSF SELECT ITEM PATTERN</h1>
            <dir>
                <h2>Direct Write</h2>
                <select jsfc="h:selectOneMenu" value="#{selectItem.selectItemValueDirectWrite}">
                    <option jsfc="f:selectItem" itemLabel="#{msg['gender_male']}" itemValue="0"></option>
                    <option jsfc="f:selectItem" itemLabel="#{msg['gender_female']}" itemValue="1"></option>
                </select>
            </dir>
            <hr />
            <dir>
                <h2>Enum has code and property </h2>
                <select jsfc="h:selectOneMenu"  value="#{selectItem.selectItemValueEnumCodeProperty}">
                    <option jsfc="f:selectItems" value="#{selectItem.enumCodeProperty.values}"></option>
                </select>
            </dir>
            <hr />
            <dir>
                <h2>Map</h2>
                <select jsfc="h:selectOneMenu"  value="#{selectItem.selectItemValueMap}">
                    <option jsfc="f:selectItems" value="#{selectItem.selectItemMap.values}"></option>
                </select>
            </dir>
            <hr />
            <dir>
                <input type="submit" jsfc="h:commandButton" value="更新" action="#{selectItem.submit()}"/>
            </dir>
        </form>
    </body>
</html>

実行結果

画面

f:id:vermeer-1977:20161219140146p:plain

コンソールログ
label = init
selectItemValueDirectWrite = 0
selectItemValueEnumCodeProperty = 2
selectItemValueEnumCodeProperty codeOf = FEMALE
selectItemValueMap = 5
selectItemValueMap value = チョコ
label = submit
selectItemValueDirectWrite = 0
selectItemValueEnumCodeProperty = 1
selectItemValueEnumCodeProperty codeOf = MALE
selectItemValueMap = 1
selectItemValueMap value = 大福
label = submit
selectItemValueDirectWrite = 0
selectItemValueEnumCodeProperty = 2
selectItemValueEnumCodeProperty codeOf = FEMALE
selectItemValueMap = 5
selectItemValueMap value = チョコ

さいごに

JSFのSelectItemは、今回のクラスで、Enumであれ、Mapであれ統一して処理できます。また、lombokのおかげで 可視化されていませんが of(List values) というメソッドも自動生成されています。
冗長的で使わないように思いますが、ファーストクラスコレクションとして統一した実装が出来るので用途が無いわけではないと思います。

今回はSubmitでMapのリストが変更するようにしました。
でも、リストの更新のためだけにForm全てをリクエストするのは性能面で好ましくないです。
次回はAjaxを使って、性能面にも配慮のある実装をしてみたいと思います。

*1:mike-neckのブログで「あぁ、そうかちゃんとしたいんだったら国際化対応すべきだ」と思い至り、とりあえずでも実装しておこうと思いました。

*2:ResourceBundleは、ちゃんとやろうとすると結構なボリュームになるので今後の課題にしたいと思います

*3:RDBの表現と同じものをmessage.propertiesにすべて準備する?いえいえ、それはそもそも設計から見直した方が良いように思います

EnumにInterfaceを使ってユーティリティを作成する

前回の続き
vermeer.hatenablog.jp

前回のEnumのようにテーブル値と表記がペアとなっているものは
Interfaceを持たせて共通に操作する仕組みを作成しておくと便利です。

Enumクラスにメソッドを追加

package com.mycompany.samples.enumclass;

import lombok.NonNull;

public enum GenderNoLombok {
    MALE(1, "男"),
    FEMALE(2, "女");

    private final int code;
    private final String property;

    private GenderNoLombok(int code, String property) {
	this.code = code;
	this.property = property;
    }

    public int getCode() {
	return code;
    }

    public String getProperty() {
	return property;
    }

    public static GenderNoLombok codeOf(int code) {
	for (GenderNoLombok enumClass : values()) {
	    if (enumClass.getCode() == code) {
		return enumClass;
	    }
	}
	throw new IllegalArgumentException();
    }

    public static GenderNoLombok propertyOf(@NonNull String property) {
	for (GenderNoLombok enumClass : values()) {
	    if (enumClass.getProperty().equals(property)) {
		return enumClass;
	    }
	}
	throw new IllegalArgumentException();
    }
}

シンプルに実装すれば、こんな感じになると思います。
Interfaceもいりません。
これはこれでシンプルで良いと思いますし、嫌いではないです。*1
ただ、すべてのEnumクラスに同じような記述しないといけないので冗長です。

Interfaceとユーティリティを作成

Interface
package com.mycompany.samples.enumclass;

public interface EnumCodePropertyInterface {

    public Object getCode();

    public String getProperty();

}
Enumクラス

共通操作が出来るようにInterfaceをimpliment

package com.mycompany.samples.enumclass;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor @Getter
public enum Gender implements EnumCodePropertyInterface {
    MALE(1, "男"),
    FEMALE(2, "女");

    private final Integer code;
    private final String property;

}

Enumクラス側はlombokのおかげでOverrideの警告も出ません。

ユーティリティ

implimentしたInterfaceに合わせた操作を実装
今回は、code値/property値、それぞれの値から、その値とペアになるEnumクラスを生成するstaticメソッドを作成しました。
また、code値については、テーブル定義に左右されるのでObjectにしています。
もし型安全にするのであれば、想定する型のメソッドを準備してオーバーロードしておけば良いと思います。
propertyは明らかに文字列だけということでString固定にしています。

package com.mycompany.samples.enumclass;

import lombok.NonNull;

public class EnumCodeProperty {

    public static <E extends Enum<E> & EnumCodePropertyInterface> E codeOf(Class<E> enumType, @NonNull Object code) {
	for (E type : enumType.getEnumConstants()) {
	    if (type.getCode().equals(code)) {
		return type;
	    }
	}
	throw new IllegalArgumentException();
    }

    public static <E extends Enum<E> & EnumCodePropertyInterface> E propertyOf(Class<E> enumType, @NonNull String property) {
	for (E type : enumType.getEnumConstants()) {
	    if (type.getProperty().equals(property)) {
		return type;
	    }
	}
	throw new IllegalArgumentException();
    }
}
実行コード
package com.mycompany.samples.enumclass;

import lombok.NoArgsConstructor;

@NoArgsConstructor
public class EnumTest {

    public void execUtil() {
	GenderNoLombok genderNoLombok = GenderNoLombok.codeOf(GenderNoLombok.MALE.getCode());
	System.out.println("genderNoLombok.getCode() = " + genderNoLombok.getCode());
	System.out.println("genderNoLombok.getProperty() = " + genderNoLombok.getProperty());

	genderNoLombok = GenderNoLombok.propertyOf(GenderNoLombok.FEMALE.getProperty());
	System.out.println("genderNoLombok.getCode() = " + genderNoLombok.getCode());
	System.out.println("genderNoLombok.getProperty() = " + genderNoLombok.getProperty());

	Gender gender = EnumCodeProperty.codeOf(Gender.class, Gender.MALE.getCode());
	System.out.println("gender.getCode() = " + gender.getCode());
	System.out.println("gender.getProperty() = " + gender.getProperty());

	gender = EnumCodeProperty.propertyOf(Gender.class, Gender.FEMALE.getProperty());
	System.out.println("gender.getCode() = " + gender.getCode());
	System.out.println("gender.getProperty() = " + gender.getProperty());

    }
}
実行結果
genderNoLombok.getCode() = 1
genderNoLombok.getProperty() = 男
genderNoLombok.getCode() = 2
genderNoLombok.getProperty() = 女
gender.getCode() = 1
gender.getProperty() = 男
gender.getCode() = 2
gender.getProperty() = 女

最後に

Enumが通常のクラスであればInterfaceではなく抽象クラスで今回と同様の操作を実装できるかもしれませんがEnumは継承をさせることが出来ません。
そういった技術的な側面もありますが、Enumは定数定義のためのクラスとして、あまり多くの事をしすぎないというのも、1つの整理なのかなぁと思ったりもします。*2

この「Interface+ユーティリティ」のやり方は、Enumだけでなく、リファクタリングをしているときに個人的によく使っています。
ユーティリティで実装の集約を一旦した上で、主語と述語を整理しユーティリティからクラスに格上げをする、というやり方をしています。ロジックだけを見たら似ている気がしたけど概念で考えると違ったり、ユーティリティで汎用的な実装へリファクタリングをしている中で、同分類とすべきドメインが見つかって更に見直しを深めるということは結構ありました。


次は、このEnumクラスを使って、JSFのSelectItemについて汎用的な部品クラスの作成について整理してみたいと思います。*3

追記:2017/1/2

EnumのInterfaceに共通処理を実装してみました。
vermeer.hatenablog.jp

*1:ちなみに以前は、こういう実装をしていました

*2:実はEnumでもInterfaceのdefault実装で対応をしよう色々としたのですが、うまくいかず、トータルの実装量を鑑みて落としどころをユーティリティしたというのが本当のところです

*3:実際の整理の流れは逆です。SelectItemの部品化をしようと思ったのが先で、その過程でEnumの整理に至りました