アラカン"BOKU"のITな日常

あれこれ興味をもって考えたことを書いてます

入力チェック:複数項目の相関チェックアノテーションの作り方 STS 3.8.3(Spring Boot 1.5.1)+thymeleaf

複数項目の相関チェック処理を共通処理化して、アノテーションで使えるようにする方法を試してみます。

 

複数項目の相関チェックとは、例えば、履歴管理するデータの開始日・終了日が、「開始日より終了日が過去ではない」などの相互の整合性を確認するような場合に使います。

 

個別にロジックを書いてもいいんですけど、よく使うチェックは、単一項目の入力チェックと同様にアノテーションで指定できるようにしておくと便利ですよね。

 

ということで、今回は、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()がわかりやすくていいかなと思ってます。

 

次は、実装クラスです。基本的なところは、単項目の入力チェックを独自実装する時と同じです。

arakan-pgm-ai.hatenablog.com

 

ちょっと長いので細切れにして、単項目の時と違う部分を解説していきます。

 

まず、クラスの定義です。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オブジェクトの生成に失敗すると例外が発生します。try - catchブロックで受けて例外を処理するわけですが、このときに「③ LocalDateオブジェクト生成で例外が発生したらtrueを返す。」をおすすめします。

 

理由は、単項目チェックのアノテーションを優先させるためです。基本的に日付項目には@DateValid(以前の記事で作成した独自アノテーション)などの単項目入力チェックアノテーションもつけてますから、ここでfalseを返すと、エラーメッセージがダブって表示されたります。あくまで、相関チェックのエラー以外では、trueを返しておくと、そのへんがスッキリします。

 

「 ④ 開始日オブジェクトと終了日オブジェクトを比較する」は、LocalDateのCompareToメソッドを使います。

if(endDateObj.compareTo(startDateObj) >= 0){
        ret = true;
}else{
       ret = false;
}

 

終了日を前にしているので、終了日>=開始日の時は、0以上の数値が返り、そうでないときはマイナス数値が返るわけです。

 

「⑤ 開始日<=終了日なら trueを返す」と「⑥ 開始日>終了日ならエラーメッセージを生成し、falseを返す。」はまとめてやります。

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は、相関チェックがはいっても変わるところはありません。

<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>

 

実行してみます。

f:id:arakan_no_boku:20170401133933j:plain

 

いけてますね。

 

参考に、前に書いた入力チェックについて書いた記事のリンクを再掲しておきます。アノテーション定義のルールとか、今回書いていない部分は、こちらの記事に書いてますので、参照ください。

arakan-pgm-ai.hatenablog.com

arakan-pgm-ai.hatenablog.com

arakan-pgm-ai.hatenablog.com

arakan-pgm-ai.hatenablog.com

arakan-pgm-ai.hatenablog.com

 

 


 STS  3.8.3(Spring Boot 1.5.1)+thymeleaf 関連記事

 

入力画面に関連する記事

画像をSUBMITボタン代わりに使う

ラジオボタンとラジオボタングループを使う 

ラジオボタンとラジオボタングループを使う 

チェックボックスを使う。 

HTMLのタグの閉じ忘れで例外が発生する!

プルダウンリストとマルチセレクトボックスを使う。

今度はテキストエリアで複数行入力する。 

テキストボックスの入力と基本的なチェックを使う。

 

参照画面・画面遷移に関連する記事

参照画面:テーブルを使い、行毎に色分けした一覧表を表示する。 

参照画面:条件に一致した時のみHTML要素を出力する。

セッションを使って画面間で情報を受け渡す 

画面遷移:GET時のリクエストパラメータを受け取る 

日本語しか使わなくても、i18N対応はする意味がある。

 

入力チェックに関連する記事

入力チェック:未入力時に空文字が渡される仕様を回避する。 

入力チェック:@Patternと正規表現で独自チェックする。

入力チェック:アノテーション定義を自分で作る。(再利用版)

入力チェック用アノテーション定義を自分で作る。(独自実装版)

入力チェック:複数項目の相関チェックアノテーションの作り方

 

データアクセス・その他に関連する記事

データアクセス:Jprepositoryを使って簡単にCRUDする。 

データアクセス:ネイティブなSQL文を実行する 

クラスパス内の静的ファイルにアクセスする 

SpringBootだとログの書き出しも楽ちんです。 

SpringBootプロジェクトでJUNIT4を使った単体テストをする。