I know I'm a little late to the party (11 years late to be exact), but I'd still like to contribute.
The answers presented here are great, they solve the problem in most cases, but in my opinion they lack the personalization touch.
What do I mean?
All solutions create the ConstraintValidator<EnumValidator, String>
and implement the validation logic in it.
And that is great, it solve the problem, but, what happens if I want to make the comparison by the toString() of the enum, or better, I have another one that I want to compare by the name, two different comparisons. For this, it would be necessary to implement a ConstraintValidator
for each type of comparison that is needed, when in fact their logic is VERY similar.
In my particular case, a very old system had comparisons, some with toUpperCase
, others with toLoweCase
, others with trim
, some with name
, others with toString
, total chaos, and the idea was to generalize all this in the same behavior.
The solution that I present to you combines the excellent answer of @Rajeev with the necessary customization to be able to reuse the ConstraintValidator
and implement only one comparison method for each different enum.
The general idea: Make the emun implement an interface to standardize the comparison.
First of all, the @annotation, nothing fancy:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target(value = ElementType.FIELD)
@Retention(value = RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {EnumValidatorRegister_String.class})
public @interface EnumValidator {
String message() default "Value is not present in enum list.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
public String detailMessage() default "";
public Class<? extends Enum<?>> target();
}
Keep in mind that the enum class will be presented in the target
field.
Now the interface to generalize the behavior:
public interface EnumValidatorComparator<T> {
public boolean test(T other);
}
A general combination of these two elements results in an enum with the generalized comparison behavior inside it, which in case it changes would only affect said implementation and no other elements of the system.
public enum Type implements EnumValidatorComparator<String> {
a("a"),
b("b"),
c("c"),
d("d"),
e("e"),
f("f"),
g("g"),
h("h"),
i("i");
private final String name;
private Type(String name) {
this.name = name;
}
@Override
public boolean test(String other) {
return this.toString().equalsIgnoreCase(other.trim().toLowerCase());
}
}
Finally the ConstraintValidator
, this is where the 'magic' happens.
import java.util.function.BiPredicate;
import java.util.stream.Stream;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
@Slf4j
public class EnumValidatorRegister_String implements ConstraintValidator<EnumValidator, String> {
//general comparator in case EnumValidator don't implement EnumValidatorComparator interface
private static BiPredicate<? super Enum, String> defaultComparison = (currentEnumValue, testValue) -> {
return currentEnumValue.toString().equals(testValue);
};
//setter for default comparator
public static void setDefaultEnumComparator(BiPredicate<? super Enum, String> defaultComparison) {
Assert.notNull(defaultComparison, "Default comparison can't be null");
EnumValidatorRegister_String.defaultComparison = defaultComparison;
}
//Enum class
private Class<? extends Enum<?>> clazz;
//values of enum
private Enum[] valuesArr;
@Override
public void initialize(EnumValidator _enum) {
ConstraintValidator.super.initialize(_enum);
clazz = _enum.target();
valuesArr = clazz.getEnumConstants();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
boolean present;
//if enum's targes is assignable from EnumValidatorComparator, compare by the `.test`
if (EnumValidatorComparator.class.isAssignableFrom(clazz)) {
present = Stream.of(valuesArr).anyMatch((t) -> {
return ((EnumValidatorComparator<String>) t).test(value);
});
} else { //if enum's targes is NOT assignable from EnumValidatorComparator, compare by the default
present = Stream.of(valuesArr).anyMatch((t) -> {
return defaultComparison.test(t, value);
});
}
//if the value is NOT present, show custom error
if (!present) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(
String.format(
"'%s' is not one of the one of allowable values: %s".formatted(
value,
Stream.of(valuesArr).map((Object object) -> {
return object.toString();
}).toList().toString()
)
)
).addConstraintViolation();
}
return present;
}
}
Note: lombok
dependencies are used for @Slf4j
for easy logging and springframework
's Assert
for validations of null value.
its use is as simple as expected:
public class People {
@EnumValidator(target = Type.class)
private String name;
private String someOtherField;
//getters, setters and constructor
}
In this way, if you have another enum with another comparison logic, it is as simple as creating said enum with its embedded logic, like:
public enum OtherType implements EnumValidatorComparator<String> {
A("A"),
B("B"),
C("C"),
D("D"),
E("E");
private final String name;
private OtherType(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
@Override
public boolean test(String other) {
return this.toString().equals(other);
}
}
And this way the ConstraintValidator
it's reused along all application, and changes only affect the class responsible for them without breaking the rest of the system logic.
Believe it or not this solution guaranteed me a raise at work, I hope I do something similar for you. :+1: