Validating #RequestParam using #Annotations - spring

I'm trying to validate #RequestParam of my parameter. Here's my Controller:
#RequestMapping(value = "/addCategory", method = RequestMethod.POST)
public ModelAndView addCategory(#ValidCategoryName #RequestParam(value = "categoryName") String categoryName) {
ModelAndView modelAndView = new ModelAndView();
boolean addedSuccessfully = sideViewControllerDelegate.addMenuCategory(categoryName);
modelAndView.setViewName("home_partial/side_view/category_completed");
if (addedSuccessfully) {
modelAndView.addObject("responseMessage", "ADDED");
}
return modelAndView;
}
And The #ValidCaegoryName is defined like this:
#Target({METHOD, FIELD, ANNOTATION_TYPE, PARAMETER})
#Retention(RUNTIME)
#Documented
#NotNull
#Constraint(validatedBy = ValidCategoryImpl.class)
public #interface ValidCategoryName {
String message() default "This Category Does not Seem to Allowed";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
int min() default 30;
}
And my Impl class is this:
public class ValidCategoryImpl implements ConstraintValidator<ValidCategoryName, String> {
#Autowired
MenuCategoriesService menuCategoriesService;
private int min;
#Override
public void initialize(ValidCategoryName constraintAnnotation){
min = constraintAnnotation.min();
}
#Override
public boolean isValid(String categoryName, ConstraintValidatorContext context){
return categoryName.length() < min && menuCategoriesService.containsCategoryName(categoryName);
}
}
Am i Missing anything?
Thanks!

Related

Spring MVC #ExceptionHandler does not catch MethodArgumentNotValidException

I wanted to create a registration form where the passed data is observed whether it is valid or not.
Here is my code:
#Controller
public class RegistrationController {
#Autowired
private UserService userService;
#GetMapping("/register")
public String registration(Model model) {
model.addAttribute("user", new RegistrationRequest());
return "register";
}
#PostMapping("/register")
public String register(#Valid #ModelAttribute("user") RegistrationRequest user,
HttpServletResponse response) {
String token = userService.registerUser(user);
response.addHeader("Set-Cookie", token + "; HttpOnly; Secure; SameSite=Lax;");
return "index";
}
#ExceptionHandler(MethodArgumentNotValidException.class)
public String handleIncorrectRequests(MethodArgumentNotValidException ex, Model model) {
String errorFields = ex.getBindingResult()
.getAllErrors().stream()
.map(err -> ((FieldError) err)
.getField())
.distinct()
.collect(Collectors.joining(", "));
String message = String.format("The next fields are incorrectly filled: %s!",
errorFields);
model.addAttribute("error", "Registration failed");
model.addAttribute("cause", message);
return "error";
}
#ExceptionHandler(EmailAlreadyInUseException.class)
public String handleEmailAlreadyInUse(Model model) {
model.addAttribute("error", "Registration failed");
model.addAttribute("cause", "The given email is already being used! Please try with a different one!");
return "error";
}
}
Here is the RegistrationRequest's implementation:
#Data
#ValidateConfirmPassword
public class RegistrationRequest {
#NotBlank
#Size(min = 4 ,max = 100)
private String username;
#NotBlank
private String email;
#NotBlank
#Size(min = 6)
private String password;
#NotBlank
#Size(min = 6)
private String confirmPassword;
}
The additional validator classes:
#Documented
#Constraint(validatedBy = ConfirmPasswordValidator.class)
#Target( { ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface ValidateConfirmPassword {
String message() default "Confirm password not matches the password!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class ConfirmPasswordValidator implements ConstraintValidator<ValidateConfirmPassword, RegistrationRequest>{
#Override
public boolean isValid(RegistrationRequest value, ConstraintValidatorContext context) {
return value.getPassword().equals(value.getConfirmPassword());
}
}
When the data is valid the system does its job, but when any constraint violation happen I get the following log and bad request screen:
[36m.w.s.m.s.DefaultHandlerExceptionResolver[0;39m [2m:[0;39m Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: ... (list of incorrectly filled fields)
I have no clue why the ExceptionHandler does not catch it, while the other one for EmailAlreadyInUseException works properly. Any help would be a blessing.

Converter works for RequestParameter but not for RequestBody field

I have the following converter:
#Component
public class CountryEnumConverter implements Converter<String, CountryEnum> {
#Override
public CountryEnum convert(String country) {
CountryEnum countryEnum = CountryEnum.getBySign(country);
if (countryEnum == null) {
throw new IllegalArgumentException(country + " - Country is not supported!");
}
return countryEnum;
}
}
Registered it is invoked when used for RequestParam
#GetMapping(value = RestApiEndpoints.RESULTS, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ResultDto> getResults(
Principal principal,
#RequestParam CountryEnum country) {
....
}
But this converter is never invoked when used for field in the RequstBody:
#GetMapping(value = RestApiEndpoints.RESULTS, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ResultDto> getResults(
Principal principal,
#RequestBody MyBody myBody) {
....
}
public class MyBody {
#NotNull
private CountryEnum country;
public MyBody() {
}
public CountryEnum getCountry() {
return country;
}
public void setCountry(CountryEnum country) {
this.country = country;
}
}
Your existing org.springframework.core.convert.converter.Converter instance will only work with data submitted as form encoded data. With #RequestBody you are sending JSON data which will be deserialized using using the Jackson library.
You can then create an instance of com.fasterxml.jackson.databind.util.StdConverter<IN, OUT>
public class StringToCountryTypeConverter extends StdConverter<String, CountryType> {
#Override
public CountryType convert(String value) {
//convert and return
}
}
and then apply this on the target property:
public class MyBody {
#NotNull
#JsonDeserialize(converter = StringToCountryTypeConverter.class)
private CountryEnum country;
}
Given the similarity of the 2 interfaces I would expect that you could create one class to handle both scenarios:
public class StringToCountryTypeConverter extends StdConverter<String, CountryType>
implements org.springframework.core.convert.converter.Converter<String, CountryType> {
#Override
public CountryType convert(String value) {
//convert and return
}
}
I found out that if I add the following code to my CountryEnum will do the trick.
#JsonCreator
public static CountryEnum fromString(String value) {
CountryEnumConverter converter = new CountryEnumConverter();
return converter.convert(value);
}

Spring Boot : Custom Validation in Request Params

I want to validate one of the request parameters in my controller . The request parameter should be from one of the list of given values , if not , an error should be thrown . In the below code , I want the request param orderBy to be from the list of values present in #ValuesAllowed.
#RestController
#RequestMapping("/api/opportunity")
#Api(value = "Opportunity APIs")
#ValuesAllowed(propName = "orderBy", values = { "OpportunityCount", "OpportunityPublishedCount", "ApplicationCount",
"ApplicationsApprovedCount" })
public class OpportunityController {
#GetMapping("/vendors/list")
#ApiOperation(value = "Get all vendors")
public ResultWrapperDTO getVendorpage(#RequestParam(required = false) String term,
#RequestParam(required = false) Integer page, #RequestParam(required = false) Integer size,
#RequestParam(required = false) String orderBy, #RequestParam(required = false) String sortDir) {
I have written a custom bean validator but somehow this is not working . Even if am passing any random values for the query param , its not validating and throwing an error.
#Repeatable(ValuesAllowedMultiple.class)
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = {ValuesAllowedValidator.class})
public #interface ValuesAllowed {
String message() default "Field value should be from list of ";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String propName();
String[] values();
}
public class ValuesAllowedValidator implements ConstraintValidator<ValuesAllowed, Object> {
private String propName;
private String message;
private String[] values;
#Override
public void initialize(ValuesAllowed requiredIfChecked) {
propName = requiredIfChecked.propName();
message = requiredIfChecked.message();
values = requiredIfChecked.values();
}
#Override
public boolean isValid(Object object, ConstraintValidatorContext context) {
Boolean valid = true;
try {
Object checkedValue = BeanUtils.getProperty(object, propName);
if (checkedValue != null) {
valid = Arrays.asList(values).contains(checkedValue.toString().toLowerCase());
}
if (!valid) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message.concat(Arrays.toString(values)))
.addPropertyNode(propName).addConstraintViolation();
}
} catch (IllegalAccessException e) {
log.error("Accessor method is not available for class : {}, exception : {}", object.getClass().getName(), e);
return false;
} catch (NoSuchMethodException e) {
log.error("Field or method is not present on class : {}, exception : {}", object.getClass().getName(), e);
return false;
} catch (InvocationTargetException e) {
log.error("An exception occurred while accessing class : {}, exception : {}", object.getClass().getName(), e);
return false;
}
return valid;
}
}
Case 1: If the annotation ValuesAllowed is not triggered at all, it could be because of not annotating the controller with #Validated.
#Validated
#ValuesAllowed(propName = "orderBy", values = { "OpportunityCount", "OpportunityPublishedCount", "ApplicationCount", "ApplicationsApprovedCount" })
public class OpportunityController {
#GetMapping("/vendors/list")
public String getVendorpage(#RequestParam(required = false) String term,..{
}
Case 2: If it is triggered and throwing an error, it could be because of the BeanUtils.getProperty not resolving the properties and throwing exceptions.
If the above solutions do not work, you can try moving the annotation to the method level and update the Validator to use the list of valid values for the OrderBy parameter. This worked for me. Below is the sample code.
#RestController
#RequestMapping("/api/opportunity")
#Validated
public class OpportunityController {
#GetMapping("/vendors/list")
public String getVendorpage(#RequestParam(required = false) String term,
#RequestParam(required = false) Integer page, #RequestParam(required = false) Integer size,
#ValuesAllowed(propName = "orderBy", values = { "OpportunityCount", "OpportunityPublishedCount", "ApplicationCount",
"ApplicationsApprovedCount" }) #RequestParam(required = false) String orderBy, #RequestParam(required = false) String sortDir) {
return "success";
}
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = { ValuesAllowed.Validator.class })
public #interface ValuesAllowed {
String message() default "Field value should be from list of ";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String propName();
String[] values();
class Validator implements ConstraintValidator<ValuesAllowed, String> {
private String propName;
private String message;
private List<String> allowable;
#Override
public void initialize(ValuesAllowed requiredIfChecked) {
this.propName = requiredIfChecked.propName();
this.message = requiredIfChecked.message();
this.allowable = Arrays.asList(requiredIfChecked.values());
}
public boolean isValid(String value, ConstraintValidatorContext context) {
Boolean valid = value == null || this.allowable.contains(value);
if (!valid) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message.concat(this.allowable.toString()))
.addPropertyNode(this.propName).addConstraintViolation();
}
return valid;
}
}
}
You would have to change few things for this validation to work.
Controller should be annotated with #Validated and #ValuesAllowed should annotate the target parameter in method.
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
#Validated
#RestController
#RequestMapping("/api/opportunity")
public class OpportunityController {
#GetMapping("/vendors/list")
public String getVendorpage(
#RequestParam(required = false)
#ValuesAllowed(values = {
"OpportunityCount",
"OpportunityPublishedCount",
"ApplicationCount",
"ApplicationsApprovedCount"
}) String orderBy,
#RequestParam(required = false) String term,
#RequestParam(required = false) Integer page, #RequestParam(required = false) Integer size,
#RequestParam(required = false) String sortDir) {
return "OK";
}
}
#ValuesAllowed should target ElementType.PARAMETER and in this case you no longer need propName property because Spring will validate the desired param.
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Target({ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = {ValuesAllowedValidator.class})
public #interface ValuesAllowed {
String message() default "Field value should be from list of ";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String[] values();
}
Validator:
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.List;
public class ValuesAllowedValidator implements ConstraintValidator<ValuesAllowed, String> {
private List<String> expectedValues;
private String returnMessage;
#Override
public void initialize(ValuesAllowed requiredIfChecked) {
expectedValues = Arrays.asList(requiredIfChecked.values());
returnMessage = requiredIfChecked.message().concat(expectedValues.toString());
}
#Override
public boolean isValid(String testValue, ConstraintValidatorContext context) {
boolean valid = expectedValues.contains(testValue);
if (!valid) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(returnMessage)
.addConstraintViolation();
}
return valid;
}
}
But the code above returns HTTP 500 and pollutes logs with ugly stacktrace. To avoid it, you can put such #ExceptionHandler method in controller body (so it will be scoped only to this controller) and you gain control over HTTP status:
#ExceptionHandler(ConstraintViolationException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
String handleConstraintViolationException(ConstraintViolationException e) {
return "Validation error: " + e.getMessage();
}
... or you can put this method to the separate #ControllerAdvice class and have even more control over this validation like using it across all the controllers or only desired ones.
I found that I was missing this dependency after doing everything else. Regular validation steps were working but the custom validators didn't work until I added this to my pom.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Spring class level validation and Thymeleaf

I am learning Spring Framework and Thymeleaf. I have known how to display field error by using something like ${#fields.errors("xx")}. However, I get stuck about how to display object error message in Thymeleaf.
Here is my UserForm class:
#PasswordMatches
public class UserForm {
#NotNull
#NotEmpty
private String username;
#NotNull
#NotEmpty
private String password;
#NotNull
#NotEmpty
private String matchingPassword;
#NotNull
#NotEmpty
#ValidEmail
private String email;
/* setter and getter methods */
Here is my PasswordMatches annotation:
#Target({ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = PasswordMatchesValidator.class)
#Documented
public #interface PasswordMatches {
String message() default "Passwords don't match";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
class PasswordMatchesValidator implements ConstraintValidator<PasswordMatches, Object> {
#Override
public void initialize(PasswordMatches constraintAnnotation) {
}
#Override
public boolean isValid(Object obj, ConstraintValidatorContext context){
UserDto user = (UserDto) obj;
return user.getPassword().equals(user.getMatchingPassword());
}
}
Here is my Controller method:
#RequestMapping(value="/registration", method=RequestMethod.POST)
public ModelAndView registerUserAccount(#ModelAttribute("user") #Valid UserForm userForm,
BindingResult result, WebRequest request, Errors errors) {
if (!result.hasErrors()) {
return new ModelAndView("registerSuccess");
}
else {
return new ModelAndView("registration", "user", userForm);
}
}
Now here is my problem: If the password field and confirmPass field doesn't match, how can I get the default error message returned by the class level annotation in Thymeleaf?
I know this is old post but I also encountered this problem and here is the soulution (maybe it will also help someone else):
Modify PasswordMatchesValidator to this:
class PasswordMatchesValidator implements ConstraintValidator<PasswordMatches, Object> {
#Override
public void initialize(PasswordMatches constraintAnnotation) {
}
#Override
public boolean isValid(Object obj, ConstraintValidatorContext context){
UserDto user = (UserDto) obj;
boolean isValid = user.getPassword().equals(user.getMatchingPassword());
if(!isValid){
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
.addPropertyNode( "matchingPassword" ).addConstraintViolation();
}
return isValid;
}
it will bind the validation result to your 'matchingPassword' attribute. So in your thymeleaf template us it like this:
${#fields.errors("matchingPassword")}
Add this inside the form tag:
<p data-th-each="err : ${#fields.allErrors()}" data-th-text="${err}" class="error">
Invalid input.
</p>
<p th:if="${#fields.hasErrors('${yourObject}')}" th:errors="${yourObject}"></p>

Custom JSR 303 validation is not invoked

My custom JSR 303 validation is not getting invoked. Here is my code
my spring config has
<mvc:annotation-driven />
My controller's handler method:
#RequestMapping(value="update", method = RequestMethod.POST ,
consumes="application/json" ,
produces="application/json"))
#ResponseBody
public String update(#Valid #RequestBody MyBean myBean){
return process(myBean);
}
MyBean (annotated with ValidMyBeanRequest):
#ValidMyBeanRequest
public class MyBean {
private int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
ValidMyBeanRequest annotaion:
#Target({ TYPE })
#Retention(RUNTIME)
#Documented
#Constraint(validatedBy = {MyBeanValidator.class})
public #interface ValidMyBeanRequest {
String message() default "{validMyBeanRequest.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
MyBeanValidator class:
public class MyBeanValidator implements
ConstraintValidator<ValidMyBeanRequest, MyBean> {
#Override
public void initialize(ValidMyBeanRequest constraintAnnotation) {
// TODO Auto-generated method stub
}
#Override
public boolean isValid(MyBean myBean, ConstraintValidatorContext context) {
boolean isValid = true;
int id = myBean.getId();
if(id == 0){
isValid = false;
}
return isValid;
}
}
My http POST request has below JSON data:
{id:100}
The problem is MyBeanValidator's isValid is not getting invoked. I am using Spring 3.1.0 and HibernateValidator is in classpath.
Please see what I am missing??
Update: Updated handler method to include POST request type and consumes, produces values. Also included my http request with JSON data.
Assuming that you do get model correctly, in this case you are doing everything right, except one thing: you need to handle your validation's result manually.
For achieving this you need to add BindingResult object into list of your handler parameters, and then process validation constraints in the way you would like:
#RequestMapping(value="update")
#ResponseBody
public String update(#Valid #ModelAttribute #RequestBody MyBean myBean, BindingResult result) {
if (result.hasErrors()){
return processErrors(myBean);
}
return process(myBean);
}

Resources