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

Javaで主にシステム開発をしながら思うところをツラツラを綴る

画面項目の順序にあわせたメッセージを出力する

vermeer.hatenablog.jp

の続き。
メッセージを任意の順番で固定で出力させました。

具体的にはメッセージを画面項目にあわせた出力をしています。

出力結果

FormValidation

Form側で行っているValidationは、入力必須検証 だけです。
それ以外の桁や型については後述のValueObjectにて検証をしています。

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

ValueObjectValidation

入力された内容を検証したメッセージです。

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

実装

順序の指定

/**
 * ClassのFieldの順序を指定します.
 * <p>
 * Javaの仕様としてFieldの順序は保証されないため、ClassのFiled順序を指定するためのAnnotationです.
 *
 * @author Yamashita,Takahiro
 */
@Target({FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldOrder {

    short value() default Short.MAX_VALUE;
}

順序指定に使うAnnotation はこんな感じで

    @SuppressFBWarnings("URF_UNREAD_FIELD")
    private static class ValidationForm {

        @Valid
        @FieldOrder(1)
        private EmailForm userEmail;

        @Valid
        @FieldOrder(2)
        private NameForm name;

        @Valid
        @FieldOrder(3)
        private DateOfBirthForm dateOfBirth;

        @Valid
        @FieldOrder(4)
        private PhoneNumberForm phoneNumber;

    }

FieldOrderで項目順序を指定します。

検証実施

検証実施部分は変更ありません。

    public String confirm() {
        validator.validate(registrationPage.getValidationForm());
        return "persistconfirm.xhtml";
    }

並べ替え

import ddd.domain.javabean.annotation.FieldOrder;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.validation.ConstraintViolation;

/**
 * ConstraintViolationを{@link ddd.domain.javabean.annotation.FieldOrder} で指定した順にソートする機能を提供します.
 *
 * @author Yamashita,Takahiro
 */
public class ConstraintViolationSorting {

    Set<ConstraintViolation<?>> beforeItems;

    private ConstraintViolationSorting(Set<ConstraintViolation<?>> constraintViolations) {
        this.beforeItems = constraintViolations;
    }

    /**
     * インスタンスを構築します.
     *
     * @param constraintViolations BeanValidationの検証結果
     * @return 構築したインスタンス
     */
    public static ConstraintViolationSorting of(Set<ConstraintViolation<?>> constraintViolations) {
        return new ConstraintViolationSorting(constraintViolations);
    }

    /**
     * ConstraintViolationを{@link ddd.domain.javabean.annotation.FieldOrder} で指定した順にソートしたリストを返却します.
     *
     * @return ソート済みのリスト
     * @throws ConstraintViolationSortingException 順序取得の際にフィールドの情報を取得できなかった場合
     */
    public List<ConstraintViolation<?>> toList() {
        return beforeItems.stream()
                .map(this::makeSortKey)
                .sorted(Comparator.comparing(ConstraintViolationWithSortKey::getKey))
                .map(ConstraintViolationWithSortKey::getConstraintViolation)
                .collect(Collectors.toList());
    }

    //
    ConstraintViolationWithSortKey makeSortKey(ConstraintViolation<?> constraintViolation) {
        Class<?> clazz = constraintViolation.getRootBeanClass();
        List<String> paths = Arrays.asList(constraintViolation.getPropertyPath().toString().split("\\."));
        String key = this.recursiveAppendKey(clazz, paths, 0, clazz.getCanonicalName());
        return new ConstraintViolationWithSortKey(key, constraintViolation);
    }

    //
    String recursiveAppendKey(Class<?> clazz, List<String> paths, Integer index, String appendedKey) {
        if (paths.size() - 1 <= index) {
            return appendedKey;
        }

        String field = paths.get(index);
        String fieldOrder = fieldOrder(clazz, field);
        String key = appendedKey + fieldOrder + field;

        try {
            Class<?> nextClass = clazz.getDeclaredField(field).getType();
            return this.recursiveAppendKey(nextClass, paths, index + 1, key);
        } catch (NoSuchFieldException | SecurityException ex) {
            throw new ConstraintViolationSortingException("Target field or filedtype can not get.", ex);
        }
    }

    //
    String fieldOrder(Class<?> clazz, String property) {
        short index = Short.MAX_VALUE;

        try {
            Field field = clazz.getDeclaredField(property);
            FieldOrder fieldOrder = field.getAnnotation(FieldOrder.class);
            if (fieldOrder != null) {
                index = fieldOrder.value();
            }
        } catch (NoSuchFieldException | SecurityException ex) {
            throw new ConstraintViolationSortingException("Target field can not get.", ex);
        }

        return String.format("%03d", index);
    }

    //
    static class ConstraintViolationWithSortKey {

        private final String key;
        private final ConstraintViolation<?> constraintViolation;

        public ConstraintViolationWithSortKey(String key, ConstraintViolation<?> constraintViolation) {
            this.key = key;
            this.constraintViolation = constraintViolation;
        }

        public String getKey() {
            return key;
        }

        public ConstraintViolation<?> getConstraintViolation() {
            return constraintViolation;
        }

    }

}

今回のメインとなる実装です。

ソートキーを文字列で作りこんでソートをしています。

検証不正の起因となったプロパティパスを対象となるようにしているので、FormクラスのDomainであるValueObjectでの不正も出力対象にしています。

メッセージ出力

/**
 * メッセージ出力する機能を提供します.
 *
 * @author Yamashita,Takahiro
 */
public class JsfMessageHandler implements MessageHandler {

    /**
     * {@inheritDoc }
     */
    @Override
    public void appendMessage(Set<ConstraintViolation<?>> validatedResults) {

        FacesContext facesContext = FacesContext.getCurrentInstance();

        MessageInterpolatorFactory interpolatorFactory
                                   = MessageInterpolatorFactory.of("Messages", "FormMessages", "FormLabels");

        MessageInterpolator interpolator = interpolatorFactory.create();

        List<ConstraintViolation<?>> sortedValidatedResults = ConstraintViolationSorting.of(validatedResults).toList();

        sortedValidatedResults.stream()
                .map(result -> {
                    return interpolator.toMessage(result);
                })
                .distinct()
                .forEachOrdered(message -> {

                    FacesMessage facemsg = new FacesMessage(FacesMessage.SEVERITY_ERROR, message, null);
                    facesContext.addMessage(null, facemsg);
                });

        // リダイレクトしてもFacesMessageが消えないように設定
        facesContext.getExternalContext().getFlash().setKeepMessages(true);

    }

}

InterceptorでValidation結果からメッセージに変換しているところを 対応前は順序の無い Setから 指定順に並べ替えたListからメッセージを出力するように変更します。

メモ(振り返り)

今回の対応をするにあたって、順序を示すFieldOrderを注釈する以外の影響は exsample.jsf.*パッケージ配下のクラスにはありません。 意識したのは 主だった機能への影響が「メッセージの順序を示すこと」以外にならないようにしたところです。
まぁ、どうでも良い話といえば、そうなのですが、こういうところに一々こだわっていきたいなぁと思っている次第です。

Code

vermeer_etc / jsf-ddd / source / — Bitbucket

コミット
vermeer_etc / jsf-ddd / commit / f5edeb0a9361 — Bitbucket

参考情報

Java リフレクションで取得するフィールドの順番 | memorandum

アノテーションの値順でフィールドを出力 | 「Javaを復習する初心者」が復習・学習するブログ

さいごに

あくまで本サンプルで確認できるレベルのところしか確認をしていませんが*1、最低限の要件は満たせたかな という感じです。

ただ今回の実装方法では、厳密に画面の項目順通りかというと、ちょっと違うと思っています。
画面入出力項目で使用しているフィールドではなく、あくまで検証に使用しているリクエストのプロパティで順序を指定しているためです。
正直なところ、実際に実装を進めていく中で、これまでの整理と 実現方法可否の塩梅から、いくつか見直しをすべきかもしれない、というところも少し感じています。


*1:テーブル構造の場合の出力など、複数のFormをまとめたコンポーネントで構成した階層状態の場合の確認していません