以下を参考に、以前から気になっていたアノテーションを使った相関チェックバリデーションをテスト。

入力チェック:複数項目の相関チェックアノテーションの作り方 STS 3.8.3(Spring Boot 1.5.1)+thymeleaf - アラカン”BOKU”のITな日常

背景

アノテーションによる入力チェックは単項目チェックしかできないものと思っていたが、色々調べてみるとできるらしい。

汎用性の高いバリデーションについては、アノテーションを使った方が再利用しやすいが、やり方がよく分からなかったのでとりあえず作ってみた。

基本的な考え

今回作ったのは上記ページにある開始日、終了日の相関チェックアノテーションで、同様に以下のような仕様とする。

  • アノテーションはフィールドではなくフォームクラスに付ける
  • チェック対象フィールドはアノテーションのパラメータで指定
  • 書式チェック、必須チェックは他のバリデータに任せる

実装イメージ

springboot-study/correlation-check-annotation at master · orimajp/springboot-study

アノテーションインタフェース

package com.example.correlationcheckannotation;

import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Target({TYPE,ANNOTATION_TYPE})
@Constraint(validatedBy = DateCorrelationValidator.class)
@Retention(RUNTIME)
@ReportAsSingleViolation
public @interface DateCorrelationValid {

	String message() default "終了日は開始日より過去にはできません。";
	Class<?>[] groups() default {};
	Class<? extends Payload>[] payload() default {};

	String startDateProperty();
	String endDateProperty();

	@Target({TYPE,ANNOTATION_TYPE})
	@Retention(RUNTIME)
	@Documented
	@interface List {
		DateCorrelationValid[] value();
	}

}

バリデータクラス

日付書式例外は正常とする。

package com.example.correlationcheckannotation;

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class DateCorrelationValidator implements ConstraintValidator<DateCorrelationValid, Object> {

	private String startDateProperty;
	private String endDateProperty;
	private String message;

	@Override
	public void initialize(DateCorrelationValid constraintAnnotation) {
		startDateProperty = constraintAnnotation.startDateProperty();
		endDateProperty = constraintAnnotation.endDateProperty();
		message = constraintAnnotation.message();
	}

	final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd");

	@Override
	public boolean isValid(Object value, ConstraintValidatorContext context) {
		if (value == null) {
			return true;
		}

		final BeanWrapper beanWrapper = new BeanWrapperImpl(value);
		final String start = (String)beanWrapper.getPropertyValue(startDateProperty);
		final String end = (String)beanWrapper.getPropertyValue(endDateProperty);
		final LocalDate startDate;
		final LocalDate endDate;
		try {
			startDate = LocalDate.parse(start, dtf);
			endDate = LocalDate.parse(end, dtf);
		} catch (Exception e) {
			return true;
		}

		if (startDate.equals(endDate) || startDate.isBefore(endDate)) {
			return true;
		}

		context.disableDefaultConstraintViolation();
		context.buildConstraintViolationWithTemplate(message)
				.addPropertyNode(endDateProperty)
				.addConstraintViolation();

		return false;
	}
}

フォーム

対象フィールドをクラスに付加したアノテーションのパラメータで指定。

package com.example.correlationcheckannotation;

import lombok.Data;

@Data
@DateCorrelationValid(startDateProperty = "startDate", endDateProperty = "endDate")
public class TestForm {

	private String startDate;

	private String endDate;

}

コントローラ

package com.example.correlationcheckannotation;

import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.validation.Valid;

@Controller
public class TopController {

	@RequestMapping("/")
	public String index(TestForm testForm) {
		return "index";
	}

	@RequestMapping(path = "/post", method = RequestMethod.POST)
	public String post(@Valid TestForm testForm, BindingResult result) {
		if (result.hasErrors()) {
			return "index";
		}
		return "result";
	}

}

入力画面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<title>入力画面</title>
	<style>
		.error_msg {
			color: red;
		}
	</style>
</head>
<body>

<h1>開始日、終了日チェック</h1>

<form th:action="@{/post}" th:object="${testForm}" method="post">
	<div>
		<label for="startDate">開始日</label>
		<input type="text" id="startDate" th:field="*{startDate}">
	</div>
	<div>
		<label for="endDate">終了日</label>
		<input type="text" id="endDate" th:field="*{endDate}">
		<span class="error_msg" th:if="${#fields.hasErrors('endDate')}" th:errors="*{endDate}">error!</span>
	</div>
	<div>
		<input type="submit" value="送信">
	</div>
</form>

</body>
</html>

結果画面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<title>結果画面</title>
</head>
<body>

<h1>入力OK</h1>

<a th:href="@{/}">入力画面へ</a>

</body>
</html>