Skip to content

Commit

Permalink
1. Added OneOf annotation and constraint details in BuiltinConstraint…
Browse files Browse the repository at this point in the history
…, ConstraintHelper, TypeNames.

2. Added OneOfDef and its unit test.
3. Added documentation in ch02.asciidoc.
4. Updated OneOf constraints to accept different array data types like int, long, float and double.
5. Updated OneOfValidator to map to Object instead of CharSequence so that it can be used on fields with Integer, Long, Float, Double and String types.
6. Added detailed unit tests for OneOf in OneOfValidatorTest.
7. Added validation messages for OneOf message code or key in ValidationMessages_[LOCALE].properties files.
  • Loading branch information
Yusuf Alamu Musa committed Jan 17, 2025
1 parent b29d458 commit 798f3a0
Show file tree
Hide file tree
Showing 37 changed files with 709 additions and 144 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ public ConstraintHelper(Types typeUtils, AnnotationApiHelper annotationApiHelper
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.NOT_BLANK, CharSequence.class );
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.NOT_EMPTY, TYPES_SUPPORTED_BY_SIZE_AND_NOT_EMPTY_ANNOTATIONS );
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.NORMALIZED, CharSequence.class );
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.ONE_OF, Object.class );
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.SCRIPT_ASSERT, Object.class );
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.UNIQUE_ELEMENTS, Collection.class );
registerAllowedTypesForBuiltInConstraint( HibernateValidatorTypes.URL, CharSequence.class );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ public static class HibernateValidatorTypes {
public static final String UUID = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".UUID";
public static final String NOT_BLANK = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".NotBlank";
public static final String NOT_EMPTY = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".NotEmpty";
public static final String ONE_OF = ORG_HIBERNATE_VALIDATOR_CONSTRAINTS + ".OneOf";
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";
Expand Down
4 changes: 4 additions & 0 deletions documentation/src/main/asciidoc/ch02.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,10 @@ With one exception also these constraints apply to the field/property level, onl
Supported data types::: `CharSequence`
Hibernate metadata impact::: None

`@OneOf`:: Checks that the annotated character sequence or object is one of the allowed values. The allowed values are defined using `allowedValues`, `allowedIntegers`, `allowedLongs`, `allowedFloats`, `allowedDoubles` or by specifying an `enumClass`. The validation occurs after converting the annotated object to a `String`.
Supported data types::: `CharSequence`, `Integer`, `Long`, `Float`, `Double`, `Enum`
Hibernate metadata impact::: None

`@Range(min=, max=)`:: Checks whether the annotated value lies between (inclusive) the specified minimum and maximum
Supported data types::: `BigDecimal`, `BigInteger`, `CharSequence`, `byte`, `short`, `int`, `long` and the respective wrappers of the primitive types
Hibernate metadata impact::: None
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.validator.cfg.defs;

import org.hibernate.validator.cfg.ConstraintDef;
import org.hibernate.validator.constraints.OneOf;

public class OneOfDef extends ConstraintDef<OneOfDef, OneOf> {

public OneOfDef() {
super( OneOf.class );
}

public OneOfDef enumClass(Class<? extends Enum<?>> enumClass) {
addParameter( "enumClass", enumClass );
return this;
}

public OneOfDef allowedIntegers(int[] allowedIntegers) {
addParameter( "allowedIntegers", allowedIntegers );
return this;
}

public OneOfDef allowedLongs(long[] allowedLongs) {
addParameter( "allowedLongs", allowedLongs );
return this;
}

public OneOfDef allowedFloats(float[] allowedFloats) {
addParameter( "allowedFloats", allowedFloats );
return this;
}

public OneOfDef allowedDoubles(double[] allowedDoubles) {
addParameter( "allowedDoubles", allowedDoubles );
return this;
}

public OneOfDef allowedValues(String[] allowedValues) {
addParameter( "allowedValues", allowedValues );
return this;
}
}
56 changes: 46 additions & 10 deletions engine/src/main/java/org/hibernate/validator/constraints/OneOf.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,61 @@
import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import org.hibernate.validator.internal.constraintvalidators.bv.OneOfValidator;

/**
* Annotation to specify that a field or parameter must be one of a defined set of values.
* This can be enforced using string, integer, float, or double values, or by restricting the values to those
* within an enum type.
*
* <p> For string values, the annotation supports case-insensitive matching. </p>
*
* <p> Usage example: </p>
* <pre>{@code
* @OneOf(allowedValues = {"ACTIVE", "INACTIVE"}, ignoreCase = true)
* private String status;
* }</pre>
*
* <p>The message attribute provides a customizable error message when validation fails. The groups and payload
* attributes allow the validation to be applied to specific validation groups or custom payload types.</p>
*
* <p> You can use the following fields in the annotation: </p>
* <ul>
* <li><code>allowedIntegers</code>: A set of allowed integer values.</li>
* <li><code>allowedLongs</code>: A set of allowed long values.</li>
* <li><code>allowedFloats</code>: A set of allowed float values.</li>
* <li><code>allowedDoubles</code>: A set of allowed double values.</li>
* <li><code>allowedValues</code>: A set of allowed string values.</li>
* <li><code>enumClass</code>: The class of the enum type, if applicable.</li>
* <li><code>ignoreCase</code>: If true, string matching is case-insensitive.</li>
* </ul>
*
* @author Yusuf Álàmù
* @since 9.0.0
*/
@Documented
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = OneOfValidator.class)
public @interface OneOf {

String[] allowedValues() default { };
String message() default "{org.hibernate.validator.constraints.OneOf.message}";

Class<? extends Enum<?>> enumClass() default DefaultEnum.class;
Class<?>[] groups() default { };

boolean ignoreCase() default false;
Class<? extends Payload>[] payload() default { };

String message() default "must be one of {allowedValues} or is an invalid enum";
int[] allowedIntegers() default { };

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

Class<? extends Payload>[] payload() default { };
float[] allowedFloats() default { };

double[] allowedDoubles() default { };

String[] allowedValues() default { };

Class<? extends Enum<?>> enumClass() default DefaultEnum.class;

boolean ignoreCase() default false;

enum DefaultEnum {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.validator.internal.constraintvalidators.bv;
package org.hibernate.validator.internal.constraintvalidators.hv;


import static java.util.Objects.nonNull;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.stream.Stream;
Expand All @@ -27,7 +28,7 @@
* @author Yusuf Àlàmù Musa
* @version 1.0
*/
public class OneOfValidator implements ConstraintValidator<OneOf, CharSequence> {
public class OneOfValidator implements ConstraintValidator<OneOf, Object> {

private final List<String> acceptedValues = new ArrayList<>();
private boolean ignoreCase;
Expand All @@ -54,6 +55,30 @@ public void initialize(final OneOf constraintAnnotation) {
if ( constraintAnnotation.allowedValues() != null ) {
initializeAcceptedValues( constraintAnnotation.allowedValues() );
}

// If specific allowed values are provided, initialize accepted values from them
if ( constraintAnnotation.allowedIntegers() != null ) {
final String[] acceptedValues = convertIntToStringArray( constraintAnnotation.allowedIntegers() );
initializeAcceptedValues( acceptedValues );
}

// If specific allowed values are provided, initialize accepted values from them
if ( constraintAnnotation.allowedLongs() != null ) {
final String[] acceptedValues = convertLongToStringArray( constraintAnnotation.allowedLongs() );
initializeAcceptedValues( acceptedValues );
}

// If specific allowed values are provided, initialize accepted values from them
if ( constraintAnnotation.allowedFloats() != null ) {
final String[] acceptedValues = convertFloatToStringArray( constraintAnnotation.allowedFloats() );
initializeAcceptedValues( acceptedValues );
}

// If specific allowed values are provided, initialize accepted values from them
if ( constraintAnnotation.allowedDoubles() != null ) {
final String[] acceptedValues = convertDoubleToStringArray( constraintAnnotation.allowedDoubles() );
initializeAcceptedValues( acceptedValues );
}
}

/**
Expand All @@ -67,7 +92,7 @@ public void initialize(final OneOf constraintAnnotation) {
* @return {@code true} if the value is valid, {@code false} otherwise.
*/
@Override
public boolean isValid(final CharSequence value, final ConstraintValidatorContext context) {
public boolean isValid(final Object value, final ConstraintValidatorContext context) {
if ( nonNull( value ) ) {
return checkIfValueTheSame( value.toString() );
}
Expand Down Expand Up @@ -118,4 +143,55 @@ protected void initializeAcceptedValues(final String... values) {
acceptedValues.addAll( Stream.of( values ).map( String::trim ).toList() );
}
}

/**
* Converts an array of integers to an array of their corresponding string representations.
*
* @param allowedIntegers The array of integers to be converted.
* @return A new array of strings, where each element is the string representation of the corresponding integer from the input array.
*/
private static String[] convertIntToStringArray(final int[] allowedIntegers) {
return Arrays.stream( allowedIntegers )
.mapToObj( String::valueOf ) // Convert each int to String
.toArray( String[]::new );
}

/**
* Converts an array of longs to an array of their corresponding string representations.
*
* @param allowedLongs The array of longs to be converted.
* @return A new array of strings, where each element is the string representation of the corresponding long from the input array.
*/
private static String[] convertLongToStringArray(final long[] allowedLongs) {
return Arrays.stream( allowedLongs )
.mapToObj( String::valueOf ) // Convert each long to String
.toArray( String[]::new );
}

/**
* Converts an array of doubles to an array of their corresponding string representations.
*
* @param allowedDoubles The array of doubles to be converted.
* @return A new array of strings, where each element is the string representation of the corresponding double from the input array.
*/
private static String[] convertDoubleToStringArray(final double[] allowedDoubles) {
return Arrays.stream( allowedDoubles )
.mapToObj( String::valueOf ) // Convert each double to String
.toArray( String[]::new );
}

/**
* Converts an array of floats to an array of their corresponding string representations.
*
* @param allowedFloats The array of floats to be converted.
* @return A new array of strings, where each element is the string representation of the corresponding float from the input array.
*/
private static String[] convertFloatToStringArray(final float[] allowedFloats) {
final String[] acceptedValues = new String[allowedFloats.length];
for ( int i = 0; i < allowedFloats.length; i++ ) {
acceptedValues[i] = String.valueOf( allowedFloats[i] ); // Convert each float to String
}
return acceptedValues;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ enum BuiltinConstraint {
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_TIME_DURATION_MAX( "org.hibernate.validator.constraints.time.DurationMax" ),
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_TIME_DURATION_MIN( "org.hibernate.validator.constraints.time.DurationMin" ),
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_UUID( "org.hibernate.validator.constraints.UUID" ),
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_BITCOIN_ADDRESS( "org.hibernate.validator.constraints.BitcoinAddress" );
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_BITCOIN_ADDRESS( "org.hibernate.validator.constraints.BitcoinAddress" ),
ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_ONE_OF( "org.hibernate.validator.constraints.OneOf" );

private static final Map<String, Set<BuiltinConstraint>> CONSTRAINT_MAPPING;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_MOD10_CHECK;
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_MOD11_CHECK;
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_NORMALIZED;
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_ONE_OF;
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_PARAMETER_SCRIPT_ASSERT;
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_PL_NIP;
import static org.hibernate.validator.internal.metadata.core.BuiltinConstraint.ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_PL_PESEL;
Expand Down Expand Up @@ -112,6 +113,7 @@
import org.hibernate.validator.constraints.Mod10Check;
import org.hibernate.validator.constraints.Mod11Check;
import org.hibernate.validator.constraints.Normalized;
import org.hibernate.validator.constraints.OneOf;
import org.hibernate.validator.constraints.ParameterScriptAssert;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.ScriptAssert;
Expand Down Expand Up @@ -332,6 +334,7 @@
import org.hibernate.validator.internal.constraintvalidators.hv.Mod10CheckValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.Mod11CheckValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.NormalizedValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.OneOfValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.ParameterScriptAssertValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.ScriptAssertValidator;
import org.hibernate.validator.internal.constraintvalidators.hv.URLValidator;
Expand Down Expand Up @@ -814,6 +817,9 @@ protected Map<Class<? extends Annotation>, List<? extends ConstraintValidatorDes
if ( enabledBuiltinConstraints.contains( ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_BITCOIN_ADDRESS ) ) {
putBuiltinConstraint( tmpConstraints, BitcoinAddress.class, BitcoinAddressValidator.class );
}
if ( enabledBuiltinConstraints.contains( ORG_HIBERNATE_VALIDATOR_CONSTRAINTS_ONE_OF ) ) {
putBuiltinConstraint( tmpConstraints, OneOf.class, OneOfValidator.class );
}

return tmpConstraints;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ org.hibernate.validator.constraints.LuhnCheck.message = the check
org.hibernate.validator.constraints.Mod10Check.message = the check digit for ${validatedValue} is invalid, Modulo 10 checksum failed
org.hibernate.validator.constraints.Mod11Check.message = the check digit for ${validatedValue} is invalid, Modulo 11 checksum failed
org.hibernate.validator.constraints.Normalized.message = must be normalized
org.hibernate.validator.constraints.OneOf.message = invalid value
org.hibernate.validator.constraints.ParametersScriptAssert.message = script expression "{script}" didn't evaluate to true
org.hibernate.validator.constraints.Range.message = must be between {min} and {max}
org.hibernate.validator.constraints.ScriptAssert.message = script expression "{script}" didn't evaluate to true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ org.hibernate.validator.constraints.CreditCardNumber.message = \u0631\u06
org.hibernate.validator.constraints.EAN.message = \u0627\u0644\u0634\u0641\u0631\u0629 {type} \u063a\u064a\u0631 \u0635\u0627\u0644\u062d\u0629
org.hibernate.validator.constraints.Length.message = \u0627\u0644\u0637\u0648\u0644 \u064a\u062c\u0628 \u0623\u0646 \u064a\u0643\u0648\u0646 \u0628\u064a\u0646 {min} \u0648{max}
org.hibernate.validator.constraints.CodePointLength.message = \u0627\u0644\u0637\u0648\u0644 \u064a\u062c\u0628 \u0623\u0646 \u064a\u0643\u0648\u0646 \u0628\u064a\u0646 {min} \u0648{max}
org.hibernate.validator.constraints.OneOf.message = \u0642\u064A\u0645\u0629 \u063A\u064A\u0631 \u0635\u062D\u064A\u062D\u0629
org.hibernate.validator.constraints.ParametersScriptAssert.message = \u0627\u0644\u062a\u0639\u0628\u064a\u0631 \u0627\u0644\u0646\u0635\u064a "{script}" \u0644\u0645 \u064a\u062a\u0645 \u062a\u0642\u064a\u064a\u0645\u0647 \u0635\u062d\u064a\u062d\u0627
org.hibernate.validator.constraints.Range.message = \u064a\u062c\u0628 \u0623\u0646 \u064a\u0643\u0648\u0646 \u0628\u064a\u0646 {min} \u0648{max}
org.hibernate.validator.constraints.ScriptAssert.message = \u0627\u0644\u062a\u0639\u0628\u064a\u0631 \u0627\u0644\u0646\u0635\u064a "{script}" \u0644\u0645 \u064a\u062a\u0645 \u062a\u0642\u064a\u064a\u0645\u0647 \u0635\u062d\u064a\u062d\u0627
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ org.hibernate.validator.constraints.LuhnCheck.message = ${validate
org.hibernate.validator.constraints.Mod10Check.message = ${validatedValue} \u00fc\u00e7\u00fcn yoxlama r\u0259q\u0259mi etibars\u0131zd\u0131r, Modulo 10 yoxlamas\u0131 u\u011fursuz oldu
org.hibernate.validator.constraints.Mod11Check.message = ${validatedValue} \u00fc\u00e7\u00fcn yoxlama r\u0259q\u0259mi etibars\u0131zd\u0131r, Modulo 11 yoxlamas\u0131 u\u011fursuz oldu
org.hibernate.validator.constraints.Normalized.message = normalla\u015fd\u0131r\u0131lm\u0131\u015f olmal\u0131d\u0131r
org.hibernate.validator.constraints.OneOf.message = etibars\u0131z d\u0259y\u0259r
org.hibernate.validator.constraints.ParametersScriptAssert.message = skript ifad\u0259si "{script}" 'true' deyil
org.hibernate.validator.constraints.Range.message = {min} v\u0259 {max} aras\u0131nda olmal\u0131d\u0131r
org.hibernate.validator.constraints.ScriptAssert.message = skript ifad\u0259si "{script}" 'true' deyil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ org.hibernate.validator.constraints.CodePointLength.message = d\u00e9lka
org.hibernate.validator.constraints.LuhnCheck.message = kontroln\u00ed \u010d\u00edslice pro ${validatedValue} je neplatn\u00e1, kontroln\u00ed sou\u010det Luhn Modulo 10 se nezda\u0159il
org.hibernate.validator.constraints.Mod10Check.message = kontroln\u00ed \u010d\u00edslice pro ${validatedValue} je neplatn\u00e1, kontroln\u00ed sou\u010det Modulo 10 se nezda\u0159il
org.hibernate.validator.constraints.Mod11Check.message = kontroln\u00ed \u010d\u00edslice pro ${validatedValue} je neplatn\u00e1, kontroln\u00ed sou\u010det Modulo 11 se nezda\u0159il
org.hibernate.validator.constraints.OneOf.message = neplatn\u00e1 hodnota
org.hibernate.validator.constraints.ParametersScriptAssert.message = v\u00fdraz skriptu "{script}" se nevyhodnotil na true
org.hibernate.validator.constraints.Range.message = mus\u00ed le\u017eet v rozsahu {min} a\u017e {max}
org.hibernate.validator.constraints.ScriptAssert.message = v\u00fdraz skriptu "{script}" se nevyhodnotil na true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ org.hibernate.validator.constraints.CodePointLength.message = l\u00e6ngde
org.hibernate.validator.constraints.LuhnCheck.message = kontrolcifferet for ${validatedValue} er ugyldigt, Luhn Modulo 10 checksum mislykkedes
org.hibernate.validator.constraints.Mod10Check.message = kontrolcifferet for ${validatedValue} er ugyldigt, Modulo 10 checksum mislykkedes
org.hibernate.validator.constraints.Mod11Check.message = kontrolcifferet for ${validatedValue} er ugyldigt, Modulo 11 checksum mislykkedes
org.hibernate.validator.constraints.OneOf.message = ugyldig v\u00e6rdi
org.hibernate.validator.constraints.ParametersScriptAssert.message = script udtryk "{script}" evaluerede ikke til true
org.hibernate.validator.constraints.Range.message = skal v\u00e6re mellem {min} og {max}
org.hibernate.validator.constraints.ScriptAssert.message = script udtryk "{script}" evaluerede ikke til true
Expand Down
Loading

0 comments on commit 798f3a0

Please sign in to comment.