Adding custom ConstraintValidator for @Future and LocalDate to a Spring Boot project
Asked Answered
C

5

8

I have a Spring MVC form for inputting a date, the input gets sent to a Controller and validated via standard Spring MVC validation.

Model:

public class InvoiceForm {
    @Future
    private LocalDate invoicedate;
}

Controller:

public String postAdd(@Valid @ModelAttribute InvoiceForm invoiceForm, BindingResult result) {
    ....
}

When submitting the form I get the following error:

javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint 'javax.validation.constraints.Future' validating type 'java.time.LocalDate'. Check configuration for 'invoicedate'
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.throwExceptionForNullValidator(ConstraintTree.java:229) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.getConstraintValidatorNoUnwrapping(ConstraintTree.java:310) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.getConstraintValidatorInstanceForAutomaticUnwrapping(ConstraintTree.java:244) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.getInitializedConstraintValidator(ConstraintTree.java:163) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateConstraints(ConstraintTree.java:116) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateConstraints(ConstraintTree.java:87) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.metadata.core.MetaConstraint.validateConstraint(MetaConstraint.java:73) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateMetaConstraint(ValidatorImpl.java:617) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraint(ValidatorImpl.java:580) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForSingleDefaultGroupElement(ValidatorImpl.java:524) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForDefaultGroup(ValidatorImpl.java:492) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForCurrentGroup(ValidatorImpl.java:457) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateInContext(ValidatorImpl.java:407) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validate(ValidatorImpl.java:205) ~[hibernate-validator-5.2.4.Final.jar:5.2.4.Final]
at org.springframework.validation.beanvalidation.SpringValidatorAdapter.validate(SpringValidatorAdapter.java:108) ~[spring-context-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.validation.DataBinder.validate(DataBinder.java:866) ~[spring-context-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.validateIfApplicable(ModelAttributeMethodProcessor.java:164) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:111) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:99) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:161) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:128) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:110) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:832) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:743) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:961) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:895) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:967) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:869) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:648) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:843) ~[spring-webmvc-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:729) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:292) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) ~[tomcat-embed-websocket-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:316) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:126) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:90) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:114) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:122) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:169) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at com.balticfinance.jwt.StatelessAuthenticationFilter.doFilter(StatelessAuthenticationFilter.java:46) ~[classes/:na]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176) ~[spring-security-web-4.0.4.RELEASE.jar:4.0.4.RELEASE]
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:87) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:121) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.2.7.RELEASE.jar:4.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:212) ~[tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:676) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:141) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:528) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1099) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:670) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1520) [tomcat-embed-core-8.0.36.jar:8.0.36]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1476) [tomcat-embed-core-8.0.36.jar:8.0.36]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_91]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_91]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-8.0.36.jar:8.0.36]
at java.lang.Thread.run(Thread.java:745) [na:1.8.0_91]

I implemented my own ConstraintValidator for this case. But somehow Spring Boot does not pick it up for validation.

@Component
public class LocalDateFutureValidator implements ConstraintValidator<Future, LocalDate> {

    @Override
    public void initialize(Future future) {
    }

    @Override
    public boolean isValid(LocalDate localDate, ConstraintValidatorContext constraintValidatorContext) {
        LocalDate today = LocalDate.now();
        return localDate.isEqual(today) || localDate.isAfter(today);
    }
}

I know I could simply write my own annotation and specify the validator there, but isn't there a cleaner and easier way?

Canaille answered 15/7, 2016 at 10:27 Comment(5)
The @Component is pretty useless, as the validators aren't managed by spring. Your @Future should specify which validator to use. If you use a recent version of hibernate validator this is supported out-of-the-box btw and you don't need a custom validator at all.Humbug
Thanks, judging by hibernate.atlassian.net/browse/HV-874 @Future with LocalDate should be supported since hibernate-validator 5.2. I'm actually using hibernate-validator 5.2.4.Final (automatically pulled by spring-boot-starter-data-jpa i guess). I'm using spring-boot-starter-parent 1.3.6.RELEASECanaille
The validators for LocalDate were removed, probably I should use another type?! hibernate.atlassian.net/browse/HV-981Canaille
Well then the only thing which would work is to create your own validator and annotations I guess. I'm not sure how to register a custom validator for one of the default annotations.Humbug
Registering custom validators should go through META-INF/validation.xmlMerlon
S
6

You'll need to add your own validator to the META-INF/validation.xml file, like so:

<constraint-mappings
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/mapping validation-mapping-1.1.xsd"
    xmlns="http://jboss.org/xml/ns/javax/validation/mapping" version="1.1">

    <constraint-definition annotation="javax.validation.constraints.Future">
        <validated-by include-existing-validators="true">
            <value>package.to.LocalDateFutureValidator</value>
        </validated-by>
    </constraint-definition>
</constraint-mappings>

For more details, refer to the official documentation.

Source answered 15/7, 2016 at 13:39 Comment(4)
You would not have to exclude the existing validators, but in general using XML to add the new ConstraintValidator is the right thing to do. If you are happy to use a non standardized approach you can look at ConstraintDefinitionContributor as well. In this case you can use the Java ServiceLoader approach to add the new constraint validator. That said, there is a good reason why there is no @Future implementation for LocalDate. Per definition is not possible to determine whether a LocalDate is in the future, since it does not contain time zone information. This might be ok in your case, but ..Uphill
They aren't excluded but, in fact, included :)Merlon
I have a Spring Boot app that I'm trying to basically do this same thing. How do I tell Spring about my custom validator? I keep getting the UnexpectedTypeException despite having a validation.xml file (in my Resources folder)Jacobin
Could you share more details or (ideally) open up a new question and share some code that reproduces the issue? Because this approach worked for me in a Boot project...Merlon
I
9

I'd agree with Miloš that using the META-INF/validation.xml is probably the cleanest and easiest way, but if you do really want to set it up in a Spring @Confguration class then it is possible and here's one way you can do it.

The beauty of Spring Boot is that does a lot of configuration on your behalf so you don't have to worry about it. However, this can also cause problems when you want to specifically configure something yourself and it's not terribly obvious how to do it.

So yesterday I set about trying to add a CustomerValidator for @Past and LocalDate using the ConstraintDefinitionContributor mechanism that Hardy suggests (and is referred to in the Hibernate documentation).

The simple bit was to write the implementing class to do the validation, which for my highly specific purposes consisted of:

public class PastValidator implements ConstraintValidator<Past, LocalDate> {

    @Override
    public void initialize(Past constraintAnnotation) {}

    @Override
    public boolean isValid(LocalDate value, ConstraintValidatorContext context) {
        return null != value && value.isBefore(LocalDate.now());
    }
}

Then I got lazy and just instantiated a @Bean in my configuration, just on the off-chance that one of Spring's auto-configuration classes would just pick it up and wire it into the Hibernate validator. This was quite a long shot, given the available documentation (or lack thereof) and what Hardy and others had said, and it didn't pay off.

So I fired up a debugger and worked backwards from the exception being thrown in org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree which was telling me that it couldn't find a validator for @Past and LocalDate.

Looking at the type hierarchy of the ConstraintValidatorFactory I discovered that there were two implementing classes in my Spring MVC application: SpringConstraintValidatorFactory and SpringWebConstraintValidatorFactory which both just try and get a bean from the context of the correct class. This told me that I do have to have my validator registered with Spring's BeanFactory, however when I stuck a breakpoint on this but it didn't get hit for my PastValidator, which meant that Hibernate wasn't aware that it should be even requesting this class.

This made sense: there wasn't any ConstraintDefinitionContributor anywhere to do tell Hibernate it needed to ask Spring for an instance of the PastValidator. The example in the documentation at http://docs.jboss.org/hibernate/validator/5.2/reference/en-US/html_single/#section-constraint-definition-contributor suggests that I'd need access to a HibernateValidatorConfiguration so I just needed to find where Spring was doing its configuring.

After a little bit of digging I found that it was all happening in Spring's LocalValidatorFactoryBean class, specifically in its afterPropertiesSet() method. From its javadoc:

/*
 * This is the central class for {@code javax.validation} (JSR-303) setup in a Spring
 * application context: It bootstraps a {@code javax.validation.ValidationFactory} and
 * exposes it through the Spring {@link org.springframework.validation.Validator} interface
 * as well as through the JSR-303 {@link javax.validation.Validator} interface and the
 * {@link javax.validation.ValidatorFactory} interface itself.
 */

Basically, if you don't set up and configure your own Validator then this is where Spring tries to do it for you, and in true Spring style it provides a handy extension method so that you can let it do its configuration and then add your own into the mix.

So my solution was to just extend the LocalValidatorFactoryBean so that I'd be able to register my own ConstraintDefinitionContributor instances:

import java.util.ArrayList;
import java.util.List;
import javax.validation.Configuration;
import org.hibernate.validator.internal.engine.ConfigurationImpl;
import org.hibernate.validator.spi.constraintdefinition.ConstraintDefinitionContributor;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

public class ConstraintContributingValidatorFactoryBean extends LocalValidatorFactoryBean {

    private List<ConstraintDefinitionContributor> contributors = new ArrayList<>();

    public void addConstraintDefinitionContributor(ConstraintDefinitionContributor contributor) {
        contributors.add(contributor);
    }

    @Override
    protected void postProcessConfiguration(Configuration<?> configuration) {
        if(configuration instanceof ConfigurationImpl) {
            ConfigurationImpl config = ConfigurationImpl.class.cast(configuration);
            for(ConstraintDefinitionContributor contributor : contributors)
                config.addConstraintDefinitionContributor(contributor);
        }
    }
}

and then instantiate and configure this in my Spring config:

    @Bean
    public ConstraintContributingValidatorFactoryBean validatorFactory() {
        ConstraintContributingValidatorFactoryBean validatorFactory = new ConstraintContributingValidatorFactoryBean();
        validatorFactory.addConstraintDefinitionContributor(new ConstraintDefinitionContributor() {
            @Override
            public void collectConstraintDefinitions(ConstraintDefinitionBuilder builder) {
                    builder.constraint( Past.class )
                            .includeExistingValidators( true )
                            .validatedBy( PastValidator.class );
            }
        });
        return validatorFactory;
    }

and for completeness, here's also where I'd instantiated by PastValidator bean:

    @Bean
    public PastValidator pastValidator() {
        return new PastValidator();
    }

Other springy-thingies

I noticed in my debugging that because I've got quite a large Spring MVC application, I was seeing two instances of SpringConstraintValidatorFactory and one of SpringWebConstraintValidatorFactory. I found the latter was never used during validation so I just ignored it for the time being.

Spring also has a mechanism for deciding which implementation of ValidatorFactory to use, so it's possible for it to not use your ConstraintContributingValidatorFactoryBean and instead use something else (sorry, I found the class in which it did this yesterday but couldn't find it again today although I only spent about 2 minutes looking). If you're using Spring MVC in any kind of non-trivial way then chances are you've already had to write your own configuration class such as this one which implements WebMvcConfigurer where you can explicitly wire in your Validator bean:

public static class MvcConfigurer implements WebMvcConfigurer {

    @Autowired
    private ConstraintContributingValidatorFactoryBean validatorFactory;

    @Override
    public Validator getValidator() {
        return validatorFactory;
    }

    // ...
    // <snip>lots of other overridden methods</snip>
    // ...
}

This is wrong

As has been pointed out, you should be wary of applying a @Past validation to a LocalDate because there's no time zone information. However, if you're using LocalDate because everything will just run in the same time zone, or you deliberately want to ignore time zones, or you just don't care, then this is fine for you.

Ines answered 15/7, 2016 at 10:27 Comment(1)
Note that ConstraintDefinitionContributor only existed in 5.2, and subsequent versions of Hibernate Validator use a similar but slightly different approach.Hageman
S
6

You'll need to add your own validator to the META-INF/validation.xml file, like so:

<constraint-mappings
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/mapping validation-mapping-1.1.xsd"
    xmlns="http://jboss.org/xml/ns/javax/validation/mapping" version="1.1">

    <constraint-definition annotation="javax.validation.constraints.Future">
        <validated-by include-existing-validators="true">
            <value>package.to.LocalDateFutureValidator</value>
        </validated-by>
    </constraint-definition>
</constraint-mappings>

For more details, refer to the official documentation.

Source answered 15/7, 2016 at 13:39 Comment(4)
You would not have to exclude the existing validators, but in general using XML to add the new ConstraintValidator is the right thing to do. If you are happy to use a non standardized approach you can look at ConstraintDefinitionContributor as well. In this case you can use the Java ServiceLoader approach to add the new constraint validator. That said, there is a good reason why there is no @Future implementation for LocalDate. Per definition is not possible to determine whether a LocalDate is in the future, since it does not contain time zone information. This might be ok in your case, but ..Uphill
They aren't excluded but, in fact, included :)Merlon
I have a Spring Boot app that I'm trying to basically do this same thing. How do I tell Spring about my custom validator? I keep getting the UnexpectedTypeException despite having a validation.xml file (in my Resources folder)Jacobin
Could you share more details or (ideally) open up a new question and share some code that reproduces the issue? Because this approach worked for me in a Boot project...Merlon
S
2

In case someone has a problem with the validation.xml approach and is getting Cannot find the declaration of element constraint-mappings error, as I did, I had to make the following modifications. Hope this will save somebody the time I spent to figure this out.

META-INF/validation.xml:

<validation-config
        xmlns="http://jboss.org/xml/ns/javax/validation/configuration"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="
                    http://jboss.org/xml/ns/javax/validation/configuration
                    validation-configuration-1.1.xsd"
        version="1.1">
    <constraint-mapping>META-INF/validation/past.xml</constraint-mapping>
</validation-config>

META-INF/validation/past.xml:

<constraint-mappings
        xmlns="http://jboss.org/xml/ns/javax/validation/mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="
http://jboss.org/xml/ns/javax/validation/mapping
validation-mapping-1.1.xsd"
        version="1.1">
    <constraint-definition annotation="javax.validation.constraints.Past">
        <validated-by include-existing-validators="false">
            <value>your.package.PastConstraintValidator</value>
        </validated-by>
    </constraint-definition>
</constraint-mappings>
Superego answered 6/5, 2018 at 22:6 Comment(0)
H
2

A custom LocalValidatorFactoryBean bean can configured with the custom mapping and then be wired as a Spring Bean, replacing the default autowired Spring Boot validator:

@Configuration
public class ValidationConfig {
  @Bean
  public LocalValidatorFactoryBean defaultValidator() {
    return new CustomValidatorFactoryBean();
  }
  
  private static class CustomValidatorFactoryBean extends LocalValidatorFactoryBean {
      @Override
      protected void postProcessConfiguration(Configuration<?> configuration) {
          HibernateValidatorConfiguration hibernateConfiguration = (HibernateValidatorConfiguration) configuration;
          ConstraintMapping constraintMapping = hibernateConfiguration.createConstraintMapping();
          constraintMapping
                  .constraintDefinition(Future.class)
                  .validatedBy(LocalDateFutureValidator.class)
                  .includeExistingValidators(true);
          hibernateConfiguration.addMapping(constraintMapping);
      }
  }
}

Note that Spring does not automatically inject or otherwise automatically handle validators as beans, so the @Component annotation on LocalDateFutureValidator is not needed, is not accomplishing anything, and is misleading. It should therefore be removed.

Note also that newer versions of Hibernate Validator have LocalDate @Future functionality by default, so this approach is no longer necessary for LocalDate validation. However, this approach is still useful for custom ConstraintValidators for types that are not built in.

Explanation

First, note that Spring does not automatically inject or otherwise handle validators as beans, so the @Component annotation on LocalDateFutureValidator is not needed, is not accomplishing anything, and is misleading. It should therefore be removed.

The Hibernate Bean Validation reference guide has a section devoted to adding constraint definitions. It discusses two ways of adding constraint definitions: via Java's ServiceLoader and programmatically. For this answer, I will discuss how to add them programmatically.

Using this approach, your new constraint could be added to the Hibernate configuration as follows:

HibernateValidatorConfiguration hibernateConfiguration = [...];

ConstraintMapping constraintMapping = hibernateConfiguration.createConstraintMapping();
constraintMapping
        .constraintDefinition(Future.class)
        .validatedBy(LocalDateFutureValidator.class)
        .includeExistingValidators(true);
hibernateConfiguration.addMapping(constraintMapping);

In order to do this, we need to modify the HibernateValidatorConfiguration used by the application's validator. The LocalValidatorFactoryBean class is the bean factory type used by Spring:

This is the central class for javax.validation (JSR-303) setup in a Spring application context: It bootstraps a javax.validation.ValidationFactory and exposes it through the Spring Validator interface as well as through the JSR-303 Validator interface and the ValidatorFactory interface itself.

Spring Boot provides a bean of this type through the defaultValidator bean per ValidationAutoConfiguration.defaultValidator(). This bean definition is annotated with @ConditionalOnMissingBean(Validator.class), which means your Boot app can replace it by declaring its own Validator bean, such as a LocalValidatorFactoryBean.

To add custom mappings, one can extend LocalValidatorFactoryBean and override the protected postProcessConfiguration method:

public class CustomValidatorFactoryBean extends LocalValidatorFactoryBean {
    @Override
    protected void postProcessConfiguration(Configuration<?> configuration) {
        HibernateValidatorConfiguration hibernateConfiguration = (HibernateValidatorConfiguration) configuration;
        ConstraintMapping constraintMapping = hibernateConfiguration.createConstraintMapping();
        constraintMapping
                .constraintDefinition(Future.class)
                .validatedBy(LocalDateFutureValidator.class)
                .includeExistingValidators(true);
        hibernateConfiguration.addMapping(constraintMapping);
    }
}

All that's left at this point is to wire it into the Spring context so that it replaces the default Validator bean. This can be done by using a @Configuration class with a @Bean definition method, or by annotating the CustomValidatorFactoryBean class as a @Component.

Hageman answered 29/1, 2021 at 0:49 Comment(0)
H
0

Hibernate 6.0 added built-in support for @Future with LocalDate values, per FutureValidatorForLocalDate. Therefore, if the code gets upgraded to Hibernate 6.0 or later, it should work out of the box.

Hageman answered 28/1, 2021 at 23:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.