Use Bean Validation
to create an annotation that checks if the entered value is included in the code-defined list.
The usage is like ↓.
Sample.java
public class Sample{
@CodeExists(StringCode.class)
private String code;
:
String
type.The code is defined by Map
(code value, code name). Create a method ʻexiststhat checks if the entered value is code-defined. Also, create the code class in a singleton so that it is not
new. There are several ways to implement a singleton, but I used the
Holder` class by referring to Design pattern" Singleton ".
StringCode.java
public class StringCode {
public static final StringCode INSTANCE = StringCodeHolder.INSTANCE;
private final Map<String, String> codeMap = createCodeMap();
private StringCode() {}
public boolean exists(String code) {
return codeMap.containsKey(code);
}
private Map<String, String> createCodeMap() {
Map<String, String> map = new LinkedHashMap<>();
map.put("1", "foo");
map.put("2", "bar");
map.put("3", "hage");
return Collections.unmodifiableMap(map);
}
private static class StringCodeHolder {
private static final StringCode INSTANCE = new StringCode();
}
}
Creating an instance from the annotation's argument class is messy, but it returns the result of the ʻexists` method.
CodeExistsValidator.java
public class CodeExistsValidator implements ConstraintValidator<CodeExists, String> {
StringCode target;
@Override
public void initialize(CodeExists constraintAnnotation) {
try {
//Get an instance of the target constant class
target = (StringCode) (constraintAnnotation.value().getDeclaredField("INSTANCE").get(this));
} catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException
| SecurityException e) {
//suitable
e.printStackTrace();
}
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
//Returns the result of the exists method
return target.exists(value);
}
}
There is nothing special to write here.
CodeExists.java
@Documented
@Constraint(validatedBy = {CodeExistsValidator.class})
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface CodeExists {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Class<?> value();
@Target({METHOD, FIELD})
@Retention(RUNTIME)
@Documented
public @interface List {
CodeExists[] value();
}
}
Let's test it now.
ValidationSampleApplicationTests.java
@RunWith(SpringRunner.class)
@SpringBootTest
public class ValidationSampleApplicationTests {
private static final String EXIST_CODE = "1";
private static final String NOT_EXIST_CODE = "5";
private ValidatorFactory validatorFactory;
private Validator validator;
@Before
public void setup() {
validatorFactory = Validation.buildDefaultValidatorFactory();
validator = validatorFactory.getValidator();
}
@Test
public void existStringCode() throws Exception {
StringCodeSample code = new StringCodeSample(EXIST_CODE);
Set<ConstraintViolation<StringCodeSample>> result = validator.validate(code);
assertThat(result.isEmpty(), is(true));
}
@Test
public void notExistStringCode() throws Exception {
StringCodeSample code = new StringCodeSample(NOT_EXIST_CODE);
Set<ConstraintViolation<StringCodeSample>> result = validator.validate(code);
assertThat(result.size(), is(1));
assertThat(result.iterator().next().getInvalidValue(), is(NOT_EXIST_CODE));
}
/**
*Class with String type code
*/
private static class StringCodeSample {
@CodeExists(StringCode.class)
private String code;
public StringCodeSample(String code) {
this.code = code;
}
}
}
You can confirm that if you set " 1 "
, which is code-defined in StringCode.java
, the input check error will be 0, and if you set"5"
, which is not code-defined, you will get an input check error.
The introduction has become very long, but ...
In ↑, the code definition was limited to String
, but suppose that the requirement to be able to define the code even with ʻint and
boolean` types is added.
Create an interface to create a generic code definition class. The code existence check is common regardless of the type, so define it with the default method.
String
type.Code.java
public interface Code<T> {
/**
*Check if the specified code exists in the code list.
*/
default boolean exists(T code) {
return asMap().containsKey(code);
}
/**
*Returns a map that associates the code value with the code name.
*/
Map<T, String> asMap();
}
Modify the StringCode.java
created in ↑.
--Implemented Code <String>
--Removed ʻexists method --ʻAsMap
returns codeMap
StringCode.java
public class StringCode implements Code<String>{
public static final StringCode INSTANCE = StringCodeHolder.INSTANCE;
private final Map<String, String> codeMap = createCodeMap();
private StringCode() {}
@Override
public Map<String, String> asMap() {
return codeMap;
}
private Map<String, String> createCodeMap() {
Map<String, String> map = new LinkedHashMap<>();
map.put("1", "foo");
map.put("2", "bar");
map.put("3", "hage");
return Collections.unmodifiableMap(map);
}
private static class StringCodeHolder {
private static final StringCode INSTANCE = new StringCode();
}
}
Modify the CodeExistsValidator.java
created in ↑.
--Changed the parameter of ConstraintValidator
from String
to T
--Changed the type of target
toCode <T>
--Extract instance acquisition from code definition class to method
CodeExistsValidator.java
public class CodeExistsValidator<T> implements ConstraintValidator<CodeExists, T> {
Code<T> target;
@Override
public void initialize(CodeExists constraintAnnotation) {
try {
//Get an instance of the target constant class
target = convert(constraintAnnotation.value().getDeclaredField("INSTANCE").get(this));
} catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException
| SecurityException e) {
//suitable
e.printStackTrace();
}
}
@Override
public boolean isValid(T value, ConstraintValidatorContext context) {
//Returns the result of the exists method
return target.exists(value);
}
@SuppressWarnings("unchecked")
private Code<T> convert(Object o) {
return (Code<T>) o;
}
}
Now that we've modified the code definition and input checking process, all we have to do is modify the annotations. …… But the parameter specified in @ Constraint
is a real class that implements ConstraintValidator
, so it seems that formal arguments cannot be included.
Well, it's a problem even if you specify a generic type.
CodeExists.java
@Documented
@Constraint(validatedBy = {CodeExistsValidator.class}) // NG
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface CodeExists {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Class<? extends Code<?>> value();
@Target({METHOD, FIELD})
@Retention(RUNTIME)
@Documented
public @interface List {
CodeExists[] value();
}
}
Output error
Type mismatch: cannot convert from Class<CodeExistsValidator> to Class<? extends ConstraintValidator<?,?>>
@ Constraint
It would have been nice if I could write something like @ Constraint (validatedBy = {CodeExistsValidator.class})
, but it seems impossible ... Bean Validation
document 5.7.4. ConstraintValidator resolution algorithm I noticed while reading (: //beanvalidation.org/latest-draft/spec/#constraintdeclarationvalidationprocess-validationroutine-typevalidatorresolution), but the input check that matches T
ofConstraintValidator <A, T>
is There seems to be a mechanism to be executed automatically.
In other words, it is OK if you specify the input check for String
and the input check for ʻint`.
So, implement the input check process for String
which inherits CodeExistsValidator
. However, the processing content is already implemented in CodeExistsValidator
, so the contents are empty.
StringCodeExistsValidator.java
public class StringCodeExistsValidator extends CodeExistsValidator<String> {
}
In the annotation, specify the implementation class.
CodeExists.java
@Documented
@Constraint(validatedBy = {StringCodeExistsValidator.class}) // OK
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface CodeExists {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Class<? extends Code<?>> value(); //Only the class that implements Code
@Target({METHOD, FIELD})
@Retention(RUNTIME)
@Documented
public @interface List {
CodeExists[] value();
}
}
Create a class that returns a Map
with a ʻinttype as a key. Except for the type, it is the same as
String`, so only a part is excerpted.
IntCode.java
public class IntCode implements Code<Integer> {
:
@Override
public Map<Integer, String> asMap() {
return codeMap;
}
private Map<Integer, String> createCodeMap() {
Map<Integer, String> map = new LinkedHashMap<>();
map.put(4, "foo");
map.put(5, "bar");
map.put(6, "hage");
return Collections.unmodifiableMap(map);
}
:
}
The input check process just creates a class like String
and the implementation is empty.
Also, make the original CodeExistsValidator
an abstract class.
IntCodeExistsValidator.java
public class IntCodeExistsValidator extends CodeExistsValidator<Integer> {
}
Finally, add the implementation class to the annotation.
CodeExists.java
@Documented
@Constraint(validatedBy = {StringCodeExistsValidator.class, IntCodeExistsValidator.class}) //here
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface CodeExists {
:
}
In this state, add a test.
ValidationSampleApplicationTests.java
@RunWith(SpringRunner.class)
@SpringBootTest
public class ValidationSampleApplicationTests {
private static final String STRING_EXIST_CODE = "1";
private static final String STRING_NOT_EXIST_CODE = "5";
private static final int INT_EXIST_CODE = 4;
private static final int INT_NOT_EXIST_CODE = 1;
private ValidatorFactory validatorFactory;
private Validator validator;
@Before
public void setup() {
validatorFactory = Validation.buildDefaultValidatorFactory();
validator = validatorFactory.getValidator();
}
@Test
public void existStringCode() throws Exception {
StringCodeSample code = new StringCodeSample(STRING_EXIST_CODE);
Set<ConstraintViolation<StringCodeSample>> result = validator.validate(code);
assertThat(result.isEmpty(), is(true));
}
@Test
public void notExistStringCode() throws Exception {
StringCodeSample code = new StringCodeSample(STRING_NOT_EXIST_CODE);
Set<ConstraintViolation<StringCodeSample>> result = validator.validate(code);
assertThat(result.size(), is(1));
assertThat(result.iterator().next().getInvalidValue(), is(STRING_NOT_EXIST_CODE));
}
@Test
public void existIntCode() throws Exception {
IntCodeSample code = new IntCodeSample(INT_EXIST_CODE);
Set<ConstraintViolation<IntCodeSample>> result = validator.validate(code);
assertThat(result.isEmpty(), is(true));
}
@Test
public void notExistIntCode() throws Exception {
IntCodeSample code = new IntCodeSample(INT_NOT_EXIST_CODE);
Set<ConstraintViolation<IntCodeSample>> result = validator.validate(code);
assertThat(result.size(), is(1));
assertThat(result.iterator().next().getInvalidValue(), is(INT_NOT_EXIST_CODE));
}
/**
*Class with String type code
*/
private static class StringCodeSample {
@CodeExists(StringCode.class)
private String code;
public StringCodeSample(String code) {
this.code = code;
}
}
/**
*Class with int type code
*/
private static class IntCodeSample {
@CodeExists(IntCode.class)
private int code;
public IntCodeSample(int code) {
this.code = code;
}
}
}
It was confirmed that the code definition for input check changes depending on the class specified in @ CodeExists
.
However, as the number of types such as boolean
and char
increases, it is unavoidable that the number of classes increases.
Question posted on Stack OverFlow: Hibernate Validator - Add a Dynamic ConstraintValidator was also helpful.
The code in this article is published on GitHub.
Recommended Posts