Springboot #Valid annotation with String object - spring

So, I have the following controller method:
#RequestMapping(path = "/{application}/users", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public MyObject getUsers(#RequestParam("itemId") String itemId, #PathVariable("application") String application) {
return userService.get(itemId, application);
}
I would like to check if the request parameter itemId exists in the related application (in the path).
My first idea was to create a validator :
#RequestMapping(path = "/{application}/users", method = RequestMethod.GET, produces =
MediaType.APPLICATION_JSON_VALUE)
#CheckItemId
public MyObject getUsers(#RequestParam("itemId") String itemId, #PathVariable("application") String application) {
return userService.get(itemId, application);
}
CheckItemId.java :
#Target({METHOD})
#Retention(RUNTIME)
#Constraint(validatedBy = CheckItemIdValidator.class)
#Documented
public #interface CheckItemId {
String message() default "error";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
CheckItemIdValidator.java :
#SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class CheckItemIdValidator implements ConstraintValidator<CheckItemId, Object[]>{
#Override
public boolean isValid(Object[] arg0, ConstraintValidatorContext arg1) {
String itemId= (String) arg0[0];
String application = (String) arg0[1];
// Logic business ...
return true;
}
}
This implementation works well, I managed to get the values itemId and application in the validator. I can now do my verification.
I was wondering if there is a better way to do something like that? Since I handle an array of Object, I need to cast it to String and If I change the parameters order, I will not get the same values since I need to use arg0[0] and arg0[1].
Thank you !

You can use spring validation library. Add #Valid on controller level. Then add #NotBlank on method level as below.
getUsers(#RequestParam("itemId") #NotBlank String itemId)

Related

Springboot show error message for invalid date (YearMonth) formats: eg 2020-15

I have a project with Spring Boot and I want to show an error response if the given date format is incorrect.
The correct format is yyyy-MM (java.time.YearMonth) but I want to want to show a message if someone sends 2020-13, 2020-111 or 2020-1.
When I've added a custom validator the debugger goes in there with a valid request but not with an incorrect request. I also tried to use the message.properties with the typeMismatch.project.startdate=Please enter a valid date. but I also don't see that message in my response body.
It seems like the application does not understand my incorrect request and then always throws a BAD REQUEST with empty body, which is not strange because it is not a valid date.
Can someone explain me how I can show an errormessage in the response for these incorrect values?
Or is there no other way then use a String and convert that to the YearMonth object so I can show catch and show an error message?
Request object:
#Getter
#Setter
public class Project {
#NotNull(message = "mandatory")
#DateTimeFormat(pattern = "yyyy-MM")
private YearMonth startdate;
}
Controller:
#RestController
#RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public class ProjectController {
#PostMapping(value = "/project", consumes = MediaType.APPLICATION_JSON_VALUE)
public Project newProject(#Valid #RequestBody Project newProject) {
return projectService.newProject(newProject);
}
}
ExceptionHandler:
#RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
#SneakyThrows
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
headers.add("Content-Type", "application/json");
ObjectMapper mapper = new ObjectMapper();
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String name;
if (error instanceof FieldError)
name = ((FieldError) error).getField();
else
name = error.getObjectName();
String errorMessage = error.getDefaultMessage();
errors.put(name, errorMessage);
});
return new ResponseEntity<>(mapper.writeValueAsString(errors), headers, status);
}
}
Okay, I made a solution which is workable for me.
I've added the solution below for people who find this thread in the future and has the same problem I had.
Create a custom validator with a simple regex pattern:
#Target({ FIELD })
#Retention(RUNTIME)
#Constraint(validatedBy = YearMonthValidator.class)
#Documented
public #interface YearMonthPattern {
String message() default "{YearMonth.invalid}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
public class YearMonthValidator implements ConstraintValidator<YearMonthPattern, String> {
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
Pattern pattern = Pattern.compile("^([0-9]{4})-([0-9]{2})$");
Matcher matcher = pattern.matcher(value);
try {
return matcher.matches();
} catch (Exception e) {
return false;
}
}
}
Update the request object:
#Getter
#Setter
public class Project {
#NotNull(message = "mandatory")
#YearMonthPattern
private String startdate;
public YearMonth toYearMonth(){
return YearMonth.parse(startdate);
}
}
The DateTimeFormat annotation is replaced with our new custom validator and instead of a YearMonth, make it a String. Now the validator annotation can be executed because the mapping to the YearMonth won't fail anymore.
We also add a new method to convert the String startdate to a YearMonth after Spring has validated the request body, so we can use it in the service as a YearMonth instead of having to translate it each time.
Now when we send a requestbody with:
{
"startdate": "2020-1"
}
we get a nice 400 bad request with the following response:
{
"endDate": "{YearMonth.invalid}"
}

Jackson deserialization errorhandling in spring-framework

I'm looking for a clean way to handle Jackson Deserialization errors for REST web requests.
More precisely: I have an Enum in a incoming DTO object, mapped from JSON. But if the user sends a wrong value, a 400 Bad Request is returned. I would like to return a 422 Unprocessable Entity with a correct message.
One option would be to accept a String, and use bean validation. However, it's not possible to pass all enum values as a list to the annotation (not a constant), so I would need to pass all enum values separately and keep them up to date. This will be very error prone over the whole application. I'm looking for a more structural way to handle this.
I solved this by using a String in the DTO and using a public #interface EnumValueas annotation.
The EnumValue:
#ReportAsSingleViolation
#Documented
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = EnumValueValidator.class)
#Target(ElementType.FIELD)
public #interface EnumValue {
Class<? extends Enum> value();
String message() default "The input contains validation errors.";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
The validator:
public class EnumValueValidator implements ConstraintValidator<EnumValue, String> {
private Class<? extends Enum> enumClass;
private String message;
#Override
public void initialize(final EnumValue constraintAnnotation) {
this.enumClass = constraintAnnotation.value();
this.message = constraintAnnotation.message();
}
#Override
public boolean isValid(final String value, final ConstraintValidatorContext context) {
boolean valid = false;
for (final Enum enumValue : enumClass.getEnumConstants()) {
if (enumValue.name().equals(value)) {
valid = true;
}
}
if (!valid) {
context.buildConstraintViolationWithTemplate(message) //
.addConstraintViolation();
}
return valid;
}
}

How to get validate a #PathVariable before authorization in Spring MVC

I have a REST handler with an endpoint for the GET verb. Where from an identifier (ObjectID of MongoDB) I get the information of that entity.
To validate that the ObjectID is valid and avoid errors when using Spring Data Mongo. I have developed a simple validator following the guidelines of the JPA bean validation standard.
#Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
#Retention(RUNTIME)
#Constraint(validatedBy = ValidObjectIdValidator.class)
#NotNull
#Documented
public #interface ValidObjectId {
String message() default "{constraints.valid.objectid}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class ValidObjectIdValidator implements ConstraintValidator<ValidObjectId, String> {
#Override
public void initialize(ValidObjectId constraintAnnotation) {}
#Override
public boolean isValid(String id, ConstraintValidatorContext context) {
return ObjectId.isValid(id);
}
}
Then I apply the variable-level validation of the controller using the following configuration:
#Api
#RestController("RestUserController")
#Validated
#RequestMapping("/api/v1/children/")
public class ChildrenController implements ISonHAL, ICommentHAL, ISocialMediaHAL
Using #Validated annotation at controller level.
#GetMapping(path = "/{id}")
#ApiOperation(value = "GET_SON_BY_ID", nickname = "GET_SON_BY_ID", notes = "Get Son By Id",
response = SonDTO.class)
#PreAuthorize("#authorizationService.hasParentRole() && #authorizationService.isYourSon(#id)")
public ResponseEntity<APIResponse<SonDTO>> getSonById(
#Valid #ValidObjectId(message = "{son.id.notvalid}")
#ApiParam(value = "id", required = true) #PathVariable String id) throws Throwable {
logger.debug("Get User with id: " + id);
return Optional.ofNullable(sonService.getSonById(id))
.map(sonResource -> addLinksToSon(sonResource))
.map(sonResource -> ApiHelper.<SonDTO>createAndSendResponse(ChildrenResponseCode.SINGLE_USER, HttpStatus.OK, sonResource))
.orElseThrow(() -> { throw new SonNotFoundException(); });
}
Using the #Valid annotation on the #PathVariable.
The problem is that I must verify that the user is currently authenticated is the parent of the child for whom he wants to see his information. This is verified by the execution of:
#PreAuthorize("#authorizationService.hasParentRole() && #authorizationService.isYourSon(#id)")
And here the error occurs. Because I must convert the received id to an ObjectID mediate new ObjectId (id). And this may not be valid.
Is there any way to configure validation to occur before authorization?.
This is my configuration to enable security at the method level:
#EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
Thanks in advance.

Hibernate validation with custom messages in property file

Hi am using hibernate validator in jersey rest service.
Here how can we pass value to the property file message as follows
empty.check= Please enter {0}
here in {0} i need to pass the value from annotation
#EmptyCheck(message = "{empty.check}") private String userName
here in the {0} i need to pass "user name", similarly i need to reuse message
please help me out to solve this.
You can do this by altering your annotation to provide a field description and then exposing this in the validator.
First, add a description field to your annotation:
#Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = EmptyCheckValidator.class)
#Documented
public #interface EmptyCheck {
String description() default "";
String message() default "{empty.check}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Next, change your message so that it uses a named parameter; this is more readable.
empty.check= Please enter ${description}
Since you're using hibernate-validator, you can get the hibernate validator context within your validation class and add a context variable.
public class EmptyCheckValidator
implements ConstraintValidator<EmptyCheck, String> {
String description;
public final void initialize(final EmptyCheck annotation) {
this.description = annotation.description();
}
public final boolean isValid(final String value,
final ConstraintValidatorContext context) {
if(null != value && !value.isEmpty) {
return true;
}
HibernateConstraintValidatorContext ctx =
context.unwrap(HibernateConstraintValidatorContext.class);
ctx.addExpressionVariable("description", this.description);
return false;
}
}
Finally, add the description to the field:
#EmptyCheck(description = "a user name") private String userName
This should produce the following error when userName is null or empty:
Please enter a user name

Custom JSR 303 validation is not invoked

My custom JSR 303 validation is not getting invoked. Here is my code
my spring config has
<mvc:annotation-driven />
My controller's handler method:
#RequestMapping(value="update", method = RequestMethod.POST ,
consumes="application/json" ,
produces="application/json"))
#ResponseBody
public String update(#Valid #RequestBody MyBean myBean){
return process(myBean);
}
MyBean (annotated with ValidMyBeanRequest):
#ValidMyBeanRequest
public class MyBean {
private int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
ValidMyBeanRequest annotaion:
#Target({ TYPE })
#Retention(RUNTIME)
#Documented
#Constraint(validatedBy = {MyBeanValidator.class})
public #interface ValidMyBeanRequest {
String message() default "{validMyBeanRequest.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
MyBeanValidator class:
public class MyBeanValidator implements
ConstraintValidator<ValidMyBeanRequest, MyBean> {
#Override
public void initialize(ValidMyBeanRequest constraintAnnotation) {
// TODO Auto-generated method stub
}
#Override
public boolean isValid(MyBean myBean, ConstraintValidatorContext context) {
boolean isValid = true;
int id = myBean.getId();
if(id == 0){
isValid = false;
}
return isValid;
}
}
My http POST request has below JSON data:
{id:100}
The problem is MyBeanValidator's isValid is not getting invoked. I am using Spring 3.1.0 and HibernateValidator is in classpath.
Please see what I am missing??
Update: Updated handler method to include POST request type and consumes, produces values. Also included my http request with JSON data.
Assuming that you do get model correctly, in this case you are doing everything right, except one thing: you need to handle your validation's result manually.
For achieving this you need to add BindingResult object into list of your handler parameters, and then process validation constraints in the way you would like:
#RequestMapping(value="update")
#ResponseBody
public String update(#Valid #ModelAttribute #RequestBody MyBean myBean, BindingResult result) {
if (result.hasErrors()){
return processErrors(myBean);
}
return process(myBean);
}

Resources