[JAVA] Until custom validation by annotation is implemented in Spring Framework and message is output

Hello, I'm messing with Spring for the first time in a while. I haven't used it for about half a year at work.

As the title suggests, I made a note of the following contents.

  1. Creating annotations
  2. Create a validator class to which validation processing is transferred from annotation
  3. Display custom messages

Introduction

background

I want to check the consistency with the master data stored in the database.

What you want to achieve

Implement a conference room reservation form. In the conference room reservation form, select the conference room to use with the radio button, and enter the date and time of use and the number of people. There are multiple meeting rooms, each with a fixed capacity. If more than the capacity is entered, an input error will occur.

Implementation

Validation specifications

Get the conference room information based on the entered conference room ID. Obtain the capacity from the information in the conference room and determine whether the entered number of users is less than or equal to the capacity. If the value is invalid, a validation message will be displayed for the capacity form control.

1. Creating annotations.

We named it SeatingCapacityValid because it is a validation of the capacity. According to the validation specifications, at least the conference room ID and the capacity field name must be received as annotation arguments. Each must be declared as a method.

SeatingCapacityValid.java


@Documented
@Constraint(validatedBy = {SeatingCapacityValidator.class}) //Specify the class that implements the validation logic.
@Target(ElementType.TYPE) //Because it's validation for multiple attributes of the form,Designated to be granted to the class.
@Retention(RetentionPolicy.RUNTIME)
public @interface SeatingCapacityValid{
    //Specify the key of the message to be displayed by default.
    String message() default "{com.example.demo.form.validator.SeatingCapacityValid.message}";
    String roomIdProperty(); //Receive specified the name of the property where the entered conference room ID is stored.
    String numberOfUsersProperty(); //Receive specified the name of the property where the entered number of users is stored.

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

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface List {
        SeatingCapacityValid[] value();
    }
}

2. Create a validator class to which validation processing is transferred from annotation.

Implements the ConstraintValidator interface. The first type argument is an annotation, and the target to which the annotation is added (in this case, the Form class, but for versatility, the Object type) is specified.

SeatingCapacityValidator.java


public class SeatingCapacityValidator implements ConstraintValidator<SeatingCapacityValid, Object> {

    @Autowired
    private RoomRepository roomRepository;
    
    private String roomIdProperty;
    private String timesProperty;
    private String message;
    
    @Override
    public void initialize(CommodityTimesValid constraintAnnotation) {
        roomIdProperty = constraintAnnotation.roomIdProperty();
        numberOfUsersProperty = constraintAnnotation.numberOfUsersProperty();
        message = constraintAnnotation.message();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {        
        BeanWrapper beanWrapper = new BeanWrapperImpl(value);
        Integer roomId = (Integer)beanWrapper.getPropertyValue(roomIdProperty);
        Integer numberOfUsers = (Integer) beanWrapper.getPropertyValue(numberOfUsersProperty);

        if (roomId == null || numberOfUsers == null) {
            return true;
        }

        Optional<Room> mayBeRoom = roomRepository.findOne(roomId);

        return mayBeRoom.map(room -> {
            Integer capacity = room.getCapacity();
            if (capacity >= numberOfUsers) {
                return true;
            //Customize TODO messages
            context
                    .buildConstraintViolationWithTemplate(message)
                    .addPropertyNode(numberOfUsersProperty)
                    .addConstraintViolation();
            return false;
        }).orElse(true);
    }
}

3. Display custom messages

The message display logic is described in the part written as TODO in SeatingCapacityValidator.java. In the property file, we define `{0} as the number of people less than {1} and the goal is to display the capacity of each room in {1}.

SeatingCapacityValidator.java


public class SeatingCapacityValidator implements ConstraintValidator<SeatingCapacityValid, Object> {
    //Omission
    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        //Omission
        return mayBeRoom.map(room -> {
            Integer capacity = room.getCapacity();
            if (capacity >= numberOfUsers) {
                return true;
            }
            HibernateConstraintValidatorContext hibernateContext =
                    context.unwrap(HibernateConstraintValidatorContext.class);

            hibernateContext.disableDefaultConstraintViolation();
            hibernateContext
                    .addMessageParameter("1", capacity) //Here is the number of people in each room{1}Set to.
                    .buildConstraintViolationWithTemplate(message)
                    .addPropertyNode(numberOfUsersProperty)
                    .addConstraintViolation();
            return false;
        }).orElse(true);
    }
}

Note

In validation using annotation, you should be careful about the following.

1. Constant specified by the argument of @Target.

Where can annotations be added? Is it a single field validation or a correlation check? Is it really necessary to annotate?

This time, I wanted to bind the input value from the screen to an object that puts it in the domain layer. So I created an annotation so that it will be evaluated at the time of binding.

2. The class specified by the argument of @Constraint.

Which class should the validation logic be delegated to?

3. Method declared by annotation, annotation argument.

Make sure that the necessary information for validation is received from the annotation.

4. How to display information other than annotation arguments in the message.

The message template placeholders ({0} or {1}) are replaced with the information that can be obtained from the annotation. {0} is the field name, and after {1} is the argument of the annotation.

To set something other than the above, use the ConstraintValidatorContext # unwrap (HibernateConstraintValidatorContext.class) method to convert to the HibernateConstraintValidatorContext class, and then set the string to be displayed.

The name of the method to use Placeholder notation
addMessageParameter(String s, Object o) {s}
addExpressionVariable(String s, Object o) ${s}

at the end

There are many parts that I haven't written about the prerequisites, so I think I'll organize it when the whole code is released.

Recommended Posts

Until custom validation by annotation is implemented in Spring Framework and message is output
Switching beans by profile annotation in Spring
[Rails] I implemented the validation error message by asynchronous communication!