Custom Validation — Spring MVC
Need to apply Validation within a @RequestMapping and not on a Java Bean?
Hmm… You are at the right place :)
There may be instances where we use the same Java bean for more than one view.
Okay, what happens if the bean is configured this way :P
@Entity
@Table(name=”user_data”)public class UserInfoBean {@Column(name = "country")
private String country;@NotNull(message="First Name is a required field")
@Column(name = "first_name")
private String first_name;@NotNull(message="Last Name is a required field")
@Column(name = "last_name")
private String last_name;... [ getters / setters ]}
You are right!!! The component using this bean would be forced to abide the constraints :(
Now, if there is a scenario where it would be required to use UserInfoBean without first_name and last_name as mandatory fields.. we would need to prepare the bean in a generic way and apply the constraints only to those service end points which would require it.
So, how do we start?
Let us comment the field level validations in the UserInfoBean:
@Entity
@Table(name=”user_data”)public class UserInfoBean {@Column(name = "country")
private String country;// @NotNull(message="First Name is a required field")
@Column(name = "first_name")
private String first_name;// @NotNull(message="Last Name is a required field")
@Column(name = "last_name")
private String last_name;... [ getters / setters ]}
Then the Controller goes this way -
@Controller
public class IndexController {@Autowired
private UserInfoBeanValidator userInfoBeanValidate;//all form data will be pre-processed
@InitBinder
public void initBinder(WebDataBinder dataBinder) { StringTrimmerEditor editor = new StringTrimmerEditor(true);
// true will make it null if empty dataBinder.registerCustomEditor(String.class, editor); // dataBinder.addValidators(userInfoBeanValidate);
// The above line is commented as I don't want to apply the
// custom validation to all the service end points below}@RequestMapping(value = "/getUserInfo", method = RequestMethod.POST)
public String getUserInfoWithValidation(@ModelAttribute("userInfo") UserInfoBean userBean, BindingResult result,Model model) { userInfoBeanValidate.validate(userBean, result); if(!result.hasErrors()) { // Do Actions...
return "successPage"; } else { return "index"; }}}
Let’s look at the above code,
Here, UserInfoBeanValidator is AutoWired and the validate() method is called within the getUserInfoWithValidation() method.
We could have globalised the validation to be applied across all the Service End Points by uncommenting the line within the initBinder() method
dataBinder.addValidators(userInfoBeanValidate);
However, I wanted to demonstrate by applying it just to a particular service end point [ /getUserInfo ].
Okay, now lets look at the implementation of the heart of this Blog —
The Custom Validator Class — UserInfoBeanValidator
@Component
public class UserInfoBeanValidator implements Validator
{private static final Logger logger = Logger.getLogger(UserInfoBeanValidator.class);@Override
public boolean supports(Class clazz) { return UserInfoBean.class.equals(clazz);}@Override
public void validate(Object target, Errors errors) { String errorName = ""; UserInfoBean user = (UserInfoBean) target; ValidationUtils.rejectIfEmptyOrWhitespace(errors,
"userInfoBean.first_name", "firstname.required"); ValidationUtils.rejectIfEmptyOrWhitespace(errors,
"userInfoBean.last_name", "lastname.required"); ValidationUtils.rejectIfEmptyOrWhitespace(errors,
"userInfoBean.country", "country.required"); if(user.getFirst_name()!=null) { if(user.getFirst_name().matches("\\S+")) {
errorName = "UserInfoBean.first_name";
errors.rejectValue(errorName,
"firstname.void.space","FirstName should not contain any
spaces");
} }}}
Let’s look at the above code again,
We have implemented the Validator interface by overriding the supports and validate methods…
For simple not null validations, we can use
ValidationUtils.rejectIfEmptyOrWhitespace(errors,
"userInfoBean.first_name", "firstname.required");
where as for custom conditions below would be the approach
if(user.getFirst_name()!=null) {if(user.getFirst_name().matches("\\S+")) {
errorName = "UserInfoBean.first_name";
errors.rejectValue(errorName,
"firstname.void.space","FirstName should not contain any
spaces");
}}
So where does that look for suitable error messages in the below line, as we never defined any default error value?
ValidationUtils.rejectIfEmptyOrWhitespace(errors,
"userInfoBean.first_name", "firstname.required");
Hmm, that checks for the messageSource configuration of the project and picks the value from the corresponding properties file
Here, I have named it as messages.properties and Yes, an Important point to note — place this file under src/main/resources/ . This is default path which would be scanned for messageSource.
The config Bean implementation goes this way
@Configuration
public class AppConfig {@Bean
public ReloadableResourceBundleMessageSource messageSource() { ReloadableResourceBundleMessageSource messageSource = new
ReloadableResourceBundleMessageSource(); messageSource.setBasename("classpath:messages"); messageSource.setDefaultEncoding("UTF-8");
messageSource.setUseCodeAsDefaultMessage(true); return messageSource;}}
And my messages.properties
firstname.required = First Name is required...
Finally the view to display the error : index.jsp
<form:form action="getUserInfo" method="post" modelAttribute="userInfo">
<form:input placeholder="Enter your first name" type="text"
path="userInfoBean.first_name"/> <form:errors path="userInfoBean.first_name" cssClass="errors"> .
</form:errors>
</form>
An important point to focus on -
The path value in the above form field points to userInfoBean.first_name and it should be same as the fieldName parameter of the rejectIfEmptyOrWhitespace OR rejectValue OR.. any other methods which were used to notify the failed validation while overriding the validate() method
Here, userInfoBean.first_name is the path used in the View Page while userInfoBean.first_name is the fieldName to which the Validation Error is bound to in the validate() implementation
ValidationUtils.rejectIfEmptyOrWhitespace(errors,
"userInfoBean.first_name", "firstname.required");
The Complete Code to achieve Custom Validation with Spring MVC is shown below
UserInfoBean
@Entity
@Table(name=”user_data”)public class UserInfoBean {@Column(name = "country")
private String country;// @NotNull(message="First Name is a required field")
@Column(name = "first_name")
private String first_name;// @NotNull(message="Last Name is a required field")
@Column(name = "last_name")
private String last_name;... [ getters / setters ]}
IndexController
@Controller
public class IndexController {@Autowired
private UserInfoBeanValidator userInfoBeanValidate;//all form data will be pre-processed
@InitBinder
public void initBinder(WebDataBinder dataBinder) { StringTrimmerEditor editor = new StringTrimmerEditor(true);
// true will make it null if empty dataBinder.registerCustomEditor(String.class, editor); // dataBinder.addValidators(userInfoBeanValidate);
// The above line is commented as I don't want to apply the
// custom validation to all the service end points below}@RequestMapping(value = "/getUserInfo", method = RequestMethod.POST)
public String getUserInfoWithValidation(@ModelAttribute("userInfo") UserInfoBean userBean, BindingResult result,Model model) { userInfoBeanValidate.validate(userBean, result); if(!result.hasErrors()) { // Do Actions...
return "successPage"; } else { return "index"; }}}
UserInfoBeanValidator
@Component
public class UserInfoBeanValidator implements Validator
{private static final Logger logger = Logger.getLogger(UserInfoBeanValidator.class);@Override
public boolean supports(Class clazz) { return UserInfoBean.class.equals(clazz);}@Override
public void validate(Object target, Errors errors) { String errorName = ""; UserInfoBean user = (UserInfoBean) target; ValidationUtils.rejectIfEmptyOrWhitespace(errors,
"userInfoBean.first_name", "firstname.required"); ValidationUtils.rejectIfEmptyOrWhitespace(errors,
"userInfoBean.last_name", "lastname.required"); ValidationUtils.rejectIfEmptyOrWhitespace(errors,
"userInfoBean.country", "country.required"); if(user.getFirst_name()!=null) { if(user.getFirst_name().matches("\\S+")) {
errorName = "UserInfoBean.first_name";
errors.rejectValue(errorName,
"firstname.void.space","FirstName should not contain any
spaces");
} }}}
AppConfig
@Configuration
public class AppConfig {@Bean
public ReloadableResourceBundleMessageSource messageSource() { ReloadableResourceBundleMessageSource messageSource = new
ReloadableResourceBundleMessageSource(); messageSource.setBasename("classpath:messages"); messageSource.setDefaultEncoding("UTF-8");
messageSource.setUseCodeAsDefaultMessage(true); return messageSource;}}
messages.properties [ src/main/resources/ ]
firstname.required = First Name is required
View — Index.jsp
<form:form action="getUserInfo" method="post" modelAttribute="userInfo">
<form:input placeholder="Enter your first name" type="text"
path="userInfoBean.first_name"/> <form:errors path="userInfoBean.first_name" cssClass="errors">
</form:errors>
</form>
Happy Coding!!
Please feel free to correct or leave your suggestions in comments below