- Spring 5.0 Cookbook
- Sherwin John Calleja Tragura
- 1411字
- 2021-07-08 10:16:24
How to do it...
To apply form validation and input type conversion, do the following procedures:
- The validation process will progress after modifying the EmployeeForm form model of the previous recipe to contain three more request data, namely the email, age, and birthday of the employee:
public class EmployeeForm { private String firstName; private String lastName; private String position; // additional information private Integer age; private Date birthday; private String email; // getters and setters }
Primitive types are not recommended in declaring form model properties because form validation and type conversion works easily with object types. Thus, wrapper classes must be used instead of their primitive counterparts.
- The most straightforward and easiest way to apply the validator is to use the JSR-303/349 and Hibernate Validator annotations. These are two different external libraries which need to be included in our pom.xml. To access the JSR-303/349 bean annotations, this Maven library must be listed:
<dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>1.1.0.Final</version> </dependency>
Whereas, to utilize some Hibernate 5.x annotations for form model validation, we need this Maven dependency:
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.0.1.Final</version> </dependency>
Spring MVC projects must only choose either of the two libraries, but not both, to avoid confusion. But for the sake of this recipe, ch03 will be mixing annotations from both.
- To apply the two rules, modify EmployeeForm by adding the following annotations:
public class EmployeeForm { @Size(min=2, max=30) private String firstName; @Size(min=2, max=30) private String lastName; @NotNull @Size(min=5, max=100) private String position; @NotNull @Min(0) @Max(100) private Integer age; @NotNull @Past private Date birthday; @Email(message="Must be email formatted.") @NotEmpty private String email; // getters and setters }
@Size, @NotNull, @Min, @Max, and @Past are rule-defining annotations of JSR-303/349, while @NotEmpty and @Email are both under the Hibernate Validation annotation group.
- Next, create a bundle of error messages that will be displayed every time a violation on the rules is encountered. There are three ways to link an error message to each data rule:
- By creating an errors.properties file to be referenced by our ReloadableResourceBundleMessageSource. This file contains a list of code/message pairs wherein the code part is written using the {annotation-name}.{modelAttribute}.{property-name} pattern. In the case of firstName, the entry for its error message must be:
Size.employeeForm.firstName=Employee First Name should be between {2} and {1} characters long inclusive
- In the preceding code {2} and {1} are placeholders for the maximum and minimum range values, respectively.
- By writing a code/message entry in errors.properties, where the message is immediately mapped to the annotation name:
Past=Date should be Past NotEmpty=Email Address must not be null
- By assigning a hardcoded value to the message attribute of some annotations. The preceding email property has an annotation @Email whose error message is hardcoded within its bound:
@Email(message="Must be email formatted.") @NotEmpty private String email;
- Before proceeding, modify the @Bean configuration of the ReloadableResourceBundleMessage to include the errors.properties in its reference. This is done by replacing the property basename with basenames:
@Bean public MessageSource messageSource() { ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); messageSource.setBasenames( "classpath:config/messages_en_US", "classpath:config/errors"); // refer to sources return messageSource; }
- Create an empty SpringContextConfig that will define some of the @Bean needed for form validation and type checking. This definition class must scan and recognize all these classes from their respective packages.
@Configuration @EnableWebMvc @ComponentScan(basePackages = "org.packt.dissect.mvc, org.packt.dissect.mvc.controller, org.packt.dissect.mvc.validator") public class SpringContextConfig { }
- In order for the ApplicationContext to recognize all these external annotations, the org.springframework.validation.beanvalidation.LocalValidatorFactoryBean must be injected to the container. Once configured, LocalValidatorFactoryBean, Spring's central API for JSR-303/349 support can now explicitly validate any annotated form backing objects. Just add this @Bean to our SpringContextConfig definition and the rest will be taken care of by the @Controller:
@Bean public LocalValidatorFactoryBean validator(){ return new LocalValidatorFactoryBean(); }
- After its injection, @Autowire the LocalValidatorFactoryBean into the FormController to validate() the employeeForm parameter of submitForm().To capture all the error messages encountered during validation, add the BindingResult object in the parameter list. BindingResult has helper methods such as hasErrors(), which are essential in detecting registered errors during the validation process. Now, the new submitForm() must be written this way:
@Controller @RequestMapping("/employee_form.html") public class FormController { @Autowired private LocalValidatorFactoryBean validator; @RequestMapping(method=RequestMethod.POST) public String submitForm(Model model, @ModelAttribute("employeeForm") EmployeeForm employeeForm, BindingResult result ){ model.addAttribute("employeeForm", employeeForm); validator.validate(employeeForm, result); if(result.hasErrors()){ return "form_page"; } return "success_page"; } }
In Spring 5.0, using @Valid of JSR-303/349 to auto-detect and execute bean annotations does not work anymore, unlike in Spring 3.0 and lower. The validate() method of LocalValidatorFactoryBean is the only feasible way to explicitly read EmployeeForm, execute all the annotations, validate all the request data that complies with the rules, and register all errors in BindingResult.
- Given the changes in submitForm(), it is designed, that when hashErrors() encounters some non-compliance to the rules, it will re-load the form_page displaying all the registered error messages. The Spring Form tag library has a <form:errors> tag which displays all error messages linked to each property of the modelAttribute. At this point, modify the form_page view to include the <form:errors> tags:
<form:form modelAttribute="employeeForm" method="post"> <spring:message code="fnameLbl" /> <form:input path="firstName"/> <form:errors path="firstName"/><br/> <spring:message code="lnameLbl" /> <form:input path="lastName"/> <form:errors path="lastName"/><br/> <spring:message code="posLbl" /> <form:input path="position"/> <form:errors path="position"/><br/> <hr/> <em>Added Information</em><br/> <spring:message code="ageLbl" /> <form:input path="age"/> <form:errors path="age"/><br/> <spring:message code="bdayLbl" /> <form:input path="birthday"/> <form:errors path="birthday"/><br/> <spring:message code="emailLbl" /> <form:input path="email"/> <form:errors path="email"/><br/> <input type="submit" value="Add Employee"/><br> </form:form>
- Save all files. Then clean, install, and deploy ch03 into the Tomcat 9 container. Execute and test https://localhost:8443/ch03/employee_form.html to check if our annotations are working appropriately:
- There are data rules that are so complex to handle an annotation and can only be implemented through programming. The Spring framework supports highly customized validation through its org.springframework.validation.Validator interface. Once implemented, this validator requires two methods to implement, and these are supports() and validate(). The supports() method checks and verifies the @ModelAttribute to be validated, whereas validate() performs the custom validation process. Create a new package, org.packt.dissect.mvc.validator, to store our EmployeeValidator:
public class EmployeeValidator implements Validator{ @Override public boolean supports(Class<?> clazz) { return clazz.equals(EmployeeForm.class); } @Override public void validate(Object model, Errors errors) { EmployeeForm empForm = (EmployeeForm) model; ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "empty.firstName"); ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "empty.lastName"); if(empForm.getAge() < 0) errors.rejectValue("age", "negative.age"); if(empForm.getAge() > 65) errors.rejectValue("age", "retirable.age"); if(empForm.getBirthday().before(new Date(50,0,1))) errors.rejectValue("birthday", "old.birthday"); Date now = new Date(); if(empForm.getBirthday().getYear() == now.getYear() || empForm.getBirthday().after(new Date(99,0,1))) errors.rejectValue("birthday", "underage.birthday"); } }
- Inject EmployeeValidator into the SpringContextConfig container using the JavaConfig specification:
@Bean public Validator employeeValidator(){ return new EmployeeValidator(); }
- The bean employeeValidator must be @Autowired into the FormController to be registered in a @InitBinder method. The main purpose of the initBinder() method is to bind the @ModelAttribute to validators through the WebDataBinder object:
@Controller @RequestMapping("/employee_form.html") public class FormController { @Autowired private Validator employeeValidator; // refer to sources @InitBinder("employeeForm") public void initBinder(WebDataBinder binder){ binder.setValidator(employeeValidator); } }
- To let the Spring framework know that the EmployeeForm can now be validated using EmployeeValidator, the @Validated annotation of Spring must be added before the employeeForm parameter of submitForm():
@RequestMapping(method=RequestMethod.POST) public String submitForm(Model model, @ModelAttribute("employeeForm") @Validated EmployeeForm employeeForm, BindingResult result ){ model.addAttribute("employeeForm", employeeForm); validator.validate(employeeForm, result); if(result.hasErrors()){ return "form_page"; } return "success_page"; }
- Save all files. Then clean, compile, and deploy the project. Run and test https://localhost:8443/ch03/employee_form.html, again but focus on the EmployeeValidator rules:
- The whole recipe will not be complete without a type conversion mechanism for incoming request data. Spring validation has a limitation and that is to convert request parameters to their qualified modelAttribute types. Employee Form, Age and Date of Birth are examples of data that will throw an exception once saved into the database or processed in a mathematical formula, because they remain as strings at this point. To fix the bug, create custom editors named AgeEditor and DateEditor inside a new package, org.packt.dissect.mvc.editor:
public class AgeEditor extends PropertyEditorSupport{ @Override public void setAsText(String text) throws IllegalArgumentException { try{ int age = Integer.parseInt(text); setValue(age); }catch(NumberFormatException e){ setValue(0); } } @Override public String getAsText() { return "0"; } } public class DateEditor extends PropertyEditorSupport{ @Override public void setAsText(String text) throws IllegalArgumentException { SimpleDateFormat sdf = new SimpleDateFormat("MMMM dd, yyyy"); try { Date dateParam = sdf.parse(text); setValue(dateParam); } catch (ParseException e) { setValue(new Date()); } } @Override public String getAsText() { SimpleDateFormat sdf = new SimpleDateFormat("MMMM dd, yyyy"); String bdayFmt = sdf.format(new Date()); return bdayFmt; } }
- Just like validators, these custom editors must be added into the @InitBinder through its WebDataBinder to serve the main objective, type conversion. AgeEditor will convert the age parameter into Integer after clicking the submit button, while DateEditor, on the other hand, will manage the conversion of all request parameters to be saved as java.util.Date types. Modify the existing initBinder() method to bind all these custom editors to employeeForm:
@InitBinder("employeeForm") public void initBinder(WebDataBinder binder){ binder.setValidator(employeeValidator); binder.registerCustomEditor(Date.class, new DateEditor()); binder.registerCustomEditor(Integer.class, "age",new AgeEditor()); }
- Update the success_page views to include all the changes in the modelAttribute properties. This view will also introduce JSTL tags <fmt> for formatting rendered data:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title><spring:message code="employee_profile" /></title> </head> <body> <h1><spring:message code="employee_profile" /></h1> <table> <tr> // refer to sources </tr> <tr> <td><c:out value='${ employeeForm.firstName }'/></td> <td><c:out value='${ employeeForm.lastName }' /></td> <td><c:out value='${ employeeForm.position }' /></td> <td><c:out value='${ employeeForm.age }' /></td> <td><fmt:formatDate value="${employeeForm.birthday}" type="date" /></td> <td><c:out value='${ employeeForm.email }' /></td> </tr> </table> </body> </html>
- Save all files of ch03. Then clean, build, and deploy it.