I want to create custom email validator using annotations. This solution was useful while creating the validator.
Here's the annotation :
#Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = {CommonsEmailValidator.class})
#Documented
#ReportAsSingleViolation
public #interface ExEmailValidator {
String message() default " {org.hibernate.validator.constraints.Email.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
#Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface List {
ExEmailValidator[] value();
}
}
And here's the class CommonsEmailValidator :
public class CommonsEmailValidator implements ConstraintValidator<ExEmailValidator, String> {
private static final boolean ALLOW_LOCAL = false;
private EmailValidator realValidator = EmailValidator.getInstance(ALLOW_LOCAL);
#Override
public void initialize(ExEmailValidator email) {
// TODO Auto-generated method stub
}
#Override
public boolean isValid(String email, ConstraintValidatorContext constraintValidatorContext) {
if( email == null ) return true;
return realValidator.isValid(email);
}
}
I run the project, but, when I click submit on the registration form, while the email format is not valid I have the following exception :
Request processing failed; nested exception is javax.validation.ConstraintViolationException: Validation failed for classes [...] during persist time for groups [javax.validation.groups.Default, ]
the exception was as excpected, But instead of showing a 500 error, I would like to see an error message explaning what's wrong.
So, I think I have to use ConstraintValidatorContext to define custom error messages. But, I don't know how to do it.
Any ideas please ?
You should use #ControllerAdvice and handle validation exceptions there with #ExceptionHandler(MethodArgumentNotValidException.class) for example.
Related
I have a custom annotation called Matches which has a default value for message. This is essentially a class-level custom constraint validator but that's not part of the problem.
#Constraint(validatedBy = MatchesValidator.class)
#Documented
#Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Repeatable(Matches.List.class)
public #interface Matches {
String field();
String otherField();
String message() default "{com.example.api.validation.constraints.Matches.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
#Documented
#Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
#Retention(RetentionPolicy.RUNTIME)
#interface List {
Matches[] value();
}
}
This is a simple constraint that will be applied on a class to validate if two fields have the same value:
#Matches(field = "password", otherField = "confirmPassword")
class UserRegistration {
private String password;
private String confirmPassword;
// Getters
}
I'm writing a test using JUnit and Mockito and and I'm trying to mock the Matches annotation:
#Test
void isValid_whenFieldsDoNotMatch_thenReturnsFalse() {
Matches invalidPropertyMatches = Mockito.mock(Matches.class);
when(invalidPropertyMatches.field()).thenReturn("password");
when(invalidPropertyMatches.field()).thenReturn("confirmPassword");
when(invalidPropertyMatches.message()).thenCallRealMethod(); // This throws
}
I want that when the Matches.message() value is called, I get back the default defined value "{com.example.api.validation.constraints.Matches.message}", so I added:
when(invalidPropertyMatches.message()).thenCallRealMethod();
However, this throws the following exception:
org.mockito.exceptions.base.MockitoException:
Cannot call abstract real method on java object!
Calling real methods is only possible when mocking non abstract method.
//correct example:
when(mockOfConcreteClass.nonAbstractMethod()).thenCallRealMethod();
It essentially says that Matches.message() is abstract and cannot be called. Are annotation properties in Java abstract? Is there a way to fix this?
I'm trying to make a custom java validation annotation and returns me
Request processing failed; nested exception is javax.validation.ConstraintDeclarationException: HV000144: Cross parameter constraint com.my.company.CustomConstraint is illegally placed on field 'private java.util.List com.my.company.ElementOfTheList'."
the code is really naive
#Documented
#Retention(RUNTIME)
#Target({ FIELD, METHOD})
#Constraint(validatedBy = ConstraintValidation.class)
public #interface CustomConstraint {
String message() default "this is the default message";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
#SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class ConstraintValidationimplements ConstraintValidator<CustomConstraint , List<ElementOfTheList>> {
public boolean isValid(List<ElementOfTheList> value, ConstraintValidatorContext context) {
System.out.println("only a sysout to test");
return true;
}
}
And in the rest object model
#JsonProperty("ElementOfTheList")
#Valid
#NotNull(message ="not null message")
#NotEmpty(message = "not empty message")
#CustomConstraint
private List<ElementOfTheList> list = null;
change
#SupportedValidationTarget(ValidationTarget.PARAMETERS)
to
#SupportedValidationTarget(ValidationTarget.ANNOTATED_ELEMENT)
since you want to validate and element (here is List list)
and not the parameters of a method or a constructor
A custom enum validator annotation interface:
#Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
#Retention(RUNTIME)
#Documented
#Constraint(validatedBy = PanTypeSubSetValidator.class)
public #interface PanTypeSubset {
PanType[] anyOf();
String message() default "must be any of {anyOf}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
and the actual implementation:
public class PanTypeSubSetValidator implements ConstraintValidator<PanTypeSubset, PanType> {
private PanType[] subset;
#Override
public void initialize(PanTypeSubset constraint) {
this.subset = constraint.anyOf();
}
#Override
public boolean isValid(PanType value, ConstraintValidatorContext context) {
return value == null || Arrays.asList(subset).contains(value);
}
}
and the usage inside a request DTO:
#SuperBuilder
#Data
#NoArgsConstructor
public class PanBaseRequestDto {
#NotNull(message = "'PANTYPE' cannot be empty or null")
#PanTypeSubset(anyOf = {PanType.PAN, PanType.TOKEN}, message = "yesssss")
private PanType panType;
}
The problem is that this annotation never seems to be triggered. I get another exception kick in in the #RestControllerAdvice DefaultExceptionHandler implementation before this actual validation:
Handling generic exception: (Invalid JSON input: Cannot deserialize value of type `...pantoken.PanType` from String "PAN1": not one of the values accepted for Enum class: [TOKEN, PAN]; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `...pantoken.PanType` from String "PAN1": not one of the values accepted for Enum class: [TOKEN, PAN]
Solved it by creating a custom #JsonCreator function inside the ENUM class. Not the best approach, as we loose the value that user has submitted when displaying error to the end client, but it's ok for me.
#JsonCreator
public static PanType create(String value) {
if (Objects.isNull(value)) {
return null;
}
return Arrays.stream(PanType.values())
.filter(v -> value.equals(v.getType()))
.findFirst()
.orElse(PanType.UNKNOWN);
}
I receive list of domain object ids deserialized from JSON client request body:
#JsonProperty("workgroups")
private List<WorkgroupId> workgroupIds = new ArrayList<>();
I need to validate these ids in org.springframework.validation.Validator.
for (WorkgroupId workgroupId : project.getWorkgroupIds()) {
if (!domainObjectTools.doesWorkgroupExist(workgroupId)) {
// reject this invalid value here...
}
}
Question
How to reject invalid value in org.springframework.validation.Errors?
Validator interface:
#Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = { CustomValidatorValidator.class })
#Documented
public #interface CustomValidator{
String message() default "Put here your default message";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
ValidatorImplementation:
public final class CustomValidatorValidator implements ConstraintValidator<CustomValidator, List<WorkgroupId>> {
#Override
public void initialize(CustomValidator constraintAnnotation) {
}
#Override
public boolean isValid(List<WorkgroupId> yourlist, ConstraintValidatorContext context) {
if (yourlist== null)
return true;
else
return yourlist.stream().anyMatch(s -> /* filter here as you want */);
}
}
Notice we return true if the field is null, I do it like this because I just put #NotNull constraint if I need not to be null so I have more control over the constraint.
Finally:
#JsonProperty("workgroups")
#CustomValidator
private List<WorkgroupId> workgroupIds = new ArrayList<>();
P.S: I don't understand why you initialize the list in this last code. If that's a field you're supposed to receive through the request then you don't need to initialize it, the json deserializer will initialize it with the incoming field in the json.
You can use reject() and/or rejectValue() along with the field/error code/defaultMessage or along with a custom validator to reject the values.
I want to limit the user of Spring MVC to access only certain values of the enum, I need to throw constraint exception when the requested param contains the restricted value.
Enum Example:
public enum EnumActionValues {
WAIT,
OFFLINE,
LOGGED_IN,
LOGGED_OUT,
OTHERS,
//
;
public List<EnumActionValues> getManuallyAllowedActions() {
return Arrays.asList(
WAIT,
OFFLINE,
OTHERS
);
}
}
In the above enum I want to webrequest to the Controller should contain only getManuallyAllowedActions, the LOGGED_IN and LOGGED_OUT shouldn't be allowed by user, which will be used internally.
Is there any direct annotations to be used with #Valid/#Validated.
You can have a custom annotation and a validator that goes with it.
Your annotation could look like this:
#Documented
#Constraint(validatedBy = YourConstraintValidator.class)
#Target( { ElementType.FIELD } )
#Retention(RetentionPolicy.RUNTIME)
public #interface YourConstraint
{
String message() default "Invalid enum";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
And your validator would be:
public class YourConstraintValidator implements ConstraintValidator<YourConstraint, EnumActionValues> {
#Override
public void initialize(YourConstraint constraint) {
}
#Override
public boolean isValid(EnumActionValues obj, ConstraintValidatorContext context) {
return obj == null || obj.getManuallyAllowedActions().contains(obj);
}
}
This validator allows for the enum to be null so it will still work in case the enum is null in the request.
Note that you will have to use #ModelAttribute annotation instead of #RequestParam for this to work.
I think your requirement here is very specific and you probably have to write the check yourself. Something like this should do the trick:
public ResponseEntity someEndpoint(final EnumActionValues aAction) {
if ((aAction != null) && !EnumActionValues.getManuallyAllowedActions().contains(aAction)) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
}
...
}