Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

HV-1529 Add dynamic payload to HibernateConstraintValidator #890

Closed
wants to merge 8 commits into from
63 changes: 62 additions & 1 deletion documentation/src/main/asciidoc/ch06.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ The `initialize()` method of `HibernateConstraintValidator` takes two parameters
* The `ConstraintDescriptor` of the constraint at hand.
You can get access to the annotation using `ConstraintDescriptor#getAnnotation()`.
* The `HibernateConstraintValidatorInitializationContext` which provides useful helpers and contextual
information, such as the clock provider or the temporal validation tolerance.
information, such as the clock provider, the temporal validation tolerance or the constraint validator payload.

This extension is marked as incubating so it might be subject to change.
The plan is to standardize it and to include it in Bean Validation in the future.
Expand All @@ -210,6 +210,67 @@ include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/MyFutureVa
You should only implement one of the `initialize()` methods. Be aware that both are called when initializing the validator.
====

[[constraint-validator-payload]]
===== Passing a payload to the constraint validator

From time to time, you might want to condition the constraint validator behavior on some external parameters.

For instance, your zip code validator could vary depending on the locale of your application instance if you have one
instance per country.
Another requirement could be to have different behaviors on specific environments: the staging environment may not have
access to some external production resources necessary for the correct functioning of a validator.

The notion of constraint validator payload was introduced for all these use cases.
It is an object passed from the `Validator` instance to each constraint validator via the `HibernateConstraintValidatorInitializationContext`.

The example below shows how to set a constraint validator payload during the `ValidatorFactory` initialization.
Unless you override this default value, all the ``Validator``s created by this `ValidatorFactory` will have this
constraint validator payload value set.

[[example-constraint-validator-payload-definition-validatorfactory]]
.Defining a constraint validator payload during the `ValidatorFactory` initialization
====
[source, JAVA, indent=0]
----
include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorpayload/ConstraintValidatorPayloadTest.java[tags=setConstraintValidatorPayloadDuringValidatorFactoryInitialization]
----
====

Another option is to set the constraint validator payload per `Validator` using a context:

[[example-constraint-validator-payload-definition-validatorcontext]]
.Defining a constraint validator payload using a `Validator` context
====
[source, JAVA, indent=0]
----
include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorpayload/ConstraintValidatorPayloadTest.java[tags=setConstraintValidatorPayloadInValidatorContext]
----
====

Once you have set the constraint validator payload, it is time to use it in your constraint validators as shown in the example below:

[[example-constraint-validator-payload-usage]]
.Using the constraint validator payload in a constraint validator
====
[source, JAVA, indent=0]
----
include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/constraintvalidatorpayload/ZipCodeValidator.java[tags=include]
----
====

`HibernateConstraintValidatorInitializationContext#getConstraintValidatorPayload()` has a type parameter
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question on that one, what happens if I have registered two payloads of type A and B, B extends A and I request A? Is an order of precedence defined?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand your question right you can't have two payloads, because the last one wins.

and returns the payload only if the payload is of the given type.

[NOTE]
====
It is important to note that the constraint validator payload is different from the dynamic payload you can include in
the constraint violation raised.

The whole purpose of this constraint validator payload is to be used to condition the behavior of your constraint validators.
It is not included in the constraint violations, unless a specific `ConstraintValidator` implementation passes on the
payload to emitted constraint violations by using the <<section-dynamic-payload,constraint violation dynamic payload mechanism>>.
====

[[validator-customconstraints-errormessage]]
==== The error message

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorpayload;

import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

import org.hibernate.validator.HibernateValidator;
import org.hibernate.validator.HibernateValidatorFactory;
import org.junit.Test;

@SuppressWarnings("unused")
public class ConstraintValidatorPayloadTest {

@Test
public void setConstraintValidatorPayloadDuringValidatorFactoryInitialization() {
//tag::setConstraintValidatorPayloadDuringValidatorFactoryInitialization[]
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.constraintValidatorPayload( "US" )
.buildValidatorFactory();

Validator validator = validatorFactory.getValidator();
//end::setConstraintValidatorPayloadDuringValidatorFactoryInitialization[]
}

@Test
public void setConstraintValidatorPayloadInValidatorContext() {
//tag::setConstraintValidatorPayloadInValidatorContext[]
HibernateValidatorFactory hibernateValidatorFactory = Validation.byDefaultProvider()
.configure()
.buildValidatorFactory()
.unwrap( HibernateValidatorFactory.class );

Validator validator = hibernateValidatorFactory.usingContext()
.constraintValidatorPayload( "US" )
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I'm seeing the API used, I'm wondering whether it should be constraintValidatorPayload( String.class, "US" ). I can see two advantages:

  • It makes it clearer that there can be multiple payloads (for different types)
  • One can register a given implementation type under the key of a (public) interface

Btw. what happens if I register multiple payloads of the same type? Last one wins, or is an exception raised?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum, no, it seems the implementation actually doesn't allow what I had in mind. Wondering whether that's not a bit too limited?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had the exact same discussion about the other payloads recently and you answered me the user could use a Map or a bean of some sort.

I don't think it's limited and it's consistent with the other payloads in the API.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...and you answered me the user could use a Map or a bean of some sort.

I don't think it's limited and it's consistent with the other payloads in the API.

I fully agree.

.getValidator();

// [...] US specific validation checks

validator = hibernateValidatorFactory.usingContext()
.constraintValidatorPayload( "FR" )
.getValidator();

// [...] France specific validation checks

//end::setConstraintValidatorPayloadInValidatorContext[]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorpayload;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ METHOD, FIELD, ANNOTATION_TYPE, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = ZipCodeValidator.class)
@Documented
public @interface ZipCode {

String message() default "{org.hibernate.validator.referenceguide.chapter06.constraintvalidatorpayload.ZipCode.message}";

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

Class<? extends Payload>[] payload() default { };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//tag::include[]
package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorpayload;

//end::include[]

import javax.validation.ConstraintValidatorContext;
import javax.validation.metadata.ConstraintDescriptor;

import org.hibernate.validator.constraintvalidation.HibernateConstraintValidator;
import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorInitializationContext;

//tag::include[]
public class ZipCodeValidator implements HibernateConstraintValidator<ZipCode, String> {

public String countryCode;

@Override
public void initialize(ConstraintDescriptor<ZipCode> constraintDescriptor,
HibernateConstraintValidatorInitializationContext initializationContext) {
this.countryCode = initializationContext
.getConstraintValidatorPayload( String.class );
}

@Override
public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
if ( object == null ) {
return true;
}

boolean isValid = false;

if ( "US".equals( countryCode ) ) {
// checks specific to the United States
}
else if ( "FR".equals( countryCode ) ) {
// checks specific to France
}
else {
// ...
}

return isValid;
}
}
//end::include[]
Original file line number Diff line number Diff line change
Expand Up @@ -298,4 +298,16 @@ public interface HibernateValidatorConfiguration extends Configuration<Hibernate
*/
@Incubating
HibernateValidatorConfiguration temporalValidationTolerance(Duration temporalValidationTolerance);

/**
* Allows to set a payload which will be passed to the constraint validators.
*
* @param constraintValidatorPayload the payload passed to constraint validators
*
* @return {@code this} following the chaining method pattern
*
* @since 6.0.8
*/
@Incubating
HibernateValidatorConfiguration constraintValidatorPayload(Object constraintValidatorPayload);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How comes this is exposed at the Configuration level? IIRC the originally discussed use case, this feature was more indented for a specific context (e.g. the current user's locale) which typically will differ between multiple requests. While I probably can get behind supporting this on a per-validator basis, I don't think exposing this here is very useful. A VF should be constructed once and kept around, you definitely don't want to create a new one for each request.

Copy link
Contributor Author

@mkurz mkurz Feb 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With my PR you don't have to create a new VF for each request, you can just use vf.unwrap( HibernateValidatorFactory.class ).usingContext().constraintValidatorPayload(...). Exposing the payload at Configuration level was never my original intention, however during the work on this pull request @gsmet thought we should do that as well, probably to define a "default" payload.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was added at my specific request. I don't think it's that absurd to imagine someone needing to pass information at the VF level to the validators (might be info about the environment for instance, staging/production, with validation rules differing depending on the environment).

And it was also because I wanted the symmetry with the other features.

I think it's better with it than without.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to agree with @gsmet - it's better with it than without.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern is that this this may be people lead to do the wrong thing and provide volatile contextual information to the VF instead of getting a (rather cheap) validator instance with such information.

Really that's the purpose of VF#usingContext(), I don't think adding another means of passing in "context" is a very good idea. About that environment use case, you'd just pass that to the Validator and keep that around. I don't see what's gained by exposing this at the VF level?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you supposing that the information is volatile? My environment example is not about volatile information.

I would say it makes as much sense as any other configuration knobs we have at the VF level: we want it to be the default value for all Validator creations.

usingContext() is exclusively used to override VF configuration for a given Validator creation. That's how it's documented and coded.

What you're asking is special casing this one, not the other way around.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, let's do it then. I think the confusion stems from the very generic nature of this thing (just passing in and getting out any object). For the originally discussed use case (locale), it doesn't make really sense on the VF, but yes, perhaps for others.

On your environment example, can you detail that a bit, i.e. when would validation be different per environment?

Copy link
Contributor Author

@mkurz mkurz Feb 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... specific to my use case maybe we could run different instances of an application - each instance for a different country. Each country should use it's own locale - no matter what the user locale in the request is. E.g. app instances that server .de domains would configure the payload to be german, .com instances would be configured to be english, etc.
Just an idea of how that could be used (even if that may not be a real good example, just came into my mind right now)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gunnarmorling @mkurz Yes, that's basically what I had in mind.

Or a different behavior in staging for instance because staging doesn't have access to some component required for validation (external service only available for production for instance). I know you shouldn't do that but I also know from experience it happens.

}
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,16 @@ public interface HibernateValidatorContext extends ValidatorContext {
*/
@Incubating
HibernateValidatorContext temporalValidationTolerance(Duration temporalValidationTolerance);

/**
* Define a payload passed to the constraint validators.
*
* @param constraintValidatorPayload the payload passed to constraint validators
*
* @return {@code this} following the chaining method pattern
*
* @since 6.0.8
*/
@Incubating
HibernateValidatorContext constraintValidatorPayload(Object constraintValidatorPayload);
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,14 @@ public interface HibernateConstraintValidatorInitializationContext {
* @return the tolerance
*/
Duration getTemporalValidationTolerance();

/**
* Returns an instance of the specified type or {@code null} if no constraint validator payload of the given type
* has been set.
*
* @param type the type of payload to retrieve
* @return an instance of the specified type or null if no payload of this type has been set
* @since 6.0.8
*/
<C> C getConstraintValidatorPayload(Class<C> type);
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public class ConfigurationImpl implements HibernateValidatorConfiguration, Confi
private boolean traversableResolverResultCacheEnabled = true;
private ScriptEvaluatorFactory scriptEvaluatorFactory;
private Duration temporalValidationTolerance;
private Object constraintValidatorPayload;

public ConfigurationImpl(BootstrapState state) {
this();
Expand Down Expand Up @@ -285,6 +286,14 @@ public HibernateValidatorConfiguration temporalValidationTolerance(Duration temp
return this;
}

@Override
public HibernateValidatorConfiguration constraintValidatorPayload(Object constraintValidatorPayload) {
Contracts.assertNotNull( constraintValidatorPayload, MESSAGES.parameterMustNotBeNull( "constraintValidatorPayload" ) );

this.constraintValidatorPayload = constraintValidatorPayload;
return this;
}

public boolean isAllowParallelMethodsDefineParameterConstraints() {
return this.methodValidationConfigurationBuilder.isAllowParallelMethodsDefineParameterConstraints();
}
Expand Down Expand Up @@ -437,6 +446,10 @@ public Duration getTemporalValidationTolerance() {
return temporalValidationTolerance;
}

public Object getConstraintValidatorPayload() {
return constraintValidatorPayload;
}

@Override
public Set<ValueExtractor<?>> getValueExtractors() {
return validationBootstrapParameters.getValueExtractorDescriptors()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,11 @@ static class ValidatorScopedContext {
*/
private final boolean traversableResolverResultCacheEnabled;

/**
* Hibernate Validator specific payload passed to the constraint validators.
*/
private final Object constraintValidatorPayload;

ValidatorScopedContext(ValidatorFactoryScopedContext validatorFactoryScopedContext) {
this.messageInterpolator = validatorFactoryScopedContext.getMessageInterpolator();
this.parameterNameProvider = validatorFactoryScopedContext.getParameterNameProvider();
Expand All @@ -719,6 +724,7 @@ static class ValidatorScopedContext {
this.scriptEvaluatorFactory = validatorFactoryScopedContext.getScriptEvaluatorFactory();
this.failFast = validatorFactoryScopedContext.isFailFast();
this.traversableResolverResultCacheEnabled = validatorFactoryScopedContext.isTraversableResolverResultCacheEnabled();
this.constraintValidatorPayload = validatorFactoryScopedContext.getConstraintValidatorPayload();
}

public MessageInterpolator getMessageInterpolator() {
Expand Down Expand Up @@ -748,6 +754,10 @@ public boolean isFailFast() {
public boolean isTraversableResolverResultCacheEnabled() {
return this.traversableResolverResultCacheEnabled;
}

public Object getConstraintValidatorPayload() {
return this.constraintValidatorPayload;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ public HibernateValidatorContext temporalValidationTolerance(Duration temporalVa
return this;
}

@Override
public HibernateValidatorContext constraintValidatorPayload(Object dynamicPayload) {
validatorFactoryContextBuilder.setConstraintValidatorPayload( dynamicPayload );
return this;
}

@Override
public Validator getValidator() {
return validatorFactory.createValidator(
Expand Down