How to use an annotation element inside a custom constraint validator - java

I wrote a custom annotation in my project called CGC:
#Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
#Retention(RUNTIME)
#Documented
#Constraint(validatedBy = CGCValidator.class)
public #interface CGC {
String message() default "{person.cgc.error}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
boolean canBeNull() default false;
#Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
#Retention(RUNTIME)
#Documented
public #interface List {
CGC[] value();
}
}
I have a validator class that uses the annotation and basically, as my first validation I wanna check if the field is null, but only If the annotation for that field has specified the "canBeNull" element as true (#CGC(canBeNull="true")). My question is: how can I access the canBeNull element inside my validator class?
*The validator should be something like this:
public class CGCValidator implements ConstraintValidator<CGC, String> {
#Override
public void initialize(CGC annotation) {
}
#Override
public boolean isValid(String cgc, ConstraintValidatorContext constraintValidatorContext) {
if(!canBeNull() && cgc == null) {
return false;
}
...

You can capture the canBeNull value in the initialize function:
class CGCValidator implements ConstraintValidator<CGC, String> {
boolean canBeNull;
#Override
public void initialize(CGC constraintAnnotation) {
canBeNull = constraintAnnotation.canBeNull();
}
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return canBeNull || value != null;
}
}

Related

Spring Boot ConstraintValidator problem with null #PathVariable

I created a custom validator in Spring Boot but when I pass the #PathVariable value to the validator, it gets the parameter name for null values.
#Validated // added for controller
#GetMapping("/employees/{code}")
public ResponseEntity<ApiResponse<List<RunwayBean>>> findByCode(
#ValidInput #PathVariable("code") String code) {
// ...
}
#Documented
#Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE})
#Retention(RUNTIME)
#Constraint(validatedBy = {InputValidator.class})
public #interface ValidInput {
String message() default INVALID_INPUT;
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
And then in the validator below, str value is :code when I pass the parameter null on Postman. But when I add any string, it can be received correctly. So, how can I fix it?
public class InputValidator implements ConstraintValidator<ValidInput, String> {
#Override
public void initialize(ValidInput constraintAnnotation) {}
#Override
public boolean isValid(String str, ConstraintValidatorContext context) {
return str!= null && !str.isBlank();
// str = ":code" when #PathVariable value is null
}
}

How to return all valid enum values to API client as JSON when using custom validation annotation

I need to return to API client list of valid DecisionStates when the client passes invalid string that cannot be mapped to DecisionStates enum.
Annotataion definition:
#Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
#Retention(RUNTIME)
#Documented
#Constraint(validatedBy = ValueOfEnumValidator.class)
public #interface ValueOfEnum {
Class<? extends Enum<?>> enumClass();
String message() default "must be any of enum {enumClass}: [" + Arrays.toString(enumClass().getEnumConstants()).replaceAll("^.|.$", "") + "]"; // <-- not compiling
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
DTO:
#ValueOfEnum(enumClass = DecisionState.class)
String state;
ConstraintValidator implementation:
public class ValueOfEnumValidator implements ConstraintValidator<ValueOfEnum, CharSequence> {
private List<String> acceptedValues;
#Override
public void initialize(ValueOfEnum annotation) {
acceptedValues = Stream.of(annotation.enumClass().getEnumConstants())
.map(Enum::name)
.collect(Collectors.toList());
}
#Override
public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}
return acceptedValues.contains(value.toString());
}
}

How can we use either of the validation in spring boot?

I have two variables in my bean and I want either name or mobile to be filled, they cant be both null at the same time.
#NotNull
private String name;
#NotNull
private String mobile;
How can I achieve that?
You need to write a custom annotation for this and use on class
#AtLeastOneNotEmpty(fields = {"name", "phone"})
public class User{
Custom Annotation Implementation
#Constraint(validatedBy = AtLeastOneNotEmptyValidator.class)
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface AtLeastOneNotEmpty {
String message() default "At least one cannot be null";
String[] fields();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
And Validator of Custom Annotation
public class AtLeastOneNotEmptyValidator
implements ConstraintValidator<AtLeastOneNotEmpty, Object> {
private String[] fields;
public void initialize(AtLeastOneNotEmpty constraintAnnotation) {
this.fields = constraintAnnotation.fields();
}
public boolean isValid(Object value, ConstraintValidatorContext context) {
List<String> fieldValues = new ArrayList<String>();
for (String field : fields) {
Object propertyValue = new BeanWrapperImpl(value).getPropertyValue(field);
if (ObjectUtils.isEmpty(propertyValue)) {
fieldValues.add(null);
} else {
fieldValues.add(propertyValue.toString());
}
}
return fieldValues.stream().anyMatch(fieldValue -> fieldValue!= null);
}
}
you can create your own validation or annotation
try like this :
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface NotNullConfirmed {
String message() default "they can not be null";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
and class that implement it:
public class FieldConfirmedValidator implements ConstraintValidator<NotNullConfirmed, Object>{
#Override
public boolean isValid(Object user, ConstraintValidatorContext context) {
String name = ((Your_bo)user).getName();
String phone = ((Your_bo)user).getPhone();
return !name.isEmpty() && !phone.isEmpty();
}
}
and add this annotation to your class
#NotNullConfirmed
public class User{
}

How to interpolate message dynamically in ConstraintValidator with property node?

Assume I have simple dto with one field and this dto is also annotated with custom validation annotation:
#CustomAnnotation
public class SimpleDto {
private String field;
}
// setters and getters omited
Custom annotation:
#Target(TYPE)
#Retention(RUNTIME)
#Constraint(validatedBy = CustomValidator.class)
#Documented
public #interface CheckMondialRelayShopOrderWeight {
String message() default "{temp.key.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
and finally validator itself:
public class CustomValidator implements
ConstraintValidator<CustomAnnotation, SimpleDto> {
#Override
public void initialize(CustomAnnotation constraintAnnotation) {}
#Override
public boolean isValid(SimpleDto value,
ConstraintValidatorContext context) {
HibernateConstraintValidatorContext hibernateContext = context.unwrap(HibernateConstraintValidatorContext.class);
hibernateContext.addMessageParameter("dynamicValue", 130);
hibernateContext.buildConstraintViolationWithTemplate(hibernateContext.getDefaultConstraintMessageTemplate()).addPropertyNode("field").addConstraintViolation().disableDefaultConstraintViolation();
return false;
}
}
and in application.properties:
CustomAnnotation.simpleDto.field=Your dynamic value is {dynamicValue}
But this doesn't work, this works just fine if I put hibernateContext.buildConstraintViolationWithTemplate("Your dynamic value is {dynamicValue}").addConstraintViolation().disableDefaultConstraintViolation(); instead of hibernateContext.buildConstraintViolationWithTemplate(hibernateContext.getDefaultConstraintMessageTemplate()).addPropertyNode("field").addConstraintViolation().disableDefaultConstraintViolation();
I can't figure out how to use the interpolation with addPropertyNode. Any suggestions?

How to use Hibernate validation annotations with enums?

How can I use hibernate annotations to validate an enum member field?
The following does not work:
enum UserRole {
USER, ADMIN;
}
class User {
#NotBlank //HV000030: No validator could be found for type: UserRole.
UserRole userRole;
}
Note you can also create a validator to check a String is part of an enumeration.
public enum UserType { PERSON, COMPANY }
#NotNull
#StringEnumeration(enumClass = UserCivility.class)
private String title;
#Documented
#Constraint(validatedBy = StringEnumerationValidator.class)
#Target({ METHOD, FIELD, ANNOTATION_TYPE, PARAMETER, CONSTRUCTOR })
#Retention(RUNTIME)
public #interface StringEnumeration {
String message() default "{com.xxx.bean.validation.constraints.StringEnumeration.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Class<? extends Enum<?>> enumClass();
}
public class StringEnumerationValidator implements ConstraintValidator<StringEnumeration, String> {
private Set<String> AVAILABLE_ENUM_NAMES;
#Override
public void initialize(StringEnumeration stringEnumeration) {
Class<? extends Enum<?>> enumSelected = stringEnumeration.enumClass();
//Set<? extends Enum<?>> enumInstances = EnumSet.allOf(enumSelected);
Set<? extends Enum<?>> enumInstances = Sets.newHashSet(enumSelected.getEnumConstants());
AVAILABLE_ENUM_NAMES = FluentIterable
.from(enumInstances)
.transform(PrimitiveGuavaFunctions.ENUM_TO_NAME)
.toSet();
}
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if ( value == null ) {
return true;
} else {
return AVAILABLE_ENUM_NAMES.contains(value);
}
}
}
This is nice because you don't loose the information of the "wrong value". You can get a message like
The value "someBadUserType" is not a valid UserType. Valid UserType
values are: PERSON, COMPANY
Edit
For those who want a non-Guava version it should work with something like:
public class StringEnumerationValidator implements ConstraintValidator<StringEnumeration, String> {
private Set<String> AVAILABLE_ENUM_NAMES;
public static Set<String> getNamesSet(Class<? extends Enum<?>> e) {
Enum<?>[] enums = e.getEnumConstants();
String[] names = new String[enums.length];
for (int i = 0; i < enums.length; i++) {
names[i] = enums[i].name();
}
Set<String> mySet = new HashSet<String>(Arrays.asList(names));
return mySet;
}
#Override
public void initialize(StringEnumeration stringEnumeration) {
Class<? extends Enum<?>> enumSelected = stringEnumeration.enumClass();
AVAILABLE_ENUM_NAMES = getNamesSet(enumSelected);
}
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if ( value == null ) {
return true;
} else {
return AVAILABLE_ENUM_NAMES.contains(value);
}
}
}
And to customize the error message and display the appropriate values, check this: https://stackoverflow.com/a/19833921/82609
#NotBlank
Validate that the annotated string is not null or empty. The difference to NotEmpty is that trailing whitespaces are getting ignored.
Where as UserRole is not String and an object Use #NotNull
The annotated element must not be null. Accepts any type.
I suppose a more closely related to Sebastien's answer above with fewer lines of code and makes use of EnumSet.allOf in the expense of a rawtypes warning
Enums setup
public enum FuelTypeEnum {DIESEL, PETROL, ELECTRIC, HYBRID, ...};
public enum BodyTypeEnum {VAN, COUPE, MUV, JEEP, ...};
Annotation setup
#Target(ElementType.FIELD) //METHOD, CONSTRUCTOR, etc.
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = EnumValidator.class)
public #interface ValidateEnum {
String message() default "{com.xxx.yyy.ValidateEnum.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Class<? extends Enum<?>> targetClassType();
}
Validator setup
public class EnumValidator implements ConstraintValidator<ValidateEnum, String> {
private Set<String> allowedValues;
#SuppressWarnings({ "unchecked", "rawtypes" })
#Override
public void initialize(ValidateEnum targetEnum) {
Class<? extends Enum> enumSelected = targetEnum.targetClassType();
allowedValues = (Set<String>) EnumSet.allOf(enumSelected).stream().map(e -> ((Enum<? extends Enum<?>>) e).name())
.collect(Collectors.toSet());
}
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value == null || allowedValues.contains(value)? true : false;
}
}
Now go ahead and annotate your fields as follow
#ValidateEnum(targetClassType = FuelTypeEnum.class, message = "Please select ...."
private String fuelType;
#ValidateEnum(targetClassType = BodyTypeEnum.class, message = "Please select ...."
private String bodyType;
The above assumes you have the Hibernate Validator setup and working with default annotation.
Often times, attempting to convert to an enum is not just by name (which is the default behavior with valueOf method). For example, what if you have enums representing DayOfWeek and you want an integer to be converted to a DayOfWeek? To do that, I created the following annotation:
#Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
#Retention(RUNTIME)
#Constraint(validatedBy = {ValidEnumValueValidator.class})
public #interface ValidEnumValue {
String message() default "invalidParam";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Class<? extends Enum<?>> value();
String enumMethod() default "name";
String stringMethod() default "toString";
}
public class ValidEnumValueValidator implements ConstraintValidator<ValidEnumValue, String> {
Class<? extends Enum<?>> enumClass;
String enumMethod;
String stringMethod;
#Override
public void initialize(ValidEnumValue annotation) {
this.enumClass = annotation.value();
this.enumMethod = annotation.enumMethod();
this.stringMethod = annotation.stringMethod();
}
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
Enum<?>[] enums = enumClass.getEnumConstants();
Method method = ReflectionUtils.findMethod(enumClass, enumMethod);
return Objects.nonNull(enums) && Arrays.stream(enums)
.map(en -> ReflectionUtils.invokeMethod(method, en))
.anyMatch(en -> {
Method m = ReflectionUtils.findMethod(String.class, stringMethod);
Object o = ReflectionUtils.invokeMethod(m, value);
return Objects.equals(o, en);
});
}
}
You'd use it as follows:
public enum TestEnum {
A("test");
TestEnum(String s) {
this.val = s;
}
private String val;
public String getValue() {
return this.val;
}
}
public static class Testee {
#ValidEnumValue(value = TestEnum.class, enumMethod = "getValue", stringMethod = "toLowerCase")
private String testEnum;
}
Above implementation uses ReflectionUtils from Spring framework and Java 8+.

Categories