I want to create a generic annotation for a type

Development environment

Introduction

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;

  :

Create a code class

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 notnew. 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();
  }
}

Implement input check process

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);
  }
}

Create annotation

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();
  }
}

Annotation test

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.

Main subject

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.

Apply interface to code definition

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.

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();
}

Code definition correction

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();
  }
}

Correction of input check processing

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;
  }
}

Fix annotation

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<?,?>>

Input check class for each type can be specified in @ 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`.

Implement input check process for String

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();
  }
}

Implement input check process for int

Create a class that returns a Map with a ʻinttype as a key. Except for the type, it is the same asString`, 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

I want to create a generic annotation for a type
I want to create a chat screen for the Swift chat app!
I want to add a reference type column later
[Android] I want to create a ViewPager that can be used for tutorials
I want to create a form to select the [Rails] category
I want to create a Parquet file even in Ruby
I want to develop a web application!
I want to recursively search for files under a specific directory
I want to write a nice build.gradle
I want to write a unit test!
How to create a Maven repository for 2020
I want to create a dark web SNS with Jakarta EE 8 with Java 11
How to create a database for H2 Database anywhere
[Ruby] I want to do a method jump!
How to create pagination for a "kaminari" array
I want to simply write a repeating string
I tried to create a LINE clone app
I want to design a structured exception handling
A note that I gave up trying to make a custom annotation for Lombok
[Azure] I tried to create a Java application for free-Web App creation- [Beginner]
I want to call a method of another class
[Rails] How to create a signed URL for CloudFront
java: How to write a generic type list [Note]
I want to use a little icon in Rails
[Spring Boot] How to create a project (for beginners)
I tried to create a Clova skill in Java
I want to monitor a specific file with WatchService
I want to define a function in Rails Console
I want to click a GoogleMap pin in RSpec
I want to connect to Heroku MySQL from a client
I want to add a delete function to the comment function
[Azure] I tried to create a Java application for free ~ Connect with FTP ~ [Beginner]
I want to convert characters ...
How to create a method
I tried to create a java8 development environment with Chocolatey
Tutorial to create a blog with Rails for beginners Part 1
[Java] I want to convert a byte array to a hexadecimal number
I want to find a relative path in a situation using Path
How to create a lightweight container image for Java apps
[Rails] I tried to create a mini app with FullCalendar
I want to implement a product information editing function ~ part1 ~
I want to make a list with kotlin and java!
I want to call a method and count the number
I want to make a function with kotlin and java!
Even in Java, I want to output true with a == 1 && a == 2 && a == 3
I tried to convert a string to a LocalDate type in Java
I want to give a class name to the select attribute
I want to use FireBase to display a timeline like Twitter
I tried to create a padrino development environment with Docker
I want to return multiple return values for the input argument
How to create and launch a Dockerfile for Payara Micro
Tutorial to create a blog with Rails for beginners Part 0
Implemented a strong API for "I want to display ~~ on the screen" with simple CQRS
I want to return a type different from the input element with Java8 StreamAPI reduce ()
Swift: I want to chain arrays
I tried to create a simple map app in Android Studio
I want to make a button with a line break with link_to [Note]
Preparing to create a Rails application
I want to add a browsing function with ruby on rails
I want to use swipeback on a screen that uses XLPagerTabStrip
I want to use FormObject well