Search code examples
javaspringannotationsbean-validationhibernate-validator

Spring validator: having both annotation and validator implementation


Is it possible to have both a validator for a form and annotation constraints?

For example to have in a form object this field:

@NotEmpty
private String date;

but then validate the date's pattern in a validator.

I know there is the pattern annotation but I just want to see if I can use both types of validating.


Solution

  • Here is the link to a very good site where it's explained how you can combine the JSR-303 validator with the spring validator.

    I'll present next my solution that works. Hope it helps.

    My abstract Validator:

    import java.util.Map;
    import java.util.Set;
    
    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorFactory;
    import javax.validation.ConstraintViolation;
    import javax.validation.Validator;
    
    import org.springframework.beans.BeansException;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.ApplicationContextAware;
    import org.springframework.validation.Errors;
    
    
    public abstract class AbstractValidator implements org.springframework.validation.Validator, ApplicationContextAware,
        ConstraintValidatorFactory {
    
    @Autowired
    private Validator validator;
    
    private ApplicationContext applicationContext;
    
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
    
    public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
        Map<String, T> beansByNames = applicationContext.getBeansOfType(key);
        if (beansByNames.isEmpty()) {
            try {
                return key.newInstance();
            } catch (InstantiationException e) {
                throw new RuntimeException("Could not instantiate constraint validator class '" + key.getName() + "'", e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException("Could not instantiate constraint validator class '" + key.getName() + "'", e);
            }
        }
        if (beansByNames.size() > 1) {
            throw new RuntimeException("Only one bean of type '" + key.getName() + "' is allowed in the application context");
        }
        return (T) beansByNames.values().iterator().next();
    }
    
    public boolean supports(Class<?> c) {
        return true;
    }
    
    public void validate(Object objectForm, Errors errors) {
        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(objectForm);
        for (ConstraintViolation<Object> constraintViolation : constraintViolations) {
            String propertyPath = constraintViolation.getPropertyPath().toString();
            String message = constraintViolation.getMessage();
            errors.rejectValue(propertyPath, "", message);
        }
        addExtraValidation(objectForm, errors);
    }
    
    protected abstract void addExtraValidation(Object objectForm, Errors errors);
    

    }

    An actual Validator:

    import org.springframework.stereotype.Component;
    import org.springframework.validation.Errors;
    
    import ro.scorpionsoftware.demo3.dao.AbstractValidator;
    
    
    @Component(value="doctorValidator")
    public class DoctorValidator extends AbstractValidator {
    
        @Override
        protected void addExtraValidation(Object objectForm, Errors errors) {
            //perform typical validation
            //can autowire to context
        }
    
    
    }
    

    A controller: (At the end it's the binding of the @Valid with the validator)

    @Controller
    public class DoctorEditController {
    
    @Autowired
        private DoctorValidator doctorValidator;
    
    @RequestMapping(value = "/doctorEdit", method = RequestMethod.POST)
        public String processSubmit(
                @ModelAttribute("doctorForm") @Valid DoctorForm df,
                BindingResult result,
                ModelMap model) {
         ...
    }
    @InitBinder
        protected void initBinder(WebDataBinder binder) {
            binder.setValidator(doctorValidator);
        }
    }
    

    In context declare the JSR-303 validator:

    <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />
    

    With this approach you can get the context in both the actual validator and any other custom annotation you'd like to implement.