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-1466: UniqueElements constraint #879

Closed
wants to merge 10 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -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() );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 )
);
}
}
Original file line number Diff line number Diff line change
@@ -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 <http://www.apache.org/licenses/LICENSE-2.0>.
*/
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<String> collection;

@UniqueElements
public List<String> list;

@UniqueElements
public Set<String> set;

@UniqueElements
public String string;
}
4 changes: 4 additions & 0 deletions documentation/src/main/asciidoc/ch02.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<section-mapping-xml-constraints>>) or the programmatic API (see <<section-programmatic-constraint-definition>>).
Supported data types::: `CharSequence`
Hibernate metadata impact::: None
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <http://www.apache.org/licenses/LICENSE-2.0>.
*/

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<UniqueElementsDef, UniqueElements> {

public UniqueElementsDef() {
super( UniqueElements.class );
}
}
Original file line number Diff line number Diff line change
@@ -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 <http://www.apache.org/licenses/LICENSE-2.0>.
*/
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.
* <p>
* 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<? extends Payload>[] payload() default {};

/**
* Defines several {@code @UniqueElements} annotations on the same element.
*/
@Target({ TYPE })
@Retention(RUNTIME)
@Documented
public @interface List {
UniqueElements[] value();
}
}
Original file line number Diff line number Diff line change
@@ -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 <http://www.apache.org/licenses/LICENSE-2.0>.
*/
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.
* <p>
* Uniqueness is defined by the {@code equals()} method of the objects being compared.
*
* @author Tadhg Pearson
* @author Guillaume Smet
*/
public class UniqueElementsValidator implements ConstraintValidator<UniqueElements, Collection<?>> {

/**
* @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<Object> 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<Object> findDuplicates(Collection<?> collection) {
Set<Object> uniqueElements = CollectionHelper.newHashSet( collection.size() );
return collection.stream().filter( o -> !uniqueElements.add( o ) )
.collect( toList() );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <http://www.apache.org/licenses/LICENSE-2.0>.
*/
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<ConstraintViolation<Library>> 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<String> books;

public Library(List<String> books) {
this.books = books;
}
}
}
Loading