I want to validate a string against a set of values using annotations.
What I want is basically this:
@ValidateString(enumClass=com.co.enum)
String d
Ditch the String representation, and do a real enum.
public enum DataType {
STRING,
BOOLEAN,
INTEGER;
}
That way you avoid ever having to do string comparison of the previous String dataType
variable to determine if it is in the enum. As an aside, it also makes it impossible to assign a non-valid value to the member variable dataType
and since enums are guaranteed to be singletons within the class loader, it also saves on memory footprint.
It's worth the effort to change your code to use enums. However, assuming that you can't, you can at least change the annotation to use enums.
@ValidateString(DataType.STRING) String dataType;
and that way your ValidateString
annotation at least gets to benefit from enums, even if the rest of the code doesn't.
Now on the extremely rare chance that you can't use an enumeration at all, you can set static public integers, which map each accepted value.
public class DataType {
public static final int STRING = 1;
public static final int BOOLEAN = 2;
...
}
However, if you use a String for the annotation parameter, we don't have a type checking system which extends into the type to specify that only particular values are allowed. In other words, Java lacks the ability to do something like this:
public int<values=[1,3,5,7..9]> oddInt;
which would throw an error if you attempted to assign
oddInt = 4;
Why is this important? Because if it doesn't apply to regular Java, then it cannot apply to the enumeration which is implemented in regular Java classes.
Little bit of improvisation with Java 8 Stream API
import static java.util.stream.Collectors.toList;
import static java.util.stream.Stream.of;
import java.util.List;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class EnumValidatorImpl implements ConstraintValidator<EnumValidator, String>
{
private List<String> valueList = null;
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return valueList.contains(value.toUpperCase());
}
@Override
public void initialize(EnumValidator constraintAnnotation) {
valueList = of(constraintAnnotation.enumClazz().getEnumConstants()).map(e->e.toString()).collect(toList());
}
}
My attempt for a kotlin one:
import javax.validation.Constraint
import javax.validation.ConstraintValidator
import javax.validation.ConstraintValidatorContext
import javax.validation.ReportAsSingleViolation
import javax.validation.constraints.NotNull
import kotlin.reflect.KClass
@Constraint(validatedBy = [EnumValidatorImpl::class])
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FIELD)
@NotNull(message = "Value cannot be null")
@ReportAsSingleViolation
annotation class EnumValidator(val enumClazz: KClass<*>, val message: String = "Value is not valid")
class EnumValidatorImpl(private var valueList: List<String>? = null) : ConstraintValidator<EnumValidator, String> {
override fun isValid(value: String?, context: ConstraintValidatorContext?): Boolean =
valueList?.contains(value?.toUpperCase()) ?: false
override fun initialize(constraintAnnotation: EnumValidator) {
valueList = constraintAnnotation.enumClazz.java.enumConstants.map { it.toString().toUpperCase() }
}
}
Here is a detailed example with the feature of a dynamic error message by Hibernate Documentation
https://docs.jboss.org/hibernate/validator/4.1/reference/en-US/html/validator-customconstraints.html#validator-customconstraints-simple
I take up Rajeev Singla's response https://stackoverflow.com/a/21070806/8923905, just to optimize the code and allow the String parameter to be null, if in your application it is not mandatory and can be empty :
1- Remove the @NotNull annotation on the Interface
2- See the modified code below for the implementation.
public class EnumValidatorImpl implements ConstraintValidator <EnumValidator,String> {
private List<String> valueList = null;
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return null == value || valueList.contains(value.toUpperCase());
}
@Override
public void initialize(EnumValidator constraintAnnotation) {
valueList = new ArrayList<>();
Class<? extends Enum<?>> enumClass = constraintAnnotation.enumClass();
Enum[] enumValArr = enumClass.getEnumConstants();
for(Enum enumVal : enumValArr) {
valueList.add(enumVal.toString().toUpperCase());
}
}
}
This is what I did.
Annotation
public @interface ValidateString {
String[] acceptedValues();
String message() default "{uk.dds.ideskos.validator.ValidateString.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
Validation Class
public class StringValidator implements ConstraintValidator<ValidateString, String>{
private List<String> valueList;
@Override
public void initialize(ValidateString constraintAnnotation) {
valueList = new ArrayList<String>();
for(String val : constraintAnnotation.acceptedValues()) {
valueList.add(val.toUpperCase());
}
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return valueList.contains(value.toUpperCase());
}
}
And i used it like
@ValidateString(acceptedValues={"Integer", "String"}, message="Invalid dataType")
String dataType;
Long maxValue;
Long minValue;
Now I need to figure out how to implement conditional check ie. if String then maxValue and minValue should be null or Zero..
Any ideas?