Springbootでメッセージの管理とバリデーション処理を行う

以下の内容について記載します

* メッセージファイルの利用について
* 単一項目チェック
* 複数項目チェック

事前準備 

GitHub
https://github.com/jirentaicho/springbootsample
※メッセージの表示はコントローラーのアノテーションをRestControllerに変更します。

application.propertiesを
application.ymlとして、ymlファイルで記載を行う。

既存のpropertiesファイル

```properties:application.properties
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/misaka
spring.datasource.username=misaka
spring.datasource.password=mikoto
```

ymlファイルに置き換えた例

```yml:application.yml
spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://localhost:5432/misaka
    username: misaka
    password: mikoto
```

## メッセージを表示する

参考
https://spring-boot-doma2-sample.readthedocs.io/ja/master/messages.html

* messages.properties
* ValidationMessages.properties
* PropertyNames.properties

このうちmessages.propertiesをsrc/main/resourcesに作成します。

```properties:messages.properties
anicom.message001=パーフェクトJava
anicom.message002={0}を取得しました。
```

application.ymlを修正します。
messagesのbasenameに拡張子を省いたファイル名を記載します。

```yml:application.yml
spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://localhost:5432/misaka
    username: misaka
    password: mikoto
  messages:
    basename: messages
    cache-duration: -1
    encoding: UTF-8
```

設定したメッセージを利用してみます。
適当なコントローラーを作成してメッセージの取得ができるか確認してみます。
{0}のような埋め込みタイプは配列を渡して名称を指定します。
また、メッセージの作成にはMessageSourceを利用します。

```java:InitializeController.java
@RestController
public class InitializeController {

	@Autowired 
	private MessageSource messageSource;

	@RequestMapping("message1")
	public String getMessage1(Model model) {
		String message = messageSource.getMessage("anicom.message001", null, Locale.JAPAN);
		return message;
	}

	@RequestMapping("/message2")
	public String getMessage2(Model model) {
		String message = messageSource.getMessage("anicom.message002", new String[]{"御坂美琴"}, Locale.JAPAN);
		return message;
	}
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1835982/8f4a7480-0033-125b-5b90-a7da700da29e.png)

# バリデーション

参考
https://terasolunaorg.github.io/guideline/5.0.0.RELEASE/ja/ArchitectureInDetail/Validation.html

* 単一フィールド
 * Bean Validation(javax.validation.constraints.NotEmptyを使う)
* 複数フィールド
 * org.springframework.validation.Validatorインタフェースを実装したValidationクラス



## 単一フィールド

pomを修正します。spring-boot-starter-validationを追加します。

```xml:pom.xml
		<dependency>
		  <groupId>org.springframework.boot</groupId>
		  <artifactId>spring-boot-starter-validation</artifactId>
		</dependency>
	</dependencies>
```

コントローラーの引数になるFormクラス（ここではrequestにしています）に対してアノテーションを付与します。
今回はtitleに対してNotEmptyを付けました。

```java:AnimationRequest.java
@Setter
@Getter
@NoArgsConstructor
public class AnimationRequest {
	
	@NotEmpty
	private String title;
	
	@DateTimeFormat(pattern = "yyyy-MM-dd")
	private Date broadcast_start;
	
	@DateTimeFormat(pattern = "yyyy-MM-dd")
	private Date broadcast_end;
}
```

コントローラーの修正
メソッドの引数に@Validatedアノテーションを付与します。BindingResultにバリデーション結果が入っています。

```java:RegisterController.java
@Controller
public class RegisterController {
	
	@Autowired
	private AnimationService animationService;

	@PostMapping("/register")
	public String register(@Validated AnimationRequest request, BindingResult bindingResult , Model model) {
		if(bindingResult.hasErrors()) {
			System.out.println(bindingResult);
			System.out.println("error");
			return "redirect:/";
		}
		animationService.insert(request);
		return "redirect:/";
	}
	
}
```


メッセージの変更
Validationに関するメッセージは専用のプロパティファイルを作成します。

```
javax.validation.constraints.NotEmpty.message=タイトルは必須項目です。
```

apllication.ymlにも反映させます。
basenameにカンマ区切りでファイル名を追加します。

```yml:application.yml
spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://localhost:5432/misaka
    username: misaka
    password: mikoto
  messages:
    basename: messages,ValidationMessages
    cache-duration: -1
    encoding: UTF-8
```

エラーを発生させると以下のようなメッセージがコンソールに吐かれます

```log
Field error in object 'animationRequest' on field 'title': rejected value []; codes [NotEmpty.animationRequest.title,NotEmpty.title,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [animationRequest.title,title]; arguments []; default message [title]]; default message [タイトルは必須です。]
error
```

## 複数フィールド

少し手順が複雑です。以下のような手順になります。

1. メッセージを登録する
1. org.springframework.validation.Validatorを継承したクラスを作成する
1. コントローラーにInitBinderアノテーションを利用して、WebDataBinderに継承したクラスを登録する

1は必須でないですが、せっかくメッセージをプロパティで管理しているので利用します。

とりあえず、放送開始日に入力があった場合は、放送終了日への入力を必須とするバリデーションを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1835982/ae8c482a-c383-d279-70e3-68716588a530.png)


### メッセージを登録する

ValidationMessages.propertiesに以下のメッセージを追加します。


```properties:ValidationMessages.properties
anicom.error.message001=放送開始日を入力した場合は、放送終了日の入力は必須です。
```


###  org.springframework.validation.Validatorを継承したクラスを作成する

実装すべきインターフェースのvalidateをObject型を引数に取ります。
わざわざexecuteメソッドを作る価値はないと思いますが、現状はAnimationValidationクラスなので作りましたが、色々と穴がある状態です（別のformが来たら多分例外発生します）
でも、動きは確認できます。

```java:AnimationValidation.java
package com.volkruss.application.validation;

import java.util.Locale;
import java.util.Objects;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

import com.volkruss.application.request.AnimationRequest;

@Component
public class AnimationValidation implements Validator{
	
	@Autowired
	private MessageSource messageSource;

	@Override
	public boolean supports(Class<?> clazz) {
		// trueにしておく
		return true;
	}

	@Override
	public void validate(Object target, Errors errors) {
		// 動作確認のため汚いですがキャストします。
		execute((AnimationRequest)target,errors);
	}
	
	/**
	 * <P>
	 * 相関バリデーションを実行します。
	 * <br />
	 * 入力値の放送開始日付に入力がある場合は、放送終了日の入力が必須です。
	 * </P>
	 * 
	 * @param request アニメーションリクエスト
	 * @param errors エラー
	 */
	private void execute(AnimationRequest request,Errors errors) {
		// 放送開始日に入力があって、放送終了日に入力がない場合
		if(Objects.nonNull(request.getBroadcast_start()) && Objects.isNull(request.getBroadcast_end()) ) {
			errors.reject("broadcast_end_date",messageSource.getMessage("anicom.error.message001", null, Locale.JAPAN));
		}
	}

}
```

### コントローラーにInitBinderアノテーションを利用して、WebDataBinderに継承したクラスを登録する

@InitBinderアノテーションを利用して、WebDataBinderに対して先ほど作成したAnimationValidationを登録します。

```java:RegisterController.java
@Controller
public class RegisterController {
	
	@Autowired
	private AnimationService animationService;
	
	@Autowired
	private AnimationValidation animationValidation;
	
	@InitBinder
	public void validatorBinder(WebDataBinder binder) {
		binder.addValidators(animationValidation);
	}

	@PostMapping("/register")
	public String register(@Validated AnimationRequest request, BindingResult bindingResult , Model model) {
		if(bindingResult.hasErrors()) {
			System.out.println(bindingResult);
			System.out.println("error");
			return "redirect:/";
		}
		animationService.insert(request);
		return "redirect:/";
	}
	
}
```


これで。放送開始日にだけ入力を入れて登録すると、以下のようなログが出力されます。

```log
org.springframework.validation.BeanPropertyBindingResult: 2 errors
Field error in object 'animationRequest' on field 'title': rejected value []; codes [NotEmpty.animationRequest.title,NotEmpty.title,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [animationRequest.title,title]; arguments []; default message [title]]; default message [タイトルは必須です。]
Error in object 'animationRequest': codes [broadcast_end_date.animationRequest,broadcast_end_date]; arguments []; default message [放送開始日を入力した場合は、放送終了日の入力は必須です。]
error
```