Chapter 3. Handling Forms and Complex URL Mapping

Our application, as beautiful as it looks, would benefit from more informations about our users.

We could as them to provide the topics they are interested in.

In this chapter, we will build a profile page. It will feature server- and client-side validation and file upload for a profile picture. We will save that information in the user session and also ensure that our audience is as large as possible by translating the application into several languages. Finally, we will display a summary of Twitter activity matching users' tastes.

Sounds good? Let's get started, we have some work to do.

The profile page – a form

Forms are the cornerstones of every web application. They have been the main way to get user input since the very beginning of the Internet!

Our first task here is to create a profile page like this one:

The profile page – a form

It will let the user enter some personal information as well as a list of tastes. These tastes will then be fed to our search engine.

Let's create a new page in templates/profile/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}" method="post" class="col m8 s12 offset-m2">

        <div class="row">
            <div class="input-field col s6">
                <input id="twitterHandle" type="text"/>
                <label for="twitterHandle">Last Name</label>
            </div>
            <div class="input-field col s6">
                <input id="email" type="text"/>
                <label for="email">Email</label>
            </div>
        </div>
        <div class="row">
            <div class="input-field col s6">
                <input id="birthDate" type="text"/>
                <label for="birthDate">Birth Date</label>
            </div>
        </div>
        <div class="row s12">
            <button class="btn waves-effect waves-light" type="submit" name="save">Submit
                <i class="mdi-content-send right"></i>
            </button>
        </div>
    </form>
</div>
</body>
</html>

Note the @{} syntax that will construct the full path to a resource by prepending the server context path (in our case, localhost:8080) to its argument.

We will also create the associated controller named ProfileController in the profile package:

package masterspringmvc4.profile;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class ProfileController {

    @RequestMapping("/profile")
    public String displayProfile() {
        return "profile/profilePage";
    }
}

Now, you can go to http://localhost:8080 and behold a beautiful form that does nothing. That's because we didn't map any action to the post URL.

Let's create a Data Transfer Object (DTO) in the same package as our controller. We will name it ProfileForm. Its role will be to map the fields of our web form and describe validation rules:

package masterSpringMvc.profile;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

public class ProfileForm {
    private String twitterHandle;
    private String email;
    private LocalDate birthDate;
    private List<String> tastes = new ArrayList<>();

    // getters and setters
}

This is a regular Plain Old Java Object (POJO). Don't forget to generate the getters and setters, without which our data binding will not work properly. Note that we have a list of tastes that we will not populate right now but a bit later.

Since we are using Java 8, the birth date of our user will be using the new Java date-time API (JSR 310). This API is much better than the old java.util.Date API because it makes strong distinctions between all the nuances of human dates and uses a fluent API and immutable data structures.

In our example, a LocalDate class is a simple day without time associated to it. It can be differentiated from the LocalTime class, which represents a time within a day, the LocalDateTime class, which represents both, or the ZonedDateTime class, which uses a time zone.

Note

If you wish to learn more about the Java 8 date time API, refer to the Oracle tutorial available at https://docs.oracle.com/javase/tutorial/datetime/TOC.html.

Tip

Good advice is to always generate the toString method of our data objects like this form. It is extremely useful for debugging.

To instruct Spring to bind our field to this DTO, we will have to add some metadata in the profilePage:

<!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"/>
                <label for="twitterHandle">Last Name</label>
            </div>
            <div class="input-field col s6">
                <input th:field="${profileForm.email}" id="email" type="text"/>
                <label for="email">Email</label>
            </div>
        </div>
        <div class="row">
            <div class="input-field col s6">
                <input th:field="${profileForm.birthDate}" id="birthDate" type="text"/>
                <label for="birthDate">Birth Date</label>
            </div>
        </div>
        <div class="row s12">
            <button class="btn 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 two things:

  • The th:object attribute in the form
  • The th:field attributes in all the fields

The first one will bind an object by its type to the controller. The second ones will bind the actual fields to our form bean attributes.

For the th:object field to work, we need to add an argument of the type ProfileForm to our request mapping methods:

@Controller
public class ProfileController {

    @RequestMapping("/profile")
    public String displayProfile(ProfileForm profileForm) {
        return "profile/profilePage";
    }

    @RequestMapping(value = "/profile", method = RequestMethod.POST)
    public String saveProfile(ProfileForm profileForm) {
        System.out.println("save ok" + profileForm);
        return "redirect:/profile";
    }
}

We also added a mapping for the POST method that will be called when the form is submitted. At this point, if you try to submit the form with a date (for instance 10/10/1980), it won't work at all and give you an error 400 and no useful logging information.

Tip

Logging in Spring Boot

With Spring Boot, logging configuration is extremely simple. Just add logging.level.{package} = DEBUG to the application.properties file, where {package} is the fully qualified name of one of the classes or a package in your application. You can, of course, replace debug by any logging level you want. You can also add a classic logging configuration. Refer to http://docs.spring.io/spring-boot/docs/current/reference/html/howto-logging.html for more information.

We will need to debug our application a little bit to understand what happened. Add this line to your file application.properties:

logging.level.org.springframework.web=DEBUG

The org.springframework.web package is the base package of Spring MVC. This will allow us to see debug information generated by Spring web. If you submit the form again, you will see the following error in the log:

Field error in object 'profileForm' on field 'birthDate': rejected value [10/10/1980]; codes [typeMismatch.profileForm.birthDate,typeMismatch.birthDate,typeMismatch.java.time.LocalDate,typeMismatch]; … nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type java.lang.String to type java.time.LocalDate for value '10/10/1980'; nested exception is java.time.format.DateTimeParseException: Text '10/10/1980' could not be parsed, unparsed text found at index 8]

To understand what's going on, we need to have a look at the DateTimeFormatterRegistrar class of Spring.

In this class, you will see half a dozen parsers and printers for the JSR 310. They will all fall back on the short style date format, which is either MM/dd/yy if you live in the US or dd/MM/yy otherwise.

This will instruct Spring Boot to create a DateFormatter class when our application starts.

We need to do the same thing in our case and create our own formatter since writing a year with two digits is a bit akward.

A Formatter in Spring is a class that can both print and parse an object. It will be used to decode and print a value from and to a String.

We will create a very simple formatter in the date package called USLocalDateFormatter:

public class USLocalDateFormatter implements Formatter<LocalDate> {
    public static final String US_PATTERN = "MM/dd/yyyy";
    public static final String NORMAL_PATTERN = "dd/MM/yyyy";

    @Override public LocalDate parse(String text, Locale locale) throws ParseException {
        return LocalDate.parse(text, DateTimeFormatter.ofPattern(getPattern(locale)));
    }

    @Override public String print(LocalDate object, Locale locale) {
        return DateTimeFormatter.ofPattern(getPattern(locale)).format(object);
    }

    public static String getPattern(Locale locale) {
        return isUnitedStates(locale) ? US_PATTERN : NORMAL_PATTERN;
    }

    private static boolean isUnitedStates(Locale locale) {
        return Locale.US.getCountry().equals(locale.getCountry());
    }
}

This little class will allow us to parse the date in a more common format (with years in four digits) according to the user's locale.

Let's create a new class in the config package called WebConfiguration:

package masterSpringMvc.config;

import masterSpringMvc.dates.USLocalDateFormatter;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import java.time.LocalDate;

@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {

    @Override public void addFormatters(FormatterRegistry registry) {
        registry.addFormatterForFieldType(LocalDate.class, new USLocalDateFormatter());
    }
}

This class extends the WebMvcConfigurerAdapter, which is a very handy class to customize the Spring MVC configuration. It provides a lot of common extension points that you can access by overriding methods such as the addFormatters() method.

This time, submitting our form won't result in any error except if you don't type the date with the correct date format.

For the moment, it is impossible for the users to see the format in which they are supposed to enter their birth date, so let's add this information to the form.

In the ProfileController, let's add a dateFormat attribute:

@ModelAttribute("dateFormat")
public String localeFormat(Locale locale) {
    return USLocalDateFormatter.getPattern(locale);
}

The @ModelAttribute annotation will allow us to expose a property to the web page, exactly like the model.addAttribute() method that we saw in the previous chapter.

Now, we can use this information in our page by adding a placeholder to our date field:

<div class="row">
    <div class="input-field col s6">
        <input th:field="${profileForm.birthDate}" id="birthDate" type="text" th:placeholder="${dateFormat}"/>
        <label for="birthDate">Birth Date</label>
    </div>
</div>

This information will now be displayed to the user:

The profile page – a form
..................Content has been hidden....................

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