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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HV-1328 Add an option to validate class-level constraints only if all property constraints are valid #1031

Merged
merged 4 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions documentation/src/main/asciidoc/ch12.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -1001,3 +1001,57 @@ should be used as a default way of resolving the name (see how it is used in <<e
- `org.hibernate.validator.spi.nodenameprovider.JavaBeanProperty` is an interface that holds metadata about a bean property. It
extends `org.hibernate.validator.spi.nodenameprovider.Property` and provide some additional methods like `Class<?> getDeclaringClass()`
which returns the class that is the owner of the property.

[[section-fail-fast-on-property-violation]]
=== Fail fast on property violation mode

[WARNING]
====
This mode is marked as `@Incubating`, so it might be subject to change in the future.
====

Fail fast on property violation mode allows to perform class level constraint validation only if all property level constrints have passed without any violations.
Enabling such mode should make writing class level constraint validators easier and cleaner, as the user will be working with valid properties.
Hence, there will be no need in any additional `null` checks or data sanity checks.

This mode can be enabled using one of the Hibernate Validator common ways.
It can be done either programmatically (<<example-programmatic-fail-fast-on-property-violation>>), or through the property (<<example-property-fail-fast-on-property-violation>>).

[[example-programmatic-fail-fast-on-property-violation]]
.Using the fail fast on property violation validation mode
====
[source, JAVA, indent=0]
----
include::{sourcedir}/org/hibernate/validator/referenceguide/chapter12/failfastonpropertyviolation/Book.java[tags=include]
----

[source, JAVA, indent=0]
----
include::{sourcedir}/org/hibernate/validator/referenceguide/chapter12/failfastonpropertyviolation/FailFastOnPropertyViolationTest.java[tags=include]
----
====

Using this mode allows us to omit any additional checks in our class level validator.
We don't need to be worried about any of the properties being `null` as we've dealt with it already, using standard constraints.

[WARNING]
====
It is important to mention that only simple property constraints will be checked before going to class level ones. Any cascading constraints will be delayed till later.
====

.Implementation of `NonSelfPublishing` class level constrain validator
====
[source, JAVA, indent=0]
----
include::{sourcedir}/org/hibernate/validator/referenceguide/chapter12/failfastonpropertyviolation/NonSelfPublishing.java[tags=include]
----
====

[[example-property-fail-fast-on-property-violation]]
.Setting the fail fast on property violation validation mode using an XML property
====
[source, XML, indent=0]
----
include::{resourcesdir}/org/hibernate/validator/referenceguide/chapter12/fail-fast-on-property-violation-validation.xml[]
----
====
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// tag::include[]
package org.hibernate.validator.referenceguide.chapter12.failfastonpropertyviolation;

//end::include[]

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

import org.hibernate.validator.constraints.ISBN;

//tag::include[]
@NonSelfPublishing
public class Book {

@ISBN
private String isbn;

@NotBlank
private String title;

@NotNull
private Person author;

@NotNull
private Person publisher;

//constructors, getters and setters...
//end::include[]

public Book(String isbn, String title, Person author, Person publisher) {
this.isbn = isbn;
this.title = title;
this.author = author;
this.publisher = publisher;
}

public String getIsbn() {
return this.isbn;
}

public String getTitle() {
return this.title;
}

public Person getAuthor() {
return this.author;
}

public Person getPublisher() {
return this.publisher;
}

//tag::include[]
}
//end::include[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.hibernate.validator.referenceguide.chapter12.failfastonpropertyviolation;

import static org.junit.Assert.assertEquals;

import java.util.Set;

import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.constraints.NotNull;

import org.hibernate.validator.HibernateValidator;

import org.junit.Test;

public class FailFastOnPropertyViolationTest {

@Test
public void failFastOnPropertyViolation() {
//tag::include[]
Validator validator = Validation.byProvider( HibernateValidator.class )
.configure()
.failFastOnPropertyViolation( true )
.buildValidatorFactory()
.getValidator();

Book book = new Book( "978-1-56619-909-4", "Book", null /* author */, null /* publisher */ );

Set<ConstraintViolation<Book>> constraintViolations = validator.validate( book );

assertEquals( 2, constraintViolations.size() );

for ( ConstraintViolation<Book> constraintViolation : constraintViolations ) {
assertEquals(
NotNull.class,
constraintViolation.getConstraintDescriptor()
.getAnnotation()
.annotationType()
);
}
//end::include[]
}

@Test
public void failFastOnPropertyViolationProperty() {
//tag::property[]
Validator validator = Validation.byProvider( HibernateValidator.class )
.configure()
.addProperty( "hibernate.validator.fail_fast_on_property_violation", "true" )
.buildValidatorFactory()
.getValidator();
//end::property[]

Book book = new Book( "978-1-56619-909-4", "Book", null /* author */, null /* publisher */ );

Set<ConstraintViolation<Book>> constraintViolations = validator.validate( book );

assertEquals( 2, constraintViolations.size() );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.hibernate.validator.referenceguide.chapter12.failfastonpropertyviolation;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

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

import jakarta.validation.Constraint;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import jakarta.validation.Payload;

import org.hibernate.validator.referenceguide.chapter12.failfastonpropertyviolation.NonSelfPublishing.NonSelfPublishingValidator;

@Documented
@Constraint(validatedBy = { NonSelfPublishingValidator.class })
@Target({ TYPE })
@Retention(RUNTIME)
public @interface NonSelfPublishing {

String message() default "{org.hibernate.validator.referenceguide.chapter12.failfastonpropertyviolation.NonSelfPublishing.message}";

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

Class<? extends Payload>[] payload() default { };
// tag::include[]
class NonSelfPublishingValidator implements ConstraintValidator<NonSelfPublishing, Book> {

@Override
public boolean isValid(Book book, ConstraintValidatorContext context) {
return !book.getAuthor().equals( book.getPublisher() );
}
}
//end::include[]
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// tag::include[]
package org.hibernate.validator.referenceguide.chapter12.failfastonpropertyviolation;

//end::include[]

import jakarta.validation.constraints.NotNull;

//tag::include[]
public class Person {

@NotNull
private String firstName;

@NotNull
private String lastName;

public Person(@NotNull final String firstName, @NotNull final String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

//getters, setters, equals and hashcode...
}
//end::include[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<validation-config
xmlns="http://xmlns.jcp.org/xml/ns/validation/configuration"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/validation/configuration
http://xmlns.jcp.org/xml/ns/validation/configuration/validation-configuration-2.0.xsd"
version="2.0">

<property name="hibernate.validator.fail_fast_on_property_violation">true</property>

</validation-config>
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,15 @@ public interface BaseHibernateValidatorConfiguration<S extends BaseHibernateVali
@Incubating
String SHOW_VALIDATED_VALUE_IN_TRACE_LOGS = "hibernate.validator.show_validated_value_in_trace_logs";

/**
* Property corresponding to the {@link #failFastOnPropertyViolation} method.
* Accepts {@code true} or {@code false}. Defaults to {@code false}.
*
* @since 9.0
*/
@Incubating
String FAIL_FAST_ON_PROPERTY_VIOLATION = "hibernate.validator.fail_fast_on_property_violation";

/**
* <p>
* Returns the {@link ResourceBundleLocator} used by the
Expand Down Expand Up @@ -501,4 +510,18 @@ default S locales(Locale... locales) {
*/
@Incubating
S showValidatedValuesInTraceLogs(boolean enabled);

/**
* En- or disables the skipping of class level constraints based on validation of property level ones. When this
* mode is enabled the validation of class level constraints will not be performed if any of the property level
* constraints generated a violation.
*
* @param failFastOnPropertyViolation {@code true} to enable the skipping mode, {@code false} otherwise.
*
* @return {@code this} following the chaining method pattern
*
* @since 9.0
*/
@Incubating
S failFastOnPropertyViolation(boolean failFastOnPropertyViolation);
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,18 @@ public interface HibernateValidatorContext extends ValidatorContext {
*/
@Incubating
HibernateValidatorContext showValidatedValuesInTraceLogs(boolean enabled);

/**
* En- or disables the skipping of class level constraints based on validation of property level ones. When this
* mode is enabled the validation of class level constraints will not be performed if any of the property level
* constraints generated a violation.
*
* @param failFastOnPropertyViolation {@code true} to enable the skipping mode, {@code false} otherwise.
*
* @return {@code this} following the chaining method pattern
*
* @since 9.0
*/
@Incubating
HibernateValidatorContext failFastOnPropertyViolation(boolean failFastOnPropertyViolation);
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ public abstract class AbstractConfigurationImpl<T extends BaseHibernateValidator
// HV-specific options
private final Set<DefaultConstraintMapping> programmaticMappings = newHashSet();
private boolean failFast;
private boolean failFastOnPropertyViolation;
private ClassLoader externalClassLoader;
private final MethodValidationConfiguration.Builder methodValidationConfigurationBuilder = new MethodValidationConfiguration.Builder();
private boolean traversableResolverResultCacheEnabled = true;
Expand Down Expand Up @@ -295,6 +296,12 @@ public final T failFast(boolean failFast) {
return thisAsT();
}

@Override
public T failFastOnPropertyViolation(boolean failFastOnPropertyViolation) {
this.failFastOnPropertyViolation = failFastOnPropertyViolation;
return thisAsT();
}

@Override
public T allowOverridingMethodAlterParameterConstraint(boolean allow) {
this.methodValidationConfigurationBuilder.allowOverridingMethodAlterParameterConstraint( allow );
Expand Down Expand Up @@ -494,6 +501,10 @@ public final boolean getFailFast() {
return failFast;
}

public final boolean getFailFastOnPropertyViolation() {
return this.failFastOnPropertyViolation;
}

@Override
public final ConstraintValidatorFactory getConstraintValidatorFactory() {
return validationBootstrapParameters.getConstraintValidatorFactory();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ public HibernateValidatorContext failFast(boolean failFast) {
return this;
}

@Override
public HibernateValidatorContext failFastOnPropertyViolation(boolean failFastOnPropertyViolation) {
validatorFactoryScopedContextBuilder.setFailFastOnPropertyViolation( failFastOnPropertyViolation );
return this;
}

@Override
public HibernateValidatorContext allowOverridingMethodAlterParameterConstraint(boolean allow) {
throw new IllegalStateException( "Altering method validation configuration is not supported by the predefined scope ValidatorFactory." );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import static org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper.determineConstraintValidatorPayload;
import static org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper.determineExternalClassLoader;
import static org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper.determineFailFast;
import static org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper.determineFailFastOnPropertyViolation;
import static org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper.determineScriptEvaluatorFactory;
import static org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper.determineServiceLoadedConstraintMappings;
import static org.hibernate.validator.internal.engine.ValidatorFactoryConfigurationHelper.determineTemporalValidationTolerance;
Expand Down Expand Up @@ -126,6 +127,7 @@ public PredefinedScopeValidatorFactoryImpl(ConfigurationState configurationState
determineTemporalValidationTolerance( configurationState, properties ),
determineScriptEvaluatorFactory( configurationState, properties, externalClassLoader ),
determineFailFast( hibernateSpecificConfig, properties ),
determineFailFastOnPropertyViolation( hibernateSpecificConfig, properties ),
determineTraversableResolverResultCacheEnabled( hibernateSpecificConfig, properties ),
determineConstraintValidatorPayload( hibernateSpecificConfig ),
determineConstraintExpressionLanguageFeatureLevel( hibernateSpecificConfig, properties ),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ public HibernateValidatorContext failFast(boolean failFast) {
return this;
}

@Override
public HibernateValidatorContext failFastOnPropertyViolation(boolean failFastOnPropertyViolation) {
validatorFactoryScopedContextBuilder.setFailFastOnPropertyViolation( failFastOnPropertyViolation );
return this;
}

@Override
public HibernateValidatorContext allowOverridingMethodAlterParameterConstraint(boolean allow) {
methodValidationConfigurationBuilder.allowOverridingMethodAlterParameterConstraint( allow );
Expand Down
Loading