Get bean validation message from spring batch - spring

I'm using Spring Batch and beanValidationItemProcessor() as defined in the documentation.
#Bean
#StepScope
public BeanValidatingItemProcessor<VendorDTO> beanValidatingItemProcessor() throws Exception {
BeanValidatingItemProcessor<VendorDTO> beanValidatingItemProcessor = new BeanValidatingItemProcessor<>();
beanValidatingItemProcessor.setFilter(false);
return beanValidatingItemProcessor;
}
When a validation occurs a org.springframework.batch.item.validator.ValidationException is thrown and I'm able to see my field error like so.
Field error in object 'item' on field 'peid': rejected value []; codes
[Size.item.peid,Size.peid,Size.java.lang.String,Size]; arguments
[org.springframework.context.support.DefaultMessageSourceResolvable:
codes [item.peid,peid]; arguments []; default message [peid],12,1];
default message [size must be between 1 and 12]
How do I get a simple message object resolving the field id and default message?

I've found I've been able to gain access to the field errors by casting the ValidationException getCause() to BindException where I then have access to the field errors.
#OnSkipInProcess
public void logSkippedEmail(VendorDTO vendorDTO, Throwable t) {
JobExecution jobExecution = stepExecution.getJobExecution();
if (t instanceof ValidationException) {
ValidationException e = (ValidationException) t;
if(e.getCause() instanceof BindException) {
BindException bindException = (BindException) e.getCause();
List<FieldError> fieldErrors = bindException.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
BatchValidation batchValidation = new BatchValidation();
batchValidation.setDataField(fieldError.getField());
batchValidation.setRejectedValue(String.valueOf(fieldError.getRejectedValue()));
batchValidation.setValidationMessage(fieldError.getDefaultMessage());
batchValidation.setJobInstanceId(jobExecution.getJobId());
batchValidation.setJobName(jobExecution.getJobInstance().getJobName());
batchValidationRepository.save(batchValidation);
}
}
}
}

Related

Spring boot rest request data type validation

I am doing validation for request object in spring boot rest. I have to validate data type of request. The request has multiple boolean values and trying to validate if string in passed for boolean data type.
I have handling HttpMessageNotReadableException in my ControllerAdvice class and sending list of error message. But in my response only first field is throwing exception. If clue ,please help.
#Vishnu Dubey use this .....
#RestControllerAdvice
public class ServiceControllerAdvice {
private static final Logger log = LoggerFactory.getLogger(ServiceControllerAdvice.class);
#ExceptionHandler(value = { ConstraintViolationException.class })
#ResponseStatus(value = HttpStatus.BAD_REQUEST)
public ServiceResponse<?> constraintViolationException(final ConstraintViolationException ex) {
log.error("Validation failed", ex);
final ServiceResponse<?> response = new ServiceResponse<>(-1);
final Error error = new Error();
error.setCode("PS01");
error.setContext(ex);
error.setMessage(ex.getMessage());
response.setError(error);
return response;
}
}

How can I make sure exceptions during parsing lead to the same kind of response as the (custom) response returned for validation failures?

I'm using Spring to create an API, but I'm having some trouble introducing custom error reporting on (a part of) the validation of the request body.
When parsing/validation errors occur, I want to give a custom response back to the user.
This works well for fields annotated with #Valid along with validators like #javax.validation.constraints.NotNull by using a custom ResponseEntityExceptionHandler annotated with #ControllerAdvice.
It does not work however if an Exception is thrown while parsing the request body (before the validations even run). In that case I get an html error page with status 500 (Server Error)
How can I make sure the exceptions during parsing lead to the same kind of response as the (custom) one I return for validation failures?
My endpoint's code looks like this:
#RequestMapping(value= "/endpoint"
produces = { "application/json" },
consumes = { "application/json" },
method = RequestMethod.POST)
default ResponseEntity<Object> postSomething(#Valid #RequestBody MyRequestBody requestData){
// ...
}
MyRequestBody class looks like this:
#Validated
public class MyRequestData {
#JsonProperty("stringValue")
private String stringValue = null;
#NotNull
#Valid
public String getStringValue() {
return stringValue;
}
// ...
public enum EnumValueEnum {
VALUE_1("value 1"),
VALUE_1("value 2");
private String value;
EnumValueEnum(String value) {
this.value = value;
}
#Override
#JsonValue
public String toString() {
return String.valueOf(value);
}
#JsonCreator
public static EnumValueEnum fromValue(String text) {
if(text == null){
return null;
}
for (EnumValueEnum b : EnumValueEnum.values()){
if (String.valueOf(b.value).equals(text)) {
return b;
}
}
throw new HttpMessageNotReadableException("EnumValueEnum \"" + text + "\" does not exist");
}
}
#JsonProperty("enumValue")
private EnumValueEnum enumValue = null;
}
The custom validation error handling (and reporting) looks like this:
#Order(Ordered.HIGHEST_PRECEDENCE)
#ControllerAdvice
public class MyValidationHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
// return status(BAD_REQUEST).body(new ValidationResponse(ex.getBindingResult().getFieldErrors()));
}
#Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
// return status(BAD_REQUEST).body(new ValidationResponse((JsonMappingException) ex.getCause()));
}
}
In this code, if a user sends a request with an enum value that doesn't exist, an HttpMessageNotReadableException is thrown. I would like to catch that somewhere and replace it with a custom response that is consistent with the other exception handling I do. Where/How can I do that?
I found a solution to my own problem.
You can actually use Spring MVC's normal exception handling:
Annotating a method with #ExceptionHandler will make Spring try to use it for exception handling for the exception type specified (in the annotation's value field or the method's argument). This method can be placed in the controller or even in the ResponseEntityExceptionHandler I use for the other validation response handling.
#ExceptionHandler
public ResponseEntity handle(HttpMessageConversionException e){
// return status(BAD_REQUEST).body(new ValidationResponse((JsonMappingException) e.getCause()));
}
Mind which type of exception you handle:
The catch here was that the exception thrown while parsing is wrapped in (some subtype of) a JsonMappingException which in turn is wrapped again in a HttpMessageConversionException.
e instanceof HttpMessageConversionException
e.getCause() instanceof JsonMappingException
e.getCause().getCause() // == your original exception
The #ExceptionHandler should therefor accept HttpMessageConversionException instead of the originally thrown exception (which in my case was HttpMessageNotReadableException)
It will not work if you write an #ExceptionHandler that only accepts your original Exception!

setExpectedResponseType() method in HttpRequestExecutingMessageHandler

Below is the configuration of HttpRequestExecutingMessageHandler
#ServiceActivator(inputChannel = "rtpRequestChannel")
#Bean
public MessageHandler httResponseMessageHandler(MessageChannel rtpResponseChannel) {
HttpRequestExecutingMessageHandler handler = new HttpRequestExecutingMessageHandler(
"http://localhost:8080/rtp");
handler.setHttpMethod(HttpMethod.POST);
handler.setOutputChannel(rtpResponseChannel);
handler.setShouldTrack(true);
handler.setStatsEnabled(true);
return handler;
}
Below is the POST method in the REST controller class:
#RequestMapping(value = "/rtp", method = RequestMethod.POST)
#ResponseBody
public ResponseEntity<RTPResponse> persistRTP(#RequestBody RTPRequest request) {
System.out.println("In post method " + request);
if (request != null) {
return new ResponseEntity<RTPResponse>(new RTPResponse("12:12:2017", "Test", null, "100", "100"), HttpStatus.OK);
}
return new ResponseEntity<RTPResponse>(new RTPResponse("12:12:2017", "Dummy", null, "Dummy", "Dummy"), HttpStatus.OK);
}
Below is the config of the service activator method:
#Override
#ServiceActivator(inputChannel="rtpResponseChannel")
public void makeCall(ResponseEntity<RTPResponse> message) {
System.out.println("Message: " + message.getBody());
System.out.println(message.getClass().getCanonicalName());
}
I am receiving null in the body of the ResponseEntity object. Which configuration am I missing?
Edit 1:
When I use the setExpectedResponseType(), with the same controller configuration as above.
#ServiceActivator(inputChannel = "rtpRequestPostOperationRequestChannel")
#Bean
public MessageHandler httResponseMessageHandler(MessageChannel rtpRequestPostOperationResponseChannel) {
HttpRequestExecutingMessageHandler handler = new HttpRequestExecutingMessageHandler(
"http://localhost:8080/rtp");
handler.setHttpMethod(HttpMethod.POST);
handler.setOutputChannel(rtpRequestPostOperationResponseChannel);
handler.setExpectedResponseType(RTPResponse.class);
return handler;
}
The RTPResponse object is not wrapped in the ResponseEntity.
I get the error as below:
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1004E: Method call: Method makeCall(rtp.model.RTPResponse) cannot be found on rtp.RTPRequestServiceClient type
Edit 2:
In other words, what configuration should I use on the HttpRequestExecutingMessageHandler to get hold of the message object so that I have the extracted body in the message payload and all the headers to the MessageHeaders, including status.
I tried using GenericMessage being passed to the setExpectedResponseType method of HttpRequestExecutingMessageHandler class.
But it gave me the error as below which is understandable:
Can not construct instance of org.springframework.messaging.support.GenericMessage: no suitable constructor found, can not deserialize from Object value (missing default constructor or creator, or perhaps need to add/enable type information?)
But you said yourself - setExpectedResponseType().
You really miss exactly this configuration.
In that case the body of response entity is empty:
private class ResponseEntityResponseExtractor<T> implements ResponseExtractor<ResponseEntity<T>> {
#Nullable
private final HttpMessageConverterExtractor<T> delegate;
public ResponseEntityResponseExtractor(#Nullable Type responseType) {
if (responseType != null && Void.class != responseType) {
this.delegate = new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);
}
else {
this.delegate = null;
}
}
#Override
public ResponseEntity<T> extractData(ClientHttpResponse response) throws IOException {
if (this.delegate != null) {
T body = this.delegate.extractData(response);
return ResponseEntity.status(response.getRawStatusCode()).headers(response.getHeaders()).body(body);
}
else {
return ResponseEntity.status(response.getRawStatusCode()).headers(response.getHeaders()).build();
}
}
}
If you don't like to provide a Class<?> for that option, you can consider to use:
/**
* Specify the {#link Expression} to determine the type for the expected response
* The returned value of the expression could be an instance of {#link Class} or
* {#link String} representing a fully qualified class name.
* #param expectedResponseTypeExpression The expected response type expression.
* Also see {#link #setExpectedResponseType}
*/
public void setExpectedResponseTypeExpression(Expression expectedResponseTypeExpression) {
instead. In this case you really can resolve the target expected response type against a requestMessage and also get access to the whole BeanFactory for some other beans calls.

AssertTrue validation does not give right error message in jhi-alert-error

I have added a validation in my DTO like this:
#AssertTrue(message = "validation.amountNot0RateAbove1" )
public boolean isAmountNot0RateAbove1() {
return amount == 0 ? true : rate != null && rate > 1;
}
this gives an error like this:
Resolved exception caused by Handler execution: org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument at index 0 in method: public org.springframework.http.ResponseEntity<nl.tibi.sbys.service.dto.ProjectDTO> nl.tibi.sbys.web.rest.client.ClientProjectResource.updateProject(nl.tibi.sbys.service.dto.ProjectDTO) throws java.net.URISyntaxException, with 1 error(s): [Field error in object 'projectDTO' on field 'position[0].amountNot0RateAbove1': rejected value [false]; codes [AssertTrue.projectDTO.position[0].amountNot0RateAbove1,AssertTrue.projectDTO.position.amountNot0RateAbove1,AssertTrue.position[0].amountNot0RateAbove1,AssertTrue.position.amountNot0RateAbove1,AssertTrue.amountNot0RateAbove1,AssertTrue.boolean,AssertTrue]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [projectDTO.position[0].amountNot0RateAbove1,position[0].amountNot0RateAbove1]; arguments []; default message [position[0].amountNot0RateAbove1]]; default message [validation.amountNot0RateAbove1]]
the response is (type httpErrorResponse):
error:
fieldErrors:
Array(1)0:
field:"position[0].amountNot0RateAbove1"
message:"AssertTrue"
objectName:"projectDTO"
jhi-alert-error shows this message:
translation-not-found[error.AssertTrue]
what i would like to show is a custom error message so with amountNot0RateAbove1 key.
somehow the errors from the java code are not send to the front-end only the last key AssertTrue is send.
how should a change it?
i think it would be best if the first error from the java code would be send as message and not the last. so AssertTrue.projectDTO.position[0].amountNot0RateAbove1 instead or even better take the default message if set which is the message from the annotation:
#AssertTrue(message = "validation.amountNot0RateAbove1" )
any help?
as a not so nice workaround i tried to edit the respons setting the message in the line of this:
private onSaveError(res: HttpErrorResponse) {
console.log('res:', res);
res.error.fieldErrors[0].message = 'validation.amountNot0RateAbove1';
console.log('res:', res);
this.isSaving = false;
}
but although the code is executed it did not change the message.
this, src/main/webapp/app/shared/alert/alert-error.component.ts component makes the error message. i could do something there unless someone has a better solution :)
i think ExceptionTranslator is the place to be for improving the returned error message.
===========================edit==========================
here it can be changed. not sure what the effects will be for other errors:
#Override
public ResponseEntity<Problem> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, #Nonnull NativeWebRequest request) {
BindingResult result = ex.getBindingResult();
List<FieldErrorVM> fieldErrors = result.getFieldErrors().stream()
.map(f -> new FieldErrorVM(f.getObjectName(), f.getField(), f.getCode()))
.collect(Collectors.toList());
the f.getCode() will return the last code in the list which is the most general one. the first is the most specific.
seconde the message set in the AssertTrue annotation is found in the f.getDefaultMessage()
not sure if i should go for the first code or the default message
I still hope someone has a better solution but this is what i did. This code will keep the default way of working which is getting the most general message from the fielderror with getCode. But in rare cases you can overwrite it in this way:
ExceptionTranslator
public static final String USE_MESSAGE_AS_KEY = "USE_MESSAGE_AS_KEY:";
#Override
public ResponseEntity<Problem> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, #Nonnull NativeWebRequest request) {
BindingResult result = ex.getBindingResult();
List<FieldErrorVM> fieldErrors = new ArrayList<>();
for (FieldError fieldError : result.getFieldErrors()) {
String errorCode = null;
if (StringUtils.startsWith(fieldError.getDefaultMessage(), USE_MESSAGE_AS_KEY)) {
errorCode = StringUtils.removeStart(fieldError.getDefaultMessage(), USE_MESSAGE_AS_KEY);
}else {
errorCode = fieldError.getCode();
}
fieldErrors.add(new FieldErrorVM(fieldError.getObjectName(), fieldError.getField(), errorCode));
}
Problem problem = Problem.builder()
.withType(ErrorConstants.CONSTRAINT_VIOLATION_TYPE)
.withTitle("Method argument not valid")
.withStatus(defaultConstraintViolationStatus())
.with("message", ErrorConstants.ERR_VALIDATION)
.with("fieldErrors", fieldErrors)
.build();
return create(ex, problem, request);
}
In a DTO you can now add this:
#AssertTrue(message = USE_MESSAGE_AS_KEY + "amountNot0RateAbove1")
public boolean isRate() {
return amount == 0 ? true : rate != null && rate > 0;
}
and it will use the message amountNot0RateAbove1 (prefixed with error.) as key to translate in the frontent to a nice error message.
I had similar issue - but I just wanted to output the #AssertTrue message in the response. I used this:
#Override
public ResponseEntity<Problem> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, #Nonnull NativeWebRequest request) {
BindingResult result = ex.getBindingResult();
List<FieldErrorVM> fieldErrors = result.getFieldErrors().stream()
.map(this::createFieldErrorVM)
.collect(Collectors.toList());
Problem problem = Problem.builder()
.withType(ErrorConstants.CONSTRAINT_VIOLATION_TYPE)
.withTitle("Method argument not valid")
.withStatus(defaultConstraintViolationStatus())
.with("message", ErrorConstants.ERR_VALIDATION)
.with("fieldErrors", fieldErrors)
.build();
return create(ex, problem, request);
}
private FieldErrorVM createFieldErrorVM(FieldError fieldError) {
String errorCode;
if (fieldError.getCode().equals("AssertTrue")) {
errorCode = fieldError.getDefaultMessage();
}else {
errorCode = fieldError.getCode();
}
return new FieldErrorVM(fieldError.getObjectName(), fieldError.getField(), errorCode);
}

Empty Exception Body in Spring MVC Test

I am having trouble while trying to make MockMvc to include the exception message in the response body. I have a controller as follows:
#RequestMapping("/user/new")
public AbstractResponse create(#Valid NewUserParameters params, BindingResult bindingResult) {
if (bindingResult.hasErrors()) throw BadRequestException.of(bindingResult);
// ...
}
where BadRequestException looks sth like this:
#ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "bad request")
public class BadRequestException extends IllegalArgumentException {
public BadRequestException(String cause) { super(cause); }
public static BadRequestException of(BindingResult bindingResult) { /* ... */ }
}
And I run the following test against /user/new controller:
#Test
public void testUserNew() throws Exception {
getMockMvc().perform(post("/user/new")
.param("username", username)
.param("password", password))
.andDo(print())
.andExpect(status().isOk());
}
which prints the following output:
Resolved Exception:
Type = controller.exception.BadRequestException
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
MockHttpServletResponse:
Status = 400
Error message = bad request
Headers = {X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block], Cache-Control=[no-cache, no-store, max-age=0, must-revalidate], Pragma=[no-cache], Expires=[0], X-Frame-Options=[DENY]}
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
Does anybody have an idea on why is Body missing in the print() output?
Edit: I am not using any custom exception handlers and the code works as expected when I run the server. That is, running the application and making the same request to the server returns back
{"timestamp":1423076185822,
"status":400,
"error":"Bad Request",
"exception":"controller.exception.BadRequestException",
"message":"binding failed for field(s): password, username, username",
"path":"/user/new"}
as expected. Hence, there is a problem with the MockMvc I suppose. It somehow misses to capture the message field of the exception, whereas the default exception handler of the regular application server works as expected.
After opening a ticket for the issue, I was told that the error message in the body is taken care of by Spring Boot which configures error mappings at the Servlet container level and since Spring MVC Test runs with a mock Servlet request/response, there is no such error mapping. Further, they recommended me to create at least one #WebIntegrationTest and stick to Spring MVC Test for my controller logic.
Eventually, I decided to go with my own custom exception handler and stick to MockMvc for the rest as before.
#ControllerAdvice
public class CustomExceptionHandler {
#ExceptionHandler(Throwable.class)
public #ResponseBody
ExceptionResponse handle(HttpServletResponse response, Throwable throwable) {
HttpStatus status = Optional
.ofNullable(AnnotationUtils.getAnnotation(throwable.getClass(), ResponseStatus.class))
.map(ResponseStatus::value)
.orElse(HttpStatus.INTERNAL_SERVER_ERROR);
response.setStatus(status.value());
return new ExceptionResponse(throwable.getMessage());
}
}
#Data
public class ExceptionResponse extends AbstractResponse {
private final long timestamp = System.currentTimeMillis();
private final String message;
#JsonCreator
public ExceptionResponse(String message) {
checkNotNull(message, "message == NULL");
this.message = message;
}
}
This likely means that you either didn't handle the exception or you've really left the body empty. To handle the exception either add an error handler in the controller
#ExceptionHandler
public #ResponseBody String handle(BadRequestException e) {
return "I'm the body";
}
or user the global error handler if you're on 3.2 or above
#ControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler
public #ResponseBody String handleBadRequestException(BadRequestException ex) {
return "I'm the body";
}
}
with this the body will be populate, you should populate it with your error message
Updated solution:
If you don't want to do a full integration test but still want to make sure the message is as expected, you can still do the following:
String errorMessage = getMockMvc()
.perform(post("/user/new"))
...
.andReturn().getResolvedException().getMessage();
assertThat(errorMessage, is("This is the error message!");

Resources