We wouldn't want our user to enter invalid or empty information and that's why we will need to add some validation logic to our ProfileForm
.
package masterspringmvc4.profile; import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.NotEmpty; import javax.validation.constraints.NotNull; import javax.validation.constraints.Past; import javax.validation.constraints.Size; import java.util.ArrayList; import java.util.Date; import java.util.List; public class ProfileForm { @Size(min = 2) private String twitterHandle; @Email @NotEmpty private String email; @NotNull private Date birthDate; @NotEmpty private List<String> tastes = new ArrayList<>(); }
As you can see, we added a few validation constraints. These annotations come from the JSR-303 specification, which specifies bean validation. The most popular implementation of this specification is hibernate-validator
, which is included in Spring Boot.
You can see that we use annotations coming from the javax.validation.constraints
package (defined in the API) and some coming from the org.hibernate.validator.constraints
package (additional constraints). Both work, I encourage you to take a look at what is available in those packages in the jars validation-api
and hibernate-validator
.
You can also take a look at the constraints available in the hibernate validator in the documentation at http://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#section-builtin-constraints.
We will need to add a few more things for validation to work. First, the controller needs to say that it wants a valid model on form submission. Adding the javax.validation.Valid
annotation to the parameter representing the form does just that:
@RequestMapping(value = "/profile", method = RequestMethod.POST) public String saveProfile(@Valid ProfileForm profileForm, BindingResult bindingResult) { if (bindingResult.hasErrors()) { return "profile/profilePage"; } System.out.println("save ok" + profileForm); return "redirect:/profile"; }
Note that we do not redirect the user if the form contains any errors. This will allow us to display them on the same web page.
Speaking of which, we need to add a place on the web page where those errors will be displayed.
Add these lines just at the beginning of the form tag in profilePage.html
:
<ul th:if="${#fields.hasErrors('*')}" class="errorlist"> <li th:each="err : ${#fields.errors('*')}" th:text="${err}">Input is incorrect</li> </ul>
This will iterate through every error found in the form and display them in a list. If you try to submit an empty form, you will see a bunch of errors:
Note that the @NotEmpty
check on the tastes will prevent the form from being submitted. Indeed, we do not yet have a way to provide them.
These error messages are not very useful for our user yet. The first thing we need to do is to associate them properly to their respective fields. Let's modify profilePage.html
:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorator="layout/default"> <head lang="en"> <title>Your Profile</title> </head> <body> <div class="row" layout:fragment="content"> <h2 class="indigo-text center">Personal info</h2> <form th:action="@{/profile}" th:object="${profileForm}" method="post" class="col m8 s12 offset-m2"> <div class="row"> <div class="input-field col s6"> <input th:field="${profileForm.twitterHandle}" id="twitterHandle" type="text" th:errorclass="invalid"/> <label for="twitterHandle">Twitter handle</label> <div th:errors="*{twitterHandle}" class="red-text">Error</div> </div> <div class="input-field col s6"> <input th:field="${profileForm.email}" id="email" type="text" th:errorclass="invalid"/> <label for="email">Email</label> <div th:errors="*{email}" class="red-text">Error</div> </div> </div> <div class="row"> <div class="input-field col s6"> <input th:field="${profileForm.birthDate}" id="birthDate" type="text" th:errorclass="invalid" th:placeholder="${dateFormat}"/> <label for="birthDate">Birth Date</label> <div th:errors="*{birthDate}" class="red-text">Error</div> </div> </div> <div class="row s12"> <button class="btn indigo waves-effect waves-light" type="submit" name="save">Submit <i class="mdi-content-send right"></i> </button> </div> </form> </div> </body> </html>
You will notice that we added a th:errors
tag below each field in the form. We also added a th:errorclass
tag to each field. If the field contains an error, the associated css class will be added to the DOM.
The validation looks much better already:
The next thing we need to do is to customize the error messages to reflect the business rules of our application in a better way.
Remember that Spring Boot takes care of creating a message source bean for us? The default location for this message source is in src/main/resources/messages.properties
.
Let's create such a bundle, and add the following text:
Size.profileForm.twitterHandle=Please type in your twitter user name Email.profileForm.email=Please specify a valid email address NotEmpty.profileForm.email=Please specify your email address PastLocalDate.profileForm.birthDate=Please specify a real birth date NotNull.profileForm.birthDate=Please specify your birth date typeMismatch.birthDate = Invalid birth date format.
The class responsible for resolving the error messages in Spring is DefaultMessageCodesResolver
. In the case of field validation, this class tries to resolve the following messages in the given order:
In the preceding rules, the code part can be two things: an annotation type such as Size
or Email
, or an exception code such as typeMismatch
. Remember when we got an exception caused by an incorrect date format? The associated error code was indeed typeMismatch
.
With the preceding messages, we chose to be very specific. A good practice is to define default messages as follows:
Size=the {0} field must be between {2} and {1} characters long typeMismatch.java.util.Date = Invalid date format.
Note the placeholders; each validation error has a number of arguments associated with it.
The last way to declare error messages would involve defining the error message directly in the validation annotations as follows:
@Size(min = 2, message = "Please specify a valid twitter handle") private String twitterHandle;
However, the downside of this method is that it is not compatible with internationalization.
For Java dates, there is an annotation called @Past
, which ensures that a date is from the past.
We don't want our user to pretend they are coming from the future, so we need to validate the birth date. To do this, we will define our own annotation in the date
package:
package masterSpringMvc.date; import javax.validation.Constraint; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import javax.validation.Payload; import java.lang.annotation.*; import java.time.LocalDate; @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = PastLocalDate.PastValidator.class) @Documented public @interface PastLocalDate { String message() default "{javax.validation.constraints.Past.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; class PastValidator implements ConstraintValidator<PastLocalDate, LocalDate> { public void initialize(PastLocalDate past) { } public boolean isValid(LocalDate localDate, ConstraintValidatorContext context) { return localDate == null || localDate.isBefore(LocalDate.now()); } } }
Simple isn't it? This code will verify that our date is really from the past.
We can now add it to the birthDate
field in the profile form:
@NotNull @PastLocalDate private LocalDate birthDate;