diff --git a/annotation-processor/src/main/java/org/hibernate/validator/ap/internal/util/ConstraintHelper.java b/annotation-processor/src/main/java/org/hibernate/validator/ap/internal/util/ConstraintHelper.java index f2039d83af..031c771c69 100644 --- a/annotation-processor/src/main/java/org/hibernate/validator/ap/internal/util/ConstraintHelper.java +++ b/annotation-processor/src/main/java/org/hibernate/validator/ap/internal/util/ConstraintHelper.java @@ -303,6 +303,7 @@ public ConstraintHelper(Types typeUtils, AnnotationApiHelper annotationApiHelper registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.NOT_EMPTY, TYPES_SUPPORTED_BY_SIZE_AND_NOT_EMPTY_ANNOTATIONS ); registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.SAFE_HTML, CharSequence.class ); registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.SCRIPT_ASSERT, Object.class ); + registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.UNIQUE_ELEMENTS, Collection.class ); registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.URL, CharSequence.class ); registerSupportedTypesUnwrappedByDefault( SupportedForUnwrapTypes.OPTIONAL_INT, Integer.class.getName() ); diff --git a/annotation-processor/src/main/java/org/hibernate/validator/ap/internal/util/TypeNames.java b/annotation-processor/src/main/java/org/hibernate/validator/ap/internal/util/TypeNames.java index d26e94bbf9..f6881af552 100644 --- a/annotation-processor/src/main/java/org/hibernate/validator/ap/internal/util/TypeNames.java +++ b/annotation-processor/src/main/java/org/hibernate/validator/ap/internal/util/TypeNames.java @@ -80,6 +80,7 @@ public static class HibernateValidatorTypes { public static final String NOT_EMPTY = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".NotEmpty"; public static final String SAFE_HTML = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".SafeHtml"; public static final String SCRIPT_ASSERT = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".ScriptAssert"; + public static final String UNIQUE_ELEMENTS = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".UniqueElements"; public static final String URL = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".URL"; public static final String DURATION_MIN = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".time.DurationMin"; public static final String DURATION_MAX = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".time.DurationMax"; diff --git a/annotation-processor/src/test/java/org/hibernate/validator/ap/ConstraintValidationProcessorTest.java b/annotation-processor/src/test/java/org/hibernate/validator/ap/ConstraintValidationProcessorTest.java index 6eeda13e0b..b66fc7310b 100644 --- a/annotation-processor/src/test/java/org/hibernate/validator/ap/ConstraintValidationProcessorTest.java +++ b/annotation-processor/src/test/java/org/hibernate/validator/ap/ConstraintValidationProcessorTest.java @@ -24,6 +24,7 @@ import org.hibernate.validator.ap.testmodel.ModelWithJava8DateTime; import org.hibernate.validator.ap.testmodel.ModelWithJavaMoneyTypes; import org.hibernate.validator.ap.testmodel.ModelWithJodaTypes; +import org.hibernate.validator.ap.testmodel.ModelWithUniqueElementsConstraints; import org.hibernate.validator.ap.testmodel.ModelWithoutConstraints; import org.hibernate.validator.ap.testmodel.MultipleConstraintsOfSameType; import org.hibernate.validator.ap.testmodel.ValidationUsingAtValidAnnotation; @@ -685,4 +686,21 @@ public void unwrappingConstraints() { new DiagnosticExpectation( Kind.WARNING, 54 ) ); } + + @Test + @TestForIssue(jiraKey = "HV-1466") + public void uniqueElementsConstraints() { + File[] sourceFiles = new File[] { + compilerHelper.getSourceFile( ModelWithUniqueElementsConstraints.class ) + }; + + boolean compilationResult = + compilerHelper.compile( new ConstraintValidationProcessor(), diagnostics, false, true, sourceFiles ); + + assertFalse( compilationResult ); + assertThatDiagnosticsMatch( + diagnostics, + new DiagnosticExpectation( Kind.ERROR, 26 ) + ); + } } diff --git a/annotation-processor/src/test/java/org/hibernate/validator/ap/testmodel/ModelWithUniqueElementsConstraints.java b/annotation-processor/src/test/java/org/hibernate/validator/ap/testmodel/ModelWithUniqueElementsConstraints.java new file mode 100644 index 0000000000..ae0bf13c1f --- /dev/null +++ b/annotation-processor/src/test/java/org/hibernate/validator/ap/testmodel/ModelWithUniqueElementsConstraints.java @@ -0,0 +1,28 @@ +/* + * Hibernate Validator, declare and validate application constraints + * + * License: Apache License, Version 2.0 + * See the license.txt file in the root directory or . + */ +package org.hibernate.validator.ap.testmodel; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import org.hibernate.validator.constraints.UniqueElements; + +public class ModelWithUniqueElementsConstraints { + + @UniqueElements + public Collection collection; + + @UniqueElements + public List list; + + @UniqueElements + public Set set; + + @UniqueElements + public String string; +} diff --git a/documentation/src/main/asciidoc/ch02.asciidoc b/documentation/src/main/asciidoc/ch02.asciidoc index 6e0dffe59e..4f45cc6bb7 100644 --- a/documentation/src/main/asciidoc/ch02.asciidoc +++ b/documentation/src/main/asciidoc/ch02.asciidoc @@ -716,6 +716,10 @@ In addition, `baseURI` allows to specify the base URI used to resolve relative U Supported data types::: Any type Hibernate metadata impact::: None +`@UniqueElements`:: Checks that the annotated collection only contains unique elements. The equality is determined using the `equals()` method. The default message does not include the list of duplicate elements but you can include it by overriding the message and using the `{duplicates}` message parameter. The list of duplicate elements is also included in the dynamic payload of the constraint violation. + Supported data types::: `Collection` + Hibernate metadata impact::: None + `@URL(protocol=, host=, port=, regexp=, flags=)`:: Checks if the annotated character sequence is a valid URL according to RFC2396. If any of the optional parameters `protocol`, `host` or `port` are specified, the corresponding URL fragments must match the specified values. The optional parameters `regexp` and `flags` allow to specify an additional regular expression (including regular expression flags) which the URL must match. Per default this constraint used the `java.net.URL` constructor to verify whether a given string represents a valid URL. A regular expression based version is also available - `RegexpURLValidator` - which can be configured via XML (see <>) or the programmatic API (see <>). Supported data types::: `CharSequence` Hibernate metadata impact::: None diff --git a/engine/src/main/java/org/hibernate/validator/cfg/defs/UniqueElementsDef.java b/engine/src/main/java/org/hibernate/validator/cfg/defs/UniqueElementsDef.java new file mode 100644 index 0000000000..6bb54dfc33 --- /dev/null +++ b/engine/src/main/java/org/hibernate/validator/cfg/defs/UniqueElementsDef.java @@ -0,0 +1,22 @@ +/* + * Hibernate Validator, declare and validate application constraints + * + * License: Apache License, Version 2.0 + * See the license.txt file in the root directory or . + */ + +package org.hibernate.validator.cfg.defs; + +import org.hibernate.validator.cfg.ConstraintDef; +import org.hibernate.validator.constraints.UniqueElements; + +/** + * @author Guillaume Smet + * @since 6.0.5 + */ +public class UniqueElementsDef extends ConstraintDef { + + public UniqueElementsDef() { + super( UniqueElements.class ); + } +} diff --git a/engine/src/main/java/org/hibernate/validator/constraints/UniqueElements.java b/engine/src/main/java/org/hibernate/validator/constraints/UniqueElements.java new file mode 100644 index 0000000000..2a61ec63e3 --- /dev/null +++ b/engine/src/main/java/org/hibernate/validator/constraints/UniqueElements.java @@ -0,0 +1,62 @@ +/* + * Hibernate Validator, declare and validate application constraints + * + * License: Apache License, Version 2.0 + * See the license.txt file in the root directory or . + */ +package org.hibernate.validator.constraints; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.Collection; + +import javax.validation.Constraint; +import javax.validation.Payload; + +import org.hibernate.validator.constraints.UniqueElements.List; + +/** + * Validates that every object in the provided {@link Collection} is unique, i.e. that we can't find 2 equal elements in + * the collection. + *

+ * For instance, this can be useful with JAX-RS, which always deserializes collections to a list. Thus, duplicates would + * implicitly and silently be removed when converting it to a set. This constraint allows you to check for duplicates in + * the list and to raise an error instead. + * + * @author Tadhg Pearson + * @since 6.0.5 + */ +@Documented +@Constraint(validatedBy = { }) +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Repeatable(List.class) +public @interface UniqueElements { + + String message() default "{org.hibernate.validator.constraints.UniqueElements.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + /** + * Defines several {@code @UniqueElements} annotations on the same element. + */ + @Target({ TYPE }) + @Retention(RUNTIME) + @Documented + public @interface List { + UniqueElements[] value(); + } +} diff --git a/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/hv/UniqueElementsValidator.java b/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/hv/UniqueElementsValidator.java new file mode 100644 index 0000000000..2e8a8cfb33 --- /dev/null +++ b/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/hv/UniqueElementsValidator.java @@ -0,0 +1,66 @@ +/* + * Hibernate Validator, declare and validate application constraints + * + * License: Apache License, Version 2.0 + * See the license.txt file in the root directory or . + */ +package org.hibernate.validator.internal.constraintvalidators.hv; + +import static java.util.stream.Collectors.toList; + +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +import org.hibernate.validator.constraints.UniqueElements; +import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorContext; +import org.hibernate.validator.internal.util.CollectionHelper; + +/** + * Validates that the provided collection only contains unique elements, i.e. that we can't find 2 equal elements in the + * collection. + *

+ * Uniqueness is defined by the {@code equals()} method of the objects being compared. + * + * @author Tadhg Pearson + * @author Guillaume Smet + */ +public class UniqueElementsValidator implements ConstraintValidator> { + + /** + * @param collection the collection to validate + * @param constraintValidatorContext context in which the constraint is evaluated + * + * @return true if the input collection is null or does not contain duplicate elements + */ + @Override + public boolean isValid(Collection collection, ConstraintValidatorContext constraintValidatorContext) { + if ( collection == null || collection.size() < 2 ) { + return true; + } + + List duplicates = findDuplicates( collection ); + + if ( duplicates.isEmpty() ) { + return true; + } + + if ( constraintValidatorContext instanceof HibernateConstraintValidatorContext ) { + constraintValidatorContext.unwrap( HibernateConstraintValidatorContext.class ) + .addMessageParameter( "duplicates", duplicates.stream().map( String::valueOf ).collect( Collectors.joining( ", " ) ) ) + .withDynamicPayload( CollectionHelper.toImmutableList( duplicates ) ); + } + + return false; + } + + private List findDuplicates(Collection collection) { + Set uniqueElements = CollectionHelper.newHashSet( collection.size() ); + return collection.stream().filter( o -> !uniqueElements.add( o ) ) + .collect( toList() ); + } +} diff --git a/engine/src/main/java/org/hibernate/validator/internal/metadata/core/ConstraintHelper.java b/engine/src/main/java/org/hibernate/validator/internal/metadata/core/ConstraintHelper.java index f86e55703e..beceb8b01e 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/metadata/core/ConstraintHelper.java +++ b/engine/src/main/java/org/hibernate/validator/internal/metadata/core/ConstraintHelper.java @@ -66,6 +66,7 @@ import org.hibernate.validator.constraints.SafeHtml; import org.hibernate.validator.constraints.ScriptAssert; import org.hibernate.validator.constraints.URL; +import org.hibernate.validator.constraints.UniqueElements; import org.hibernate.validator.constraints.br.CNPJ; import org.hibernate.validator.constraints.br.CPF; import org.hibernate.validator.constraints.pl.NIP; @@ -262,6 +263,7 @@ import org.hibernate.validator.internal.constraintvalidators.hv.SafeHtmlValidator; import org.hibernate.validator.internal.constraintvalidators.hv.ScriptAssertValidator; import org.hibernate.validator.internal.constraintvalidators.hv.URLValidator; +import org.hibernate.validator.internal.constraintvalidators.hv.UniqueElementsValidator; import org.hibernate.validator.internal.constraintvalidators.hv.br.CNPJValidator; import org.hibernate.validator.internal.constraintvalidators.hv.br.CPFValidator; import org.hibernate.validator.internal.constraintvalidators.hv.pl.NIPValidator; @@ -667,6 +669,7 @@ public ConstraintHelper() { putConstraint( tmpConstraints, ParameterScriptAssert.class, ParameterScriptAssertValidator.class ); putConstraint( tmpConstraints, SafeHtml.class, SafeHtmlValidator.class ); putConstraint( tmpConstraints, ScriptAssert.class, ScriptAssertValidator.class ); + putConstraint( tmpConstraints, UniqueElements.class, UniqueElementsValidator.class ); putConstraint( tmpConstraints, URL.class, URLValidator.class ); this.builtinConstraints = Collections.unmodifiableMap( tmpConstraints ); diff --git a/engine/src/main/resources/org/hibernate/validator/ValidationMessages.properties b/engine/src/main/resources/org/hibernate/validator/ValidationMessages.properties index 555a2658ca..47ff74ad7a 100644 --- a/engine/src/main/resources/org/hibernate/validator/ValidationMessages.properties +++ b/engine/src/main/resources/org/hibernate/validator/ValidationMessages.properties @@ -37,6 +37,7 @@ org.hibernate.validator.constraints.ParametersScriptAssert.message = script exp org.hibernate.validator.constraints.Range.message = must be between {min} and {max} org.hibernate.validator.constraints.SafeHtml.message = may have unsafe html content org.hibernate.validator.constraints.ScriptAssert.message = script expression "{script}" didn't evaluate to true +org.hibernate.validator.constraints.UniqueElements.message = must only contain unique elements org.hibernate.validator.constraints.URL.message = must be a valid URL org.hibernate.validator.constraints.br.CNPJ.message = invalid Brazilian corporate taxpayer registry number (CNPJ) diff --git a/engine/src/main/resources/org/hibernate/validator/ValidationMessages_de.properties b/engine/src/main/resources/org/hibernate/validator/ValidationMessages_de.properties index 0257e09e89..c802fec1a1 100644 --- a/engine/src/main/resources/org/hibernate/validator/ValidationMessages_de.properties +++ b/engine/src/main/resources/org/hibernate/validator/ValidationMessages_de.properties @@ -33,6 +33,7 @@ org.hibernate.validator.constraints.ParametersScriptAssert.message = Skriptausdr org.hibernate.validator.constraints.Range.message = muss zwischen {min} und {max} liegen org.hibernate.validator.constraints.SafeHtml.message = k\u00F6nnte unsicheren HTML-Inhalt haben org.hibernate.validator.constraints.ScriptAssert.message = Skriptausdruck "{script}" gab nicht true zur\u00FCck +org.hibernate.validator.constraints.UniqueElements.message = darf keine Duplikate enthalten org.hibernate.validator.constraints.URL.message = muss eine g\u00FCltige URL sein org.hibernate.validator.constraints.time.DurationMax.message = muss k\u00FCrzer${inclusive == true ? ' oder gleich' : ' als'}${days == 0 ? '' : days == 1 ? ' 1 Tag' : ' ' += days += ' Tage'}${hours == 0 ? '' : hours == 1 ? ' 1 Stunde' : ' ' += hours += ' Stunden'}${minutes == 0 ? '' : minutes == 1 ? ' 1 Minute' : ' ' += minutes += ' Minuten'}${seconds == 0 ? '' : seconds == 1 ? ' 1 Sekunde' : ' ' += seconds += ' Sekunden'}${millis == 0 ? '' : millis == 1 ? ' 1 Millisekunde' : ' ' += millis += ' Millisekunden'}${nanos == 0 ? '' : nanos == 1 ? ' 1 Nanosekunde' : ' ' += nanos += ' Nanosekunden'} sein diff --git a/engine/src/main/resources/org/hibernate/validator/ValidationMessages_fr.properties b/engine/src/main/resources/org/hibernate/validator/ValidationMessages_fr.properties index 4174e77d5f..7d9dbee9e1 100644 --- a/engine/src/main/resources/org/hibernate/validator/ValidationMessages_fr.properties +++ b/engine/src/main/resources/org/hibernate/validator/ValidationMessages_fr.properties @@ -37,6 +37,7 @@ org.hibernate.validator.constraints.ParametersScriptAssert.message = le script org.hibernate.validator.constraints.Range.message = doit \u00EAtre entre {min} et {max} org.hibernate.validator.constraints.SafeHtml.message = peut contenir du HTML dangereux org.hibernate.validator.constraints.ScriptAssert.message = le script "{script}" n'a pas \u00E9t\u00E9 \u00E9valu\u00E9 \u00E0 vrai +org.hibernate.validator.constraints.UniqueElements.message = ne doit contenir que des \u00E9l\u00E9ments uniques org.hibernate.validator.constraints.URL.message = URL mal form\u00E9e org.hibernate.validator.constraints.br.CNPJ.message = numéro d'enregistrement brésilien de société contribuable (CNPJ) invalide diff --git a/engine/src/main/resources/org/hibernate/validator/ValidationMessages_uk.properties b/engine/src/main/resources/org/hibernate/validator/ValidationMessages_uk.properties index 465e676de0..a9d2e5d8fc 100644 --- a/engine/src/main/resources/org/hibernate/validator/ValidationMessages_uk.properties +++ b/engine/src/main/resources/org/hibernate/validator/ValidationMessages_uk.properties @@ -37,6 +37,7 @@ org.hibernate.validator.constraints.ParametersScriptAssert.message = \u0441\u04 org.hibernate.validator.constraints.Range.message = \u043c\u0430\u0454 \u0431\u0443\u0442\u0438 \u043c\u0456\u0436 {min} \u0442\u0430 {max} org.hibernate.validator.constraints.SafeHtml.message = \u043c\u043e\u0436\u0435 \u043c\u0456\u0441\u0442\u0438\u0442\u0438 \u043d\u0435\u0431\u0435\u0437\u043f\u0435\u0447\u043d\u0438\u0439 html \u043a\u043e\u043d\u0442\u0435\u043d\u0442 org.hibernate.validator.constraints.ScriptAssert.message = \u0441\u043a\u0440\u0438\u043f\u0442\u043e\u0432\u0438\u0439 \u0432\u0438\u0440\u0430\u0437 "{script}" \u043d\u0435 \u0454 \u0456\u0441\u0442\u0438\u043d\u043d\u0438\u043c +org.hibernate.validator.constraints.UniqueElements.message = \u043f\u043e\u0432\u0438\u043d\u043d\u043e \u043c\u0456\u0441\u0442\u0438\u0442\u0438 \u043b\u0438\u0448\u0435 \u0443\u043d\u0456\u043a\u0430\u043b\u044c\u043d\u0456 \u0435\u043b\u0435\u043c\u0435\u043d\u0442\u0438 org.hibernate.validator.constraints.URL.message = \u043c\u0430\u0454 \u0431\u0443\u0442\u0438 \u0434\u0456\u0439\u0441\u043d\u0438\u043c URL org.hibernate.validator.constraints.br.CNPJ.message = \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u043d\u043e\u043c\u0435\u0440 \u0431\u0440\u0430\u0437\u0438\u043b\u044c\u0441\u043a\u043e\u0433\u043e \u043a\u043e\u0440\u043f\u043e\u0440\u0430\u0442\u0438\u0432\u043d\u043e\u0433\u043e \u043f\u043b\u0430\u0442\u043d\u0438\u043a\u0430 \u043f\u043e\u0434\u0430\u0442\u043a\u0456\u0432 (CNPJ) diff --git a/engine/src/test/java/org/hibernate/validator/test/cfg/UniqueElementsDefTest.java b/engine/src/test/java/org/hibernate/validator/test/cfg/UniqueElementsDefTest.java new file mode 100644 index 0000000000..f8b9d00282 --- /dev/null +++ b/engine/src/test/java/org/hibernate/validator/test/cfg/UniqueElementsDefTest.java @@ -0,0 +1,62 @@ +/* + * Hibernate Validator, declare and validate application constraints + * + * License: Apache License, Version 2.0 + * See the license.txt file in the root directory or . + */ +package org.hibernate.validator.test.cfg; + +import static org.hibernate.validator.testutil.ConstraintViolationAssert.assertThat; +import static org.hibernate.validator.testutil.ConstraintViolationAssert.violationOf; + +import java.lang.annotation.ElementType; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import javax.validation.ConstraintViolation; +import javax.validation.Validator; + +import org.hibernate.validator.HibernateValidator; +import org.hibernate.validator.HibernateValidatorConfiguration; +import org.hibernate.validator.cfg.ConstraintMapping; +import org.hibernate.validator.cfg.defs.UniqueElementsDef; +import org.hibernate.validator.constraints.UniqueElements; +import org.hibernate.validator.testutil.TestForIssue; +import org.hibernate.validator.testutils.ValidatorUtil; +import org.testng.annotations.Test; + +/** + * @author Guillaume Smet + */ +public class UniqueElementsDefTest { + + @Test + @TestForIssue(jiraKey = "HV-1466") + public void testUniqueItemsDef() { + final HibernateValidatorConfiguration configuration = ValidatorUtil.getConfiguration( HibernateValidator.class ); + + final ConstraintMapping programmaticMapping = configuration.createConstraintMapping(); + programmaticMapping.type( Library.class ) + .property( "books", ElementType.FIELD ).constraint( new UniqueElementsDef() ); + configuration.addMapping( programmaticMapping ); + + Validator validator = configuration.buildValidatorFactory().getValidator(); + Set> violations = validator.validate( new Library( + Arrays.asList( "A Prayer for Owen Meany", "The Cider House Rules", "The Cider House Rules" ) ) ); + + assertThat( violations ).containsOnlyViolations( + violationOf( UniqueElements.class ).withProperty( "books" ) + ); + } + + @SuppressWarnings("unused") + private static class Library { + + private final List books; + + public Library(List books) { + this.books = books; + } + } +} diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/constraintvalidators/hv/UniqueElementsValidatorTest.java b/engine/src/test/java/org/hibernate/validator/test/internal/constraintvalidators/hv/UniqueElementsValidatorTest.java new file mode 100644 index 0000000000..422bfead80 --- /dev/null +++ b/engine/src/test/java/org/hibernate/validator/test/internal/constraintvalidators/hv/UniqueElementsValidatorTest.java @@ -0,0 +1,180 @@ +/* + * Hibernate Validator, declare and validate application constraints + * + * License: Apache License, Version 2.0 + * See the license.txt file in the root directory or . + */ +package org.hibernate.validator.test.internal.constraintvalidators.hv; + +import static org.hibernate.validator.testutil.ConstraintViolationAssert.assertNoViolations; +import static org.hibernate.validator.testutil.ConstraintViolationAssert.assertThat; +import static org.hibernate.validator.testutil.ConstraintViolationAssert.violationOf; +import static org.testng.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import javax.validation.ConstraintViolation; +import javax.validation.MessageInterpolator; +import javax.validation.Validator; + +import org.assertj.core.api.Assertions; +import org.hibernate.validator.HibernateValidatorConfiguration; +import org.hibernate.validator.constraints.UniqueElements; +import org.hibernate.validator.engine.HibernateConstraintViolation; +import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator; +import org.hibernate.validator.resourceloading.AggregateResourceBundleLocator; +import org.hibernate.validator.testutils.ValidatorUtil; +import org.testng.annotations.Test; + +/** + * Tests the {@link UniqueElements} constraint + * + * @author Tadhg Pearson + */ +public class UniqueElementsValidatorTest { + + private static class AnnotationContainer { + + @UniqueElements + private final List validateMe; + + private AnnotationContainer(List validateMe) { + this.validateMe = validateMe; + } + } + + @Test + public void testValidDataPasses() { + List> input = new ArrayList<>(); + input.add( null ); + input.add( Collections.singletonList( null ) ); + input.add( Collections.singletonList( "" ) ); + input.add( Collections.singletonList( "a" ) ); + input.add( Arrays.asList( "a", "aa" ) ); + input.add( Arrays.asList( "^", "ˆ" ) ); + input.add( Arrays.asList( "a", "b", "c", "1", "2", "3", null ) ); + input.add( Arrays.asList( "null", null ) ); + input.add( Arrays.asList( "lorem", "lorem ipsum" ) ); + + input.add( Arrays.asList( + new TestObject( 1 ), + new TestObject( 2 ), + new TestExtendedObject( 2 ), + new TestExtendedObject( 3 ) + ) ); + + for ( List value : input ) { + Set> violations = ValidatorUtil.getValidator().validate( new AnnotationContainer( value ) ); + assertNoViolations( violations, "Validation should have passed for " + value ); + } + } + + @Test + public void testInvalidDataFails() { + List> input = new ArrayList<>(); + input.add( Arrays.asList( null, null ) ); + input.add( Arrays.asList( "a", "a" ) ); + input.add( Arrays.asList( "ˆ", "ˆ" ) ); + input.add( Arrays.asList( "a", "b", "a" ) ); + input.add( Arrays.asList( "a", "b", "c", "b" ) ); + input.add( Arrays.asList( "*", "*" ) ); + + input.add( Arrays.asList( new TestObject( 1 ), new TestObject( 2 ), new TestObject( 1 ) ) ); + input.add( Arrays.asList( new TestObject( 0 ), new TestObject( 0 ) ) ); + + for ( List value : input ) { + Set> violations = ValidatorUtil.getValidator().validate( new AnnotationContainer( value ) ); + assertThat( violations ) + .describedAs( "Validation should have failed for " + value ) + .containsOnlyViolations( violationOf( UniqueElements.class ) ); + } + } + + @Test + public void testMessageContainsDuplicatedValue() { + HibernateValidatorConfiguration configuration = ValidatorUtil.getConfiguration(); + + MessageInterpolator messageInterpolator = new ResourceBundleMessageInterpolator( + new AggregateResourceBundleLocator( + Arrays.asList( "org/hibernate/validator/test/internal/constraintvalidators/hv/UniqueElementsMessages" ), + configuration.getDefaultResourceBundleLocator(), + getClass().getClassLoader() + ) + ); + + Validator validator = configuration + .messageInterpolator( messageInterpolator ) + .buildValidatorFactory().getValidator(); + + String duplicate = "seeme"; + List fails = Arrays.asList( duplicate, duplicate ); + Set> violations = validator.validate( new AnnotationContainer( fails ) ); + + assertThat( violations ).containsOnlyViolations( violationOf( UniqueElements.class ) ); + + assertTrue( violations.stream().anyMatch( cv -> cv.getMessage().contains( duplicate ) ) ); + } + + @SuppressWarnings("unchecked") + @Test + public void testDymanicPayloadContainsDuplicatedValue() { + String duplicate = "seeme"; + List fails = Arrays.asList( duplicate, duplicate ); + Set> violations = ValidatorUtil.getValidator().validate( new AnnotationContainer( fails ) ); + + assertThat( violations ).containsOnlyViolations( violationOf( UniqueElements.class ) ); + + ConstraintViolation violation = violations.iterator().next(); + Assertions.assertThat( ((HibernateConstraintViolation) violation.unwrap( HibernateConstraintViolation.class )).getDynamicPayload( List.class ) ) + .containsOnly( duplicate ); + } + + private static class TestObject { + + private final int value; + + private TestObject(Integer value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( !( o instanceof TestObject ) ) { + return false; + } + TestObject that = (TestObject) o; + return value == that.value; + } + + @Override + public int hashCode() { + return Objects.hash( value ); + } + } + + + private static class TestExtendedObject extends TestObject { + + private TestExtendedObject(Integer value) { + super( value ); + } + + @Override + public boolean equals(Object o) { + return ( this == o ) || ( o instanceof TestExtendedObject && super.equals( o ) ); + } + + @Override + public int hashCode() { + return Objects.hash( super.hashCode() ); + } + } +} diff --git a/engine/src/test/resources/org/hibernate/validator/test/internal/constraintvalidators/hv/UniqueElementsMessages.properties b/engine/src/test/resources/org/hibernate/validator/test/internal/constraintvalidators/hv/UniqueElementsMessages.properties new file mode 100644 index 0000000000..e9234d21a8 --- /dev/null +++ b/engine/src/test/resources/org/hibernate/validator/test/internal/constraintvalidators/hv/UniqueElementsMessages.properties @@ -0,0 +1 @@ +org.hibernate.validator.constraints.UniqueElements.message = must only contain unique elements but contains duplicate elements: {duplicates} diff --git a/test-utils/src/main/java/org/hibernate/validator/testutil/ConstraintViolationAssert.java b/test-utils/src/main/java/org/hibernate/validator/testutil/ConstraintViolationAssert.java index 32adb11959..ae8830cd19 100644 --- a/test-utils/src/main/java/org/hibernate/validator/testutil/ConstraintViolationAssert.java +++ b/test-utils/src/main/java/org/hibernate/validator/testutil/ConstraintViolationAssert.java @@ -97,6 +97,15 @@ public static void assertNoViolations(Set> viol Assertions.assertThat( violations ).isEmpty(); } + /** + * Asserts that the given violation list has no violations (is empty). + * + * @param violations The violation list to verify. + */ + public static void assertNoViolations(Set> violations, String message) { + Assertions.assertThat( violations ).describedAs( message ).isEmpty(); + } + public static void assertConstraintTypes(Set> descriptors, Class... expectedConstraintTypes) { List> actualConstraintTypes = new ArrayList<>(); @@ -245,6 +254,11 @@ protected ConstraintViolationSetAssert(Set> act super( actualViolations ); } + @Override + public ConstraintViolationSetAssert describedAs(String description, Object... args) { + return (ConstraintViolationSetAssert) super.describedAs( description, args ); + } + public void containsOnlyViolations(ViolationExpectation... expectedViolations) { isNotNull();