I need to show custom messages in my Spring 3.0 application. I have a database with Hibernate and there are several constraints. I have doubts in how DataIntegrityViolationException should be handled in a good way. I wonder if there is a way to map the exception with a message set in a properties file, as it is possible in Constraints validation. Could I handle it automatically in any way or I have to catch this exception in each controller?
The problem with showing user-friendly messages in the case of constraint violation is that the constraint name is lost when Hibernate's ConstraintViolationException is being translated into Spring's DataIntegrityViolationException.
However, you can customize this translation logic. If you use LocalSessionFactoryBean to access Hibernate, you can supply it with a custom SQLExceptionTranslator (see LocalSessionFactoryBean.jdbcExceptionTranslator). This exception translator can translate a ConstraintViolationException into your own exception class, preserving the constraint name.
I treat DataIntegrityViolationException in ExceptionInfoHandler, finding DB constraints occurrences in root cause message and convert it into i18n message via constraintCodeMap:
#RestControllerAdvice
public class ExceptionInfoHandler {
#Autowired
private final MessageSourceAccessor messageSourceAccessor;
private static Map<String, String> CONSTRAINS_I18N_MAP = Map.of(
"users_unique_email_idx", EXCEPTION_DUPLICATE_EMAIL,
"meals_unique_user_datetime_idx", EXCEPTION_DUPLICATE_DATETIME);
#ResponseStatus(value = HttpStatus.CONFLICT) // 409
#ExceptionHandler(DataIntegrityViolationException.class)
public ErrorInfo conflict(HttpServletRequest req, DataIntegrityViolationException e) {
String rootMsg = ValidationUtil.getRootCause(e).getMessage();
if (rootMsg != null) {
String lowerCaseMsg = rootMsg.toLowerCase();
for (Map.Entry<String, String> entry : CONSTRAINS_I18N_MAP.entrySet()) {
if (lowerCaseMsg.contains(entry.getKey())) {
return logAndGetErrorInfo(req, e, VALIDATION_ERROR, messageSourceAccessor.getMessage(entry.getValue()));
}
}
}
return logAndGetErrorInfo(req, e, DATA_ERROR);
}
...
}
Can be simulated in my Java Enterprise training application by adding/editing user with duplicate mail or meal with duplicate dateTime.
UPDATE:
Other solution: use Controller Based Exception Handling:
#RestController
#RequestMapping("/ajax/admin/users")
public class AdminAjaxController {
#ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<ErrorInfo> duplicateEmailException(HttpServletRequest req, DataIntegrityViolationException e) {
return exceptionInfoHandler.getErrorInfoResponseEntity(req, e, EXCEPTION_DUPLICATE_EMAIL, HttpStatus.CONFLICT);
}
Spring 3 provides two ways of handling this - HandlerExceptionResolver in your beans.xml, or #ExceptionHandler in your controller. They both do the same thing - they turn the exception into a view to render.
Both are documented here.
1. In your request body class check for not null or not empty like this
public class CustomerRegisterRequestDto {
#NotEmpty(message = "first name is empty")
#NotNull(message = Constants.EntityValidators.FIRST_NAME_NULL)
private String first_name;
//other fields
//getters and setters
}
2. Then in your service check for this
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<CustomerRegisterRequestDto>> violations = validator.validate(userDto);
if (!violations.isEmpty()) {
//something is wrong in request parameters
List<String> details = new ArrayList<>();
for (ConstraintViolation<CustomerRegisterRequestDto> violation : violations) {
details.add(violation.getMessage());
}
ErrorResponse error = new ErrorResponse(Constants.ErrorResponse.REQUEST_PARAM_ERROR, details);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
3. Here is your ErrorResponse class
Related
I have custom ConstraintValidator which checks #RequestParam for valid values.
I also have hibernate entities with some attributes annotated with #NotNull.
Default behavior is that ConstraintViolationException is translated to HttpStatus=500.
This is desired for error raising from hibernate (that is persisting entities and so).
But I would like to raise HttpStatus=400 for ConstraintViolations raised from #RestController.
#ExceptionHandler works but it cannot distinguish between violations raised from hibernate and spring layer. I guess both layers uses hibernate validator dependency but we should be able to distinguish among those.
#ExceptionHandler(ConstraintViolationException.class)
protected ResponseEntity<Object> handleConstraintViolationException(ConstraintViolationException ex, WebRequest request) {
return getObjectResponseEntity(ex, request, HttpStatus.BAD_REQUEST);
}
I wonder if there are some other ways to achieve this. I have tried #InitBinder validator but it seems not to work with #RequestParam.
Do you have any ideas? Thanks.
One solution for this is to list all violations from the given exception and check if the root bean is names like *Controller but this is not quite clean solution and bound a developer to name controllers this way.
#ExceptionHandler(ConstraintViolationException.class)
protected ResponseEntity<Object> handleConstraintViolationException(ConstraintViolationException ex, WebRequest request) {
// TODO ugly
boolean isFromController = ex.getConstraintViolations().stream()
.map(cv -> cv.getRootBeanClass().getSimpleName())
.anyMatch(cv -> cv.contains("Controller"));
return getObjectResponseEntity(ex, request, isFromController ? HttpStatus.BAD_REQUEST : HttpStatus.INTERNAL_SERVER_ERROR);
}
The controller looks like this with the #SupportedFileTypes custom validator which checks string value against internal enum.
#GetMapping(value = "/{requirementId}/export-cisel", produces = {MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE})
public CisRadDto exportRequirementNumbers(
#PathVariable UUID requirementId,
#SupportedFileTypes #RequestParam(value = "formatSouboru", required = false) String fileFormat
) {
return pozadavekFacade.exportRequirementNumbers(requirementId, fileFormat != null ? fileFormat : "default");
}
pozadavekFacade only saves the created entity which has the folowing check:
#NotBlank
#Column(length = 10, nullable = false)
private String idUzivatele;
As your code is spread across different layers you should be able to catch ConstraintViolationException in each of them.
So in your facade you can catch it and rethrow them as RuntimeException (or whatever the exception you want). Then your controller layer will map this exception to 500 internal error.
And in your controller layer keep your logic and translate ConstraintViolationException to a 400 bad request response.
Updated : you're not forced to add try-catch boilerplate code everywhere.
Example : (don't copy paste this code. It is just to demonstrate how you can get rid of try-catch block at multiple places)
class Service {
String a() {throw new RuntimeException();}
String b() {throw new RuntimeException();}
String c() {throw new RuntimeException();}
}
class Facade {
Service service;
String a() {return tryRun(() -> service.a());}
String b() {return tryRun(() -> service.b());}
String c() {return tryRun(() -> service.c());}
String tryRun(Supplier<String> run) {
try {
return run.get();
}
catch(ConstraintViolationException e) {
throw new RuntimeException(""); // don't forget to extract useful information from ConstraintViolationEx type.
}
}
}
I'm working on a Rest API. Currently, I'm trying to handle some wrong client input. So far so good, I have it working with java validation.
There is an specific case where I may have an odd client input for dates. Here's a mvce:
public class Input {
#NotNull(message = "Usage to should have a valid date")
#JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
#JsonDeserialize(using = LocalDateTimeDeserializer.class)
#JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime someDateTime;
//getters and setters
}
public class Output {
//content not relevant for the case...
}
#RestController
public class FooController {
#PostMapping(path="/url", consumes = APPLICATION_JSON_UTF8_VALUE, produces = APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<Output> foo(#Valid #ResponseBody Input input) {
//implementation details...
}
}
When testing it, this input works ok:
{
"someDateTime": "2019-01-01 00:00:00"
}
And this doesn't (time is missing):
{
"someDateTime": "2019-01-01"
}
For the latest input sample, I get an HttpMessageNotReadableException (from Spring).
I was trying to solve this by adding a method in a global exception handler:
#ControllerAdvice
public class GlobalExceptionHandler {
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ErrorResponse> customSpringInputHandler(
HttpMessageNotReadableException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(CLIENT_INPUT_EXCEPTION, e.getMessage()));
}
}
With this, now I get a more detailed exception message. Sadly, this message is too verbose to return to the clients of this API because it depicts some of the internal design of the application.
I can see this HttpMessageNotReadableException wraps an InvalidFormatException (from Jackson) that's better for me to use. Thing is, handling specific cases of the case of HttpMessageNotReadableException in a single method seems kinda fishy. I was thinking on create a specific exception handler for InvalidFormatException and then delegate that execution from this method. Now I want to know how Spring can provide me those handlers and do the proper delegation.
I had something built in JAX-RS in the past, it was like this:
#SuppressWarnings("unchecked")
protected <T extends Throwable> Response callChainedMapper(T wrappedException, Exception baseException) {
Response response;
ExceptionMapper<T> mapper = (ExceptionMapper<T>) providers.getExceptionMapper(wrappedException.getClass());
//no mapper could be found, so treat it as a generic exception
if (mapper == null) {
ExceptionMapper<Exception> generalExceptionMapper = (ExceptionMapper<Exception>) providers
.getExceptionMapper(Exception.class);
response = generalExceptionMapper.toResponse(baseException);
} else {
response = mapper.toResponse(wrappedException);
}
return response;
}
Can I do something similar in Spring? Or maybe is there another alternative ?
Not aware Spring provides specific feature for such delegation. But you can simply
do it by yourself by the following pattern. The ResponseEntityExceptionHandler provided by Spring also use this kind of pattern.
#ControllerAdvice
public class GlobalExceptionHandler {
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler({
HttpMessageNotReadableException.class ,
InvalidFormatException.class})
public ResponseEntity<ErrorResponse> customSpringInputHandler(Exception ex) {
if(ex instanceof HttpMessageNotReadableException){
return handle((HttpMessageNotReadableException) ex);
}else if(ex instance of InvalidFormatException) {
return handle((InvalidFormatException) ex);
}else{
throw ex;
}
}
private ResponseEntity<ErrorResponse> handle(HttpMessageNotReadableException ex){
//get the wrapped InvalidFormatException and call handle(invalidFormatException)
}
private ResponseEntity<ErrorResponse> handle(InvalidFormatException ex){
}
}
What's the simplest approach to validating a complex JSON object being passed into a GET REST contoller in spring boot that I am mapping with com.fasterxml.jackson.databind.ObjectMapper?
Here is the controller:
#RestController
#RequestMapping("/products")
public class ProductsController {
#GetMapping
public ProductResponse getProducts(
#RequestParam(value = "params") String requestItem
) throws IOException {
final ProductRequest productRequest =
new ObjectMapper()
.readValue(requestItem, ProductRequest.class);
return productRetriever.getProductEarliestAvailabilities(productRequest);
}}
DTO request object I want to validate:
public class ProductRequest {
private String productId;
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.productId = productId;
}}
I was thinking of using annotations on the request DTO however when I do so, they are not triggering any type of exceptions, i.e. #NotNull. I've tried various combinations of using #Validated at the controller as well as #Valid in the #RequestParam and nothing is causing the validations to trigger.
In my point of view, Hibernate Bean Validator is probably one of the most convenient methods to validate the annotated fields of a bean anytime and anywhere. It's like setup and forget
Setup the Hibernate Bean Validator
Configure how the validation should be done
Trigger the validator on a bean anywhere
I followed the instructions in the documentation given here
Setup dependencies
I use Gradle so, I am going to add the required dependencies as shown below
// Hibernate Bean validator
compile('org.hibernate:hibernate-validator:5.2.4.Final')
Create a generic bean valdiator
I setup a bean validator interface as described in the documentation and then use this to validate everything that is annotated
public interface CustomBeanValidator {
/**
* Validate all annotated fields of a DTO object and collect all the validation and then throw them all at once.
*
* #param object
*/
public <T> void validateFields(T object);
}
Implement the above interface as follow
#Component
public class CustomBeanValidatorImpl implements CustomBeanValidator {
ValidatorFactory valdiatorFactory = null;
public CustomBeanValidatorImpl() {
valdiatorFactory = Validation.buildDefaultValidatorFactory();
}
#Override
public <T> void validateFields(T object) throws ValidationsFatalException {
Validator validator = valdiatorFactory.getValidator();
Set<ConstraintViolation<T>> failedValidations = validator.validate(object);
if (!failedValidations.isEmpty()) {
List<String> allErrors = failedValidations.stream().map(failure -> failure.getMessage())
.collect(Collectors.toList());
throw new ValidationsFatalException("Validation failure; Invalid request.", allErrors);
}
}
}
The Exception class
The ValidationsFatalException I used above is a custom exception class that extends RuntimeException. As you can see I am passing a message and a list of violations in case the DTO has more than one validation error.
public class ValidationsFatalException extends RuntimeException {
private String message;
private Throwable cause;
private List<String> details;
public ValidationsFatalException(String message, Throwable cause) {
super(message, cause);
}
public ValidationsFatalException(String message, Throwable cause, List<String> details) {
super(message, cause);
this.details = details;
}
public List<String> getDetails() {
return details;
}
}
Simulation of your scenario
In order to test whether this is working or not, I literally used your code to test and here is what I did
Create an endpoint as shown above
Autowire the CustomBeanValidator and trigger it's validateFields method passing the productRequest into it as shown below
Create a ProductRequest class as shown above
I annotated the productId with #NotNull and #Length(min=5, max=10)
I used Postman to make a GET request with a params having a value that is url-encoded json body
Assuming that the CustomBeanValidator is autowired in the controller, trigger the validation as follow after constructing the productRequest object.
beanValidator.validateFields(productRequest);
The above will throw exception if any violations based on annotations used.
How is the exception handled by exception controller?
As mentioned in the title, I use ExceptionController in order to handle the exceptions in my application.
Here is how the skeleton of my exception handler where the ValidationsFatalException maps to and then I update the message and set my desired status code based on exception type and return a custom object (i.e. the json you see below)
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler({SomeOtherException.class, ValidationsFatalException.class})
public #ResponseBody Object handleBadRequestExpection(HttpServletRequest req, Exception ex) {
if(ex instanceof CustomBadRequestException)
return new CustomResponse(400, HttpStatus.BAD_REQUEST, ex.getMessage());
else
return new DetailedCustomResponse(400, HttpStatus.BAD_REQUEST, ex.getMessage(),((ValidationsFatalException) ex).getDetails());
}
Test 1
Raw params = {"productId":"abc123"}
Url encoded parmas = %7B%22productId%22%3A%22abc123%22%7D
Final URL: http://localhost:8080/app/product?params=%7B%22productId%22%3A%22abc123%22%7D
Result: All good.
Test 2
Raw params = {"productId":"ab"}
Url encoded parmas = %7B%22productId%22%3A%22ab%22%7D
Final URL: http://localhost:8080/app/product?params=%7B%22productId%22%3A%22ab%22%7D
Result:
{
"statusCode": 400,
"status": "BAD_REQUEST",
"message": "Validation failure; Invalid request.",
"details": [
"length must be between 5 and 10"
]
}
You can expand the Validator implementation to provide a mapping of field vs message error message.
Do you mean something like this ?
#RequestMapping("/products")
public ResponseEntity getProducts(
#RequestParam(value = "params") String requestItem) throws IOException {
ProductRequest request = new ObjectMapper().
readValue(requestItem, ProductRequest.class);
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<ProductRequest>> violations
= validator.validate(request);
if (!violations.isEmpty()) {
return ResponseEntity.badRequest().build();
}
return ResponseEntity.ok().build();
}
public class ProductRequest {
#NotNull
#Size(min = 3)
private String id;
public String getId() {
return id;
}
public String setId( String id) {
return this.id = id;
}
}
I have a controller's method with a getMapping and a pathValue:
#ResponseBody
#Validated
#GetMapping(produces = [MediaType.APPLICATION_JSON_UTF8_VALUE],
value = '/{person}')
PersonResponse getPersonInfo (
#Valid #PersonValidation
#PathVariable('person') String personId,
BindingResult bindingResult
) {
// Abort if received data is not valid
if (bindingResult.hasErrors()) {
throw new BadHttpRequest()
}
What I am trying to achieve is validating the uri's personId using my custom validation #PersonValidation but it is not working, the code is not giving any errors but the program never goes into my custom validation class...
¿How can I fix this?
¿How can I validate my path variable with a custom validation?
I have solved my problem. Spring have some problems to validate path variables...
First, you need to add a configuration class with a MethodValidationPostProcessor like this one:
/**
* this class is required to use our custom path variable validation on
* the controller class
*/
#Configuration
class ValidatorConfiguration {
#Bean
MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor()
}
}
Now, on the controller class, it is important to add #Validated annotation
#RestController
#Validated
class YourController class {
}
now your custom validation on the path variable is gonna work, if you want to catch the exception, add this method inside a exception handler class:
#ControllerAdvice
class GlobalExceptionHandler {
#ExceptionHandler(value = [ ConstraintViolationException.class ])
#ResponseStatus(value = HttpStatus.BAD_REQUEST)
String handle(ConstraintViolationException e) {
Set<ConstraintViolation<?>> violations = e.getConstraintViolations()
StringBuilder strBuilder = new StringBuilder()
for (ConstraintViolation<?> violation : violations ) {
strBuilder.append(violation.getMessage() + "\n")
}
return strBuilder.toString()
}
}
Bean Validation is a good option to validate objects, but how to customize the response of a REST API (using RESTeasy) when a ConstraintViolationException is thrown?
For example:
#POST
#Path("company")
#Consumes("application/json")
public void saveCompany(#Valid Company company) {
...
}
A request with invalid data will return a HTTP 400 status code with the following body:
[PARAMETER]
[saveCompany.arg0.name]
[{company.name.size}]
[a]
It's nice but not enough, I would like to normalize these kind of errors in a JSON document.
How can I customize this behavior?
With JAX-RS can define an ExceptionMapper to handle ConstraintViolationExceptions.
From the ConstraintViolationException, you can get a set of ConstraintViolation, that exposes the constraint violation context, then map the details you need to an abitrary class and return in the response:
#Provider
public class ConstraintViolationExceptionMapper
implements ExceptionMapper<ConstraintViolationException> {
#Override
public Response toResponse(ConstraintViolationException exception) {
List<ValidationError> errors = exception.getConstraintViolations().stream()
.map(this::toValidationError)
.collect(Collectors.toList());
return Response.status(Response.Status.BAD_REQUEST).entity(errors)
.type(MediaType.APPLICATION_JSON).build();
}
private ValidationError toValidationError(ConstraintViolation constraintViolation) {
ValidationError error = new ValidationError();
error.setPath(constraintViolation.getPropertyPath().toString());
error.setMessage(constraintViolation.getMessage());
return error;
}
}
public class ValidationError {
private String path;
private String message;
// Getters and setters
}
If you use Jackson for JSON parsing, you may want to have a look at this answer, showing how to get the value of the actual JSON property.