複数項目の相関チェック処理を共通処理化して、アノテーションで使えるようにする方法を試します。
STS3(3.9.6)+SpringBoot2.0+Tymeleaf3.0迄動作確認しています。
複数項目の相関チェックとは。
例えば、履歴管理するデータの開始日・終了日が、「開始日より終了日が過去ではない」などの相互の整合性を確認するような場合に使います。
個別にロジックを書いてもいいんですけど、よく使うチェックは、単一項目の入力チェックと同様にアノテーションで指定できるようにしておくと便利ですよね。
今回は、Formの複数項目にまたがった関連チェックをアノテーション化します。
そのサンプルとして、「開始日・終了日の相関チェックを行うアノテーション」を作ります。
開始日・終了日の相関チェックを行うアノテーション
まず、アノテーションの定義です。
@Documented
@Constraint(validatedBy={DateIntegrationValidImp.class})
@Target({TYPE,ANNOTATION_TYPE})
@Retention(RUNTIME)
@ReportAsSingleViolation
public @interface DateIntegrationValid {
String message() default "終了日は開始日より過去にはできません。";
Class<?> groups() default {};
Class<? extends Payload> payload() default {};
String startDateProperty();
String endDateProperty();
@Target({TYPE,ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
public @interface List {
DateIntegrationValid[] value();
}
}
ほぼ、単項目で独自実装するアノテーションの定義と同じですが、大事な変更点として@Targetに、「TYPE」を指定します。
相関チェックの場合は項目にアノテーションはつけられません
単一の項目と違い、相関チェックの場合は項目にアノテーションをつけることができません。
ひとつしか、項目がとれませんからね。
だから、アノテーションをFormクラスにつける必要があり、そのためには@TargetはTYPEにしないといけないわけです。
あと、単項目と違ってアノテーションが受け取るオブジェクトがFormクラスそのものになるので、実装クラスではFormクラス内の各項目を参照することになります。
そのためには、インタフェースにその項目を取得するためのメソッド定義だけしておく必要があります。それが、この部分です。
String startDateProperty();
String endDateProperty();
名前付け規則は特にないですが、Formの項目名+Property()がわかりやすくていいかなと思ってます。
実装クラスです
基本的なところは、単項目の入力チェックを独自実装する時と同じです。
ちょっと長いので細切れにして、単項目の時と違う部分を解説していきます。
アノテーションクラスの定義の単項目と違う部分
2つ目がObjectになっているのがポイントです。
ここにはFormクラスが渡されます。
汎用的にするにはObjectでないと駄目です。
public class DateIntegrationValidImp implements ConstraintValidator<DateIntegrationValid, Object> {
次はinitializeです
@Override
public void initialize(DateIntegrationValid constraintAnnotation) {
this.startDateProperty = constraintAnnotation.startDateProperty();
this.endDateProperty = constraintAnnotation.endDateProperty();
this.message = constraintAnnotation.message();
}
単項目の時は何もしなかったんですが、今回はアノテーション定義から、Formの項目名とメッセージを受け取っておく必要があります。
受け取るmessageなどの定義は省略してますが、実際には private String message; のように定義してある前提で書いてます。
次はチェック本体を書く、isValidです
疑似コードで書くとこんな構成になります。
public boolean isValid(Object form, ConstraintValidatorContext context) {
boolean ret = true;
if(form == null){
ret = true;
}else{① フォームクラスから比較対象項目の値を得る
② 得た値(日付文字列)からLocalDateオブジェクトを生成する。
③ LocalDateオブジェクト生成で例外が発生したらtrueを返す。
④ 開始日オブジェクトと終了日オブジェクトを比較する
⑤ 開始日<=終了日なら trueを返す
⑥ 開始日>終了日ならエラーメッセージを生成し、falseを返す。
}
}
① フォームクラスから比較対象項目の値を得る
SpringのBeanWrapperというインタフェースを使います。
BeanWrapper beanWrapper = new BeanWrapperImpl(form);
String start = (String) beanWrapper.getPropertyValue(startDateProperty);
String end = (String) beanWrapper.getPropertyValue(endDateProperty);
第一引数で受け取ったObject型のformを引数にして、BeanWrapperImplでBeanWrapperオブジェクトを生成して、initializeで受け取った項目名を引数にして、getPropertyValueメソッドで入力された値(日付文字列)を受け取ります。
② 得た値(日付文字列)からLocalDateオブジェクトを生成する
は、日付文字列から年・月・日の値を取り出して生成する方法を以下に書きます。
この記事では、省略してますが、同じやり方で、開始日と終了日の両方のオブジェクトを作ります。
Pattern ptn = Pattern.compile("^(\\d{4})[-/]?(\\d{1,2})[-/]?(\\d{1,2})$");
Matcher mchStart = ptn.matcher(start);
LocalDate startDateObj = LocalDate.of(Integer.valueOf(mchStart.group(1)), Integer.valueOf(mchStart.group(2)), Integer.valueOf(mchStart.group(3)));
③ LocalDateオブジェクト生成で例外が発生したらtrueを返す。
ここで、LocalDateオブジェクトの生成に失敗すると例外が発生します。
このとき「③ LocalDateオブジェクト生成で例外が発生したらtrueを返す。」です。
理由は、単項目チェックのアノテーションを優先させるためです。
基本的に日付項目には@DateValid(以前の記事で作成した独自アノテーション)などの単項目入力チェックアノテーションもつけてますから、ここでfalseを返すと、エラーメッセージがダブって表示されたります。
あくまで、相関チェックのエラー以外では、trueを返しておくと、そのへんがスッキリします。
④ 開始日オブジェクトと終了日オブジェクトを比較する
LocalDateのCompareToメソッドを使います。
if(endDateObj.compareTo(startDateObj) >= 0){
ret = true;
}else{
ret = false;
}
終了日を前にしているので、終了日>=開始日の時は、0以上の数値が返り、そうでないときはマイナス数値が返るわけです。
⑤ 開始日<=終了日なら trueを返す
⑥ 開始日>終了日ならエラーメッセージを生成し、falseを返す。
2つまとめてやります。
if(ret){
return true;
}else{
context.disableDefaultConstraintViolation();context
.buildConstraintViolationWithTemplate(message
.addPropertyNode(endDateProperty)
.addConstraintViolation();
return false;
}
エラーメッセージを返すときの手続き部分を整理します
まず、デフォルトの制約違反情報をクリアします。
context.disableDefaultConstraintViolation();
そして、アノテーション定義でセットしたメッセージをエラーメッセージにします。
今回は終了日の横にエラーメッセージを表示したいので、addPropertyNode(endDateProperty)で、Formクラスの終了日項目の名前をセットしてます。
context
.buildConstraintViolationWithTemplate(message
.addPropertyNode(endDateProperty)
.addConstraintViolation();
作ったアノテーションを使ってみます
Formクラスのクラス定義の部分に付与します。引数として、項目名を渡しているところに注意してください。
@DateIntegrationValid(startDateProperty="startDate",endDateProperty="endDate")
public class Hello3Form implements Serializable {@DateValid
private String startDate;
public String getStartDate() {
return startDate;
}public void setStartDate(String startDate) {
this.startDate = startDate;
}@DateValid
private String endDate;public String getEndDate() {
return endDate;
}public void setEndDate(String endDate) {
this.endDate = endDate;
}}
あと、相関チェックの対象の項目単体には、前の記事で作成した@DateValidアノテーションをつけてます。別に@NotNullとかの標準アノテーションでも、なんでも普通に使えます。
HTML
HTMLは、相関チェックがはいっても変わるところはありません。
<input type="text" id="startDate" name="startDate" th:field="*{startDate}" />
<span class="error_msg" th:if="${#fields.hasErrors('startDate')}" th:errors="*{startDate}">error!</span>
<input type="text" id="endDate" name="endDate" th:field="*{endDate}" />
<span class="error_msg" th:if="${#fields.hasErrors('endDate')}" th:errors="*{endDate}">error!</span>
実行してみます。
いけてますね。
STS +Spring Boot +thymeleaf 関連記事
入力画面に関連する記事
参照画面・画面遷移に関連する記事
参照画面:テーブルを使い、行毎に色分けした一覧表を表示する。
入力チェックに関連する記事
入力チェック:@Patternと正規表現で独自チェックする。
入力チェック用アノテーション定義を自分で作る。(独自実装版)
データアクセス・その他に関連する記事