@Maxのパラメータをプロパティから設定する

### 開発環境
- Java 1.8.0
- Spring Boot 2.0.2.RELEASE
 - Spring core 5.0.6.RELEASE
- Bean Validation 1.1

### はじめに
前回に引き続き、`BeanValidation`の話題です。今回はSpring要素があります:ghost:。
`BeanValidation`で用意されている`@Max`や`@Min`など数値の検証をするバリデータが用意されていますが、これらは`value`が`long`で定義されているため、引数に整数しか指定できません。

数値の検証をするので当たり前なのですが、もしかしたら環境によって許容する値を変えたい。という場面があるかもしれません。そういう場合はプロパティから値を参照できると便利です。
というわけで、プロパティに設定した値も参照できる`@MaxFromProperty`アノテーションを作成してみたいと思います。

今回のゴール↓

```property:許容する値の設定
age.max=100
```

```java:プロパティを参照して数値をチェックする
@MaxFromProperty("age.max")
private long age; // 100以下かチェック
```

```java:数値を直接指定してもOK
@MaxFromProperty("20")
private long age; // 20以下かチェック
```

なお、この記事の最後に完成版のソースとリンクを貼っていますが、これが最適解ではありません。なので、もっと良い方法があるよ！という方のご意見を募集しています:bow_tone1:

### アノテーションを作る
では、さっそくアノテーションを作ってみます。と言っても、アノテーション自体は特筆すべき点はありません。
プロパティキーを受け取れるようにvalueを`String`にするくらいでしょうか？

```java:MaxFromProperty.java
@Documented
@Constraint(validatedBy = { MaxFromPropertyValidator.class })
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
public @interface MaxFromProperty {

	String message() default "{com.neriudon.example.validator.MaxFromProperty.message}";

	Class<?>[] groups() default {};

	Class<? extends Payload>[] payload() default {};

	String value();

	@Target({ METHOD, FIELD })
	@Retention(RUNTIME)
	@Documented
	@interface List {
		MaxFromProperty[] value();
	}
}
```

### バリデータを作る
ここが重要です。

#### プロパティ値を取得する

プロパティ値の解決には`PropertyResolver`を利用するのですが、引数として`PropertySources`が必要です。また、プロパティ値は`PropertySourcesPlaceholderConfigurer`のappliedPropertySourcesで管理されています。
なので、重要なところだけコードを載せますが

```java:MaxFromPropertyValidator.java

// コンストラクタでリゾルバを生成
public MaxFromPropertyValidator(PropertySourcesPlaceholderConfigurer configurer) {
	// プロパティ値を解決するリゾルバの取得
	resolver = new PropertySourcesPropertyResolver(configurer.getAppliedPropertySources());
}

@Override
public void initialize(MaxFromProperty constraintAnnotation) {
	max = getMaxValue(constraintAnnotation.value());
}

private long getMaxValue(String key) {
	// プロパティ値の解決
	// 2つ目の引数はデフォルト値
	String value = resolver.getProperty(key, key);
	try {
		// 数値に変換
		return Long.parseLong(value);
	} catch (NumberFormatException e) {
		// 例外ハンドリング
	}
}
```

でオッケー:v:。
と、したいところなのですが……:sweat:

#### 複数の`PropertySourcesPlaceholderConfigurer`に対応する

アプリケーションによっては`PropertySourcesPlaceholderConfigurer`が複数定義されていることもあるので、各`PropertySourcesPlaceholderConfigurer`のappliedPropertySourcesをまとめて1つの`PropertySources`にする必要があります。

そこで、`PropertySources`のデフォルト実装である`MutablePropertySources`が複数の`PropertySource`をリストで管理しているので、`MutablePropertySources`に各`PropertySourcesPlaceholderConfigurer`のappliedPropertySourcesの中身を突っ込みます。

```java:MaxFromPropertyValidatorのコンストラクタ修正版
public MaxFromPropertyValidator(List<PropertySourcesPlaceholderConfigurer> configurers) {
	MutablePropertySources propertySources = new MutablePropertySources();
	configurers.forEach(c -> c.getAppliedPropertySources().forEach(p -> {
		propertySources.addLast(temp);
	}));
	this.resolver = new PropertySourcesPropertyResolver(propertySources);
}
```

これでオッケー:v:。
と、したいところなのですが……:pensive:

実際に`PropertySourcesPlaceholderConfigurer`が複数ビーン定義された状態で動作確認をすると、想定される動作にならないことがあります。

というのも、

1. `PropertySource`は[`equals()`をオーバーライド](https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/main/java/org/springframework/core/env/PropertySource.java#L135-L138)していて`PropertySource`のnameが同じなら`true`を返すようになっている。
2. [`PropertySourcesPlaceholderConfigurer`](https://github.com/spring-projects/spring-framework/blob/master/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java)でシステムプロパティは`environmentProperties`、プロパティファイルから読み込まれたプロパティ（＝ローカルプロパティ）は`localProperties`でnameが固定されている。
3. [`MutablePropertySources`](https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/main/java/org/springframework/core/env/MutablePropertySources.java#L104-L107)は`PropertySource`を追加する際に、既に追加済みの`PropertySource`と`equals()`で比較して`true`の場合、既に追加されていた`PropertySource`を削除してから、`PropertySource`を追加する仕様になっている。

つまり、この実装だと最後に読み込んだ`PropertySourcesPlaceholderConfigurer`の内容しか`MutablePropertySources`に追加されません:confounded:。

というわけで……無理やりユニークな名前を付けて`MutablePropertySources`に追加します。残念なことに`PropertySource`には`setName()`みたいなnameを上書きするメソッドは用意されていなかったので、`CompositePropertySource`に別名で詰め替えてから`MutablePropertySources`に追加します。

```java:コンストラクタ修正版
public MaxFromPropertyValidator(List<PropertySourcesPlaceholderConfigurer> configurers) {
	MutablePropertySources propertySources = new MutablePropertySources();
	configurers.forEach(c -> c.getAppliedPropertySources().forEach(p -> {
	// ユニークな名前を付けたCompositePropertySourceに詰め替えてMutablePropertySourcesに追加
	CompositePropertySource temp = new CompositePropertySource(
		c.toString().concat("$").concat(p.getName()));
			temp.addPropertySource(p);
			propertySources.addLast(temp);
		}));
	this.resolver = new PropertySourcesPropertyResolver(propertySources);
```
自分でいうのもあれですが、ここは改善の余地があります:innocent:。

ちなみに、`PropertySourcesPlaceholderConfigurer`のビーン、`configurer1`と`configurer2`があり、`order`属性の値がそれぞれ`1`・`2`で`localOverride`属性の値が`false`の場合、`MutablePropertySources`には

1. `configurer1`のシステムプロパティ
2. `configurer1`のローカルプロパティ
3. `configurer2`のシステムプロパティ
4. `configurer2`のローカルプロパティ
 
の順で追加されるのでシステムプロパティが重複してしまいますが、`order`と`localOverride`の設定と整合性を保ちつつ改善するのは難しいかな~と思います。

#### そもそも`PropertySourcesPlaceholderConfigurer`がない場合を考慮する

これまで`PropertySourcesPlaceholderConfigurer`がビーン定義されている前提で話を進めてきましたが、アプリケーションによってはビーン定義していない場合もあるかと思います。
その場合、`Environment`が`PropertyResolver`のサブインタフェースなので`Environment`を設定します。

```java:コンストラクタ修正版
public MaxFromPropertyValidator(List<PropertySourcesPlaceholderConfigurer> configurers, Environment environment) {
	if (configurers != null) {
		// 略
	} else {
		this.resolver = environment;
	}
}
```

#### 元の`MaxValidator`をパク……参考にする
以上でプロパティから値を取得する処理は完成したので、後は検証する数値と最大値の比較をするだけです。
オリジナルである`hibernate-validator`にある`@Max`のバリデータクラスでは`Number`型と`CharSequence`型で表現された数値をサポートしているので参考にします。

ここは個人の好みになりますが、バリデータクラスのJavaファイルが複数あるとわかりづらいので、`MaxFromPropertyValidator`を抽象クラスにして`Number`用と`CharSequence`用のインナークラスに継承させます。その際、`MaxFromPropertyValidator`で`isValid`メソッドをオーバーライドしてnullチェックだけおこない、具体的な数値の比較は抽象メソッドにすることで、`Number`用と`CharSequence`用のクラスでは数値の比較に専念することができ、また`MaxFromPropertyValidator`で取得した最大値の修飾子を`private`に出来ます。
最後にアノテーションクラスの`@Constraint`に指定するクラスも`Number`用と`CharSequence`用の両方を指定します。

文章だとわかりづらいと思うので、↓のソースを読んでください:stuck_out_tongue_closed_eyes:

### 完成版
#### アノテーションクラス
```java:MaxFromProperty.java
@Documented
@Constraint(validatedBy = { MaxFromPropertyValidator.NumberMaxFromPropertyValidator.class,
		MaxFromPropertyValidator.CharSequenceMaxFromPropertyValidator.class })
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
public @interface MaxFromProperty {

	String message() default "{com.neriudon.example.validator.MaxFromProperty.message}";

	Class<?>[] groups() default {};

	Class<? extends Payload>[] payload() default {};

	String value();

	@Target({ METHOD, FIELD })
	@Retention(RUNTIME)
	@Documented
	@interface List {
		MaxFromProperty[] value();
	}
}
```

#### バリデータクラス
```java:MaxFromPropertyValidator.java
public abstract class MaxFromPropertyValidator<T> extends ApplicationObjectSupport
		implements ConstraintValidator<MaxFromProperty, T> {

	private long max;

	private final PropertyResolver resolver;

	public MaxFromPropertyValidator(List<PropertySourcesPlaceholderConfigurer> configurers, Environment environment) {
		if (configurers != null) {
			MutablePropertySources propertySources = new MutablePropertySources();
			configurers.forEach(c -> c.getAppliedPropertySources().forEach(p -> {
				// named unique name.
				// Because MutablePropertySources override propertySources if defined same name.
				CompositePropertySource temp = new CompositePropertySource(
						c.toString().concat("$").concat(p.getName()));
				temp.addPropertySource(p);
				propertySources.addLast(temp);
			}));
			this.resolver = new PropertySourcesPropertyResolver(propertySources);
		} else {
			this.resolver = environment;
		}
	}

	@Override
	public void initialize(MaxFromProperty constraintAnnotation) {
		max = getMaxValue(constraintAnnotation.value());
	}

	@Override
	public boolean isValid(T value, ConstraintValidatorContext context) {
		// null values are valid
		if (value == null) {
			return true;
		}
		return compareToMaxValue(value, max);
	}

	/**
	 * compare target value to maximum value
	 * 
	 * @param value
	 *            target value
	 * @param max
	 *            maximum value
	 * @return true if value is less than or equal to max.
	 */
	protected abstract boolean compareToMaxValue(T value, long max);

	/**
	 * return max value.<br>
	 * if no value mapped key, convert key to long.
	 */
	private long getMaxValue(String key) {
		String value = resolver.getProperty(key, key);
		try {
			return Long.parseLong(value);
		} catch (NumberFormatException e) {
			throw new IllegalArgumentException(
					"failed to get int value from Property(key:" + key + ", value:" + value + ")");
		}
	}

	/**
	 * MaxFromPropertyValidator for Number
	 */
	public static class NumberMaxFromPropertyValidator extends MaxFromPropertyValidator<Number> {

		public NumberMaxFromPropertyValidator(List<PropertySourcesPlaceholderConfigurer> configurers,
				Environment environment) {
			super(configurers, environment);
		}

		@Override
		public boolean compareToMaxValue(Number value, long max) {
			// handling of NaN, positive infinity and negative infinity
			if (value instanceof Double) {
				if ((Double) value == Double.NEGATIVE_INFINITY) {
					return true;
				} else if (Double.isNaN((Double) value) || (Double) value == Double.POSITIVE_INFINITY) {
					return false;
				}
			} else if (value instanceof Float) {
				if ((Float) value == Float.NEGATIVE_INFINITY) {
					return true;
				} else if (Float.isNaN((Float) value) || (Float) value == Float.POSITIVE_INFINITY) {
					return false;
				}
			}
			if (value instanceof BigDecimal) {
				return ((BigDecimal) value).compareTo(BigDecimal.valueOf(max)) != 1;
			} else if (value instanceof BigInteger) {
				return ((BigInteger) value).compareTo(BigInteger.valueOf(max)) != 1;
			} else {
				long longValue = value.longValue();
				return longValue <= max;
			}
		}
	}

	/**
	 * MaxFromPropertyValidator for CharSequernce
	 */
	public static class CharSequenceMaxFromPropertyValidator extends MaxFromPropertyValidator<CharSequence> {

		public CharSequenceMaxFromPropertyValidator(List<PropertySourcesPlaceholderConfigurer> configurers,
				Environment environment) {
			super(configurers, environment);
		}

		@Override
		public boolean compareToMaxValue(CharSequence value, long max) {
			try {
				return new BigDecimal(value.toString()).compareTo(BigDecimal.valueOf(max)) != 1;
			} catch (NumberFormatException nfe) {
				return false;
			}
		}
	}
}
```

### テストしてみる
わかりやすいように

- プロパティファイルを参照して上限値を設定できること
- 上限値を直接設定することもできること
- `CharSequence`型のフィールドもチェックできること

を分けています。

```java
@RunWith(SpringRunner.class)
@SpringBootTest
public class ValidationSampleApplicationTests {
	@Autowired
	private Validator validator;

	@Test
	public void maxFromPropertySampleNg() {
		MaxFromPropertySample sample = new MaxFromPropertySample(100);
		Set<ConstraintViolation<MaxFromPropertySample>> result = validator.validate(sample);
		assertThat(result.size(), is(1));
		result.stream().forEach(r -> {
			assertThat(r.getInvalidValue(), is(100L));
		});
	}

	@Test
	public void maxValueSetDirectlyNg() {
		MaxValueSetDirectly sample = new MaxValueSetDirectly(100);
		Set<ConstraintViolation<MaxValueSetDirectly>> result = validator.validate(sample);
		assertThat(result.size(), is(1));
		result.stream().forEach(r -> {
			assertThat(r.getInvalidValue(), is(100L));
		});
	}

	@Test
	public void maxFromPropertyForCharSequenceNg() {
		MaxFromPropertyForCharSequence sample = new MaxFromPropertyForCharSequence("100");
		Set<ConstraintViolation<MaxFromPropertyForCharSequence>> result = validator.validate(sample);
		assertThat(result.size(), is(1));
		result.stream().forEach(r -> {
			assertThat(r.getInvalidValue(), is("100"));
		});
	}
	// プロパティファイルから数値を指定する
	private static class MaxFromPropertySample {

		@MaxFromProperty("max")
		private long value;

		public MaxFromPropertySample(long value) {
			this.value = value;
		}
	}
	// 数値を直接指定する
	private static class MaxValueSetDirectly {

		@MaxFromProperty("99")
		private long value;

		public MaxValueSetDirectly(long value) {
			this.value = value;
		}
	}
	// CharSequence型の数値もチェックできる
	private static class MaxFromPropertyForCharSequence {

		@MaxFromProperty("max")
		private CharSequence value;

		public MaxFromPropertyForCharSequence(CharSequence value) {
			this.value = value;
		}
	}
}
```

JavaConfigクラス

```java:TestConfig.java
@Configuration
@PropertySource("sample.properties")
public class TestConfig {

	@Bean
	public Validator getValidator() {
		return new LocalValidatorFactoryBean();
	}
}
```

クラスパス直下に`sample.properties`を置きます。

```properties:sample.properties
max=99
```

### おわりに
プロパティから値を取得するだけだし。と思って作ってみましたが、結構考えることが多くて大変でした。プロパティ値の解決にSpringの仕組みを使っているのでピュアなJavaでは作成したアノテーションは動きませんが、需要はあると思うので、いつかHibernateで提供してくれたらなぁと思います。

ちなみにバリデータクラスで`PropertyResolver`を設定する部分は汎用的に使えるので、`@Min`や`@Size`に対しても同じように拡張可能です。

今回作成したコードは[GitHub](https://github.com/neriudon/validation_sample)に挙げていますが、記事内に記載したように最適解ではありませんので、参考にする場合はご留意くださいませ:stuck_out_tongue_winking_eye:。