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-2073 Add new OneOfValidator for CharSequence validation #1540

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.validator.constraints;

import static java.lang.annotation.ElementType.*;
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;

/**
* 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)
public @interface OneOf {

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

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

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

int[] allowedIntegers() default { };

long[] allowedLongs() default { };

float[] allowedFloats() default { };

double[] allowedDoubles() default { };

String[] allowedValues() default { };

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

boolean ignoreCase() default false;

enum DefaultEnum {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
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;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

import org.hibernate.validator.constraints.OneOf;

/**
* Validator that checks if a given {@link CharSequence} matches one of the allowed values specified
* in the {@link OneOf} annotation.
*
* <p>This class implements the {@link ConstraintValidator} interface to perform the validation logic
* based on the configuration in the {@link OneOf} annotation.</p>
*
* @author Yusuf Àlàmù Musa
* @version 1.0
*/
public class OneOfValidator implements ConstraintValidator<OneOf, Object> {

private final List<String> acceptedValues = new ArrayList<>();
private boolean ignoreCase;

/**
* Initializes the validator with the values specified in the {@link OneOf} annotation.
*
* <p>This method sets the case sensitivity flag and adds the allowed values (either enum constants or specified values)
* to the list of accepted values for validation.</p>
*
* @param constraintAnnotation the {@link OneOf} annotation containing the configuration for validation.
*/
@Override
public void initialize(final OneOf constraintAnnotation) {
ignoreCase = constraintAnnotation.ignoreCase();

// If an enum class is specified, initialize accepted values from the enum constants
if ( constraintAnnotation.enumClass() != null ) {
final Enum<?>[] enumConstants = constraintAnnotation.enumClass().getEnumConstants();
initializeAcceptedValues( enumConstants );
}

// If specific allowed values are provided, initialize accepted values from them
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 );
}
}

/**
* Validates the given value based on the accepted values.
*
* <p>If the value is not null, it checks whether the value matches any of the accepted values.
* If the value is null, it is considered valid.</p>
*
* @param value the value to validate.
* @param context the validation context.
* @return {@code true} if the value is valid, {@code false} otherwise.
*/
@Override
public boolean isValid(final Object value, final ConstraintValidatorContext context) {
if ( nonNull( value ) ) {
return checkIfValueTheSame( value.toString() );
}
return true;
}

/**
* Checks if the provided value matches any of the accepted values.
*
* <p>If {@code ignoreCase} is false, the comparison is case-sensitive.
* If {@code ignoreCase} is true, the value is compared in lowercase.</p>
*
* @param value the value to check.
* @return {@code true} if the value matches an accepted value, {@code false} otherwise.
*/
protected boolean checkIfValueTheSame(final String value) {
if ( !ignoreCase ) {
return acceptedValues.contains( value );
}

for ( final String acceptedValue : acceptedValues ) {
if ( acceptedValue.toLowerCase( Locale.ROOT ).equals( value.toLowerCase( Locale.ROOT ) ) ) {
return true;
}
}

return false;
}

/**
* Initializes and adds the names of the provided enum constants to the accepted values list.
*
* @param enumConstants the enum constants to be added, ignored if null.
*/
protected void initializeAcceptedValues(final Enum<?>... enumConstants) {
if ( nonNull( enumConstants ) ) {
acceptedValues.addAll( Stream.of( enumConstants ).map( Enum::name ).toList() );
}
}

/**
* Initializes and adds the provided values to the accepted values list after trimming them.
*
* @param values the values to be added, ignored if null.
*/
protected void initializeAcceptedValues(final String... values) {
if ( nonNull( 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
Loading