I am trying to throw exception while uploading file if the file format is not csv type. Here is my implementation (some parts are omitted for clarity):
ValidFile:
#Documented
#Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
#Retention(RUNTIME)
#Constraint(validatedBy = {FileValidator.class})
public #interface ValidFile {
//...
}
FileValidator:
public class FileValidator implements ConstraintValidator<ValidFile, MultipartFile> {
#Override
public boolean isValid(MultipartFile multipartFile,
ConstraintValidatorContext context) {
// ...
}
}
ValidationErrorResponse:
public class ValidationErrorResponse {
// have "fieldName" and "response" data
}
ErrorHandlingControllerAdvice:
#ControllerAdvice
class ErrorHandlingControllerAdvice {
//I tried to use something like that >>>
#ExceptionHandler(NotValidFormatException.class)
#ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
#ResponseBody
ValidationErrorResponse onNotValidFormatException(NotValidFormatException e) {
//...
}
}
And, as there is not a file field in db, I validated uploaded file parameter as shown below (I want to validate on Controller):
Controller:
public ResponseEntity<ResponseMessage> uploadFile(
#Valid #ValidFile #RequestParam("file") MultipartFile file)
throws Exception {
employeeService.create(file);
return ResponseEntity.ok(new ResponseMessage("File uploaded.));
}
I also tried to use #Validated, #RequestPart("file") as parameter, but does not make any sense.
When I try to upload an invalid type (pdf) and trace the exception, I see "Failed to parse csv file: (line 322) invalid char between encapsulated token and delimiter" error. So, how can I catch the exception in my onNotValidFormatException method?
Related
From below log we can see rejected value is display user data(example: User PII data with some special characters)
[Field error in object 'Customer' on field 'FirstName': rejected value [robert% steve];
So we tried to use #ControllerAdvice, MethodArgumentNotValidException and customize default error msg to show defined error msg.
But somehow this approach is not working for us with feature testcases. So do there any configuration not to display rejected value? or to show rejected value with masking?
thanks.
I believe I have found the right solution here. I was not sure that "rejected value" throws MethodArgumentNotValidException. Once I verified that, other things fall into the right place.
Cause:
Spring already has a base class ResponseEntityExceptionHandler which handles MethodArgumentNotValidException specifically using handleMethodArgumentNotValid() method. So your method in #ControllerAdvice class is never called.
Solution:
Override ResponseEntityExceptionHandler.handleMethodArgumentNotValid() method & add your own custom logic there.
Sample code:
#RestControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
Map<String, String> error = Map.of("message", "Field value not valid.");
return handleExceptionInternal(ex, error, headers, HttpStatus.BAD_REQUEST, request);
}
}
You can set your own custom validation annotation, set your own validation rules and rejection message:
#Documented
#Constraint(validatedBy = MyValidator.class)
#Target( { ElementType.METHOD, ElementType.FIELD }) // set the desired context
#Retention(RetentionPolicy.RUNTIME)
public #interface MyValidation {
String message() default "Validation error! Not going to display rejected value.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
You can also set multiple validators in the #Constraint, and they will run consecutively.
The validator:
public class MyValidator implements
ConstraintValidator<MyValidation, String> { // here you set the validated field type, e.g. String
#Override
public void initialize(MyValidation value) {
}
#Override
public boolean isValid(String value,
ConstraintValidatorContext cxt) {
... // some validation logic
return true/false;
}
}
Then simply add your annotation to the validated field. You can even change the message at that point:
#MyValidation(message = "Error!")
String validField;
For more deep-dive info, you can check https://www.baeldung.com/spring-mvc-custom-validator
If you'd like to see validation exceptions logged, but without the field values, you can add your own HandlerExceptionResolver that'll log your MethodArgumentNotValidException the way you need. Here's an example:
#EnableWebMvc
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
int beforeDefaultExceptionResolver = resolvers.size() - 1;
resolvers.add(beforeDefaultExceptionResolver, new DefaultHandlerExceptionResolver() {
#Override
protected String buildLogMessage(Exception ex, HttpServletRequest request) {
if (ex instanceof MethodArgumentNotValidException validationEx) {
// example, modified from org.springframework.web.bind.MethodArgumentNotValidException#getMessage
StringBuilder sb = new StringBuilder("Validation failed for argument [")
.append(validationEx.getParameter().getParameterIndex()).append("] in ")
.append(validationEx.getParameter().getExecutable().toGenericString());
BindingResult bindingResult = validationEx.getBindingResult();
if (bindingResult.getErrorCount() > 1) {
sb.append(" with ").append(bindingResult.getErrorCount()).append(" errors");
}
sb.append(": ");
for (ObjectError error : bindingResult.getAllErrors()) {
sb.append('[');
if (error instanceof FieldError fieldError) {
sb.append("Field error in object '" + fieldError.getObjectName() + "' on field '" + fieldError.getField() + "'");
}
else {
sb.append(error);
}
sb.append("] ");
}
return sb.toString();
}
return super.buildLogMessage(ex, request);
}
});
}
}
If you don't want to see validation exceptions logged at all, just set the following loggers' level to ERROR in your application.yml:
logging:
level:
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver: error
org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver: error
You can create an #InitBinder for that, then validate your field before hand over :
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.addValidators(new FirstNameValidate());
}
public class FirstNameValidate implements Validator {
#Override
public boolean supports(Class<?> arg0) {
// code implementation ...
}
#Override
public void validate(Object target, Errors errors) {
// code implementation ...
}
}
I have a Controller where I have the path veriable resourceId as type UUID like shown below.
#GetMapping(value = "{resourceId}")
public ResponseEntity<MyClass> findOneByResourceId(#PathVariable("resourceId") UUID resourceId) {
return new ResponseEntity<>(myService.findOneByResourceId(resourceId), HttpStatus.OK);
}
Everything works fine exception when Jackson is trying to deserialize an invalid UUID if such as "9e3b414a" an exception is thrown. Failed to convert value of type 'java.lang.String' to required type 'java.util.UUID'; nested exception is java.lang.IllegalArgumentException: Invalid UUID string: 9e3b414a
I have already a custom UuidDeserializer class (shown below) which I am using in other areas of my code successfully. I'd like to use this deserializer as well on the pathVariable.
public class UuidDeserializer extends JsonDeserializer<UUID> {
#Override
public UUID deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
var stringToValidate = jsonParser.getValueAsString().trim();
if (MyUtils.isStringInvalidUUID(stringToValidate)) {
throw new MyCustomException("Invalid UUID value")
}
return UUID.fromString(stringToValidate);
}
I cannot seem to get Spring to use this custom deserializer though on a path variable. I tried putting the #JsonDeserialize on the path variable but it doesn't work. HELP!
#GetMapping(value = "{resourceId}")
public ResponseEntity<MyClass> findOneByResourceId(#PathVariable("resourceId")
#JsonDeserialize(using = UuidDeserializer.class) UUID resourceId) {
return new ResponseEntity<>(myService.findOneByResourceId(resourceId), HttpStatus.OK);
}
You can create a custom validation annotation to check UUID, for example #UUIDValidation so first create our custom annotation.
#Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })
#Retention(RUNTIME)
#Constraint(validatedBy = UUIDValidator.class)
#Documented
public #interface UUIDValidation {
String message() default "Mandatory fields missing";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Also its custom validator, let's call UUIDValidator that should implement ConstraintValidator and override the isValid method.
public class UUIDValidator implements ConstraintValidator<UUIDValidation, UUID> {
#Override
public boolean isValid(UUID value, ConstraintValidatorContext context) {
if (MyUtils.isStringInvalidUUID(value)) {
throw new MyCustomException("Invalid UUID value");
}
try {
UUID.fromString(value);
return Boolean.TRUE;
} catch (IllegalArgumentException ex) {
return Boolean.FALSE;
}
}
}
And we can call our custom validation annotation on the #PathVariable
#GetMapping(value = "{resourceId}")
public ResponseEntity<MyClass> findOneByResourceId(#PathVariable("resourceId")
#UUIDValidation UUID resourceId) {
return new ResponseEntity<>(myService.findOneByResourceId(resourceId), HttpStatus.OK);
}
I'm working on this springboot application where I need to do some validations on values passed from http call and I'm using class level validation as explained here.
I'm using somethink like this:
#ValidRequest
public class EventRequest {
String date;
}
Response create(#Valid EventRequest request) {
..
}
Response update(Long entityId, #Valid EventRequest request) {
...
}
public class ValidRequestValidator
implements ConstraintValidator<ValidRequest, EventRequest> {
In the class ValidRequestValidator, where I implement the ConstraintValidator interface, I need to check if there is another Event entity in the database that meet some conditions on field date. When I want to create a new entity is simple, I perform a query, but when I need to update I need to exclude the entity I'm currently trying to update.
Is there a way to pass entityId parameter to #ValidRequest custom validator?
I know a way is to add the field entityId to the class EventRequest, but I would like to maintain this separation because entityId is coming from a query parameter.
Thank for your help!
Additional to the field-specific(Single Parameter Constraint) you can implement constraint for the whole method(Cross-Parameter Constraint). This will provide ability to pass all parameters of certain method to validator.
Annotation definition:
Annotation used two validators and can be applied to the Method or Type.
#Constraint(validatedBy = {ValidRequestMethodValidator.class, ValidRequestTypeValidator.class})
#Target({ ElementType.METHOD, ElementType.TYPE })
#Retention(RetentionPolicy.RUNTIME)
public #interface ValidRequest {
String message() default "Request is invalid!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
ConstraintTarget validationAppliesTo() default ConstraintTarget.IMPLICIT;
}
Constraint Validator which will handle single parameter:
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class ValidRequestTypeValidator implements ConstraintValidator<ValidRequest, EventRequest> {
#Override
public boolean isValid(EventRequest request, ConstraintValidatorContext constraintValidatorContext) {
// logic here
return false;
}
}
Constraint Validator which will handle all parameters of specific method:
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.constraintvalidation.SupportedValidationTarget;
import javax.validation.constraintvalidation.ValidationTarget;
#SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class ValidRequestMethodValidator implements ConstraintValidator<ValidRequest, Object[]> {
#Override
public boolean isValid(Object[] objects, ConstraintValidatorContext constraintValidatorContext) {
Long entityId = null;
EventRequest eventRequest = null;
if (objects[0] instanceof Long) {
entityId = (Long) objects[0];
}
if (objects[0] instanceof EventRequest) {
eventRequest = (EventRequest) objects[0];
}
if (objects[1] instanceof EventRequest) {
eventRequest = (EventRequest) objects[1];
}
//logic here
return false;
}
}
Please note, we have to annotate the beans, which shall be validated, with #org.springframework.validation.annotation.Validated annotation to get method validators to work automatically.
Example of usage:
Mixed usage, #ValidRequest annotation defined on method and single parameter level.
#ValidRequest
public class EventRequest {
public String value;
}
#RestController
#Validated
public class Controller {
Response create(#Valid EventRequest request) {
return new Response();
}
#ValidRequest(validationAppliesTo = ConstraintTarget.PARAMETERS)
Response update(Long entityId, EventRequest request) {
return new Response();
}
}
For create method ValidRequestTypeValidator will be executed.
For update method ValidRequestMethodValidator will be executed.
2. Define annotation only for methods
#RestController
#Validated
public class Controller {
#ValidRequest(validationAppliesTo = ConstraintTarget.PARAMETERS)
Response create(EventRequest request) {
return new Response();
}
#ValidRequest(validationAppliesTo = ConstraintTarget.PARAMETERS)
Response update(Long entityId, EventRequest request) {
return new Response();
}
}
For create method ValidRequestMethodValidator will be executed with one element objects array
For update method ValidRequestMethodValidator will be executed with two elements objects array
3. Define annotation for a single parameter and method at the same time
#ValidRequest
public class EventRequest {
public String value;
}
#RestController
#Validated
public class Controller {
#ValidRequest(validationAppliesTo = ConstraintTarget.PARAMETERS)
Response update(Long entityId, #Valid EventRequest request) {
return new Response();
}
}
First will be executed single parameter validator ValidRequestTypeValidator.
If it will passed validation then second method validator ValidRequestMethodValidator will be executed.
Probably only one method-level validation will be sufficient to handle your issue. I described all variants, just for information maybe will be useful.
I have the following files:
ImageForm.java:
public class ImageForm {
#FileNotEmpty
private MultipartFile file;
// other code ...
}
GalleryController.java:
#Controller
#RequestMapping("/admin/galleries")
public class GalleryController {
#PostMapping("/{id}/image/create")
public ModelAndView createImage(#PathVariable("id") long galleryId, #Valid ImageForm imageForm, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
// other code ...
System.out.println(imageForm.getFile().getContentType()); // Prints: null
// other code ...
}
}
GalleryControllerIT.java:
#SqlGroup({
#Sql("classpath:test-schema.sql"),
#Sql("classpath:test-gallery-data.sql"),
#Sql("classpath:test-image-data.sql")
})
public class GalleryControllerIT extends SetupControllerIT {
#Test
public void createImage_POSTHttpMethod_ImageIsCreated() throws Exception {
Path path = Paths.get(getClass().getClassLoader().getResource("test-image.png").toURI());
byte[] image = Files.readAllBytes(path);
mvc.perform(
multipart("/admin/galleries/1/image/create")
.file("file", image) // TODO: ImageForm.file.contentType is null.
.with(csrf())
).andExpect(status().isFound());
assertThat(imageRepository.count(), is(5L));
}
}
In test GallerControllerIT#createImage_POSTHttpMethod_ImageIsCreated
I set a file.
The test sends the file to GalleryController#createImage and maps it
to ImageForm#file attribute.
ImageForm#file is of type MultipartFile which has method getContentType.
The method MultipartFile#getContentType returns null.
The question is, why does the MultipartFile#getContentType return null? It works correctly when outside of test.
For full data you should call
.file(new MockMultipartFile("image", "some_name", MediaType.MULTIPART_FORM_DATA_VALUE, image))
Because under hood in your case if you call .file("file", image) they call short version of constructor MockMultipartFile without content type
MockMvc trying to not create or declare some additional values or parameters if you are not declare their.
I have simple controller method which receives file:
#ResponseBody
public MyDto createProduct(MyDto dto, #RequestParam(value = "file") MultipartFile file) {
}
The problem is that Spring doesn't throw exception if user didn't chose file in form. But I need to be sure that user chose some file. I tried to add required = true but it didn't help (moreover is by default set to true)
Actually Spring throws exception only if my form doesn't contain parameter named file at all:
Required MultipartFile parameter 'file' is not present
But if parameter present and file is not chose in HTML form then there is no exception.
How to solve this problem?
Thanks
You can push your MultipartFile file as a property of your DTO and write a custom validation annotation e.g. #FilePresent. Your signature would than be something like
public MyDto createProduct(#Validated MyDto dto, BindingResult result)
your would annotate your file property inside MyDto
#FilePresent
private MultipartFile file;
Your custom validation code would be something like:
The #FilePresent annotation
#Documented
#Retention(RUNTIME)
#Constraint(validatedBy = {FilePresentMultipartFileValidator.class})
#Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
public #interface FilePresent {
String message() default "{your.package.FilePresent.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String value() default "true";
}
Custom Validator
public class FilePresentMultipartFileValidator implements ConstraintValidator<FilePresent, MultipartFile> {
#Override
public void initialize(FilePresent constraintAnnotation) {
//NOOP
}
#Override
public boolean isValid(MultipartFile value, ConstraintValidatorContext context) {
return !(value == null || value.isEmpty());
}
}
the final move is to have a ValidationMessages.properties file on the classpath (and/or its localized equivalents) having the key your.package.FilePresent.message with the message value you choose