Bean validation with custom annotation FieldsValueMatch doesnt work.

Hi everyone, I’m new here, so please forgive me if there is an anserw for that question on this forum, but i couldn’t found this. So I have User entity, UserDto, and registration view with fields username, password, confirm password. I’m binding registration view to UserDto with BeanValidationBinder. I also wrote annotation to validate if passwords typed by user matches. Unfortunately it doesnt work. Basic annotations as: @NotEmpty works but not this written by me. Thank in advance You all for a help!

@Constraint(validatedBy = FieldsValueMatchValidator.class)
@Target({ ElementType.TYPE , ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldsValueMatch {

    String message() default "Fields values don't match!";

    String field();

    String fieldMatch();

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

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

}
public class FieldsValueMatchValidator implements ConstraintValidator<FieldsValueMatch, String> {

    private String field;
    private String fieldMatch;

    @Override
    public void initialize(FieldsValueMatch constraintAnnotation) {
        field = constraintAnnotation.field();
        fieldMatch = constraintAnnotation.fieldMatch();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {

        Object fieldValue = new BeanWrapperImpl(value).getPropertyValue(field);
        Object fieldMatchValue = new BeanWrapperImpl(value).getPropertyValue(fieldMatch);

        if (fieldValue != null) {
            return fieldValue.equals(fieldMatchValue);
        } else {
            return fieldMatchValue == null;
        } 
     
    }
}
 @FieldsValueMatch(
        field = "password",
        fieldMatch = "confirmPassword",
        message = "Passwords do not match!"
 )
public class UserDto {

    @NotEmpty
    @NotNull
    private String username;

    @NotEmpty
    @NotNull
    private String password;

    @NotEmpty
    @NotNull
    private String confirmPassword;

    public UserDto() {
    }

    public UserDto(String username, String password, String confirmPassword) {
        this.username = username;
        this.password = password;
        this.confirmPassword = confirmPassword;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getConfirmPassword() {
        return confirmPassword;
    }

    public void setConfirmPassword(String passwordConfirm) {
        this.confirmPassword = confirmPassword;
    }
}
@Route("register")
@PageTitle("Register | Car exchange")
public class RegisterView extends VerticalLayout {

    private TextField username = new TextField("Username");
    private PasswordField password = new PasswordField("Password");
    private PasswordField confirmPassword = new PasswordField("Confirm password");
    private Button registerButton = new Button("Register");
    private Button backToLoginViewButton = new Button("Return to login page");

    private RegisterLogic registerLogic = new RegisterLogic();

    private Binder<UserDto> binder = new BeanValidationBinder<>(UserDto.class);
    private UserDto userDto;
    private UserService userService;

    @Autowired
    public RegisterView(UserService userService) {
        addClassName("register-view");
        this.userService = userService;
        setSizeFull();
        setAlignItems(Alignment.CENTER);
        setJustifyContentMode(JustifyContentMode.CENTER);

        binder.bindInstanceFields(this);
        binder.addStatusChangeListener(statusChangeEvent -> registerButton.setEnabled(binder.isValid()));

        configureRegisterButton();

        add(
                new H1("Car exchange"),
                new H2("Sign up"),
                username,
                password,
                confirmPassword,
                registerButton,
                backToLoginViewButton
        );

        setEmptyUserDtoForBinder();
    }

    private void configureRegisterButton(){
        username.setWidth("20em");
        password.setWidth("20em");
        confirmPassword.setWidth("20em");
        registerButton.setWidth("20em");
        backToLoginViewButton.setWidth("20em");

        registerButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS);

        registerButton.addClickListener(registerLogic::validateAndRegister);

        backToLoginViewButton.addClickListener(buttonClickEvent -> UI.getCurrent().navigate(LoginView.class));

    }

    private void setEmptyUserDtoForBinder(){
        userDto = new UserDto("", "", "");
        binder.readBean(userDto);
    }


    private class RegisterLogic {

        private void validateAndRegister(ClickEvent event){
            try {
                binder.writeBean(userDto);
                userService.registerUser(userDto);
                Notification.show("Registration succeed!", 3000, Notification.Position.MIDDLE);
            } catch (UserAlreadyExistsException e) {
                Notification.show("This username is already taken!", 3000, Notification.Position.MIDDLE);
            } catch (ValidationException e) {
                e.printStackTrace();
            }
        }

    }
}

As noted [in the docs]
(https://vaadin.com/docs/v14/flow/binding-data/tutorial-flow-components-binder-beans.html#using-jsr-303-bean-validation):

As an alternative to defining constraint annotations for specific properties, you can define constraints on the bean level, but Vaadin’s BeanValidationBinder does not currently support them. It simply ignores all JSR 303 validations that are not assigned directly to properties.

So you’ll need to define that kind of validation manually with Binder.

Olli Tietäväinen:
As noted [in the docs]
(https://vaadin.com/docs/v14/flow/binding-data/tutorial-flow-components-binder-beans.html#using-jsr-303-bean-validation):

As an alternative to defining constraint annotations for specific properties, you can define constraints on the bean level, but Vaadin’s BeanValidationBinder does not currently support them. It simply ignores all JSR 303 validations that are not assigned directly to properties.

So you’ll need to define that kind of validation manually with Binder.

First, Thank You very match for an answer! So if I understood well - its impossible to validate two fields of bean with custom validator using custom class-level annotation ? Is it like that, that if I want to use custom Validator (for example FieldsValueMatchValidator which i posted above) - I can only do that like this?:

Binder<UserDto> binder = new Binder<>(UserDto.class); 
binder.forField(username).withValidator( new FieldsValueMatchvalidator()).bind(UserDto::getUsername, UserDto::setUsername)

But i need to validate two fields parallel - password and confirmPassword - but its impossible to do:

binder.forField(password, confirmPassword) ????? 

There’s a handful of different approaches you can take. The absolute simplest solution would be just having value change listeners in each field.

Here’s a more proper small example project that demonstrates a scenario where you need to input a city name and zip code, and the zip code must match the city name: https://github.com/peterl1084/v8-crossfield. The project is for Vaadin 8, but the same should work in later versions too.

Olli Tietäväinen:
There’s a handful of different approaches you can take. The absolute simplest solution would be just having value change listeners in each field.

Here’s a more proper small example project that demonstrates a scenario where you need to input a city name and zip code, and the zip code must match the city name: https://github.com/peterl1084/v8-crossfield. The project is for Vaadin 8, but the same should work in later versions too.

Your suggestions were very helpful! I really appreciate that! Finally i’ve done this like that:
maybe it will help someone someday:

    private void configureBinder(){
        validationErrorLabel.addClassName("error-label");
        binder.setStatusLabel(validationErrorLabel);
        binder.bindInstanceFields(this);

        binder.withValidator(registerLogic::checkIfPasswordsMatch, PASSWORDS_DO_NOT_MATCH_ERROR )
                .withValidator(registerLogic:: checkIfUsernameAndPasswordDoNotMatch, USERNAME_AND_PASSWORD_MATCH_ERROR);

        binder.addStatusChangeListener(statusChangeEvent -> registerButton.setEnabled(binder.isValid()));
        userDto = new UserDto("", "", "");
        binder.setBean(userDto);
    }

    private class RegisterLogic {

        private boolean checkIfPasswordsMatch(UserDto userDto){
            return userDto.getPassword().equals(userDto.getConfirmPassword());
        }

        private boolean checkIfUsernameAndPasswordDoNotMatch(UserDto userDto){
            return !userDto.getUsername().equals(userDto.getPassword());
        }

        private void validateAndRegister(ClickEvent event){
            if (binder.validate().isOk()) {
                try {
                    userService.registerUser(binder.getBean());
                    Notification.show("Registration succeed!", 2000, Notification.Position.MIDDLE);
                } catch (UserAlreadyExistsException userAlreadyExistsException) {
                    Notification.show("This username is already taken!", 2000, Notification.Position.MIDDLE);
                }
            }
        }

    }