Skip to content

Commit b29d458

Browse files
author
Yusuf Alamu Musa
committed
HV-2073 Add new OneOfValidator for CharSequence validation
Description: Contributed a new OneOfValidator for the Hibernate Validator project. This validator checks if a given CharSequence matches one of the allowed values specified in the OneOf annotation. Features: - Validates if a CharSequence is one of the allowed values. - Supports case-sensitive and case-insensitive validation based on the ignoreCase flag. - Supports both Enum constants and manually provided allowed values. The validator is implemented with the following: - OneOfValidator class implementing ConstraintValidator<OneOf, CharSequence>. - Adds values from allowedValues() and enumClass() in the OneOf annotation for validation. - Provides methods to handle case-sensitive and case-insensitive validation. Tests have been added, and all tests pass.
1 parent f955216 commit b29d458

File tree

3 files changed

+288
-0
lines changed

3 files changed

+288
-0
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.validator.constraints;
6+
7+
import static java.lang.annotation.ElementType.*;
8+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
9+
10+
import java.lang.annotation.Documented;
11+
import java.lang.annotation.Retention;
12+
import java.lang.annotation.Target;
13+
14+
import jakarta.validation.Constraint;
15+
import jakarta.validation.Payload;
16+
17+
import org.hibernate.validator.internal.constraintvalidators.bv.OneOfValidator;
18+
19+
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
20+
@Retention(RUNTIME)
21+
@Documented
22+
@Constraint(validatedBy = OneOfValidator.class)
23+
public @interface OneOf {
24+
25+
String[] allowedValues() default { };
26+
27+
Class<? extends Enum<?>> enumClass() default DefaultEnum.class;
28+
29+
boolean ignoreCase() default false;
30+
31+
String message() default "must be one of {allowedValues} or is an invalid enum";
32+
33+
Class<?>[] groups() default { };
34+
35+
Class<? extends Payload>[] payload() default { };
36+
37+
enum DefaultEnum {
38+
}
39+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.validator.internal.constraintvalidators.bv;
6+
7+
8+
import static java.util.Objects.nonNull;
9+
10+
import java.util.ArrayList;
11+
import java.util.List;
12+
import java.util.Locale;
13+
import java.util.stream.Stream;
14+
15+
import jakarta.validation.ConstraintValidator;
16+
import jakarta.validation.ConstraintValidatorContext;
17+
18+
import org.hibernate.validator.constraints.OneOf;
19+
20+
/**
21+
* Validator that checks if a given {@link CharSequence} matches one of the allowed values specified
22+
* in the {@link OneOf} annotation.
23+
*
24+
* <p>This class implements the {@link ConstraintValidator} interface to perform the validation logic
25+
* based on the configuration in the {@link OneOf} annotation.</p>
26+
*
27+
* @author Yusuf Àlàmù Musa
28+
* @version 1.0
29+
*/
30+
public class OneOfValidator implements ConstraintValidator<OneOf, CharSequence> {
31+
32+
private final List<String> acceptedValues = new ArrayList<>();
33+
private boolean ignoreCase;
34+
35+
/**
36+
* Initializes the validator with the values specified in the {@link OneOf} annotation.
37+
*
38+
* <p>This method sets the case sensitivity flag and adds the allowed values (either enum constants or specified values)
39+
* to the list of accepted values for validation.</p>
40+
*
41+
* @param constraintAnnotation the {@link OneOf} annotation containing the configuration for validation.
42+
*/
43+
@Override
44+
public void initialize(final OneOf constraintAnnotation) {
45+
ignoreCase = constraintAnnotation.ignoreCase();
46+
47+
// If an enum class is specified, initialize accepted values from the enum constants
48+
if ( constraintAnnotation.enumClass() != null ) {
49+
final Enum<?>[] enumConstants = constraintAnnotation.enumClass().getEnumConstants();
50+
initializeAcceptedValues( enumConstants );
51+
}
52+
53+
// If specific allowed values are provided, initialize accepted values from them
54+
if ( constraintAnnotation.allowedValues() != null ) {
55+
initializeAcceptedValues( constraintAnnotation.allowedValues() );
56+
}
57+
}
58+
59+
/**
60+
* Validates the given value based on the accepted values.
61+
*
62+
* <p>If the value is not null, it checks whether the value matches any of the accepted values.
63+
* If the value is null, it is considered valid.</p>
64+
*
65+
* @param value the value to validate.
66+
* @param context the validation context.
67+
* @return {@code true} if the value is valid, {@code false} otherwise.
68+
*/
69+
@Override
70+
public boolean isValid(final CharSequence value, final ConstraintValidatorContext context) {
71+
if ( nonNull( value ) ) {
72+
return checkIfValueTheSame( value.toString() );
73+
}
74+
return true;
75+
}
76+
77+
/**
78+
* Checks if the provided value matches any of the accepted values.
79+
*
80+
* <p>If {@code ignoreCase} is false, the comparison is case-sensitive.
81+
* If {@code ignoreCase} is true, the value is compared in lowercase.</p>
82+
*
83+
* @param value the value to check.
84+
* @return {@code true} if the value matches an accepted value, {@code false} otherwise.
85+
*/
86+
protected boolean checkIfValueTheSame(final String value) {
87+
if ( !ignoreCase ) {
88+
return acceptedValues.contains( value );
89+
}
90+
91+
for ( final String acceptedValue : acceptedValues ) {
92+
if ( acceptedValue.toLowerCase( Locale.ROOT ).equals( value.toLowerCase( Locale.ROOT ) ) ) {
93+
return true;
94+
}
95+
}
96+
97+
return false;
98+
}
99+
100+
/**
101+
* Initializes and adds the names of the provided enum constants to the accepted values list.
102+
*
103+
* @param enumConstants the enum constants to be added, ignored if null.
104+
*/
105+
protected void initializeAcceptedValues(final Enum<?>... enumConstants) {
106+
if ( nonNull( enumConstants ) ) {
107+
acceptedValues.addAll( Stream.of( enumConstants ).map( Enum::name ).toList() );
108+
}
109+
}
110+
111+
/**
112+
* Initializes and adds the provided values to the accepted values list after trimming them.
113+
*
114+
* @param values the values to be added, ignored if null.
115+
*/
116+
protected void initializeAcceptedValues(final String... values) {
117+
if ( nonNull( values ) ) {
118+
acceptedValues.addAll( Stream.of( values ).map( String::trim ).toList() );
119+
}
120+
}
121+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.validator.internal.constraintvalidators.bv;
6+
7+
import static org.easymock.EasyMock.mock;
8+
import static org.testng.Assert.assertFalse;
9+
import static org.testng.Assert.assertTrue;
10+
11+
import jakarta.validation.ConstraintValidatorContext;
12+
import jakarta.validation.Payload;
13+
14+
import org.hibernate.validator.constraints.OneOf;
15+
import org.hibernate.validator.testutil.TestForIssue;
16+
17+
import org.testng.annotations.BeforeMethod;
18+
import org.testng.annotations.Test;
19+
20+
public class OneOfValidatorTest {
21+
22+
private ConstraintValidatorContext context;
23+
private OneOfValidator validator;
24+
25+
@BeforeMethod
26+
public void setUp() {
27+
validator = new OneOfValidator();
28+
context = mock( ConstraintValidatorContext.class );
29+
}
30+
31+
@Test
32+
@TestForIssue(jiraKey = "HV-2073")
33+
public void testIsValidNullValueShouldReturnTrue() {
34+
assertTrue( validator.isValid( null, context ), "Null value should be considered valid." );
35+
}
36+
37+
@Test
38+
@TestForIssue(jiraKey = "HV-2073")
39+
public void testIsValidCaseSensitiveMatchShouldReturnTrue() {
40+
OneOf annotation = createOneOf( false, new String[] { "Value1", "Value2" }, null );
41+
42+
validator.initialize( annotation );
43+
44+
assertTrue( validator.isValid( "Value1", context ), "Exact case-sensitive match should return true." );
45+
}
46+
47+
@Test
48+
@TestForIssue(jiraKey = "HV-2073")
49+
public void testIsValidCaseSensitiveMismatchShouldReturnFalse() {
50+
OneOf annotation = createOneOf( false, new String[] { "Value1", "Value2" }, null );
51+
52+
validator.initialize( annotation );
53+
54+
assertFalse( validator.isValid( "value1", context ), "Case-sensitive mismatch should return false." );
55+
}
56+
57+
@Test
58+
public void testIsValidIgnoreCaseMatchShouldReturnTrue() {
59+
OneOf annotation = createOneOf( true, new String[] { "Value1", "Value2" }, null );
60+
61+
validator.initialize( annotation );
62+
63+
assertTrue( validator.isValid( "value1", context ), "Ignore-case match should return true." );
64+
}
65+
66+
@Test
67+
public void testIsValidIgnoreCaseMismatchShouldReturnFalse() {
68+
OneOf annotation = createOneOf( true, new String[] { "Value1", "Value2" }, null );
69+
70+
validator.initialize( annotation );
71+
72+
assertFalse( validator.isValid( "invalid", context ), "Ignore-case mismatch should return false." );
73+
}
74+
75+
@Test
76+
public void testInitializeEnumClassShouldAcceptEnumValues() {
77+
OneOf annotation = createOneOf( false, new String[] { }, TestEnum.class );
78+
79+
validator.initialize( annotation );
80+
81+
assertTrue( validator.isValid( "ONE", context ), "Enum constant 'ONE' should be valid." );
82+
assertFalse( validator.isValid( "TWO", context ), "'TWO' should not be valid as it's not in the enum." );
83+
}
84+
85+
private OneOf createOneOf(boolean ignoreCase, String[] allowedValues, Class<? extends Enum<?>> enumClass) {
86+
return new OneOf() {
87+
@Override
88+
public String[] allowedValues() {
89+
return allowedValues;
90+
}
91+
92+
@Override
93+
public Class<? extends Enum<?>> enumClass() {
94+
return enumClass;
95+
}
96+
97+
@Override
98+
public boolean ignoreCase() {
99+
return ignoreCase;
100+
}
101+
102+
@Override
103+
public String message() {
104+
return "";
105+
}
106+
107+
@Override
108+
public Class<?>[] groups() {
109+
return new Class[0];
110+
}
111+
112+
@Override
113+
public Class<? extends Payload>[] payload() {
114+
return new Class[0];
115+
}
116+
117+
@Override
118+
public Class<OneOf> annotationType() {
119+
return OneOf.class;
120+
}
121+
};
122+
}
123+
124+
private enum TestEnum {
125+
ONE, THREE
126+
}
127+
128+
}

0 commit comments

Comments
 (0)