How to pass information from ConstraintValidator to Controller? - spring

I have a custom ConstraintValidator.
public class UrlValidator implements ConstraintValidator<ValidUrl, RemoteFile> {
#Override
public boolean isValid(RemoteFile value, ConstraintValidatorContext context) {
return true;
}
}
The RemoteFile is a very simple DTO.
public record RemoteFile(String url) {}
I'm using UrlValidator with my custom annotation.
#Documented
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = UrlValidator.class)
public #interface ValidUrl {
String message() default "invalid file";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Finally I'm using the annotation inside my controller.
#PostMapping("/hello")
public String remote(#ValidUrl #RequestBody RemoteFile body) {
// do stuff inside the controller
}
I'd like to pass some information from my validator to my controller. I found a way to do this but I was wondering why the other way does not work.
Here is what works.
#Override
public boolean isValid(RemoteFile value, ConstraintValidatorContext context) {
RequestContextHolder.getRequestAttributes()
.setAttribute("key", "value", RequestAttributes.SCOPE_REQUEST);
return true;
}
I can afterwards access the information in my controller.
#PostMapping("/hello")
public String remote(#ValidUrl #RequestBody RemoteFile body) {
var value =
(String)
RequestContextHolder.getRequestAttributes()
.getAttribute("key", RequestAttributes.SCOPE_REQUEST);
// do stuff inside the controller
}
Then I tried something different because I thought there is a simpler way. However this DOES not work and I do not understand why. Use the HttpServletRequest and set an attribute.
#Autowired HttpServletRequest request;
#Override
public boolean isValid(RemoteFile value, ConstraintValidatorContext context) {
request.setAttribute("key", "value");
return true;
}
Use the attribute directly in the controller method.
#PostMapping("/remote")
public String remote(
#ValidUrl #RequestBody RemoteFile body,
#RequestAttribute String key
) {
// do stuff in the controller
}
However I'm always getting the error message
Resolved [org.springframework.web.bind.ServletRequestBindingException: Missing request attribute 'key' of type String]
I do not understand the error message. The attribute should be there because the validator should have set it. Any ideas? I'm lost!

Related

In SpringBoot, how do I create a custom validator for a MultipartFile parameter?

I'm using Spring Boot 2.4. I have the following controller with a method that accepts a MultipartFile object.
#RestController
public class MyController extends AbstractController
...
#Override
public ResponseEntity<ResponseData> add(
...
#Parameter(description = "file detail") #Validated #RequestPart("myFile")
MultipartFile myFile,
...
) {
I would like to validate that this MultipartFile contains the data that I want (e.g. is of a particular mime type). So I have written the below validator ...
#Documented
#Constraint(validatedBy = MultipartFileValidator.class)
#Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
#Retention(RetentionPolicy.RUNTIME)
public #interface MultipartFileConstraint {
String message() default "Incorrect file type.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
and its implementation class ...
public class MultipartFileValidator
implements ConstraintValidator<MultipartFileConstraint, MultipartFile> {
#Override
public void initialize(final MultipartFileConstraint constraintAnnotation) {
log.info("\n\n\n\nconstructor called\n\n\n\n");
}
#Override
public boolean isValid(
MultipartFile file, ConstraintValidatorContext constraintValidatorContext) {
log.info("Validating file");
...
}
}
However, when I invoke my endpoint, I don't see that my validator is called (for one, the log statement is never printed nor breakpoints hit). What else do I need to do to register my validator for this MultipartFile param?
As per the Spring Documentation:
Can also be used with method level validation, indicating that a
specific class is supposed to be validated at the method level (acting
as a pointcut for the corresponding validation interceptor), but also
optionally specifying the validation groups for method-level
validation in the annotated class. Applying this annotation at the
method level allows for overriding the validation groups for a
specific method but does not serve as a pointcut; a class-level
annotation is nevertheless necessary to trigger method validation for
a specific bean to begin with. Can also be used as a meta-annotation
on a custom stereotype annotation or a custom group-specific validated
annotation.
So, here we have to keep in mind what are the placement of #Validated and validator annotation.
Code:
Controller class : #Validated added at class level and #ValidFile (Custom validator annotation) in the method
#RestController
#Validated
#Slf4j
public class MyController {
#RequestMapping("/add")
public ResponseEntity<ResponseData> add(#ValidFile #RequestParam("file") MultipartFile file) {
log.info("File Validated");
return ResponseEntity.status(HttpStatus.OK).body(new ResponseData("Valid file received"));
}
}
Validator Annotation
#Documented
#Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = {FileValidator.class})
public #interface ValidFile {
Class<? extends Payload> [] payload() default{};
Class<?>[] groups() default {};
String message() default "Only pdf,xml,jpeg,jpg files are allowed";
}
Validator class
#Slf4j
public class FileValidator implements ConstraintValidator<ValidFile, MultipartFile> {
#Override
public void initialize(ValidFile validFile) {
log.info("File validator initialized!!");
}
#Override
public boolean isValid(MultipartFile multipartFile,
ConstraintValidatorContext constraintValidatorContext) {
log.info("Validating file");
String contentType = multipartFile.getContentType();
assert contentType != null;
return isSupportedContentType(contentType);
}
private boolean isSupportedContentType(String contentType) {
return contentType.equals("application/pdf")
|| contentType.equals("text/xml")
|| contentType.equals("image/jpg")
|| contentType.equals("image/jpeg");
}
}
Output :
Success:
{
"message": "Valid file received"
}
Exception handler
#ExceptionHandler(ConstraintViolationException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) {
return new ResponseEntity<>("Validation error: " + e.getMessage(), HttpStatus.BAD_REQUEST);
}
Failure:
Validation error: Only pdf,xml,jpeg,jpg files are allowed
Below is a small example. I hope it will help.
#Component
public class MultipartFileValidator implements Validator {
#Override
public boolean supports(Class < ? > clazz) {
return MultipartFile.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors) {
MultipartFile multipartFile = (MultipartFile) target;
if (multipartFile.isEmpty()) {
// Add an error message to the errors list
errors.rejectValue("file", "required.file");
}
}
}

no validator found for custom annotation and validation for enum springboot java8

Getting this error HV000030: No validator could be found for constraint EnumChecker validating type MyEnum Check configuration for ‘myenum’.
Using the annotation on this class to be validated, I can't do private final String myenum because I need to use it as an enum in order to use the function inside the enum
public class Request {
#EnumChecker(enumclass = MyEnum.class)
private final MyEnum myenum;
}
annotation
#Documented
#Constraint(validatedBy = MyEnumValidator.class)
#Target({FIELD, PARAMETER, TYPE, TYPE_USE, TYPE_PARAMETER, METHOD, CONSTRUCTOR, ANNOTATION_TYPE})
#Retention(RUNTIME)
public #interface EnumChecker {
String message() default "must be one of these values";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Class<? extends Enum> enumClass();
}
validator
public class MyEnumValidator implements ConstraintValidator<EnumChecker, String> {
private List<String> validEnumList;
#Override
public void initialize(EnumChecker constraintAnnotation) {
validEnumList = Arrays.stream(constraintAnnotation.enumClass().getEnumConstants())
.map(Enum::name)
.collect(Collectors.toList());
}
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return validEnumList.contains(value);
}
}
enum
public enum MyEnum {
YES, NO;
private static final Map<MyEnum, String> MyEnumConverter =
Map.of(YES, "yes");
public String toMyString() {
return MyEnumConverter.get(this);
}
}
First delete final modifier in myenum and create setter and getter for that field, because Jackson is not able to bind values from http request to your Request class for example.
public class Request {
#EnumChecker(enumClass = MyEnum.class)
private MyEnum myenum;
public MyEnum getMyenum() {
return myenum;
}
public void setMyenum(MyEnum myenum) {
this.myenum = myenum;
}
}
Then change ConstraintValidator target type to match MyEnum instead of String
public class MyEnumValidator implements ConstraintValidator<EnumChecker, MyEnum> {
private List<MyEnum> validEnumList;
#Override
public void initialize(EnumChecker constraintAnnotation) {
validEnumList = Arrays.stream(constraintAnnotation.enumClass().getEnumConstants())
.collect(Collectors.toList());
}
#Override
public boolean isValid(MyEnum value, ConstraintValidatorContext context) {
return validEnumList.contains(value);
}
}
Test: I did the following test
#RestController
public class MainController {
#PostMapping("/annot")
public String filter(#RequestBody #Valid Request req) {
return req.getMyenum().name();
}
}
Also I changed the implementation of MyEnumValidator to only see validation effect and prove that it takes place. (I admit that currently this validator does not make sense. Also using only #RequestBody guarantees that myenum can only Have YES or NO values otherwise throws exception)
public class MyEnumValidator implements ConstraintValidator<EnumChecker, MyEnum> {
private List<MyEnum> validEnumList;
#Override
public void initialize(EnumChecker constraintAnnotation) {
validEnumList = Arrays.stream(constraintAnnotation.enumClass().getEnumConstants())
.collect(Collectors.toList());
}
#Override
public boolean isValid(MyEnum value, ConstraintValidatorContext context) {
return validEnumList.contains(value) && value.name().length() == 3;
}
}
For a invalid case:
stacktrace:
Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String api.sweater.controller.MainController.filter(api.sweater.request.Request): [Field error in object 'request' on field 'myenum': rejected value [NO]; codes [EnumChecker.request.myenum,EnumChecker.myenum,EnumChecker.api.sweater.request.MyEnum,EnumChecker]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [request.myenum,myenum]; arguments []; default message [myenum],class api.sweater.request.MyEnum]; default message [must be one of these values]] ]

How can I validate size of Pageable?

I'm using org.springframework.data.domain.Pageable with my #RestController.
How can I validate or limit the page size?
Without any validation, when clients call with size of 10000. The actual pageSize is 2000.
This could lead wrong signal for last page, I think.
How can I validate it and notify clients about it? Say with 400?
You can write a custom annotation to validate Pageable object
#Constraint(validatedBy = PageableValidator.class)
#Target( { ElementType.METHOD, ElementType.PARAMETER })
#Retention(RetentionPolicy.RUNTIME)
public #interface PageableConstraint {
String message() default "Invalid pagination";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int maxPerPage() default 100;;
}
and Its implementation
public class PageableValidator implements
ConstraintValidator<PageableConstraint, Pageable> {
private int maxPerPage;
#Override
public void initialize(PageableConstraint constraintAnnotation) {
maxPerPage=constraintAnnotation.maxPerPage();
}
#Override
public boolean isValid(Pageable value, ConstraintValidatorContext context) {
return value.getPageSize()<=maxPerPage;
}
}
and you can use it over your controller like any other javax validation annotations.
#RestController
#RequestMapping("/api")
#Validated
public class EmployeeRestController {
#Autowired
private EmployeeService employeeService;
#GetMapping("/employees")
public List<Employee> getAllEmployees(#PageableConstraint(message = "Invalid page size",maxPerPage = 400) Pageable pageable) {
return employeeService.getAllEmployees();
}
}
Try using Custom Annotations to validate the Pageable object. If it is not working, try the argument resolver as shown below.
You can create Argument Resolver in your Spring Configuration that is extending WebMvcConfigurerAdapter
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver() {
#Override
public Pageable resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
Pageable p = super.resolveArgument(methodParameter, mavContainer, webRequest, binderFactory);
if (webRequest.getParameter("per_page") != null) {
int pageSize = Integer.parseInt(webRequest.getParameter("per_page"));
if (pageSize < 1 || pageSize > 2000) {
message = "Invalid page size";
}
}
if (message != null) {
Set<CustomConstraintViolation> constraintViolations = new HashSet<>();
constraintViolations.add(new CustomConstraintViolationImpl(message));
throw new ConstraintViolationException(constraintViolations);
}
return new PageRequest(p.getPageNumber(), p.getPageSize());
}
};
resolver.setMaxPageSize(2000);
argumentResolvers.add(resolver);
super.addArgumentResolvers(argumentResolvers);
}
This resolver will make sure the max page size is 2000 for every request to your controller.
You need to throw a ConstraintViolationException when the size is more than 2000. For that you need to create a custom ConstraintViolation interface and implement it
public CustomConstraintViolation implements ConstraintViolation<CustomConstraintViolation> {}
public CustomConstraintViolationImpl implements CustomConstraintViolation {
...
}
You can simply use configuration
spring.data.web.pageable.max-page-size=400
This will not lead to any error if one tries to get 401 records. Only 400 will be returned.
More on stuff that can be configured via spring properties could be found here:
https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html

calling request.setattribute in Spring validator interface

How to access request.setattribute inside spring custom validator class. i need to set these values in jsp side I am trying something like below
#Component
public class ProductSearchValidator implements Validator {
#Override
public boolean supports(Class<?> clazz) {
return Product.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors) {
Product product = (Product) target;
String name = product.getName();
String cod="Validated";
request.setAttribute("isVal",cod);
}
}
You can access request this way
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
if (attrs instanceof ServletRequestAttributes) {
HttpServletRequest request = ((ServletRequestAttributes)attrs).getRequest();
}

How to make json response Unified format in spring?

I want all json response is
{
"status":"ok"
"data":"..."
}
I only care #ResponseBody function return value;Don't need wrap any object to do it;
example:
#ResponseBody
public String test(){
return "Hello,World"
}
I want get
{
"status":"ok"
"data":"Hello,World"
}
ResponseBodyAdvicecan do it.you can implement the interface to handle ReqeustBody. Here is the api.
In the documents.
Allows customizing the response after the execution of an #ResponseBody or a ResponseEntity controller method but before the body is written with an HttpMessageConverter.
Implementations may be registered directly with RequestMappingHandlerAdapter and ExceptionHandlerExceptionResolver or more likely annotated with #ControllerAdvice in which case they will be auto-detected by both.
here is a simple code.
#ControllerAdvice
public customerResponseBody implements ResponseBodyAdvice{
#Override
public boolean supports (MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType){
return true;
}
#Override
public Object beforeBodyWrite (Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response){
body = new ResponseTemplate<Object>("001",body);
return body;
}
}
You must return a Object instead of String for example :
public class CustomResponse {
private String status;
private String data;
// Getters & Setters
}
#ResponseBody
public CustomResponse test(){
CustomResponse response = new CustomResponse();
response.setStatus("OK");
response.setData("Hello,World");
return response;
}
I thought it is not a good solution but you can format string
#RequestMapping(value="testData")
public #ResponseBody String testData(){
String sdata="ok";
String value ="Hello,World";
return "{\"status\" :\""+sdata+"\",\"data \":\""+value+"\"}";
}
I found this solve problem.
rewrite default RequestResponseBodyMethodProcessor can wrap any object to return value

Resources