Home > Net >  Custom Spring validator not working properly
Custom Spring validator not working properly

Time:08-26

I'm trying to make artificial CONSTRAINT violation by Spring instead of throwing exception from DB (an expert sad DB-produced errors have high performance cost):

import javax.validation.ConstraintViolation;
import javax.validation.Validator;

@Component
public class AccountValidator implements org.springframework.validation.Validator {
    
    @Autowired
    private Validator validator;

    private final AccountService accountService;
    public AccountValidator(@Qualifier("accountServiceAlias")AccountService accountService) {
        this.accountService = accountService;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return AccountRequestDTO.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        
        Set<ConstraintViolation<Object>> validates = validator.validate(target);
        for (ConstraintViolation<Object> constraintViolation : validates) {
            String propertyPath = constraintViolation.getPropertyPath().toString();
            String message = constraintViolation.getMessage();
            errors.rejectValue(propertyPath, "", message);
        }
        
        AccountRequestDTO account = (AccountRequestDTO) target;
        if(accountService.getPhone(account.getPhone()) != null){
            errors.rejectValue("phone", "", "Validator in action! This number is already in use.");
        }
    }

}

However, second part of validate() method never works for reasons I cant understand and always pass a call from controller to be handled in try-catch block throwing exception from DB:

    public void saveAccount(AccountRequestDTO accountRequestDTO) throws Exception {
        
        LocalDate birthday = LocalDate.parse(accountRequestDTO.getBirthday());
        if (LocalDate.from(birthday).until(LocalDate.now(), ChronoUnit.YEARS) < 18) {
            throw new RegistrationException("You must be 18  to register");
        }
        
        Account account = new Account(accountRequestDTO.getName(), accountRequestDTO.getSurname(),
                accountRequestDTO.getPhone(), birthday, BCrypt.hashpw
                (accountRequestDTO.getPassword(), BCrypt.gensalt(4)));
        account.addRole(Role.CLIENT);
        try {
            accountRepository.save(account);
        }
        catch (RuntimeException exc) {
            throw new PersistenceException("Database exception: this number is already in use.");
        }
    }

Here's a controller method:

    @PostMapping("/confirm")
    public String signIn(@ModelAttribute("account") @Valid AccountRequestDTO accountRequestDTO,
            BindingResult result, Model model) {
        
        accountValidator.validate(accountRequestDTO, result);
        if(result.hasErrors()) {
            return "/auth/register";
        }
        try {
            accountService.saveAccount(accountRequestDTO);
        }
        catch (Exception exc) {
            model.addAttribute("message", exc.getMessage());
            return "/auth/register";            
        }
        return "/auth/login";
    }   

At service:

    @Transactional(readOnly = true)
    public String getPhone(String phone){
        return accountRepository.getPhone(phone);
    }

JpaRepository query:

    @Query("SELECT phone FROM Account accounts WHERE phone=:check")
    String getPhone(String check);

Tests are green:

    @BeforeAll
    static void prepare() {
        search = new String("0000000000");
    }

    @BeforeEach
    void set_up() {
        account = new Account
                ("Admin", "Adminov", "0000000000", LocalDate.of(2001, 01, 01), "superadmin");
        accountRepository.save(account);
    }
    
    @Test
    void check_if_phone_presents() {        
        assertThat(accountRepository.getPhone(search).equals(account.getPhone())).isTrue();
    }
    
    @Test
    void check_if_phone_not_presents() {
        String newPhone = "9999999999";
        assertThat(accountRepository.getPhone(newPhone)).isNull();
    }   
        
    @AfterEach
    void tear_down() {
        accountRepository.deleteAll();
        account = null;
    }
    
    @AfterAll
    static void clear() {
        search = null;
    }

CodePudding user response:

You need to register your validator.

After we've defined the validator, we need to map it to a specific event which is generated after the request is accepted.

This can be done in three ways:

Add Component annotation with name “beforeCreateAccountValidator“. Spring Boot will recognize prefix beforeCreate which determines the event we want to catch, and it will also recognize WebsiteUser class from Component name.

@Component("beforeCreateAccountValidator")
    public class AccountValidator implements Validator {
     ...
}

Create Bean in Application Context with @Bean annotation:

@Bean
public AccountValidator beforeCreateAccountValidator () {
    return new AccountValidator ();
}

Manual registration:

@SpringBootApplication
public class SpringDataRestApplication implements RepositoryRestConfigurer {
    public static void main(String[] args) {
        SpringApplication.run(SpringDataRestApplication.class, args);
    }

    @Override
    public void configureValidatingRepositoryEventListener(
      ValidatingRepositoryEventListener v) {
        v.addValidator("beforeCreate", new AccountValidator ());
    }
}
  • Related