Spring MVC #ExceptionHandler does not catch MethodArgumentNotValidException - spring

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.

Related

Spring boot entity level custom annotation not working

So I have a type level custom annotation that checks for matching passwords in a user registration form. Even when the validator is returning false, it doesn't throw an error and show the error message. Any help is appreciated!
Entity class. Annotation in question is #ValidPassword
#Entity
#ValidPassword(fields = {"password", "matchingPassword"})
public class User {
private String password;
private String matchingPassword;
Constraintvalidator class
public class PasswordValidator implements ConstraintValidator<ValidPassword, User> {
private String[] fields;
private String message;
#Override
public boolean isValid(User user, ConstraintValidatorContext context) {
if (fields[0]== null || fields[1] == null) {
return false;
}
for ( String temp : fields) {
System.out.println(temp);
}
boolean flag = Pattern.matches("^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!#$%^&*-]).{8,}$", fields[0]);
boolean flag1 = fields[0].equals(fields[1]);
if ( !flag1 ) {
message = "Passwords do not match!";
}
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message)
.addPropertyNode(fields[0]).addConstraintViolation();
return flag && flag1;
}
//Show default message if no special message is set
#Override
public void initialize(ValidPassword validPassword) {
fields = validPassword.fields();
message = validPassword.message();
}
}
validpassword
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = PasswordValidator.class)
#Documented
public #interface ValidPassword {
String message() default "Please enter at least 8 characters, 1 uppercase letter, 1 lowercase letter, and 1 special character";
String[] fields();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

Issue in custom validation message using messages.properties file in spring boot validation

I am using spring validation to validate the Rest Controller input, I would appreciate if any one can tell me is there a possibility of throwing custom message in case of exception and the custom message should come from properties file.
UserController.java
#CrossOrigin(origins = "http://localhost:3000")
#RequestMapping(
value="/",
method=RequestMethod.POST,
consumes = MimeTypeUtils.APPLICATION_JSON_VALUE,
produces = MimeTypeUtils.APPLICATION_JSON_VALUE
)
public Object[] createUser(#ModelAttribute("user") User user, BindingResult bindingResult) {
new UserValidator().validate(user,bindingResult);
if (bindingResult.hasErrors()) {
return bindingResult.getFieldErrors().toArray();
}
}
UserValidator.java
public class UserValidator implements Validator{
#Override
public boolean supports(Class<?> aClass) {
return User.class.equals(aClass);
}
#Override
public void validate(Object obj, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "user.firstName.empty");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "user.lastName.empty");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "slug", "user.slug.empty");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "email", "user.email.empty");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "user.password.empty");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "phone", "user.phone.empty");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "address", "user.address.empty");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "country", "user.country.empty");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "gender", "user.gender.empty");
User user = (User) obj;
Pattern pattern = Pattern.compile("^[A-Z0-9._%+-]+#[A-Z0-9.-]+\\.[A-Z]{2,6}$",
Pattern.CASE_INSENSITIVE);
if(!errors.hasErrors()) {
if (!(pattern.matcher(user.getEmail()).matches())) {
errors.rejectValue("email", "user.email.invalid");
}
}
}
}
messages.properties
# messages.properties
user.firstName.empty=Enter a valid first name.
user.lastName.empty = Enter a valid last name.
user.slug.empty = Select gender.
user.phone.empty = Select gender.
user.address.empty = Select gender.
user.country.empty = Select gender.
user.password.empty = Select gender.
user.gender.empty = Select gender.
user.email.empty = Enter a valid email.
user.email.invalid = Invalid email! Please enter valid email.
CustomMessageSourceConfiguration.java
#Configuration
public class CustomMessageSourceConfiguration {
#Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new
ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
#Bean
public LocalValidatorFactoryBean getValidator() {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource());
return bean;
}
}
Browser Response
{codes: ["user.firstName.empty.user.firstName", "user.firstName.empty.firstName",…], arguments:
null,…}
codes: ["user.firstName.empty.user.firstName", "user.firstName.empty.firstName",…]
0: "user.firstName.empty.user.firstName"
1: "user.firstName.empty.firstName"
2: "user.firstName.empty.java.lang.String"
3: "user.firstName.empty"
arguments: null
defaultMessage: null
objectName: "user"
field: "firstName"
rejectedValue: null
bindingFailure: false
code: "user.firstName.empty"
Another Way Of Validation
We can add 4th Parameter As Error Message without using messages.properties file.
public class UserValidator implements Validator{
#Override
public boolean supports(Class<?> aClass) {
return User.class.equals(aClass);
}
#Override
public void validate(Object obj, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName",
"user.firstName.empty","Error Message Here");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName",
"user.lastName.empty","Error Message Here");
...
...
}
}
Another Way Of Validation
We can autowire MessageSource to UserController.java which is configured in CustomMessageSourceConfiguration.java to get messages.properties file.
#Autowired
private MessageSource messageSource;
public User createUser(#Valid #ModelAttribute("user") #RequestBody User
user) {
final ArrayList errorList = new ArrayList<>() {};
bindingResult.getFieldErrors().forEach(fieldError -> {
errorList.add(new
ObjectError(fieldError.getField(),messageSource.getMessage(fieldError.getCode(),
null, Locale.getDefault())));
});
Now we get required error message mapping from messages.properties file.
Way to Validate
We can create seperate bean validation.
Users.java
#Entity
#Table(name = "users")
public class User{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#NotNull
#InBetweenNumberCustom(min = 12,max = 18)
private Integer age;
//getters and setters
}
Here we are going to create #InBetweenNumberCustom validation annotation.
import com.something.validator.ConstraintValidator.InBetweenNumberValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
#Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
#Retention(RetentionPolicy.RUNTIME)
#Repeatable(InBetweenNumberCustom.List.class)
#Documented
#Constraint(validatedBy = {InBetweenNumberValidator.class})
public #interface InBetweenNumberCustom {
String message() default "Must be in between {min} and {max}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int min() default 0;
int max() default 2147483647;
#Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface List {
InBetweenNumberCustom[] value();
}
}
InBetweenNumberValidator.java
import com.something.validator.annonations.InBetweenNumberCustom;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class InBetweenNumberValidator implements
ConstraintValidator<InBetweenNumberCustom,Integer> {
private int minValue;
private int maxValue;
#Override
public void initialize(InBetweenNumberCustom inBetweenNumberCustom) {
this.minValue = inBetweenNumberCustom.min();
this.maxValue = inBetweenNumberCustom.max();
}
#Override
public boolean isValid(Integer aInteger, ConstraintValidatorContext
constraintValidatorContext) {
// null values are not valid
if ( aInteger == null ) return false;
else return aInteger <= this.maxValue && aInteger >= this.minValue;
}
}
UserController.java
public JSONObject createUser(#Validated #RequestBody User user,
BindingResult bindingResult) {
...
...
}

How to server validate each entry in list using custom validator

I have a Springboot Rest application having a server custom validator for one of the model. There are 2 api endpoints, one receives single object which other receives list of same object. My custom validator works fine on first endpoint. How can i use same validator for other endpoint.
Model class
#Entity
#Table(name=TABLE_MESSAGE, schema = SCHEMA)
public class Message implements java.io.Serializable {
#Id #GeneratedValue(strategy=IDENTITY)
#Column(name=COLUMN_ID, unique=true)
private Long id;
#Basic(optional = false)
#Column(name = COLUMN_CREATETIMESTAMP, insertable = false, updatable = false)
#Temporal(TemporalType.TIMESTAMP)
private Date timestamp;
#Column(name=COLUMN_MESSAGE_SENDERNAME)
private String senderName;
#Column(name=COLUMN_MESSAGE_SENDEREMAIL)
private String senderEmail;
#Column(name=COLUMN_MESSAGE_SUBJECT)
private String subject;
#Column(name=COLUMN_MESSAGE_BODY)
private String body;
}
DTO class
public class MessageForm {
private List<Message> messageList;
public List<Message> getMessageList() {
return messageList;
}
public void setMessageList(List<Message> messageList) {
this.messageList = messageList;
}
}
Custom validator
#Component
public class MessageValidator implements Validator {
#Override
public boolean supports(Class<?> clazz) {
return Message.class.equals(clazz);
}
#Override
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmpty(errors, "senderName", ERRORCODE_MESSAGE_SENDERNAME_EMPTY);
ValidationUtils.rejectIfEmpty(errors, "senderEmail", ERRORCODE_MESSAGE_SENDEREMAIL_EMPTY);
ValidationUtils.rejectIfEmpty(errors, "subject", ERRORCODE_MESSAGE_SUBJECT_EMPTY);
ValidationUtils.rejectIfEmpty(errors, "body", ERRORCODE_MESSAGE_BODY_EMPTY);
Message m = (Message) target;
if (!m.getSenderName().trim().equalsIgnoreCase(EMPTY_STRING) && m.getSenderName().matches(REGEX_CONTAINS_NUMBER)) {
errors.rejectValue("senderName", ERRORCODE_MESSAGE_SENDERNAME_INVALID);
}
if (!m.getSenderEmail().trim().equalsIgnoreCase(EMPTY_STRING) && !m.getSenderEmail().matches( REGEX_EMAIL)) {
errors.rejectValue("senderEmail", ERRORCODE_MESSAGE_SENDEREMAIL_INVALID);
}
}
}
Controller
#RestController
public class MainSiteRestController
{
#Autowired
private MessageValidator messageValidator;
#InitBinder("message")
protected void initMessageBinder(WebDataBinder binder) {
binder.addValidators(messageValidator);
}
// this works fine
public ResponseForm saveMessage(#Valid #RequestBody Message message, BindingResult bindingResult) throws APIException {
if (bindingResult.hasErrors()){
throw new APIException(getErrorMesage(bindingResult.getAllErrors()));
}
return apiService.saveMessage(message);
}
// this is not working
public ResponseForm saveAllMessage(#RequestBody MessageForm messageForm, Errors errors) throws APIException {
// need to validate the complete list or particular indexed object here, tried below code but not working
// messageValidator.validate(messageForm.getMessageList().get(0), errors);
if(errors.hasErrors()) {
throw new APIException(createErrorString(errors));
}
return apiService.saveAllMessage(messageForm);
}
}
Spring validators work on a single form, therefore you will have to create a validator for list dto.

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>

Validating #RequestParam using #Annotations

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!

Resources