Skip to content

5. Thymeleaf Views

Let’s return to the architecture of a Spring MVC application.

The previous two chapters described various aspects of block [1], the actions. We will now discuss:

  • block [2] of the V views;
  • the [3] model (M) displayed by these views;

Since the creation of Spring MVC, the technology used to generate HTML pages sent to client browsers has been JSP (Java Server Pages). In recent years, [Thymeleaf] [http://www.thymeleaf.org/] technology has also become available. We will now introduce this technology.

5.1. The STS Project

We create a new project:

  • in [3], specify that the project requires the [Thymeleaf] dependencies. This will add the [Thymeleaf] framework dependencies [5] to the [Spring MVC] dependencies from the previous project;

Now, let’s evolve this project as follows:

  

We’ll draw inspiration from the previous project:

  • [istia.st.springmvc.controllers] will contain the controllers;
  • [istia.st.springmvc.models] will contain the action and view models;
  • [istia.st.springmvc.main] is the package for the Spring Boot executable class;
  • [templates] will contain the Thymeleaf views;
  • [i18n] will contain the internationalized messages displayed by the views;

The [Application] class is as follows:


package istia.st.springmvc.main;

import org.springframework.boot.SpringApplication;

public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Config.class, args);
    }
}

The [Config] class is as follows:


package istia.st.springmvc.main;

import java.util.Locale;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;

@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("i18n/messages");
        return messageSource;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("lang");
        return localeChangeInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }

    @Bean
    public CookieLocaleResolver localeResolver() {
        CookieLocaleResolver localeResolver = new CookieLocaleResolver();
        localeResolver.setCookieName("lang");
        localeResolver.setDefaultLocale(new Locale("fr"));
        return localeResolver;
    }
}

This configuration currently enables locale management.

The [ViewController] is as follows:


package istia.st.springmvc.actions;

import org.springframework.stereotype.Controller;

@Controller
public class ViewsController {

}
  • Line 5: The [@Controller] annotation has replaced the [@RestController] annotation because, from now on, the actions will not generate the response to the client. Instead, they will:
    • construct a model M
    • return a [String] type that will be the name of the [Thymeleaf] view responsible for displaying this model. It is the combination of this view V and this model M that will generate the HTML stream sent to the client;

The [messages.properties] file is currently empty.

5.2. [/v01]: Thymeleaf Basics

We’ll look at the next action in [ViewsController]:


    // Thymeleaf basics - 1
    @RequestMapping(value = "/v01", method = RequestMethod.GET)
    public String v01() {
        return "v01";
}
  • Line 3: The action returns a [String] type. This will be the name of the action;
  • line 4: this view will be [v01]. By default, it must be located in the [templates] folder and named [v01.html];

The [v01.html] view is as follows:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="'Views'">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
    <h2 th:text="'Views in Spring MVC'">Spring 4 MVC</h2>
</body>
</html>

This is an HTML file. The presence of Thymeleaf is evident:

  • in the [th] namespace on line 2;
  • in the [th:text] attributes on lines 4 and 8;

This is a valid HTML file that can be viewed. We place it in the [static] folder [2] under the name [vue-01.html] and access it directly using a browser:

If we examine the page’s source code in [2], we can see that the [th:text] attributes were sent by the server and ignored by the browser. When a view is the result of an action, Thymeleaf kicks in and interprets the [th] attributes before sending the response to the client.

The HTML tag:


<title th:text="'Views'">Spring 4 MVC</title>

is processed as follows by Thymeleaf:

  • th:text has the syntax th:text="expression" where expression is an expression to be evaluated. When this expression is a string, as in this case, it must be enclosed in single quotes;
  • the value of [expression] replaces the text of the HTML tag, in this case the text of the [title] tag;

After processing, the tag above becomes:


<title>The Views</title>

Let’s call the action [/v01]:

  • in [2], we see the replacement work done by Thymeleaf;

Now let’s request the URL [http://localhost:8080/v01.html]:

 

How should we interpret this? Was the view [templates/v01.html] served directly without going through an action? To clarify things, we create the following action [/v02]:


    // Thymeleaf basics - 2
    @RequestMapping(value = "/v02", method = RequestMethod.GET)
    public String v02() {
        System.out.println("action v02");
        return "view-02";
}

The view [vue-02.html] is a copy of [v01.html]:

  

Now let's request the URL [http://localhost:8080/vue-02.html]:

 

The URL was not found. Now let's request the URL [http://localhost:8080/v02.html]

  • In the console logs at [1], we see that the action [/v02] was called, and this caused the view [vue-02.html] to be displayed at [2];

Now we know that the URL [http://localhost:8080/v02.html] can also refer to a file [/v02.html] in the [static] folder. What happens if this file exists? Let’s try it. We create the following [v02.html] file in the [static] folder:

  

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
    <h2>Spring 4 MVC</h2>
</body>
</html>

then we request the URL [http://localhost:8080/v02.html]:

[1] and [2] show that the [/v02] action was called. We can therefore conclude that when the requested URL is in the form [/x.html], Spring / Thymeleaf:

  • executes the action [/x] if it exists;
  • serves the page [/static/x.html] if it exists;
  • throws a 404 Not Found exception otherwise;

To avoid confusion, from now on, actions and views will not have the same names.

5.3. [/v03]: View Internationalization

The Spring/Thymeleaf integration allows Thymeleaf to use Spring message files. Consider the following new action [/v03]:


    // View internationalization
    @RequestMapping(value = "/v03", method = RequestMethod.GET)
    public String v03() {
        return "view-03";
}

It displays the following view [vue-03.html]:

  

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
    <h2 th:text="#{title}">Spring 4 MVC</h2>
</body>
</html>

On lines 4 and 8, the expression for the [th:text] attribute is #{title}, whose value is the [title] key message. We create the following [messages_fr.properties] and [messages_en.properties] files:

[messages_fr.properties]

title=Views in Spring MVC

[messages_en.properties]

title=Views in Spring MVC

Let’s request the URLs [http://localhost:8080/v03.html?lang=fr] and [http://localhost:8080/v03.html?lang=en]:

Note that we have applied what we learned recently. Instead of referring to the [v03] action as [/v03], we have referred to it as [/v03.html].

5.4. [/v04]: Creating the M template for a V view

Consider the following new action [/v04]:


    // Creating the M model for a V view
    @RequestMapping(value = "/v04", method = RequestMethod.GET)
    public String v04(Model model) {
        model.addAttribute("person", new Person(7, "martin", 17));
        System.out.println(String.format("Model=%s", model));
        return "view-04";
}
  • Line 4: The view model is injected into the action parameters. By default, this initial model is empty. We will see that it is possible to pre-populate it;
  • Line 4: A model of type [Model] is a kind of dictionary of elements of type <String, Object>. On line 4, we add an entry to this dictionary with the key [person] associated with a value of type [Person];
  • line 5: we display the model on the console to see what it looks like;
  • line 6: we display the view [vue-04.html];

The [Person] class is the one used in the previous chapter:

  

package istia.st.springmvc.models;

public class Person {

    // identifier
    private Integer id;
    // name
    private String name;
    // age
    private int age;

    // constructors
    public Person() {

    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person(Integer id, String name, int age) {
        this(name, age);
        this.id = id;
    }

    @Override
    public String toString() {
        return String.format("[id=%s, name=%s, age=%d]", id, name, age);
    }

    // getters and setters
...
}

The view [vue-04.html] is as follows:

  

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <p>
            <span th:text="#{personne.nom}">Name:</span>
            <span th:text="${personne.nom}">Bill</span>
        </p>
        <p>
            <span th:text="#{person.age}">Age:</span>
            <span th:text="${person.age}">56</span>
        </p>
    </body>
</html>
  • Line 10 introduces a new Thymeleaf expression type, ${var}, where var is a key from the view's M model. Recall that the [/v04] action added a key [person] to the model, associated with a Person[id, name, age] type;
  • Line 10: displays the name of the person in the model;
  • line 14: displays their age;

The message files are modified to add the keys [person.name] and [person.age] from lines 9 and 13. The result is as follows:

and the nature of model M can be found in the console logs [2].

One might wonder why we don't write the view [view-04] as follows:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}"></title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <p>
            <span th:text="#{person.name}" /></span>
            <span th:text="${person.name}"></span>
        </p>
        <p>
            <span th:text="#{person.age}"></span>
            <span th:text="${person.age}"></span>
        </p>
    </body>
</html>

This view is perfectly valid and will produce the same result as before. One of Thymeleaf’s goals is to ensure that a Thymeleaf page can be displayed even if it does not pass through Thymeleaf. So, let’s create two new static pages:

  

The view [view-04b.html] is a copy of the view [view-04.html]. The same applies to the view [view-04a.html], but we have removed the static text from the page. If we view both pages, we get the following results:

In case [1], the page structure does not appear, whereas in case [2] it is clearly visible. This is the benefit of placing static text in a Thymeleaf view, even if it will be replaced by other text at runtime.

Now, let’s look at a technical detail. In the view [vue-04.html], we format the code using [Ctrl+Shift+F]. We get the following result:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{title}">Spring 4 MVC</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
    <p>
        <span th:text="#{personne.nom}">Last Name:</span> <span
            th:text="${personne.nom}">Bill</span>
    </p>
    <p>
        <span th:text="#{person.age}">Age:</span> <span
            th:text="${person.age}">56</span>
    </p>
</body>
</html>

The tags are misaligned, and the code becomes harder to read. If we rename [vue-04.html] to [vue-04.xml] and reformat the code, the tags will be aligned again. Therefore, the [xml] suffix would be more practical. It is possible to work with this suffix. To do so, we need to configure Thymeleaf. To avoid undoing what we’ve done, we duplicate the [springmvc-vues] project we’ve been studying into a [springmvc-vues-xml] project

  

We modify the [pom.xml] file as follows:


    <groupId>istia.st.springmvc</groupId>
    <artifactId>springmvc-vues-xml</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>springmvc-vues-xml</name>
<description>Views in Spring MVC</description>

The project name is changed on lines 2 and 6. Additionally, we change the suffix of the views in the [templates] folder:

  

The [http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html] document lists the Spring Boot configuration properties that can be used in the [application.properties] file:

  

This document lists the properties that Spring Boot uses during autoconfiguration and that can be modified by configuring [application.properties] differently. For Thymeleaf, the autoconfiguration properties are as follows:


# THYMELEAF (ThymeleafAutoConfiguration)
spring.thymeleaf.check-template-location=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html # ;charset=<encoding> is added
spring.thymeleaf.cache=true # set to false for hot refresh

So we could simply add the line


spring.thymeleaf.suffix=.xml

in [application.properties]. However, we will take a different approach: configuration via code. We will configure Thymeleaf in the [Config] class:


package istia.st.springmvc.main;

import java.util.Locale;

...
import org.thymeleaf.spring4.SpringTemplateEngine;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;

@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
    ...

    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
        templateResolver.setPrefix("classpath:/templates/");
        templateResolver.setSuffix(".xml");
        templateResolver.setTemplateMode("HTML5");
        templateResolver.setCharacterEncoding("UTF-8");
     templateResolver.setCacheable(true);
        return templateResolver;
    }

    @Bean
    SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
        return templateEngine;
    }

}
  • Lines 16–24 configure a [TemplateResolver] for Thymeleaf. This object is responsible for finding the corresponding file based on a view name provided by an action;
  • Lines 18 and 19 set the prefix and suffix to be added to the view name to locate the file. Thus, if the view name is [vue04], the file sought will be [classpath:/templates/vue04.xml]. [classpath:/templates] is a Spring syntax that refers to a [/templates] folder located at the root of the project’s classpath;
  • Line 21: so that the HTTP header in the response sent to the client is:

Content-Type:text/html;charset=UTF-8
  • line 20: indicates that the view complies with the HTML5 standard;
  • line 22: indicates that Thymeleaf views can be cached;
  • lines 26–31: sets the view resolution engine to the Spring/Thymeleaf pair using the previous resolution engine;

Let’s run the executable for this new project and request the URL [http://localhost:8080/v04.html?lang=en]:

 

Note that in the URL, the action [/v04] has again been replaced by [v04.html].

5.5. [/v05]: factoring an object into a Thymeleaf view

We create the following [/v05] action:


    // creation of the M template for a V view - 2
    @RequestMapping(value = "/v05", method = RequestMethod.GET)
    public String v05(Model model) {
        model.addAttribute("person", new Person(7, "martin", 17));
        return "view-05";
}

It is identical to the [/v04] action. The [vue-05.xml] view is as follows:

  

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${person}">        
            <p>
                <span th:text="#{person.name}">Name:</span>
                <span th:text="*{name}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age:</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • lines 8–17: within these lines, a Thymeleaf object is defined by the attribute [th:object="${person}"] (line 8). This object is the object with the key [person] found in the model:
  • line 11: the Thymeleaf expression [*{name}] is equivalent to [${object.name}] where [object] is the current Thymeleaf object. So here, the expression [*{name}] is equivalent to [${person.name}];
  • line 15: same as above;

The result:

 

5.6. [/v06]: Tests in a Thymeleaf view

Consider the following [/v06] action:


    // creation of the M model for a V view - 3
    @RequestMapping(value = "/v06", method = RequestMethod.GET)
    public String v06(Model model) {
        model.addAttribute("person", new Person(7, "martin", 17));
        return "view-06";
}

It is identical to the two previous actions. It displays the following view [vue-06.xml]:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${person}">
            <p>
                <span th:text="#{person.name}">Name:</span>
                <span th:text="*{name}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age:</span>
                <span th:text="*{age}">56</span>
            </p>
            <p th:if="*{age} >= 18" th:text="#{personne.majeure}">You are of legal age</p>
            <p th:if="*{age} &lt; 18" th:text="#{personne.mineure}">You are a minor</p>
        </div>
    </body>
</html>
  • Line 17: The [th:if] attribute evaluates a Boolean expression. If this expression is true, the tag is displayed; otherwise, it is not. So here, if ${person.age} >= 18, the text [#{person.majeure}] will be displayed, i.e., the message key [person.majeure] in the message files;
  • line 18: you cannot write [*{age} < 18] because the < sign is a reserved character. You must therefore use its HTML equivalent [&lt;], also known as the HTML entity [http://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references];

The message files are modified:

[messages_fr.properties]


title=Views in Spring MVC
person.name=Name:
person.age=Age:
person.minor=You are a minor
person.adult=You are an adult

[messages_en.properties]


title=Views in Spring MVC
person.name=Name:
person.age=Age:
person.minor=You are under 18
person.adult=You are over 18

The result is as follows:

5.7. [/v07]: Iteration in a Thymeleaf view

Consider the following action [/v07]:


    // Creating the M template for a V view - 4
    @RequestMapping(value = "/v07", method = RequestMethod.GET)
    public String v07(Model model) {
        model.addAttribute("list", new Person[] { new Person(7, "martin", 17), new Person(8, "lucie", 32),
                new Person(9, "paul", 7) });
        return "view-07";
}
  • The action creates a list of three people, adds it to the model associated with the key [list], and displays the view [view-07];

The view [view-07.xml] is as follows:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <h3 th:text="#{liste.personnes}">List of people</h3>
        <ul>
            <li th:each="element : ${list}" th:text="'['+ ${element.id} + ', ' +${element.name}+ ', ' + ${element.age} + ']'">[id,name,age]</li>
        </ul>
    </body>
</html>
  • Line 10: The [th:each] attribute repeats the tag in which it is located, in this case a <li> tag. It has two parameters here: [element : collection], where [collection] is a collection of objects, in this case a list of people. Thymeleaf will iterate through the collection and generate as many <li> tags as there are elements in the collection. For each <li> tag, [element] will represent the collection element associated with the tag. For this element, the [th:text] attribute will be evaluated. Its expression here is a string concatenation to produce the result [id, name, age];
  • line 8: we add the key [liste.personnes] to the message files;

Here is the result:

5.8. [/v08-/v10]: @ModelAttribute

We’re revisiting something we saw when studying actions: the role of the [@ModelAttribute] annotation. We’re adding the following new action:


    // --------------- Binding and ModelAttribute ----------------------------------

    // if the parameter is an object, it is instantiated and possibly modified by the request parameters
    // it will automatically become part of the view model with the key [key]
    // for a @ModelAttribute("xx") parameter, key will be equal to xx
    // for a parameter with @ModelAttribute, the key will be equal to the parameter's class name starting with a lowercase letter
    // if @ModelAttribute is absent, then everything behaves as if it were present without a key
    // Note that this automatic inclusion in the model does not occur if the parameter is not an object

    @RequestMapping(value = "/v08", method = RequestMethod.GET)
    public String v08(@ModelAttribute("someone") Person p, Model model) {
        System.out.println(String.format("Model=%s", model));
        return "view-08";
}
  • line 11: the annotation [@ModelAttribute("someone")] automatically adds the object [Person p] to the model, associated with the key [someone];
  • line 12: to check the model;
  • line 13: displays the view [vue-08.xml];

The view [view-08.xml] is as follows:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${someone}">        
            <p>
                <span th:text="#{personne.id}">ID:</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{person.name}">Name:</span>
                <span th:text="*{name}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age:</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • Line 8: The Thymeleaf object is initialized with the key object [someone];

The result is as follows:

 

and in the console, we see the following log:

Model={someone=[id=4, name=x, age=11], org.springframework.validation.BindingResult.someone=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

Now let’s consider the following [/v09] action:


    @RequestMapping(value = "/v09", method = RequestMethod.GET)
    public String v09(Person p, Model model) {
        System.out.println(String.format("Model=%s", model));
        return "view-09";
}
  • Line 1: The presence of the parameter [Person p] automatically places the person [p] into the model. Since no key is specified, the key used is the class name with its first character lowercase. Therefore, [Person p] is equivalent to [@ModelAttribute("person") Person p];

The view [view.09.xml] is as follows:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${personne}">        
            <p>
                <span th:text="#{personne.id}">ID:</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{person.name}">Name:</span>
                <span th:text="*{name}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age:</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • line 8: the template key used is [person];

Here is a result:

 

and the log in the server console:

Model={person=[id=4, name=x, age=11], org.springframework.validation.BindingResult.person=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

Now, let’s consider the following new action [/v10]:


    @ModelAttribute("anotherPerson")
    private Person getPerson(){
        return new Person(24, "Pauline", 55);
    }

    @RequestMapping(value = "/v10", method = RequestMethod.GET)
    public String v10(Model model) {
        System.out.println(String.format("Model=%s", model));
        return "view-10";
}
  • lines 1-4: define a method that creates a key element [anotherPerson] in the model for each request, associated with the object [new Person(24, "pauline", 55)];
  • lines 6-10: the action [/v10] does nothing except pass the model it receives to the view [vue-10.xml]. Note that the parameter [Model model] is only needed for the statement on line 8. Without it, it is unnecessary;

The view [view-10.xml] is as follows:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${someOtherPerson}">        
            <p>
                <span th:text="#{personne.id}">ID: </span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{person.name}">Name:</span>
                <span th:text="*{name}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age:</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>

The result is as follows:

 

and the console log is as follows:


Model={anotherPerson=[id=24, name=pauline, age=55]}

5.9. [/v11]: @SessionAttributes

We’re revisiting something we saw when studying actions: the role of the [@SessionAttributes] annotation. We’ll add the following new action [/v11]:


    @ModelAttribute("jean")
    private Person getJean(){
        return new Person(33, "jean", 10);
    }

    @RequestMapping(value = "/v11", method = RequestMethod.GET)
    public String v11(Model model, HttpSession session) {
        System.out.println(String.format("Model=%s, Session[jean]=%s", model, session.getAttribute("jean")));
        return "view-11";
}

We have something similar to what we just covered. The difference lies in an [@SessionAttributes] annotation placed on the class itself:


@Controller
@SessionAttributes("jean")
public class ViewsController {
  • Line 2: We specify that the [jean] key from the model must be placed in the session;

That is why, on line 7 of the action, we injected the session. On line 8, we display the value of the session associated with the key [jean].

The view [view-11.xml] is as follows:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${jean}">
            <p>
                <span th:text="#{personne.id}">ID:</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{person.name}">Name:</span>
                <span th:text="*{name}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age:</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
        <hr />
        <div th:object="${session.jean}">
            <p>
                <span th:text="#{personne.id}">ID:</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{person.name}">Name:</span>
                <span th:text="*{name}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age:</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>

Two people are displayed:

  • lines 8–21: the person with the key [jean] in the model;
  • lines 23–36: the person with key [jean] in the session;

The results are as follows:

  • in [1], the person with key [jean] in the model;
  • in [2], the person with key [jean] in the session;

The console log is as follows:


Model={AnotherPerson=[id=24, name=pauline, age=55], jean=[id=33, name=jean, age=10]}, Session[jean]=null

Above, we see that the key [jean] is not in the session received by the action. We can infer that the key [jean] was added to the session after the action was executed and before the view was rendered.

Now, let’s consider the case where a key is referenced by both [@ModelAttribute] and [@SessionAttributes]. We’ll create the following two actions:


    @RequestMapping(value = "/v12a", method = RequestMethod.GET)
    @ResponseBody
    public void v12a(HttpSession session) {
        session.setAttribute("paul", new Person(51, "paul", 33));
    }

    // case where the [@ModelAttribute] key is also a [@SessionAttributes] key
    // in this case, the corresponding parameter is initialized with the value from the session
    @RequestMapping(value = "/v12b", method = RequestMethod.GET)
    public String v12b(Model model, @ModelAttribute("paul") Person p) {
        System.out.println(String.format("Model=%s", model));
        return "view-12";
}

The [/v12a] action is only used to store the element ['paul', new Person(51, "paul", 33)] in the session. It does nothing else. The fact that it is tagged with [@ResponseBody] indicates that it generates the response to the client. Since its type is [void], no response is generated.

The action [/v12b] accepts [@ModelAttribute("paul") Person p] as a parameter. If nothing else is done, a [Person] object is instantiated and then initialized with the request parameters, and this object has nothing to do with the object with the key [paul] placed in the session by the action [/v12a]. We will add the key [paul] to the session attributes of the class:


@Controller
@SessionAttributes({ "jean", "paul" })
public class ViewsController {
  • Line 2 now has two session attributes;

Let's go back to the action parameters [/v12b]:


public String v12b(Model model, @ModelAttribute("paul") Person p) {

Now, the [Person p] object will not be instantiated but will reference the object with the key [paul] in the session. The rest of the process remains the same. The object with the key [paul] will appear in the view template that will be displayed. This is what we want to see on line 11 of the [/v12b] action.

The view [vue-12.xml] will be as follows:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${paul}">        
            <p>
                <span th:text="#{personne.id}">ID:</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{person.name}">Name:</span>
                <span th:text="*{name}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age:</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • line 8: references the [paul] key from the view model;

This produces the following result (after executing the [/v12a] action, which places the [paul] key in the session):

 

The console log is as follows:


Model={jean=[id=33, name=jean,  age=10], anotherPerson=[id=24, name=pauline,  age=55], paul=[id=51, name=paul,  age=33], org.springframework.validation.BindingResult.paul=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

The key [paul] was successfully added to the model with the value associated with the key [paul] in the session.

5.10. [/v13]: Generating an Input Form

We will now discuss form input and validation. We will build a first form using the following [/v13] action:


  // generates a form to enter a person
  @RequestMapping(value = "/v13", method = RequestMethod.GET)
  public String v13() {
    return "view-13";
}

which simply displays the following view [vue-13.xml]:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <form action="/someURL" th:action="@{/v14.html}" method="post">
            <h2 th:text="#{person.form.title}">Enter the following information</h2>
            <div th:object="${personne}">
                <table>
                    <thead></thead>
                    <tbody>
                        <tr>
                            <td th:text="#{person.id}">ID:</td>
                            <td>
                                <input type="text" name="id" value="11" th:value="''" />
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.nom}">Last Name:</td>
                            <td>
                                <input type="text" name="name" value="Tintin" th:value="''" />
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{person.age}">Age:</td>
                            <td>
                                <input type="text" name="age" value="17" th:value="''" />
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
            <input type="submit" value="Submit" th:value="#{personne.formulaire.valider}" />
        </form>
    </body>
</html>

If we place this view in the [static] folder under the name [view-13.html] and request the URL [http://localhost:8080/vue-13.html], we get the following page:

 
  • On line 8 of the form, we find the <form> tag with the [th:action] attribute. This attribute will be evaluated by Thymeleaf, and its value will replace the current value of the [action] attribute, which is therefore only there for decoration. Here, the value of the [th:action] attribute will be [/v14.html];
  • On lines 17, 23, and 29, the value of the [th:value] attribute will replace that of the [value] attribute. Here, this value will be the empty string;

When we request the URL [/v13.html], we get the following result:

 

Let’s look at the source code generated by Thymeleaf:


<!DOCTYPE html>

<html>
    <head>
        <title>Views in Spring MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <form action="/v14.html" method="post">
            <h2>Please enter information and validate</h2>
            <div>
                <table>
                    <thead></thead>
                    <tbody>
                        <tr>
                            <td>Identifier:</td>
                            <td>
                                <input type="text" name="id" value="" />
                            </td>
                        </tr>
                        <tr>
                            <td>Name:</td>
                            <td>
                                <input type="text" name="name" value="" />
                            </td>
                        </tr>
                        <tr>
                            <td>Age:</td>
                            <td>
                                <input type="text" name="age" value="" />
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
            <input type="submit" value="Submit" />
        </form>
    </body>
</html>

Lines 9, 18, 24, and 30 show Thymeleaf evaluating the [th:action] and [th:value] attributes.

5.11. [/v14]: Handling Values Submitted by a Form

The [/v14] action is the action that receives the posted values. It is as follows:


  // processes the form values
  @RequestMapping(value = "/v14", method = RequestMethod.POST)
  public String v14(Person p) {
    return "view-14";
}
  • Line 3: The posted values are encapsulated in an object [Person p]. We know that this object automatically becomes part of the M model of the V view that will be displayed by the action, associated with the key [person];
  • line 4: the displayed view is the [vue-14.xml] view;

The view [view-14.xml] is as follows:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
    <h2 th:text="#{person.form.inputs}">Here are your inputs</h2>
        <div th:object="${personne}">        
            <p>
                <span th:text="#{person.id}">ID:</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{person.name}">Name:</span>
                <span th:text="*{name}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age:</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • line 9: retrieve the object associated with the [person] key from the model;
  • lines 12, 16, and 20: we display the properties of this object;

This produces the following result:

5.12. [/v15-/v16]: validating a model

Using the previous example, let’s look at the following sequence:

  • in [1], we enter incorrect values for the [id] and [age] fields of type [int];
  • In [2], the server's response indicates that there were two errors;

We will use the same form, but in the event of validation errors, we will redirect to a page listing these errors so that the user can correct them.

The [/v15] action is as follows:


    // ---------------------- display a form
    @RequestMapping(value = "/v15", method = RequestMethod.GET)
    public String v15(SecuredPerson p) {
        return "view-15";
}

It receives the following [SecuredPerson] type as a parameter:

  

package istia.st.springmvc.models;

import javax.validation.constraints.NotNull;

import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;

public class SecuredPerson {

    @Range(min = 1)
    private int id;
    
    @Length(min = 4, max = 10)
    private String name;
    
    @Range(min = 8, max = 14)
    private int age;

    // constructors
    public SecuredPerson() {

    }

    public SecuredPerson(int id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    // getters and setters
...
}

The fields [id, name, age] have been annotated with validation constraints. The view [view-15.xml] displayed by the action [/v15] is as follows:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <form action="/someURL" th:action="@{/v16.html}" method="post">
            <h2 th:text="#{person.form.title}">Enter the following information</h2>
            <div th:object="${securedPerson}">
                <table>
                    <thead></thead>
                    <tbody>
                        <tr>
                            <td th:text="#{personne.id}">ID:</td>
                            <td>
                                <input type="text" name="id" value="11" th:value="*{id}" />
                            </td>
                            <td>
                                <span th:if="${#fields.hasErrors('id')}" th:errors="*{id}" style="color: red">Invalid ID</span>
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.nom}">Name:</td>
                            <td>
                                <input type="text" name="name" value="Tintin" th:value="*{name}" />
                            </td>
                            <td>
                                <span th:if="${#fields.hasErrors('name')}" th:errors="*{name}" style="color: red">Invalid name</span>
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{person.age}">Age:</td>
                            <td>
                                <input type="text" name="age" value="17" th:value="*{age}" />
                            </td>
                            <td>
                                <span th:if="${#fields.hasErrors('age')}" th:errors="*{age}" style="color: red">Incorrect age</span>
                            </td>
                        </tr>
                    </tbody>
                </table>
                <input type="submit" value="Submit" th:value="#{person.form.submit}" />
                <ul>
                    <li th:each="err : ${#fields.errors('*')}" th:text="${err}" style="color: red" />
                </ul>
            </div>
        </form>
    </body>
</html>
  • lines 10-47: the page model object associated with the [securedPerson] key is retrieved. After the GET request, we have an object with its instantiation values [id=0, name=null, age=0];
  • line 17: the value of the [securedPerson.id] field;
  • line 20: the expression [${#fields.hasErrors('id')}] determines whether there were validation errors on the [securedPerson.id] field. If so, the attribute [th:errors="*{id}"] displays the associated error message;
  • this scenario is repeated on line 29 for the [name] field and line 38 for the [age] field;
  • line 45: the expression [${#fields.errors('*')}] refers to all errors on the fields of the [securedPerson] object. Thus, it is the set of these errors that will be displayed by lines 44–46;
  • line 16: we see that the form values will be posted to the [/v16] action. This is as follows:

    // -------------------- model validation------------------
    @RequestMapping(value = "/v16", method = RequestMethod.POST)
    public String v16(@Valid SecuredPerson p, BindingResult result) {
        // errors?
        if (result.hasErrors()) {
            return "view-15";
        } else {
            return "view-16";
        }
}
  • Line 3: The [@Valid SecuredPerson p] annotation enforces validation of the posted values;
  • line 5: checks whether the action model is invalid or not;
  • line 6: if it is invalid, the form [vue-15.xml] is returned. Since this form displays error messages, we will see those;
  • line 8: if the action model is validated, then we display the following view [vue-16.xml]:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
    <h2 th:text="#{person.form.inputs}">Here are your inputs</h2>
        <div th:object="${securedPerson}">        
            <p>
                <span th:text="#{personne.id}">ID:</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{person.name}">Name:</span>
                <span th:text="*{name}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age:</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>

Here are some examples of execution:

5.13. [/v17-/v18]: Checking error messages

When the [/v15] action is requested for the first time, the following result is obtained:

 

You might want an empty form instead of zeros in the [Username, Age] fields. To achieve this, we modify the action model as follows:


package istia.st.springmvc.models;

import javax.validation.constraints.Digits;

import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;

public class StringSecuredPerson {

    @Range(min = 1)
    @Digits(fraction = 0, integer = 4)
    private String id;

    @Length(min = 4, max = 10)
    private String name;

    @Range(min = 8, max = 14)
    @Digits(fraction = 0, integer = 2)
    private String age;

    // constructors
    public StringSecuredPerson() {

    }

    public StringSecuredPerson(String id, String name, String age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    // getters and setters
...

}
  • lines 12 and 19: the [id] and [age] fields are set to type [String];
  • line 11: it is specified that the [id] field must be a number with at most four digits, without decimals;
  • line 18: same for the [age] field, which must be an integer of at most two digits;

The [/v17] action becomes the following:


    // ---------------------- displaying a form
    @RequestMapping(value = "/v17", method = RequestMethod.GET)
    public String v17(StringSecuredPerson p) {
        return "view-17";
}

The view [vue-17.xml] displayed by the action [/v17] is as follows:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <form action="/someURL" th:action="@{/v18.html}" method="post">
            <h2 th:text="#{personne.formulaire.titre}">Enter the following information</h2>
            <div th:object="${stringSecuredPerson}">
                <table>
                    <thead></thead>
                    <tbody>
                        <tr>
                            <td th:text="#{personne.id}">ID:</td>
                            <td>
                                <input type="text" name="id" value="11" th:value="*{id}" />
                            </td>
                            <td>
                                <span th:each="err,status : ${#fields.errors('id')}" th:if="${status.index}==0" th:text="${err}" style="color: red">
                                    Invalid ID
                                </span>
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.nom}">Name:</td>
                            <td>
                                <input type="text" name="name" value="Tintin" th:value="*{name}" />
                            </td>
                            <td>
                                <span th:if="${#fields.hasErrors('name')}" th:errors="*{name}" style="color: red">Invalid name</span>
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{person.age}">Age:</td>
                            <td>
                                <input type="text" name="age" value="17" th:value="*{age}" />
                            </td>
                            <td>
                                <span th:if="${#fields.hasErrors('age')}" th:errors="*{age}" style="color: red">Invalid age</span>
                            </td>
                        </tr>
                    </tbody>
                </table>
                <input type="submit" value="Submit" th:value="#{personne.formulaire.valider}" />
                <ul>
                    <li th:each="err : ${#fields.errors('*')}" th:text="${err}" style="color: red" />
                </ul>
            </div>
        </form>
    </body>
</html>

The changes are made in the following lines:

  • line 10: we now work with the [stringSecuredPerson] key model object;
  • line 20: we iterate through the list of errors for the [id] field. In the syntax [th:each="err,status : ${#fields.errors('id')}"], the variable [err] iterates through the list. The variable [status] provides information about each iteration. It is an object [index, count, size, current] where:
    • index: is the number of the current element,
    • current: is the value of the current element,
    • count, size: the size of the list being iterated over;
  • line 20: we only display the first element of the list [th:if="${status.index}==0"] ;

The action [/v18] that processes the POST from action [/v17] is as follows:


    // -------------------- model validation------------------
    @RequestMapping(value = "/v18", method = RequestMethod.POST)
    public String v18(@Valid StringSecuredPerson p, BindingResult result) {
        // errors?
        if (result.hasErrors()) {
            return "view-17";
        } else {
            return "vue-18";
        }
}

The message files are updated as follows:

[messages_fr.properties]


title=Views in Spring MVC
person.name=Name:
person.age=Age:
person.id=ID:
person.minor=You are a minor
person.adult=You are an adult
list.people=List of people
person.form.title=Enter the following information and submit
person.form.submit=Submit
person.form.inputs=Here are your entries
notNull=This field is required
Range.securedPerson.id=The ID must be an integer >=1
Range.securedPerson.age=Only people between the ages of 8 and 14 are allowed on this site
Length.securedPerson.name=The name must be between 1 and 4 characters
typeMismatch=Invalid data
Range.stringSecuredPerson.id=The ID must be an integer >=1
Range.stringSecuredPerson.age=Only people between the ages of 8 and 14 are allowed on this site
Length.stringSecuredPerson.name=The name must be between 1 and 4 characters
Digits.stringSecuredPerson.id=Enter a 4-digit integer
Digits.stringSecuredPerson.age=Enter a 2-digit integer

[messages_en.properties]


title=Views in Spring MVC
person.name=Name:
person.age=Age:
person.id=ID:
person.minor=You are under 18
person.adult=You are over 18
list.people=People's list
person.form.title=Please enter information and validate
person.form.validate=Validate
person.form.inputs=Here are your inputs
NotNull=Data is required
Range.securedPerson.id=The identifier must be an integer greater than or equal to 1
Range.securedPerson.age=Only children aged 8 to 14 are allowed on this site
Length.securedPerson.name=Name must be 4 to 10 characters long
typeMismatch=Invalid format
Range.stringSecuredPerson.id=Identifier must be an integer greater than or equal to 1
Range.stringSecuredPerson.age=Only children aged 8 to 14 are allowed on this site
Length.stringSecuredPerson.name=Name must be 4 to 10 characters long
Digits.stringSecuredPerson.id=Must be an integer with at most four digits
Digits.stringSecuredPerson.age=Must be an integer with at most two digits

Let’s look at a few examples:

 

In [1], we see that both validators for the [age] field have been executed:


    @Range(min = 8, max = 14)
    @Digits(fraction = 0, integer = 2)
    private String age;

Is there a specific order to the error messages? For the [age] field, it appears that the validators were executed in the order [Digits, Range]. However, if we make multiple requests, we can see that this order may change. Therefore, we cannot rely on the order of the validators. In [2], only one of the two error messages for the [id] field is displayed. In [3], all error messages are shown.

5.14. [/v19-/v20]: Using Different Validators

Consider the following new action model:

  

package istia.st.springmvc.models;

import java.util.Date;

import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.NotEmpty;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;
import org.springframework.format.annotation.DateTimeFormat;

public class Form19 {

    @NotNull
    @AssertFalse
    private Boolean assertFalse;

    @NotNull
    @AssertTrue
    private Boolean assertTrue;
    
    @NotNull
    @Future
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInFuture;
    
    @NotNull
    @Past
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInPast;
    
    @NotNull
    @Max(value = 100)
    private Integer intMax100;
    
    @NotNull
    @Min(value = 10)
    private Integer intMin10;
    
    @NotNull
    @NotEmpty
    private String strNotEmpty;
    
    @NotNull
    @NotBlank
    private String strNotBlank;
    
    @NotNull
    @Size(min = 4, max = 6)
    private String strBetween4and6;
    
    @NotNull
    @Pattern(regexp = "^\\d{2}:\\d{2}:\\d{2}$")
    private String hhmmss;
    
    @NotNull
    @Email
    @NotBlank
    private String email;
    
    @NotNull
    @Length(max = 4, min = 4)
    private String str4;
    
    @Range(min = 10, max = 14)
    @NotNull
    private Integer int1014;
    
    @URL
    @NotBlank
    private String url;

    // getters and setters
...
}

It will be displayed by the following [/v19] action:


    // ------------------ displaying a form
    @RequestMapping(value = "/v19", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String v19(Form19 form) {
        return "view-19";
}
  • Line 3: The action receives a [Form19 form] object as a parameter. If the GET request does not receive any parameters, this object will be initialized with Java's default values;
  • line 4: the view [vue-19.xml] is displayed. It is as follows:

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>
        <h3>Form - Server-side validation</h3>
        <form action="/someURL" th:action="@{/v20.html}" method="post" th:object="${form19}">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Constraint</th>
                        <th class="col2">Input</th>
                        <th class="col3">Error</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">@NotEmpty</td>
                        <td class="col2">
                            <input type="text" th:field="*{strNotEmpty}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('strNotEmpty')}" th:errors="*{strNotEmpty}" class="error">Invalid data</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@NotBlank</td>
                        <td class="col2">
                            <input type="text" th:field="*{strNotBlank}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('strNotBlank')}" th:errors="*{strNotBlank}" class="error">Invalid data</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@assertFalse</td>
                        <td class="col2">
                            <input type="radio" th:field="*{assertFalse}" value="true" />
                            <label th:for="${#ids.prev('assertFalse')}">True</label>
                            <input type="radio" th:field="*{assertFalse}" value="false" />
                            <label th:for="${#ids.prev('assertFalse')}">False</label>
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('assertFalse')}" th:errors="*{assertFalse}" class="error">Invalid data</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@assertTrue</td>
                        <td class="col2">
                            <select th:field="*{assertTrue}">
                                <option value="true">True</option>
                                <option value="false">False</option>
                            </select>
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('assertTrue')}" th:errors="*{assertTrue}" class="error">Invalid data</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Past</td>
                        <td class="col2">
                            <input type="date" th:field="*{dateInPast}" th:value="*{dateInPast}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('dateInPast')}" th:errors="*{dateInPast}" class="error">Invalid data</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Future</td>
                        <td class="col2">
                            <input type="date" th:field="*{dateInFuture}" th:value="*{dateInFuture}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('dateInFuture')}" th:errors="*{dateInFuture}" class="error">Invalid data</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Max</td>
                        <td class="col2">
                            <input type="text" th:field="*{intMax100}" th:value="*{intMax100}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('intMax100')}" th:errors="*{intMax100}" class="error">Invalid data</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Min</td>
                        <td class="col2">
                            <input type="text" th:field="*{intMin10}" th:value="*{intMin10}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('intMin10')}" th:errors="*{intMin10}" class="error">Invalid data</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Size</td>
                        <td class="col2">
                            <input type="text" th:field="*{strBetween4and6}" th:value="*{strBetween4and6}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('strBetween4and6')}" th:errors="*{strBetween4and6}" class="error">Invalid data</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Pattern(hh:mm:ss)</td>
                        <td class="col2">
                            <input type="text" th:field="*{hhmmss}" th:value="*{hhmmss}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('hhmmss')}" th:errors="*{hhmmss}" class="error">Invalid data</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Email</td>
                        <td class="col2">
                            <input type="text" th:field="*{email}" th:value="*{email}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="error">Invalid data</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Length</td>
                        <td class="col2">
                            <input type="text" th:field="*{str4}" th:value="*{str4}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('str4')}" th:errors="*{str4}" class="error">Invalid data</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@Range</td>
                        <td class="col2">
                            <input type="text" th:field="*{int1014}" th:value="*{int1014}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('int1014')}" th:errors="*{int1014}" class="error">Invalid data</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">@URL</td>
                        <td class="col2">
                            <input type="text" th:field="*{url}" th:value="*{url}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('url')}" th:errors="*{url}" class="error">Invalid data</span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Submit" />
            </p>
        </form>
    </body>
</html>

This code displays the following view:

 

The page displays a three-column table:

  • Column 1: the input field validator;
  • column 2: the input field;
  • column 3: error messages for the input field;

Let’s examine, for example, the view code [/v19.html] for the [@Pattern] validator:


                    <tr>
                        <td class="col1">@Pattern(hh:mm:ss)</td>
                        <td class="col2">
                            <input type="text" th:field="*{hhmmss}" th:value="*{hhmmss}" />
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('hhmmss')}" th:errors="*{hhmmss}" class="error">Invalid data</span>
                        </td>
</tr>

We see code that we just studied with [Person] forms:

  • line 2: the first column: the name of the validator being tested;
  • line 4: the Thymeleaf attribute [th:field="*{hhmmss}] will generate the HTML attributes [id="hhmmss"] and [name="hhmmss"]. The Thymeleaf attribute [th:value="*{hhmmss}"] will generate the HTML attribute [value="value of [form19.hhmmss]]";
  • line 7: if the value entered for the [form19.hhmmss] field is incorrect, then line 7 displays the error messages associated with this field;

The posted values are processed by the following [/v20] action:


    // ----------------- form model validation
    @RequestMapping(value = "/v20", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String v20(@Valid Form19 form, BindingResult result, RedirectAttributes redirectAttributes) {
        if (result.hasErrors()) {
            return "view-19";
        } else {
            // redirect to [vue-19]
            redirectAttributes.addFlashAttribute("form19", form);
            return "redirect:/v19.html";
        }
}
  • line 3: the posted values will populate the fields of the [Form19 form] object if they are valid;
  • lines 4–6: if the posted values are invalid, then the form [view-19] is redisplayed with error messages;
  • lines 6–10: if the posted values are valid, then the [Form19] object constructed with these values is made available to the next request, in this case the redirect. It is then destroyed;
  • line 9: the client is redirected to the action [/v19.html]. This will redisplay the form [vue-19], which contains code such as:

<form action="/someURL" th:action="@{/v20.html}" method="post" th:object="${form19}">

The [th:object="${form19}"] attribute will then retrieve the object associated with the Flash attribute [form19] and thus redisplay the form as it was entered.

The form code warrants further explanation. Consider the following code:


                    <tr>
                        <td class="col1">@assertFalse</td>
                        <td class="col2">
                            <input type="radio" th:field="*{assertFalse}" value="true" />
                            <label th:for="${#ids.prev('assertFalse')}">True</label>
                            <input type="radio" th:field="*{assertFalse}" value="false" />
                            <label th:for="${#ids.prev('assertFalse')}">False</label>
                        </td>
                        <td class="col3">
                            <span th:if="${#fields.hasErrors('assertFalse')}" th:errors="*{assertFalse}" class="error">Invalid data</span>
                        </td>
</tr>

This generates the following HTML code:


<tr>
  <td class="col1">@assertFalse</td>
  <td class="col2">
    <input type="radio" value="true" id="assertFalse1" name="assertFalse" />
    <label for="assertFalse1">True</label>
    <input type="radio" value="false" id="assertFalse2" name="assertFalse" />
    <label for="assertFalse2">False</label>
  </td>
  <td class="col3">
  </td>
</tr>

In the code


<input type="radio" th:field="*{assertFalse}" value="true" />
<label th:for="${#ids.prev('assertFalse')}">True</label>
<input type="radio" th:field="*{assertFalse}" value="false" />
<label th:for="${#ids.prev('assertFalse')}">False</label>

The Thymeleaf attributes in lines 1 and 3 [th:field="*{assertFalse}"] pose a problem. We noted that this attribute generates the HTML attributes [id=assertFalse] and [name=assertFalse]. The difficulty arises because, since this is generated on lines 1 and 3, we end up with two identical [name] attributes and two identical [id] attributes. While this is possible with the [name] attribute, it is not with the [id] attribute. As seen in the generated HTML code, Thymeleaf generated two different [id] attributes: [id=assertFalse1] and [id=assertFalse2]. This is a good thing. The problem is that we don’t know these identifiers and we might need them. This is the case for the [label] tag on line 2. The [for] attribute of an HTML [label] tag must reference an [id] attribute, in this case the one generated for the [input] tag on line 1. The Thymeleaf documentation states that the expression [${#ids.prev('assertFalse')}] retrieves the last [id] attribute generated for the [assertFalse] field.

Now let’s look at the code for the form’s dropdown list:


<select th:field="*{assertTrue}">
   <option value="true">True</option>
   <option value="false">False</option>
</select>

This code generates the HTML code for a dropdown list:

1
2
3
4
<select id="assertTrue" name="assertTrue">
  <option value="true">True</option>
  <option value="false">False</option>
</select>

The posted value will be sent with the name [name="assertTrue"].

The view [vue-19.xml] uses a stylesheet:


    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
</head>

Line 4: The stylesheet used must be placed in the project's [static] folder:

  

Its content is as follows:


@CHARSET "UTF-8";

.col1 {
    background: lightblue;
}

.col2 {
    background: Cornsilk;
}

.col3 {
    background: #e2d31d;
}

.error {
    color: red;
}

Now, let's look at the dates:


    @NotNull
    @Future
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInFuture;
    
    @NotNull
    @Past
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInPast;

Examining network traffic in Chrome DevTools (Ctrl+Shift+I) shows that dates are posted in the (yyyy-mm-dd) format:

 

This is why the dates were annotated with the validator:


@DateTimeFormat(pattern = "yyyy-MM-dd")

which sets the expected format for the posted date values.

Finally, the French messages file [messages_fr.properties]:


title=Views in Spring MVC
person.name=Name:
person.age=Age:
person.id=ID:
person.minor=You are a minor
person.adult=You are an adult
list.people=List of people
person.form.title=Enter the following information and submit
person.form.submit=Submit
person.form.inputs=Here are your entries
NotNull=This field is required
Range.securedPerson.id=The ID must be an integer >=1
Range.securedPerson.age=Only people between the ages of 8 and 14 are allowed on this site
Length.securedPerson.name=The name must be between 1 and 4 characters
typeMismatch=Invalid data
Range.stringSecuredPerson.id=The ID must be an integer >=1
Range.stringSecuredPerson.age=Only people between the ages of 8 and 14 are allowed on this site
Length.stringSecuredPerson.name=The name must be between 1 and 4 characters
Digits.stringSecuredPerson.id=Enter a 4-digit integer
Digits.stringSecuredPerson.age=Enter a 2-digit integer
Future.form19.dateInFuture=The date must be later than today
Past.form19.dateInPast=The date must be earlier than today
Size.form19.strBetween4and6=The string must be between 4 and 6 characters long
Min.form19.intMin10=The value must be greater than or equal to 10
Max.form19.intMax100=The value must be less than or equal to 100
Length.form19.str4=The string must be exactly four characters long
Email.form19.email=Invalid email address
URL.form19.url=Invalid URL
Range.form19.int1014=The value must be in the range [10,14]
AssertTrue=Only the value True is accepted
AssertFalse=Only the value False is accepted
Pattern.form19.hhmmss=Enter the time in the format hh:mm:ss
NotEmpty=The field cannot be empty
NotBlank=The data cannot be blank

Let's look at some examples of execution:

 
 

Above, between [1] and [2], it looks like nothing happened. However, if we look at the network traffic (Ctrl-Shift-I), we see that there were two network exchanges with the server:

  • in [1], the initial POST to [/v20];
  • at [2], the response to this action is a redirect;
  • at [3], the second request, this time to [/v19];

The action [/v19] is then executed:


    // ------------------ Displaying a form
    @RequestMapping(value = "/v19", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String v19(Form19 form) {
        return "view-19";
}
  • line 3: the [Form19 form] parameter is initialized with the Flash attribute [form19], which was created by the previous action [/v19] and is an object of type [Form19] containing the values posted to the [/v19] action;
  • Line 4: The view [view-19.xml] will be displayed with an object [Form19 form] in its template, initialized with the posted values. This is why the user sees the form exactly as they posted it;

Why a redirect? Why didn’t we simply post to the [/v19] action above? We would have gotten the same result, with a few differences:

  • the browser would have entered [http://localhost:8080/v20.html] in its address bar instead of [http://localhost:8080/v19.html] as it did here, because it displays the last URL called;
  • if the user refreshes the page (F5), the result is completely different:
    • in the case of redirection, the displayed URL is [http://localhost:8080/v19.html], obtained via a GET request. The browser will re-execute this command and will then receive a brand-new form (the Flash attribute is used only once),
    • in the case of no redirection, the displayed URL is [http://localhost:8080/v20.html], obtained via a POST request. The browser will re-execute this command and therefore perform another POST with the same values as before. Here, this has no consequences, but it is often undesirable, so redirection is generally preferred;

5.15. [/v21-/v22]: Handling radio buttons

Consider the following Spring [Lists] component:

  

package istia.st.springmvc.models;

import org.springframework.stereotype.Component;

@Component
public class Lists {

    private String[] modesOfTransport = new String[] { "0", "1", "2", "3", "4" };
    private String[] travelModes = new String[] { "bike", "walk", "train", "plane", "other" };
    private String[] gemNames = new String[] { "emerald", "ruby", "diamond", "opal" };

    // getters and setters
  ...

}
  • line 5: the [Lists] class will be a Spring component;
  • lines 8–10: lists used to populate radio buttons, checkboxes, and dropdown lists;

In the [Config] configuration class, it says:


@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
  • Line 2: The [models] package, where the [Lists] component is located, will be scanned by Spring;

We create the following new actions:


    // ------------------ form with radio buttons
    @Autowired
    private Lists lists;

    @RequestMapping(value = "/v21", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String v21(@ModelAttribute("form") Form21 form, Model model) {
        model.addAttribute("lists", lists);
        return "view-21";
    }

    @RequestMapping(value = "/v22", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String v22(@ModelAttribute("form") Form21 form, RedirectAttributes redirectAttributes) {
        redirectAttributes.addFlashAttribute("form", form);
        return "redirect:/v21.html";
}
  • lines 2-3: the [Lists] component is injected into the controller;
  • line 6: we handle a [Form21] form, which we will describe. Note that we specified its key [form] in the view model. Recall that by default, this would have been [form21];
  • line 7: we inject the [Lists] component into the model. The view will need it;
  • line 8: we display the view [vue-21.xml]. This view will display the [Form21] form, and the posted values will be sent to the [/v22] action in lines 12–15;
  • lines 12–15: the [/v22] action simply redirects to the [/v21] action, placing the posted values it received into a Flash attribute with the key [form]. It is important that this key matches the one used in line 6;

The [Form21] model is as follows:

  

package istia.st.springmvc.models;

public class Form21 {

    // posted values
    private String marie = "no";
    private String trip = "4";
    private String[] colors;
    private String strColors;
    private String[] jewelry;
    private String strJewelry;
    private int color2;
    private int[] jewelry2;
    private String strJewelry2;

    // getters and setters
    ...
}

The view [view-21.xml] is as follows:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>

        <h3>Form - Radio Buttons</h3>
        <form action="/someURL" th:action="@{/v22.html}" method="post" th:object="${form}">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Text</th>
                        <th class="col2">Input</th>
                        <th class="col3">Value</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Are you married?</td>
                        <td class="col2">
                            <input type="radio" th:field="*{marie}" value="yes" />
                            <label th:for="${#ids.prev('marie')}">Yes</label>
                            <input type="radio" th:field="*{marie}" value="no" />
                            <label th:for="${#ids.prev('marie')}">No</label>
                        </td>
                        <td class="col3">
                            <span th:text="*{marie}"></span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Mode of travel</td>
                        <td class="col2">
                            <span th:each="mode, status : ${listes.deplacements}">
                                <input type="radio" th:field="*{deplacement}" th:value="${mode}" />
                                <label th:for="${#ids.prev('deplacement')}" th:text="${listes.libellesDeplacements[status.index]}">Other</label>
                            </span>
                        </td>
                        <td class="col3">
                            <span th:text="*{deplacement}"></span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Submit" />
            </p>
        </form>
    </body>
</html>
  • Lines 36–40: Note the use of the [Lists] component in the template to generate the labels for the checkboxes;
  • Column 3 displays the value submitted via POST, or the initial value of the form during the initial GET request;

This code displays the following page:

 

corresponding to the following HTML code:


<!DOCTYPE HTML>

<html>
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>

        <h3>Form - Radio Buttons</h3>
        <form action="/v22.html" method="post">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Text</th>
                        <th class="col2">Input</th>
                        <th class="col3">Value</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Are you married?</td>
                        <td class="col2">
                            <input type="radio" value="yes" id="marie1" name="marie" />
                            <label for="marie1">Yes</label>
                            <input type="radio" value="no" id="marie2" name="marie" checked="checked" />
                            <label for="marie2">No</label>
                        </td>
                        <td class="col3">
                            <span>No</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Mode of travel</td>
                        <td class="col2">
                            <span>
                                <input type="radio" value="0" id="deplacement1" name="deplacement" />
                                <label for="deplacement1">bike</label>
                            </span>
                            <span>
                                <input type="radio" value="1" id="deplacement2" name="deplacement" />
                                <label for="deplacement2">walk</label>
                            </span>
                            <span>
                                <input type="radio" value="2" id="deplacement3" name="deplacement" />
                                <label for="deplacement3">train</label>
                            </span>
                            <span>
                                <input type="radio" value="3" id="deplacement4" name="deplacement" />
                                <label for="deplacement4">plane</label>
                            </span>
                            <span>
                                <input type="radio" value="4" id="deplacement5" name="deplacement" checked="checked" />
                                <label for="deplacement5">Other</label>
                            </span>
                        </td>
                        <td class="col3">
                            <span>4</span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Submit" />
            </p>
        </form>
    </body>
</html>

We can see that the posted values (name attributes) are posted to the following fields in the [Form21] model:


    private String married = "no";
    private String deplacement = "4";

Readers are encouraged to run tests. Note that it is the [value] attribute of the radio buttons that is posted.

5.16. [/v23-/v24]: managing checkboxes

We add the following new action:


    // ------------------ form with checkboxes
    @RequestMapping(value = "/v23", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String av20(@ModelAttribute("form") Form21 form, Model model) {
        model.addAttribute("listes", listes);
        return "view-23";
}
  • Line 3: We continue to use the [Form21] model;

The view [vue-23.xml] is as follows:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>
        <h3>Form - Checkboxes</h3>
        <form action="/someURL" th:action="@{/v24.html}" method="post" th:object="${form}">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Text</th>
                        <th class="col2">Input</th>
                        <th class="col3">Value</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Your favorite colors</td>
                        <td class="col2">
                            <input type="checkbox" th:field="*{colors}" value="0" />
                            <label th:for="${#ids.prev('colors')}">red</label>
                            <input type="checkbox" th:field="*{colors}" value="1" />
                            <label th:for="${#ids.prev('colors')}">green</label>
                            <input type="checkbox" th:field="*{colors}" value="2" />
                            <label th:for="${#ids.prev('colors')}">blue</label>
                        </td>
                        <td class="col3">
                            <span th:text="*{strColors}"></span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Favorite stones</td>
                        <td class="col2">
                            <span th:each="label, status : ${listes.libellesBijoux}">
                                <input type="checkbox" th:field="*{bijoux}" th:value="${status.index}" />
                                <label th:for="${#ids.prev('bijoux')}" th:text="${label}">Other</label>
                            </span>
                        </td>
                        <td class="col3">
                            <span th:text="*{strBijoux}"></span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Submit" />
            </p>
        </form>
    </body>
</html>
  • Lines 37–41: Note the use of the [Lists] component to generate the labels for the checkboxes;

This code displays the following page:

 

generated from the following HTML code:


<!DOCTYPE HTML>

<html>
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>
        <h3>Form - Checkboxes</h3>
        <form action="/v24.html" method="post">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Text</th>
                        <th class="col2">Input</th>
                        <th class="col3">Value</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Your favorite colors</td>
                        <td class="col2">
                            <input type="checkbox" value="0" id="colors1" name="colors" /><input type="hidden" name="_colors" value="on" />
                            <label for="colors1">red</label>
                            <input type="checkbox" value="1" id="colors2" name="colors" /><input type="hidden" name="_colors" value="on" />
                            <label for="colors2">green</label>
                            <input type="checkbox" value="2" id="colors3" name="colors" /><input type="hidden" name="_colors" value="on" />
                            <label for="colors3">blue</label>
                        </td>
                        <td class="col3">
                            <span></span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Favorite stones</td>
                        <td class="col2">
                            <span>
                                <input type="checkbox" value="0" id="jewelry1" name="jewelry" /><input type="hidden" name="_jewelry" value="on" />
                                <label for="bijoux1">emerald</label>
                            </span>
                            <span>
                                <input type="checkbox" value="1" id="jewelry2" name="jewelry" /><input type="hidden" name="_jewelry" value="on" />
                                <label for="bijoux2">ruby</label>
                            </span>
                            <span>
                                <input type="checkbox" value="2" id="jewelry3" name="jewelry" /><input type="hidden" name="_jewelry" value="on" />
                                <label for="bijoux3">diamond</label>
                            </span>
                            <span>
                                <input type="checkbox" value="3" id="jewelry4" name="jewelry" /><input type="hidden" name="_jewelry" value="on" />
                                <label for="bijoux4">opal</label>
                            </span>
                        </td>
                        <td class="col3">
                            <span></span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Submit" />
            </p>
        </form>
    </body>
</html>

Note that the posted values (name attributes) are posted to the following fields in [Form21]:


    private String[] colors;
    private String[] jewelry;

These are arrays because for each field, there are multiple checkboxes labeled with the field’s name. It is therefore possible for multiple posted values to arrive with the same name (the form’s name attribute). An array is therefore needed to retrieve them.

Let’s return to the Thymeleaf code in column 3 of the page:


  <td class="col3">
    <span th:text="*{strColors}"></span>
  </td>
</tr>
<tr>
  <td class="col1">Favorite stones</td>
  <td class="col2">
    <span th:each="label, status : ${listes.libellesBijoux}">
      <input type="checkbox" th:field="*{bijoux}" th:value="${status.index}" />
      <label th:for="${#ids.prev('bijoux')}" th:text="${label}">Other</label>
    </span>
  </td>
  <td class="col3">
    <span th:text="*{strBijoux}"></span>
  </td>
</tr>

The fields referenced in lines 2 and 14 are as follows:


    private String strColors;
    private String strJewelry;

They are calculated by the [/v24] action that handles the POST:


    // Jackson / JSON mapper
    private ObjectMapper mapper = new ObjectMapper();

    @RequestMapping(value = "/v24", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String av21(@ModelAttribute("form") Form21 form, RedirectAttributes redirectAttributes) throws JsonProcessingException {
        redirectAttributes.addFlashAttribute("form", form);
        form.setStrColors(mapper.writeValueAsString(form.getColors()));
        form.setStrJewelry(mapper.writeValueAsString(form.getJewelry()));
        return "redirect:/v23.html";
}

Keep in mind that the Jackson/JSON library is included in the project dependencies.

  • Line 2: We create an [ObjectMapper] type that allows us to serialize and deserialize objects to and from JSON.
  • Line 7: We serialize the colors array into JSON. The result is placed in the [strCouleurs] field;
  • Line 8: We serialize the jewelry array into JSON. The result is stored in the [strBijoux] field;

Here is an example of execution:

Note that it is the [value] attribute of the checkboxes that is posted.

5.17. [/25-/v26]: managing lists

We add the following action [/v25]:


  // ------------------ form with lists
  @RequestMapping(value = "/v25", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
  public String v25(@ModelAttribute("form") Form21 form, Model model) {
        model.addAttribute("lists", lists);
        return "view-25";
}

The view [vue-25.xml] is as follows:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>

        <h3>Form - Lists</h3>
        <form action="/someURL" th:action="@{/v26.html}" method="post"
            th:object="${form}">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Text</th>
                        <th class="col2">Input</th>
                        <th class="col3">Value</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Your favorite color</td>
                        <td class="col2">
                            <select th:field="*{color2}">
                                <option value="0">red</option>
                                <option value="1">blue</option>
                                <option value="2">green</option>
                            </select>
                        </td>
                        <td class="col3">
                            <span th:text="*{color2}"></span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Favorite stones (multiple choice)</td>
                        <td class="col2">
                            <select th:field="*{bijoux2}" multiple="multiple" size="3">
                                <option th:each="label, status : ${listes.libellesBijoux}"
                                    th:text="${label}" th:value="${status.index}">
                                </option>
                            </select>
                        </td>
                        <td class="col3">
                            <span th:text="*{strBijoux2}"></span>
                        </td>
                    </tr>

                </tbody>
            </table>
            <input type="submit" value="Submit" />
        </form>
    </body>
</html>
  • lines 38-42: generating a dropdown list where the labels are taken from the [Lists] component we have already used;

The displayed page is as follows:

 

generated by the following HTML code:


<!DOCTYPE HTML>

<html>
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form19.css" />
    </head>
    <body>

        <h3>Form - Lists</h3>
        <form action="/v26.html" method="post">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Text</th>
                        <th class="col2">Input</th>
                        <th class="col3">Value</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Your favorite color</td>
                        <td class="col2">
                            <select id="color2" name="color2">
                                <option value="0" selected="selected">red</option>
                                <option value="1">blue</option>
                                <option value="2">green</option>
                            </select>
                        </td>
                        <td class="col3">
                            <span>0</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Favorite stones (multiple choice)</td>
                        <td class="col2">
                            <select multiple="multiple" size="3" id="jewelry2" name="jewelry2">
                                <option value="0">emerald</option>
                                <option value="1">ruby</option>
                                <option value="2">Diamond</option>
                                <option value="3">opal</option>
                            </select>
                            <input type="hidden" name="_bijoux2" value="1" />
                        </td>
                        <td class="col3">
                            <span></span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Submit" />
            </p>
        </form>
    </body>
</html>
  • Line 44: Note that Thymeleaf has created a hidden field. I don’t understand its purpose:
  • the posted values (value attributes of the option tags) will be stored in the following fields (name attributes) of [Form21]:

    private int color2;
    private int[] jewelry2;
  • line 38: the list [jewelry2] is a multiple-choice list. Therefore, multiple values can be posted associated with the name [jewelry2]. To retrieve them, the field [jewelry2] must be an array. Note that it is an array of integers. This is possible because the posted values can be converted to this type;

The values are posted to the following [/v26] action:


  @RequestMapping(value = "/v26", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
  public String v26(@ModelAttribute("form") Form21 form, RedirectAttributes redirectAttributes) throws JsonProcessingException {
    redirectAttributes.addFlashAttribute("form", formulaire);
    form.setStrBijoux2(mapper.writeValueAsString(form.getBijoux2()));
    return "redirect:/v25.html";
}

There is nothing here that we haven't seen before. Here is an example of execution:

5.18. [/v27]: configuring messages

Consider the following [/v27] action:


  // ------------------ configured messages
  @RequestMapping(value = "/v27", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
  public String v27(Model model) {
        model.addAttribute("param1", "parameter one");
        model.addAttribute("param2", "parameter two");
        model.addAttribute("param3", "parameter three");
        model.addAttribute("param4", "messages.param4");        
        return "view-27";
}

The action simply sets four values in the model and displays the following view [view-27.xml]:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{messages.title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <h2 th:text="#{messages.title}">Spring 4 MVC</h2>
        <p th:text="#{messages.msg1(${param1})}"></p>
        <p th:text="#{messages.msg2(${param2},${param3})}"></p>
        <p th:text="#{messages.msg3(#{${param4}})}"></p>
    </body>
</html>
  • line 8: a message without parameters;
  • line 9: a message with one parameter [$param1] taken from the template;
  • line 10: a message with two parameters [$param2, $param3] taken from the template;
  • line 11: a message with one parameter. This parameter is itself a message key (indicated by #). The key is provided by [$param4];

The French message file is as follows:

[messages_fr.properties]


messages.title=Parameterized messages
messages.msg1=A message with one parameter: {0}
messages.msg2=A message with two parameters: {0}, {1}
messages.msg3=A message with a message key as a parameter: {0}
messages.param4=parameter four

To indicate the presence of parameters in the message, we use the symbols {0}, {1}, ...

Merging the template built by the [/v27] action with the [vue-27] view will produce the following HTML code:


<!DOCTYPE html>

<html>
    <head>
        <title>Parameterized messages</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <h2>Parameterized messages</h2>
        <p>A message with one parameter: parameter one</p>
        <p>A message with two parameters: parameter two, parameter three</p>
        <p>A message with a message key as a parameter: parameter four</p>
    </body>
</html>

which results in the following view:

 

The English message file is as follows:

[messages_fr.properties]


messages.title=Parameterized messages
messages.msg1=Message with one parameter: {0}
messages.msg2=Message with two parameters: {0}, {1}
messages.msg3=Message with a message key as a parameter: {0}
messages.param4=parameter four

Merging the template built by the [/v27] action with the [vue-27] view will produce the following HTML code:


<!DOCTYPE html>

<html>
    <head>
        <title>Parameterized messages</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <h2>Parameterized messages</h2>
        <p>Message with one parameter: parameter one</p>
        <p>Message with two parameters: parameter two, parameter three</p>
        <p>Message with a message key as a parameter: parameter four</p>
    </body>
</html>

which results in the following view:

 

We can see that the last message has been fully internationalized, which is not the case for the previous two.

5.19. Using a master page

In a web application, views often share a number of elements that can be factored into a master page. Here is an example:

Above, we have two similar pages where fragment [1] has been replaced by fragment [2]. The view is that of a master page with three fixed fragments [3-5] and one variable fragment [6].

5.19.1. The project

We are building a project [springmvc-masterpage] following the approach described in Section 5.1.

  

The [pom.xml] file is as follows:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>istia.st.springmvc</groupId>
    <artifactId>springmvc-masterpage</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>springmvc-masterpage</name>
    <description>Master page</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.9.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>istia.st.springmvc.main.Main</start-class>
        <java.version>1.7</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

One of the dependencies included in this file is required for the master page:

 

The [config] and [main] packages are identical to those of the same names in the previous project.

5.19.2. The master page

  

The master page is the following [layout.xml] view:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head>
        <title>Layout</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <table style="width: 400px">
            <tr>
                <td colspan="2" bgcolor="#ccccff">
                    <div th:include="header" />
                </td>
            </tr>
            <tr style="height: 200px">
                <td bgcolor="#ffcccc">
                    <div th:include="menu" />
                </td>
                <td>
                    <section layout:fragment="content">
                        <h2>Content</h2>
                    </section>
                </td>
            </tr>
            <tr bgcolor="#ffcc66">
                <td colspan="2">
                    <div th:include="footer" />
                </td>
            </tr>
        </table>
    </body>
</html>
  • line 2: the master page must define the namespace [xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"], an element of which is used on line 19;
  • lines 10–12: generate the area [1] below. The Thymeleaf tag [th:include] allows you to include a fragment defined in another file into the current view. This allows you to reuse fragments across multiple views;
  • lines 15–17: generate the [2] area below;
  • lines 19–20: generate the [3] area below. The [layout:fragment] attribute belongs to the [xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"] namespace. It indicates an area that can be replaced by another at runtime;
  • lines 24–28: generate the area [4] below;

5.19.3. The fragments

The fragments [entete.xml], [menu.xml], and [basdepage.xml] are as follows:

[entete.xml]


<!DOCTYPE html>
<html>
    <h2>header</h2>
</html>

[menu.xml]


<!DOCTYPE html>
<html>
    <h2>menu</h2>
</html>

[footer.xml]


<!DOCTYPE html>
<html>
    <h2>footer</h2>
</html>

The fragment [page1.xml] is as follows:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorator="layout">
    <section layout:fragment="content">
        <h2>Page 1</h2>
        <form action="/someURL" th:action="@{/page2.html}" method="post">
            <input type="submit" value="Page 2" />
        </form>
    </section>
</html>
  • line 2: the [layout:decorator="layout"] attribute indicates that the current page [page1.xml] is "decorated," i.e., it belongs to a master page. This is the value of the attribute, in this case the view [layout.xml];
  • line 3: this specifies which fragment of the master page [page1.xml] will be inserted into. The [layout:fragment="contenu"] attribute indicates that [page1.xml] will be inserted into the fragment named [contenu], i.e., zone [3] of the master page;
  • lines 5–7: the content of the fragment is a form that includes a POST button pointing to the action [/page2.html];

The fragment [page2.xml] is similar:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
    layout:decorator="layout">
    <section layout:fragment="content">
        <h2>Page 2</h2>
        <form action="/someURL" th:action="@{/page1.html}" method="post">
            <input type="submit" value="Page 1" />
        </form>
    </section>
</html>

5.19.4. The actions

 

The controller [Layout.java] is as follows:


package istia.st.springmvc.controllers;

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

@Controller
public class Layout {
    @RequestMapping(value = "/page1")
    public String page1() {
        return "page1";
    }

    @RequestMapping(value = "/page2", method=RequestMethod.POST)
    public String page2() {
        return "page2";
    }
}
  • lines 10–12: the [/page1] action simply displays the [page1.xml] view;
  • lines 15-17: same for the [/page2] action, which displays the [page2.xml] view;