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
62 changes: 61 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,66 @@ 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 constrait validators as shown in the example below:
Copy link
Member

Choose a reason for hiding this comment

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

s/constrait/constraint/

Copy link
Member

Choose a reason for hiding this comment

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

Fixed.


[[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.
Copy link
Member

Choose a reason for hiding this comment

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

, unless a specific ConstraintValidator implementation passes on the payload to emitted constraint violations by calling xyz.

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 leave that up to @gsmet if he wants to mention that.

====

[[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[]