Using Spring #Validated Annotation to Validate a Get Request Path Variable - java

I'm trying to use the Spring Validator and #Validated annotation to validate a Get Request parameter but cannot get the validator to run. I'm using a ModelAttribute to try and get the validator to run on the Path Variable instead of the Request Body. Is it possible to run a validator on a Get Request Path Variable?
Here is my controller class and method
#RestController
public class ProfileController {
#RequestMapping(value = "/profile/{param}", method = RequestMethod.GET)
public IVRProfile getProfile(#Validated(ParamValidator.class) #ModelAttribute("param") String param) {
return sampleProfile();
}
#ModelAttribute("param")
public String paramAsModelAttribute(#PathVariable String param) {
return param;
}
}
And the Validator class
#Component
public class ParamValidator implements Validator
{
#Override
public boolean supports(Class<?> clazz)
{
System.out.println("Validator supports test");
return String.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors)
{
System.out.println("Validator Test");
// Validation code
}
}
Neither prints statements are executed when hitting the endpoint.
Any help on what I could be missing or do differently would be greatly appreciated, thanks!

You can implement desired validation functionality as following.
public class ParamValidator implements ConstraintValidator<ParamConstraint, String> {
#Override
public void initialize(ParamConstraint paramConstraint) {
}
#Override
public boolean isValid(String paramField, ConstraintValidatorContext cxt) {
//Perform paramField validation
return true;
}
}
-
#Documented
#Constraint(validatedBy = ParamValidator.class)
#Target( { ElementType.PARAMETER })
#Retention(RetentionPolicy.RUNTIME)
public #interface ParamConstraint {
String message() default "Default validation message";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
-
#RequestMapping(value = "/profile/{param}", method = RequestMethod.GET)
public IVRProfile getProfile(#Valid #ParamConstraint #ModelAttribute("param") String param) {
return sampleProfile();
}
And finally don't forget to annotate Controller with #Validated.
#RestController
#Validated
public class ProfileController {
//...
}
More details you can find in the example as mentioned here.

You can create the answer you want by using the fields in the ConstraintViolationException with the following method;
#ExceptionHandler(ConstraintViolationException.class)
protected ResponseEntity<Object> handlePathVariableError(final ConstraintViolationException exception) {
log.error(exception.getMessage(), exception);
final List<SisSubError> subErrors = new ArrayList<>();
exception.getConstraintViolations().forEach(constraintViolation -> subErrors.add(generateSubError(constraintViolation)));
final SisError error = generateErrorWithSubErrors(VALIDATION_ERROR, HttpStatus.BAD_REQUEST, subErrors);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
You need to added an #Validated annotation to Controller class and any validation annotation before path variable field

If you want to get single RequestParams like status, you can force it by following the code below.
#RestController
public class ProfileController {
#RequestMapping(value = "/profile/{param}", method = RequestMethod.GET)
public IVRProfile getProfile(#RequestParam(name = "status", required = true) String status, #ModelAttribute("param") String param) {}
}
if you want to force PathVariable, then do this.
#RestController
public class ProfileController {
#RequestMapping(value = "/profile/{param}", method = RequestMethod.GET)
public IVRProfile getProfile(#PathVariable(name = "param", required = true) String param, #ModelAttribute("param") String param) {}
}
Hope this work!!!

Related

ConstraintValidator on Rest layer not working

I'm working on a base repo in which I want to add a custom validation example in the rest layer, but it is not working, is not printing the flags at the moment of the validation, it just go right through the controller layer instead of the validator class.
What am I missing?
This is the rest layer:
#RequestMapping("equipment/")
public interface DeviceREST extends RestConstants {
#GetMapping("test")
#Produces(MediaType.APPLICATION_JSON)
BaseResult<UserDTO> test(
/* Example of a validation layer before moving forward device impl */
#QueryParam("genre") #Valid #AllowedGenderExampleValidations final String genre
) throws Exception;
}
AllowedGenderExampleValidations class
#Documented
#Retention(RUNTIME)
#Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
#Constraint(validatedBy = ExampleValidationsValidator.class)
public #interface AllowedGenderExampleValidations {
String message() default "Invalid value for genre query param";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
class ExampleValidationsValidator implements ConstraintValidator<AllowedGenderExampleValidations, String> {
private List<String> allowedGenres = new ArrayList<>();
#Override
public void initialize(AllowedGenderExampleValidations constraint) {
System.out.println("##########################################################################");
List<GenderExampleEnum> allowedValues = new ArrayList<>(EnumSet.allOf(GenderExampleEnum.class));
for(GenderExampleEnum g : allowedValues) {
this.allowedGenres.add(g.name().toLowerCase());
}
}
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
System.out.println("##########################################################################");
return value != null && this.allowedGenres.contains(value) && this.allowedGenres.contains(value.toLowerCase());
}
}
GenderExampleEnum class
public enum GenderExampleEnum {
M("Man"),
W("Woman"),
O("Other");
private final String genre;
public String getGenre() { return genre; };
GenderExampleEnum(String genre) { this.genre = genre; }
public static GenderExampleEnum fromValue(String code) throws IllegalArgumentException {
for(var g : GenderExampleEnum.values()) {
if(code.toLowerCase().equalsIgnoreCase(g.name())) {
return g;
}
}
return GenderExampleEnum.O;
}
}
Controller class
#Controller
public class DeviceImpl implements DeviceREST {
private static final Logger logger = LoggerFactory.getLogger(DeviceImpl.class);
#Autowired private DeviceService deviceService;
#Autowired private DataTransferUtil dataTransferUtil;
#Override
public BaseResult<UserDTO> test(String genre) throws Exception {
var serviceResponse = deviceService.testFirstMethod();
var mappingResponse = dataTransferUtil.mapFirstTestMethod(serviceResponse);
return new BaseResult<UserDTO>(mappingResponse);
}
}
Test response, missing validations for query param
URL: localhost:8080/equipment/test?genre=o
I can't derive for sure from the context which Spring Boot version you are using, but for Spring boot 2.3.0 or later, a common mistake is relying on the spring-boot-starter-validation that is not included by default anymore.
You should make sure to add the following dependency if you want to use validation in a Spring Boot 2.3.0 or up project:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
See spring docs here.
Could you please let me know if this solved your issue by replying or accepting this answer?
I should have added this annotation in the Rest Layer -> "#Validated" to make it work, without that dependency, validators won't be triggered

How to get rid of propertyPath in title attribute Error Response of ConstraintValidatorContext - SpringBoot Custom Validation

I implemented custom validation annotation for a request in Spring Rest Controller which validates if field2 is available in the input request body, then field1 should not be empty. My question is how do I get rid of Field UserInputDTO.userInputDTO in my error response (before passing it to the ExceptionHandler)?
Note: I need this because I don't want to expose sensitive data(Class name and Instance name) in the error response. I debugged this and tried but failed to remove the default path or base path(from PathImpl) from the object ConstraintValidatorContextImpl.
Error Response
{
status : 400
title : Bad Request: Field 'UserInputDTO.userInputDTO' : field1 should not be empty if Field2 is present
}
Annotation Class
#Documented
#Constraint(validatedBy = UserInputValidator.class)
#Target( { ElementType.METHOD, ElementType.FIELD })
#Retention(RetentionPolicy.RUNTIME)
public #interface UserInputConstraint {
String message() default "Invalid request";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Validator Class
public class UserInputValidator implements
ConstraintValidator<UserInputConstraint, UserInputDTO> {
#Override
public void initialize(UserInputConstraint userInputConstraint) {
}
#Override
public boolean isValid(UserInputDTO userInputDTO,
ConstraintValidatorContext cxt) {
boolean isValid=true;
String errorMessage = null;
if (userInputDTO.field2 != null && userInputDTO.field1 == null) {
isValid = false;
context.buildConstraintViolationWithTemplate("field1 should not be empty if Field2 is presen").addConstraintViolation();
}
return isValid;
}
You can #Override handleMethodArgumentNotValid and customize your error response body.
Example:
#RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
#Override
public ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException exception, HttpHeaders headers, HttpStatus status,
WebRequest request) {
List<Object> errors = new ArrayList<Object>();
for (FieldError fielderror : exception.getBindingResult().getFieldErrors()) {
Map<String, Object> error = new HashMap<>();
error.put("code", fielderror.getCode());
error.put("message", fielderror.getDefaultMessage());
errors.add(error);
}
return new ResponseEntity<>(errors, apiError.getStatus());
}
}
And disable DefaultConstraintViolation in annotation validator
context.disableDefaultConstraintViolation();

Spring validator custom HTTP status

I'd like to return a custom HTTP status 422 instead of a default 400 on a spring validation.
My validator:
#Component
#RequiredArgsConstructor
public class EmailUpdateDtoValidator implements Validator {
private Errors errors;
private EmailUpdateDto emailUpdateDto;
#Override
public boolean supports(Class<?> clazz) {
return EmailUpdateDto.class.equals(clazz);
}
#Override
public void validate(Object object, Errors errors) {
this.errors = errors;
this.emailUpdateDto = (EmailUpdateDto) object;
validateEmail();
}
private void validateEmail() {
if (!Email.isValid(emailUpdateDto.getEmail())) {
errors.rejectValue("email", UserValidationErrorCodes.EMAIL_NOT_VALID.name());
}
}
}
How I setup the validation in the Controller:
#Slf4j
#RestController
#RequiredArgsConstructor
public class UserController {
private final EmailUpdateDtoValidator emailUpdateDtoValidator;
#InitBinder("emailUpdateDto")
protected void initEmailValidationBinder(final WebDataBinder binder) {
binder.addValidators(emailUpdateDtoValidator);
}
#RequestMapping(value = "/users/{hashedId}/email", method = RequestMethod.PUT)
public void updateEmail(#RequestBody #Valid EmailUpdateDto emailUpdateDto) {
...
}
}
Using this setup I always get a 400. How could I customize the HTTP status on the return?
Thanks
The validation process would throw an org.springframework.web.bind.MethodArgumentNotValidException, therefore you can add an exception handler to your controller:
import org.springframework.web.bind.MethodArgumentNotValidException;
#ExceptionHandler
public ResponseEntity<String> handleException(MethodArgumentNotValidException ex) {
return new ResponseEntity<String>(HttpStatus.UNPROCESSABLE_ENTITY);
}
As workaround you can define a ExceptionHandler and override the default behavior.
#ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity<Object> customHttpStatus() {
return ResponseEntity.status(422).build();
}
}

AOP for annotation is not working in Spring Boot

I have myannotation and whenever my method(which has myannotation) is executed then AOP should be called but which is not working in my spring boot controller.But which is working for methods which has other annotations.Please help me to understand about what happens.
Update: MyAnnotation
#Retention(RUNTIME)
#Target({ METHOD, CONSTRUCTOR })
public #interface MyAnnotation {
}
#Aspect
#Component
public class AnnotationAspect {
private static final String POINTCUT_METHOD1 = "#annotation(com.somepackage.MyAnnotation)";
#Around(POINTCUT_METHOD1)
public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
try {
System.out.println("Beforeee " + joinPoint);
joinPoint.proceed();
} finally {
System.out.println("Afterrr " + joinPoint);
}
return null;
}
}
Scenario 1:(Working)
#Controller
#RequestMapping("user")
public class ArticleController {
#GetMapping("article/{id}")
#MyAnnotation // here it is
public ResponseEntity<String> getArticleById(#PathVariable("id") Integer id)
{
return new ResponseEntity<String>(dummyMethod(), HttpStatus.OK);
}
public String dummyMethod() {
System.out.println("Dummy method with MyAnnotation");
return "HelloWorld!";
}
}
Log:(Working)
Beforeee execution(ResponseEntity com.mypackage.getArticleById(Integer))
Dummy method with MyAnnotation
Afterrr execution(ResponseEntity com.mypackage.getArticleById(Integer))
Scenario 2:(Not Working)
#Controller
#RequestMapping("user")
public class ArticleController {
#GetMapping("article/{id}")
public ResponseEntity<String> getArticleById(#PathVariable("id") Integer id)
{
return new ResponseEntity<String>(dummyMethod(), HttpStatus.OK);
}
#MyAnnotation //here it is
public String dummyMethod() {
System.out.println("Dummy method with MyAnnotation");
return "HelloWorld!";
}
}
Log:(Not Working)
Dummy method with MyAnnotation
Scenario 3: (Not Working)
#Service
public class ArticleService {
#MyAnnotation //here it is
public String dummyMethod() {
System.out.println("Dummy method with MyAnnotation");
return "HelloWorld!";
}
}
It might not work because you call dummyMethod() from the same class. Try moving dummyMethod() to another service class. The reason is that calls within the same class does not go though the Spring proxy. The call to getArticleById() is proxied and will be handled by AOP but dummyMethod() might as well be a private method.

Spring Boot JPA - paging and sorting

I am trying to implement pagination to my Spring Data JPA repository in Spring Boot but I am stuck with the following exception when running uni tests:
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.data.domain.Pageable]: Specified class is an interface
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:982)
...
Could someone point out to me what am I missing here? This is my repository:
#Repository
public interface VenueRepository extends PagingAndSortingRepository<Venue, Long> {
public Page<Venue> findAll(Pageable pageable);
}
and controller:
#RestController
#RequestMapping("/venues")
public class VenueController {
#Autowired
private VenueRepository venueRepo;
#RequestMapping(method = RequestMethod.GET)
public ResponseEntity<Page<Venue>> getVenues(Pageable pageable) {
return new ResponseEntity<>(venueRepo.findAll(pageable), HttpStatus.OK);
}
}
and finally my test:
#Test
public void responseOkVenuesTest() throws Exception {
mvc.perform(get("/venues").accept(MediaType.APPLICATION_JSON_VALUE)).andExpect(status().isOk());
}
I spent couple of hours trying to make this work and am running out of ideas. Thank you for any tips!
Change your method getVenues in the way that you can pass the parameters to instantiate a PageRequest instead of passing Pageable :
#RequestMapping(method = RequestMethod.GET)
public ResponseEntity<List<Venue>> getVenues(int from,int to) {
return new ResponseEntity<>(
venueRepo.findAll((new PageRequest(from, to)), HttpStatus.OK).getContent();
}
In addition to #SEY_91's answer you might also like to use the following solution inspired with How to remove redundant Spring MVC method by providing POST-only #Valid? and used in my Spring Boot-driven application for long time.
In short, here is an annotation to annotate controller method parameters:
#Target(PARAMETER)
#Retention(RUNTIME)
public #interface PlainModelAttribute {
}
Now, just a method processor that would scan for parameters annotated with #PlainModelAttribute:
public final class PlainModelAttributeMethodProcessor
extends ModelAttributeMethodProcessor {
private final Map<TypeToken<?>, Converter<? super NativeWebRequest, ?>> index;
private PlainModelAttributeMethodProcessor(final Map<TypeToken<?>, Converter<? super NativeWebRequest, ?>> index) {
super(true);
this.index = index;
}
public static HandlerMethodArgumentResolver plainModelAttributeMethodProcessor(final Map<TypeToken<?>, Converter<? super NativeWebRequest, ?>> index) {
return new PlainModelAttributeMethodProcessor(index);
}
#Override
public boolean supportsParameter(final MethodParameter parameter) {
return parameter.hasParameterAnnotation(PlainModelAttribute.class) || super.supportsParameter(parameter);
}
#Override
protected Object createAttribute(final String attributeName, final MethodParameter parameter, final WebDataBinderFactory binderFactory,
final NativeWebRequest request) {
final TypeToken<?> typeToken = TypeToken.of(parameter.getGenericParameterType());
final Converter<? super NativeWebRequest, ?> converter = index.get(typeToken);
if ( converter == null ) {
throw new IllegalArgumentException("Cannot find a converter for " + typeToken.getType());
}
return converter.convert(request);
}
#Override
protected void bindRequestParameters(final WebDataBinder binder, final NativeWebRequest request) {
final HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
if ( !isSafe(resolve(servletRequest.getMethod())) ) {
((ServletRequestDataBinder) binder).bind(servletRequest);
}
}
private static HttpMethod resolve(final String name) {
return HttpMethod.valueOf(name.toUpperCase());
}
private static boolean isSafe(final HttpMethod method)
throws UnsupportedOperationException {
switch ( method ) {
case GET:
case HEAD:
case OPTIONS:
return true;
case POST:
case PUT:
case PATCH:
case DELETE:
return false;
case TRACE:
throw new UnsupportedOperationException();
default:
throw new AssertionError(method);
}
}
}
I don't really remember, but a resolve() method equivalent should be present in Spring Framework somewhere. Note that I use Google Guava TypeToken in order to let the processor be compatible with generic types (since I use models like IQuery<Foo> and IQuery<Bar> in controllers). Now just register the processor:
#Configuration
#EnableWebMvc
public class MvcConfiguration
extends WebMvcConfigurerAdapter {
#Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(createModelAttributeMethodProcessor());
}
private static HandlerMethodArgumentResolver createModelAttributeMethodProcessor() {
return plainModelAttributeMethodProcessor(ImmutableMap.of(pageableTypeToken, MvcConfiguration::toPageable));
}
private static final TypeToken<Pageable> pageableTypeToken = new TypeToken<Pageable>() {
};
private static Pageable toPageable(final WebRequest request) {
return new PageRequest(
ofNullable(request.getParameter("page")).map(Integer::parseInt).orElse(0),
ofNullable(request.getParameter("size")).map(Integer::parseInt).orElse(1)
);
}
}
Here is a web request to a Pageable DTO conversion, and the converter must be registered as an argument resolver. So now it's ready to use:
#RestController
#RequestMapping("/")
public class Controller {
#RequestMapping(method = GET)
public String get(#PlainModelAttribute final Pageable pageable) {
return toStringHelper(pageable)
.add("offset", pageable.getOffset())
.add("pageNumber", pageable.getPageNumber())
.add("pageSize", pageable.getPageSize())
.add("sort", pageable.getSort())
.toString();
}
}
A few examples:
/ ⇒ PageRequest{offset=0, pageNumber=0, pageSize=1, sort=null}
/?page=43 ⇒ PageRequest{offset=43, pageNumber=43, pageSize=1, sort=null}
/?size=32 ⇒ PageRequest{offset=0, pageNumber=0, pageSize=32, sort=null}
/?page=22&size=32 ⇒ PageRequest{offset=704, pageNumber=22, pageSize=32, sort=null}

Categories