diff --git a/documentation/pom.xml b/documentation/pom.xml index 31b005bb52..a6a454e5a3 100644 --- a/documentation/pom.xml +++ b/documentation/pom.xml @@ -34,7 +34,7 @@ true true - -Duser.language=en -Duser.country=US + --add-opens java.base/java.lang=ALL-UNNAMED -Duser.language=en -Duser.country=US forbidden-allow-junit.txt .. @@ -97,6 +97,11 @@ junit test + + org.easymock + easymock + test + org.assertj diff --git a/documentation/src/main/asciidoc/ch06.asciidoc b/documentation/src/main/asciidoc/ch06.asciidoc index 3b44e36e9e..479b6049c6 100644 --- a/documentation/src/main/asciidoc/ch06.asciidoc +++ b/documentation/src/main/asciidoc/ch06.asciidoc @@ -323,6 +323,61 @@ include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/CarTest.ja ---- ==== +[[validator-dependency-testing]] +==== Testing constraint validator with dependencies + +Some DI frameworks (e.g. Spring) are capable of injecting dependencies into constraint validator instance: + +[[example-person-with-checkcase]] +.Hibernate Validator test utilities Maven dependency +==== +[source, XML] +[subs="verbatim,attributes"] +---- + + org.hibernate.validator + hibernate-validator-test-utils + {hvVersion} + test + +---- +==== + +.Defining the `@ZipCode` constraint annotation +==== +[source, JAVA, indent=0] +---- +include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/ZipCode.java[tags=include] +---- +==== + +.Applying the `@ZipCode` constraint +==== +[source, JAVA, indent=0] +---- +include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/Person.java[tags=include] +---- +==== + +.Using injected dependency in a constraint validator +==== +[source, JAVA, indent=0] +---- +include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/ZipCodeValidator.java[tags=include] +---- +==== + +Finally, <> demonstrates how validating a `Person` instance which calls custom mocked validator. + +[[example-using-validator-dependency]] +.Validating objects with the `@ZipCode` constraint +==== +[source, JAVA, indent=0] +---- +include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/CustomValidatorWithDependencyTest.java[tags=field] +---- +==== + [[section-class-level-constraints]] === Class-level constraints diff --git a/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/CustomValidatorWithDependencyTest.java b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/CustomValidatorWithDependencyTest.java new file mode 100644 index 0000000000..3ef533f154 --- /dev/null +++ b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/CustomValidatorWithDependencyTest.java @@ -0,0 +1,56 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.referenceguide.chapter06.customvalidatorwithdependency; + +import static org.easymock.EasyMock.eq; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.isA; +import static org.easymock.EasyMock.mock; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertEquals; + +import java.util.Map; +import java.util.Set; + +import jakarta.validation.ConstraintValidatorContext; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; + +import org.hibernate.validator.testutil.PreconfiguredValidatorsValidatorFactory; + +import org.junit.Test; + +@SuppressWarnings("unused") +//tag::field[] +public class CustomValidatorWithDependencyTest { + + @Test + public void mockCustomValidatorWithDependency() { + ZipCodeValidator zipCodeValidator = mock( ZipCodeValidator.class ); + + expect( zipCodeValidator.isValid( eq( "1234" ), isA( ConstraintValidatorContext.class ) ) ) + .andStubReturn( true ); + zipCodeValidator.initialize( isA( ZipCode.class ) ); + + replay( zipCodeValidator ); + + ValidatorFactory validatorFactory = PreconfiguredValidatorsValidatorFactory.builder() + .defaultValidators( Map.of( ZipCodeValidator.class, zipCodeValidator ) ) + .build(); + + Validator validator = validatorFactory.getValidator(); + + Person person = new Person( "1234" ); + + Set> constraintViolations = validator.validate( person ); + + assertEquals( 0, constraintViolations.size() ); + + verify( zipCodeValidator ); + } +} +//end::field[] diff --git a/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/Person.java b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/Person.java new file mode 100644 index 0000000000..d0f6032fde --- /dev/null +++ b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/Person.java @@ -0,0 +1,17 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +//tag::include[] +package org.hibernate.validator.referenceguide.chapter06.customvalidatorwithdependency; + +public class Person { + + @ZipCode + private String zipCode; + + public Person(String zipCode) { + this.zipCode = zipCode; + } +} +//end::include[] diff --git a/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/ZipCode.java b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/ZipCode.java new file mode 100644 index 0000000000..62cb7d8b83 --- /dev/null +++ b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/ZipCode.java @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +//tag::include[] +package org.hibernate.validator.referenceguide.chapter06.customvalidatorwithdependency; + +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 jakarta.validation.Constraint; +import jakarta.validation.Payload; + +//tag::include[] +@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." + + "customvalidatorwithdependency.ZipCode.message}"; + + Class[] groups() default { }; + + Class[] payload() default { }; +} +//end::include[] diff --git a/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/ZipCodeRepository.java b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/ZipCodeRepository.java new file mode 100644 index 0000000000..a0ef602801 --- /dev/null +++ b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/ZipCodeRepository.java @@ -0,0 +1,9 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.referenceguide.chapter06.customvalidatorwithdependency; + +public interface ZipCodeRepository { + boolean isExist(String zipCode); +} diff --git a/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/ZipCodeValidator.java b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/ZipCodeValidator.java new file mode 100644 index 0000000000..58efb2a8ff --- /dev/null +++ b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/ZipCodeValidator.java @@ -0,0 +1,29 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +//tag::include[] +package org.hibernate.validator.referenceguide.chapter06.customvalidatorwithdependency; + +//end::include[] + +import jakarta.inject.Inject; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +//tag::include[] +public class ZipCodeValidator implements ConstraintValidator { + + @Inject + public ZipCodeRepository zipCodeRepository; + + @Override + public boolean isValid(String zipCode, ConstraintValidatorContext constraintContext) { + if ( zipCode == null ) { + return true; + } + + return zipCodeRepository.isExist( zipCode ); + } +} +//end::include[] diff --git a/test-utils/pom.xml b/test-utils/pom.xml index 40bbacd520..cbee1b29ed 100644 --- a/test-utils/pom.xml +++ b/test-utils/pom.xml @@ -28,6 +28,14 @@ maven-jar-plugin + + org.apache.maven.plugins + maven-surefire-plugin + + + org.apache.maven.plugins + maven-surefire-report-plugin + org.moditect moditect-maven-plugin @@ -60,5 +68,17 @@ assertj-core provided + + + + org.testng + testng + test + + + org.easymock + easymock + test + diff --git a/test-utils/src/main/java/org/hibernate/validator/testutil/PreconfiguredConstraintValidatorFactory.java b/test-utils/src/main/java/org/hibernate/validator/testutil/PreconfiguredConstraintValidatorFactory.java new file mode 100644 index 0000000000..32c223e5ee --- /dev/null +++ b/test-utils/src/main/java/org/hibernate/validator/testutil/PreconfiguredConstraintValidatorFactory.java @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.testutil; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorFactory; + +public class PreconfiguredConstraintValidatorFactory implements ConstraintValidatorFactory { + + private final Map, ConstraintValidator> defaultValidators; + private final ConstraintValidatorFactory delegated; + + private PreconfiguredConstraintValidatorFactory(Builder builder) { + this.defaultValidators = builder.defaultValidators; + this.delegated = builder.delegated; + } + + public static Builder builder() { + return new Builder(); + } + + @SuppressWarnings("unchecked") + @Override + public > T getInstance(Class key) { + if ( defaultValidators.containsKey( key ) ) { + return (T) defaultValidators.get( key ); + } + + return delegated.getInstance( key ); + } + + @Override + public void releaseInstance(ConstraintValidator instance) { + delegated.releaseInstance( instance ); + } + + public static class Builder { + + private ConstraintValidatorFactory delegated; + private final Map, ConstraintValidator> defaultValidators = new HashMap<>(); + + private Builder() { + } + + public Builder defaultValidators( + Map, ConstraintValidator> validators) { + this.defaultValidators.putAll( validators ); + return this; + } + + public Builder delegated( + ConstraintValidatorFactory delegated) { + this.delegated = delegated; + return this; + } + + public PreconfiguredConstraintValidatorFactory build() { + return new PreconfiguredConstraintValidatorFactory( this ); + } + } +} diff --git a/test-utils/src/main/java/org/hibernate/validator/testutil/PreconfiguredValidatorsValidatorFactory.java b/test-utils/src/main/java/org/hibernate/validator/testutil/PreconfiguredValidatorsValidatorFactory.java new file mode 100644 index 0000000000..3b54d0d510 --- /dev/null +++ b/test-utils/src/main/java/org/hibernate/validator/testutil/PreconfiguredValidatorsValidatorFactory.java @@ -0,0 +1,112 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.testutil; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.validation.ClockProvider; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorFactory; +import jakarta.validation.MessageInterpolator; +import jakarta.validation.ParameterNameProvider; +import jakarta.validation.TraversableResolver; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorContext; +import jakarta.validation.ValidatorFactory; + +/** + * This class provides useful functions to create {@code ValidatorFactory} with preconfigured validators to test Bean + * validation without creation of custom validator instances. + * + * @author Attila Hajdu + */ +@SuppressWarnings("rawtypes") +public class PreconfiguredValidatorsValidatorFactory implements ValidatorFactory { + private final Map, ConstraintValidator> defaultValidators; + private final ValidatorFactory delegated; + + private PreconfiguredValidatorsValidatorFactory(Builder builder) { + this.defaultValidators = builder.defaultValidators; + + ValidatorFactory defaultValidationFactory = Validation.buildDefaultValidatorFactory(); + ConstraintValidatorFactory wrappedConstraintValidatorFactory = PreconfiguredConstraintValidatorFactory.builder() + .delegated( defaultValidationFactory.getConstraintValidatorFactory() ) + .defaultValidators( this.defaultValidators ).build(); + + this.delegated = Validation.byDefaultProvider().configure() + .constraintValidatorFactory( wrappedConstraintValidatorFactory ) + .buildValidatorFactory(); + + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public Validator getValidator() { + return delegated.getValidator(); + } + + @Override + public ValidatorContext usingContext() { + return delegated.usingContext(); + } + + @Override + public MessageInterpolator getMessageInterpolator() { + return delegated.getMessageInterpolator(); + } + + @Override + public TraversableResolver getTraversableResolver() { + return delegated.getTraversableResolver(); + } + + @Override + public ConstraintValidatorFactory getConstraintValidatorFactory() { + return delegated.getConstraintValidatorFactory(); + } + + @Override + public ParameterNameProvider getParameterNameProvider() { + return delegated.getParameterNameProvider(); + } + + @Override + public ClockProvider getClockProvider() { + return delegated.getClockProvider(); + } + + @Override + public T unwrap(Class type) { + return delegated.unwrap( type ); + } + + @Override + public void close() { + delegated.close(); + } + + public static class Builder { + + private Builder() { + } + + private final Map, ConstraintValidator> defaultValidators = new HashMap<>(); + + public Builder defaultValidators( + Map, ConstraintValidator> defaultValidators) { + this.defaultValidators.putAll( defaultValidators ); + return this; + } + + public PreconfiguredValidatorsValidatorFactory build() { + return new PreconfiguredValidatorsValidatorFactory( this ); + } + } +} diff --git a/test-utils/src/test/java/org/hibernate/validator/testutil/PreconfiguredConstraintValidatorFactoryTest.java b/test-utils/src/test/java/org/hibernate/validator/testutil/PreconfiguredConstraintValidatorFactoryTest.java new file mode 100644 index 0000000000..9e83dbd98a --- /dev/null +++ b/test-utils/src/test/java/org/hibernate/validator/testutil/PreconfiguredConstraintValidatorFactoryTest.java @@ -0,0 +1,74 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.testutil; + + +import static org.assertj.core.api.Assertions.assertThat; +import static org.easymock.EasyMock.*; + +import java.util.Map; + +import jakarta.validation.ConstraintValidatorFactory; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +public class PreconfiguredConstraintValidatorFactoryTest { + + private ConstraintValidatorFactory delegatedConstraintValidatorFactory; + + @BeforeMethod + public void setUp() { + delegatedConstraintValidatorFactory = createMock( ConstraintValidatorFactory.class ); + } + + @Test + public void testGetInstanceWithPreconfiguredValidator() { + CountValidationCallsValidator constraintValidator = new CountValidationCallsValidator(); + + PreconfiguredConstraintValidatorFactory constraintValidatorFactory = PreconfiguredConstraintValidatorFactory.builder() + .delegated( delegatedConstraintValidatorFactory ) + .defaultValidators( Map.of( CountValidationCallsValidator.class, constraintValidator ) ) + .build(); + + assertThat( constraintValidatorFactory.getInstance( CountValidationCallsValidator.class ) ) + .isEqualTo( constraintValidator ); + } + + @Test + public void testGetInstanceWithDefaultValidator() { + CountValidationCallsValidator constraintValidator = new CountValidationCallsValidator(); + + expect( delegatedConstraintValidatorFactory.getInstance( CountValidationCallsValidator.class ) ).andReturn( constraintValidator ); + + PreconfiguredConstraintValidatorFactory constraintValidatorFactory = PreconfiguredConstraintValidatorFactory.builder() + .delegated( delegatedConstraintValidatorFactory ) + .build(); + + replay( delegatedConstraintValidatorFactory ); + + assertThat( constraintValidatorFactory.getInstance( CountValidationCallsValidator.class ) ) + .isEqualTo( constraintValidator ); + + verify( delegatedConstraintValidatorFactory ); + } + + @Test + public void testReleaseInstance() { + CountValidationCallsValidator constraintValidator = new CountValidationCallsValidator(); + + delegatedConstraintValidatorFactory.releaseInstance( constraintValidator ); + + PreconfiguredConstraintValidatorFactory constraintValidatorFactory = PreconfiguredConstraintValidatorFactory.builder() + .delegated( delegatedConstraintValidatorFactory ) + .build(); + + replay( delegatedConstraintValidatorFactory ); + + constraintValidatorFactory.releaseInstance( constraintValidator ); + + verify( delegatedConstraintValidatorFactory ); + } +}