Spring MVC+Bean Validation(Hibernate Validator)で日付バリデーションの現在日時を変更する

今回は、Spring MVC+Bean Validation(Hibernate Validator)において、日付バリデーション（`@Future`,`@Past`など）の現在日時を変更する方法を紹介します。

アプリで一意のシステム日時を使用したい場合や、テスト時のみ任意の日時を使用したい場合に役立つと思います。

### 今回使用するライブラリ

#### Hibernate Validator 6 (Bean Validation 2.0)
* Spring MVC 5.1.4
* Hibernate Validator 6.0.14
* Lombok

#### Hibernate Validator 5 (Bean Validation 1.1)
* Spring MVC 4.3.22
* Hibernate Validator 5.4.3
* Lombok

### 現在日時を提供するプロバイダ

Hibernate Validatorの日付バリデーション（`@Future`,`@Past`など）実装では、現在日時を単純に`new Date()`のように取得しているわけではなく、現在日時を提供するプロバイダを利用して取得します。

Hibernate Validator 6と5でプロバイダが異なります。

#### Hibernate Validator 6 (Bean Validation 2.0)
Bean Validation 2.0で導入された[ClockProvider](https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#section-clock-provider)インターフェイスが現在日時を提供します。

```java
Validator validator = Validation.byDefaultProvider()
                        .configure()
                        .clockProvider(new MyClockProvider())
                        .buildValidatorFactory()
                        .getValidator();
```

#### Hibernate Validator 5 (Bean Validation 1.1)
Hibernate Validatorの[TimeProvider](https://docs.jboss.org/hibernate/validator/5.4/reference/en-US/html_single/#section-time-provider)インターフェイスが現在日時を提供します。

```java
Validator validator = Validation.byDefaultProvider()
                        .configure()
                        .timeProvider(new MyTimeProvider())
                        .buildValidatorFactory()
                        .getValidator();
```

### プロバイダの作成

今回は、Spring MVCアプリのテスト時のみ任意の日時を使用するためのプロバイダを作成してみます。

#### Hibernate Validator 6 (Bean Validation 2.0)

`ClockProvider#getClock`で現在日時を取得するための`Clock`を提供します。
ここでは、日時文字列とタイムゾーンを指定して、常に同じ日時を提供することにします。

```java
public class FixedClockProvider implements ClockProvider, InitializingBean {

    @Setter
    private String instantPattern;
    
    @Setter
    private String zoneIdPattern;
    
    private Clock clock;
    
    @Override
    public Clock getClock() {
        return clock;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Instant instant = instantPattern == null ? Instant.now() : Instant.parse(instantPattern);
        ZoneId zoneId = zoneIdPattern == null ? ZoneId.systemDefault() : ZoneId.of(zoneIdPattern);
        clock = Clock.fixed(instant, zoneId);
    }
}
```
Bean生成時に`Clock#fixed`で`Clock`を生成しているのがポイントですね。

#### Hibernate Validator 5 (Bean Validation 1.1)

`TimeProvider#getCurrentTime`で現在日時のミリ秒を提供します。
同様に日時文字列とタイムゾーンを指定して、常に同じ日時を提供することにします。

```java
public class FixedTimeProvider implements TimeProvider, InitializingBean {

    @Setter
    private String instantPattern;
    
    @Setter
    private String zoneIdPattern;
    
    private Clock clock;
    
    @Override
    public long getCurrentTime() {
        return clock.millis();
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Instant instant = instantPattern == null ? Instant.now() : Instant.parse(instantPattern);
        ZoneId zoneId = zoneIdPattern == null ? ZoneId.systemDefault() : ZoneId.of(zoneIdPattern);
        clock = Clock.fixed(instant, zoneId);
    }
}
```
Hibernate Validator 6向けの実装と互換性を持たせるため、一度`Clock`を経由してミリ秒を取得していますが、必ずしも必要な実装ではありませんw

### プロバイダの適用

作成したプロバイダを適用するには、Springの`LocalValidatorFactoryBean`を拡張する必要があります。

#### Hibernate Validator 6 (Bean Validation 2.0)

`LocalValidatorFactoryBean#postProcessConfiguration`でBean Validationの設定を追加することができます。
ここでは、Bean ValidationにHibernate Validatorが利用されている場合に、`ClockProvider`を適用します。

```java
public class LocalHibernateValidatorFactoryBean extends LocalValidatorFactoryBean {

    @Setter
    private ClockProvider clockProvider;
    
    @Override
    protected void postProcessConfiguration(Configuration<?> configuration) {
        if (configuration instanceof HibernateValidatorConfiguration) {
            postProcessHibernateValidatorConfiguration((HibernateValidatorConfiguration) configuration);
        }
    }

    protected void postProcessHibernateValidatorConfiguration(HibernateValidatorConfiguration configuration) {
        configuration.clockProvider(clockProvider);
    }
}
```

`Configuration<?>`を`HibernateValidatorConfiguration`にキャストすることで、Hibernate Validator特有の設定を追加することができます。

#### Hibernate Validator 5 (Bean Validation 1.1)

同様に、Bean Validationにhibernate Validatorが利用されている場合は、`TimeProvider`を適用します。

```java
public class LocalHibernateValidatorFactoryBean extends LocalValidatorFactoryBean {

    @Setter
    private TimeProvider timeProvider;
    
    @Override
    protected void postProcessConfiguration(Configuration<?> configuration) {
        if (configuration instanceof HibernateValidatorConfiguration) {
            postProcessHibernateValidatorConfiguration((HibernateValidatorConfiguration) configuration);
        }
    }

    protected void postProcessHibernateValidatorConfiguration(HibernateValidatorConfiguration configuration) {
        configuration.timeProvider(timeProvider);
    }
}
```

### 拡張した`LocalValidatorFactoryBean`の適用

Bean定義で拡張した`LocalValidatorFactoryBean`を適用します。

#### Hibernate Validator 6 (Bean Validation 2.0)

* XML Config

```xml
    <mvc:annotation-driven validator="validator">
        ...
    </mvc:annotation-driven>
    <bean id="validator" class="xxx.LocalHibernateValidatorFactoryBean">
        <property name="clockProvider">
            <bean class="xxx.FixedClockProvider">
                <property name="instantPattern" value="2010-06-30T01:20:00Z" />
            </bean>
        </property>
    </bean>
```

* Java Config

```java
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public Validator getValidator() {
        return validator();
    }

    @Bean
    public LocalValidatorFactoryBean validator() {
        LocalHibernateValidatorFactoryBean validator = new LocalHibernateValidatorFactoryBean();
        validator.setClockProvider(clockProvider());
        return validator;
    }

    @Bean
    public ClockProvider clockProvider() {
        FixedClockProvider provider = new FixedClockProvider();
        provider.setInstantPattern("2010-06-30T01:20:00Z");
        return provider;
    }
}
```

#### Hibernate Validator 5 (Bean Validation 1.1)

* XML Config

```xml
    <mvc:annotation-driven validator="validator">
        ...
    </mvc:annotation-driven>
    <bean id="validator" class="xxx.LocalHibernateValidatorFactoryBean">
        <property name="timeProvider">
            <bean class="xxx.FixedTimeProvider">
                <property name="instantPattern" value="2010-06-30T01:20:00Z" />
            </bean>
        </property>
    </bean>
```

* Java Config

```java
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public Validator getValidator() {
        return validator();
    }

    @Bean
    public LocalValidatorFactoryBean validator() {
        LocalHibernateValidatorFactoryBean validator = new LocalHibernateValidatorFactoryBean();
        validator.setTimeProvider(timeProvider());
        return validator;
    }

    @Bean
    public TimeProvider timeProvider() {
        FixedTimeProvider provider = new FixedTimeProvider();
        provider.setInstantPattern("2010-06-30T01:20:00Z");
        return provider;
    }
}
```

### 現在日時を使用する`ConstraintValidator`の作成

では次に、現在日時を使用して入力チェックを行う`ConstraintValidator`の作り方を見ていきます。

#### Hibernate Validator 6 (Bean Validation 2.0)

`ClockProvider`から`Clock`を取得して、現在日時を取得します。

```java
public class MyValidator implements HibernateConstraintValidator<MyCheck, LocalDate> {

    private Clock clock;
    
    @Override
    public void initialize(ConstraintDescriptor<MyCheck> constraintDescriptor,
            HibernateConstraintValidatorInitializationContext initializationContext) {
        clock = initializationContext.getClockProvider().getClock();
    }
    
    @Override
    public boolean isValid(LocalDate value, ConstraintValidatorContext context) {
        LocalDate referenceDate = clock.instant().atZone(ZoneId.systemDefault()).toLocalDate();

        // バリデーションを実装する
    }
}
```
`ClockProvider`を取得するためにHibernate Validatorの`HibernateConstraintValidator`インターフェイスを継承する必要があるのがポイントですね。

`Clock`は時を刻み続けるため、`initialize`で一度だけ取得しています。（これが後で問題になるのですが）

> `isValid`で`ConstraintValidatorContext`を`HibernateConstraintValidatorContext`にキャストしても`ClockProvider`を取得できると思いますが、`@Future`や`@Past`に実装を合わせています。

#### Hibernate Validator 5 (Bean Validation 1.1)

`TimProvider`から現在日時を取得します。

```java
public class MyValidator implements ConstraintValidator<MyCheck, LocalDate> {

    @Override
    public void initialize(ThisYear constraintAnnotation) {
    }
    
    @Override
    public boolean isValid(LocalDate value, ConstraintValidatorContext context) {
        TimeProvider timeProvider = context.unwrap(HibernateConstraintValidatorContext.class).getTimeProvider();
       LocalDate referenceDate = Instant.ofEpochMilli(timeProvider.getCurrentTime()).atZone(ZoneId.systemDefault()).toLocalDate();

        // バリデーションを実装する
    }
}
```
`TimeProvider`を取得するために`ConstraintValidatorContext`を`HibernateConstraintValidatorContext`にキャストするのがポイントですね。

### おまけ：TERASOLUNAの`DateFactory`をプロバイダに適用する

TERASOLUNAの`DateFactory`は、データベースが持つシステム日時を複数のアプリで共有するなど、エンタープライズ向けWebアプリに役立つ機能です。

今度はこれを適用したプロバイダを作成してみます。

#### Hibernate Validator 6 (Bean Validation 2.0)

以下のように、`DateFactory`によりシステム日時を取得する`ClockProvider`を実装できます。

```java
public class DateFactoryClockProvider2 implements ClockProvider {

    @Setter
    private JodaTimeDateFactory dateFactory;

    @Override
    public Clock getClock() {
        return Clock.offset(Clock.systemDefaultZone(), Duration.between(Instant.now(), dateFactory.newDate().toInstant()));
    }
}
```
> TERASOLUNA 5.4.1では、JSR310 (Java 8 Date and Time API)に対応した`DateFactory`が提供されていないため、`DateFactory`が取得した日付から`Clock`を生成する部分は独自に実装する必要があります。

しかし、`DateFactory`を完全にラップする`ClockProvider`を作るのは不可能ではないかと思います。

`DateFactory`はデータベースからシステム日時を取得するため、システム運用中や総合試験中の日時変更が可能ですが、Validatorは`HibernateConstraintValidator#initialize`で一度だけ`Clock`を取得するため、`@Future`や`@Past`のような標準Validatorに運用中の日時変更を反映することができない。。。と思われます。

#### Hibernate Validator 5 (Bean Validation 1.1)

以下のように、`DateFactory`によりシステム日時を取得する`TimeProvider`を実装できます。

```java
public class DateFactoryTimeProvider implements TimeProvider {

    @Setter
    private JodaTimeDateFactory dateFactory;
    
    @Override
    public long getCurrentTime() {
        return dateFactory.newDateTime().getMillis();
    }
}
```

### まとめ

仕組みはそれほど難しくないので、機会があれば適用してみようと思います。

Bean Validation 2.0から現在日時の制御が標準化されたので、実装方法が変わりましたが、使い勝手も変わったので微妙なところですね。

ちなみに、Springの`LocalValidatorFactoryBean`は1.1と2.0の互換性を保つため、`ClockProvider`のカスタマイズをあえて採用していないようです。 -> [spring-framework#21994](https://github.com/spring-projects/spring-framework/issues/21994)