Validation

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:

Validation

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.

Customize validation messages

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:

Customize validation messages

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.

Tip

It can be very handy in development to configure the message source to always reload our bundles. Add the following property to application.properties:

spring.messages.cache-seconds=0

0 means always reload, whereas -1 means never reload.

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:

  • code + "." + object name + "." + field
  • code + "." + field
  • code + "." + field type
  • code

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.

Custom annotation for validation

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;
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.2.240