Skip to content

5. Les vues Thymeleaf

Revenons à l'architecture d'une application Spring MVC.

Les deux chapitres précédents ont décrit divers aspects du bloc [1], les actions. Nous abordons maintenant :

  • le bloc [2] des vues V ;
  • le bloc [3] du modèle M affiché par ces vues ;

Depuis la création de Spring MVC, la technologie de génération des pages HTML envoyées aux navigateurs client était celle des pages JSP (Java Server Pages). Depuis quelques années, la technologie [Thymeleaf] [http://www.thymeleaf.org/] peut être également utilisée. C'est elle que nous présentons maintenant.

5.1. Le projet STS

Nous créons un nouveau projet :

  • en [3], indiquer que le projet a besoin des dépendances [Thymeleaf]. Cela amènera en plus des dépendances [Spring MVC] du projet précédent, celles du framework [Thymeleaf] [5] ;

Maintenant, faisons évoluer ce projet de la façon suivante :

  

Nous nous inspirons du projet précédent :

  • [istia.st.springmvc.controllers] contiendra les contrôleurs ;
  • [istia.st.springmvc.models] contiendra les modèles des actions et des vues ;
  • [istia.st.springmvc.main] est le package de la classe exécutable Spring Boot ;
  • [templates] contiendra les vues Thymeleaf ;
  • [i18n] contiendra les messages internationalisés affichés par les vues ;

La classe [Application] est la suivante :


package istia.st.springmvc.main;

import org.springframework.boot.SpringApplication;

public class Application {

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

La classe [Config] est la suivante :


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;
    }
}

Cette configuration permet pour l'instant la gestion des locales.

Le contrôleur [ViewController] est le suivant :


package istia.st.springmvc.actions;

import org.springframework.stereotype.Controller;

@Controller
public class ViewsController {

}
  • ligne 5, l'annotation [@Controller] a remplacé l'annotation [@RestController] car désormais, les actions ne vont pas générer la réponse au client. Elles vont :
    • construire un modèle M
    • rendre un type [String] qui sera le nom de la vue [Thymeleaf] chargée d'afficher ce modèle. C'est la combinaison de cette vue V et de ce modèle M qui va générer le flux HTML envoyé au client ;

Le fichier [messages.properties] est pour l'instant vide.

5.2. [/v01] : les bases de Thymeleaf

Nous considérons la première action suivante dans [ViewsController] :


    // les bases de Thymeleaf - 1
    @RequestMapping(value = "/v01", method = RequestMethod.GET)
    public String v01() {
        return "v01";
}
  • ligne 3 : l'action rend un type [String]. Ce sera le nom de l'action ;
  • ligne 4 : cette vue sera [v01]. Par défaut, elle doit se trouver dans le dossier [templates] et s'appeler [v01.html] ;

La vue [v01.html] est la suivante :


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

C'est un fichier HTML. La présence de Thymeleaf se voit :

  • à l'espace de noms [th] de la ligne 2 ;
  • aux attributs [th:text] des lignes 4 et 8 ;

On a là un fichier HTML valide qui peut être visualisé. Nous le mettons dans le dossier [static] [2] sous le nom [vue-01.html] et nous le demandons directement avec un navigateur :

Si nous examinons le code source de la page en [2], nous pouvons constater que les attributs [th:text] ont été envoyés par le serveur et été ignorés par le navigateur. Lorsqu'une vue est le résultat d'une action, Thymeleaf entre en oeuvre et interprète les attributs [th] avant l'envoi de la réponse au client.

La balise HTML :


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

est traitée de la façon suivante par Thymeleaf :

  • th:text a la syntaxe th:text="expression" où expression est une expression à évaluer. Lorsque cette expression est une chaîne de caractères comme ici, il faut entourer celle-ci par des apostrophes ;
  • la valeur de [expression] remplace le texte de la balise HTML, ici le texte de la balise [title] ;

Après traitement, la balise ci-dessus est devenue :


<title>Les vues</title>

Demandons l'action [/v01] :

  • en [2], on voit le travail de remplacement fait par Thymeleaf ;

Maintenant demandons l'URL [http://localhost:8080/v01.html] :

 

Comment faut-il interpréter cela ? La vue [templates/v01.html] a-t-elle été servie directement sans passer par une action ? Pour éclaircir les choses, nous créons l'action [/v02] suivante :


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

La vue [vue-02.html] est une copie de [v01.html] :

  

Maintenant demandons l'URL [http://localhost:8080/vue-02.html] :

 

L'URL n'a pas été trouvée. Maintenant demandons l'URL [http://localhost:8080/v02.html]

  • dans les logs console en [1], on voit que l'action [/v02] a été appelée, et celle-ci a fait afficher la vue [vue-02.html] en [2] ;

Maintenant on sait que l'URL [http://localhost:8080/v02.html] peut désigner également un fichier [/v02.html] dans le dossier [static]. Que se passe-t-il si ce fichier existe ? Nous essayons. Nous créons dans le dossier [static] le fichier [v02.html] suivant :

  

<!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>

puis nous demandons l'URL [http://localhost:8080/v02.html] :

[1] et [2] montrent que c'est l'action [/v02] qui a été appelée. On retiendra donc que lorsque l'URL demandée est de la forme [/x.html], Spring / Thymeleaf :

  • exécute l'action [/x] si elle existe ;
  • sert la page [/static/x.html] si elle existe ;
  • lance une exception 404 Not found sinon ;

Pour éviter des confusions, à partir de maintenant, les actions et les vues n'auront pas les mêmes noms.

5.3. [/v03] : internationalisation des vues

L'intégration Spring / Thymeleaf permet à Thymeleaf d'utiliser les fichiers de messages de Spring. Considérons la nouvelle action [/v03] suivante :


    // internationalisation des vues
    @RequestMapping(value = "/v03", method = RequestMethod.GET)
    public String v03() {
        return "vue-03";
}

Elle fait afficher la vue [vue-03.html] suivante :

  

<!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>

Aux lignes 4 et 8, l'expression de l'attribut [th:text] est #{title} dont la valeur est le message de clé [title]. Nous créons les fichiers [messages_fr.properties] et [messages_en.properties] suivants :

[messages_fr.properties]


title=Les vues dans Spring MVC

[messages_en.properties]


title=Views in Spring MVC

Demandons les URL [http://localhost:8080/v03.html?lang=fr] et [http://localhost:8080/v03.html?lang=en] :

Remarquons que nous avons utilisé ce que nous avons appris récemment. Plutôt que de désigner l'action [v03] par [/v03], nous l'avons désigné par [/v03.html].

5.4. [/v04] : création du modèle M d'une vue V

Considérons la nouvelle action [/v04] suivante :


    // création du modèle M d'une vue V
    @RequestMapping(value = "/v04", method = RequestMethod.GET)
    public String v04(Model model) {
        model.addAttribute("personne", new Personne(7, "martin", 17));
        System.out.println(String.format("Modèle=%s", model));
        return "vue-04";
}
  • ligne 4 : le modèle de la vue est injecté dans les paramètres de l'action. Par défaut, ce modèle initial est vide. On verra qu'il est possible de le pré-remplir ;
  • ligne 4 : un modèle de type [Model] est une sorte de dictionnaire d'éléments de type <String, Object>. Ligne 4, nous ajoutons une entrée dans ce dictionnaire avec la clé [personne] associée à une valeur de type [Personne] ;
  • ligne 5 : on affiche sur la console le modèle pour voir à quoi il ressemble ;
  • ligne 6 : on fait afficher la vue [vue-04.html] ;

La classe [Personne] est celle utilisée dans le chapitre précédent :

  

package istia.st.springmvc.models;

public class Personne {

    // identifiant
    private Integer id;
    // nom
    private String nom;
    // âge
    private int age;

    // constructeurs
    public Personne() {

    }

    public Personne(String nom, int age) {
        this.nom = nom;
        this.age = age;
    }

    public Personne(Integer id, String nom, int age) {
        this(nom, age);
        this.id = id;
    }

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

    // getters et setters
...
}

La vue [vue-04.html] est la suivante :

  

<!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}">Nom :</span>
            <span th:text="${personne.nom}">Bill</span>
        </p>
        <p>
            <span th:text="#{personne.age}">Age :</span>
            <span th:text="${personne.age}">56</span>
        </p>
    </body>
</html>
  • la ligne 10, introduit un nouveau type d'expression Thymeleaf ${var}var est une clé du modèle M de la vue. On se rappelle que l'action [/v04] a mis dans le modèle une clé [personne] associée à un type Personne[id, nom, age] ;
  • ligne 10 : affiche le nom de la personne présente dans le modèle ;
  • ligne 14 : affiche son âge ;

Les fichiers de messages sont modifiés pour ajouter les clés [personne.nom] et [personne.age] des lignes 9 et 13. Le résultat est le suivant :

et on trouve la nature du modèle M dans les logs de la console [2].

On peut se demander pourquoi on n'écrit pas la vue [vue-04] de la façon suivante :


<!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="#{personne.nom}" /></span>
            <span th:text="${personne.nom}"></span>
        </p>
        <p>
            <span th:text="#{personne.age}"></span>
            <span th:text="${personne.age}"></span>
        </p>
    </body>
</html>

Cette vue est parfaitement licite et donnera le même résultat que précédemment. L'un des objectifs de Thymeleaf est que la page Thymeleaf puisse être affichée même si elle ne passe pas dans les mains de Thymeleaf. Ainsi, créons deux nouvelles pages statiques :

  

La vue [vue-04b.html] est une copie de la vue [vue-04.html]. Il en est de même pour la vue [vue-04a.html] mais on a enlevé les textes statiques de la page. Si nous visualisons les deux pages, on a les résultats suivants :

Dans le cas [1], la structure de la page n'apparaît pas alors que dans le cas [2] elle est bien visible. Voilà l'intérêt de mettre des textes statiques dans une vue Thymeleaf même si à l'exécution ils vont être remplacés par d'autres textes.

Maintenant, regardons un détail technique. Dans la vue [vue-04.html], nous mettons le code en forme par [ctrl-Maj-F]. Nous obtenons le résultat suivant :


<!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}">Nom :</span> <span
            th:text="${personne.nom}">Bill</span>
    </p>
    <p>
        <span th:text="#{personne.age}">Age :</span> <span
            th:text="${personne.age}">56</span>
    </p>
</body>
</html>

Les balises sont mal alignées et le code devient plus difficile à lire. Si nous renommons [vue-04.html] en [vue-04.xml] et que nous reformatons le code, alors les balises redeviennent alignées. Donc le suffixe [xml] serait plus pratique. Il est possible de travailler avec ce suffixe. Il faut pour cela configurer Thymeleaf. Pour ne pas défaire ce que nous avons fait, nous dupliquons le projet [springmvc-vues] étudié en un projet [springmvc-vues-xml]

  

Nous modifions le fichier [pom.xml] de la façon suivante :


    <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>Les vues dans Spring MVC</description>

Le nom du projet est changé aux lignes 2 et 6. Par ailleurs, nous changeons le suffixe des vues présentes dans le dossier [templates] :

  

Le document [http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html] liste les propriétés de configuration de Spring Boot utilisables dans le fichier [application.properties] :

  

Ce document donne les propriétés que Spring Boot utilise lorsqu'il fait de l'autoconfiguration et qu'on peut modifier en faisant une configuration différente dans [application.properties]. Pour Thymeleaf, les propriétés d'autoconfiguration sont les suivantes :


# 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

On pourrait donc se contenter de mette la ligne


spring.thymeleaf.suffix=.xml

dans [application.properties]. Nous allons suivre une autre voie, celle de la configuration par programmation. Nous allons configurer Thymeleaf dans la classe [Config] :


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;
    }

}
  • les lignes 16-24 configurent un [TemplateResolver] pour Thymeleaf. C'est cet objet qui est chargé à partir d'un nom de vue délivré par une action, de trouver le fichier correspondant ;
  • lignes 18 et 19 fixent le préfixe et le suffixe à ajouter au nom de la vue pour trouver le fichier. Ainsi si le nom de la vue est [vue04], le fichier cherché sera [classpath:/templates/vue04.xml]. [classpath:/templates] est une syntaxe Spring qui désigne un dossier [/templates] placé à la racine du Classpath du projet ;
  • ligne 21 : pour que dans la réponse faite au client on ait l'entête HTTP :

Content-Type:text/html;charset=UTF-8
  • ligne 20 : indique que la vue respecte la norme HTML5 ;
  • ligne 22 : indique que les vues Thymeleaf peuvent être mises en cache ;
  • lignes 26-31 : fixe le moteur de résolution des vues du couple Spring / Thymeleaf avec le moteur de résolution précédent ;

Lançons l'exécutable de ce nouveau projet et demandons l'URL [http://localhost:8080/v04.html?lang=en] :

 

On remarque que dans l'URL, l'action [/v04] a pu être remplacée là encore par [v04.html].

5.5. [/v05] : factorisation d'un objet dans une vue Thymeleaf

Nous créons l'action [/v05] suivante :


    // création du modèle M d'une vue V - 2
    @RequestMapping(value = "/v05", method = RequestMethod.GET)
    public String v05(Model model) {
        model.addAttribute("personne", new Personne(7, "martin", 17));
        return "vue-05";
}

Elle est identique à l'action [/v04]. La vue [vue-05.xml] est la suivante :

  

<!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.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • lignes 8-17 : à l'intérieur de ces lignes un objet Thymeleaf est défini par l'attribut [th:object="${personne}"] (ligne 8). Cet objet est ici l'objet de clé [personne] qui est dans le modèle :
  • ligne 11 : l'expression Thymeleaf [*{nom}] est équivalente à [${objet.nom}] où [objet] est l'objet Thymeleaf courant. Donc ici l'expression [*{nom}] est équivalente à [${personne.nom}] ;
  • ligne 15 : idem ;

Le résultat :

 

5.6. [/v06] : les tests dans une vue Thymeleaf

Considérons l'action [/v06] suivante :


    // création du modèle M d'une vue V - 3
    @RequestMapping(value = "/v06", method = RequestMethod.GET)
    public String v06(Model model) {
        model.addAttribute("personne", new Personne(7, "martin", 17));
        return "vue-06";
}

Elle est identique aux deux précédentes actions. Elle affiche la vue [vue-06.xml] suivante :


<!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.nom}">Nom :</span>
                <span th:text="*{nom}">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}">Vous êtes majeur</p>
            <p th:if="*{age} &lt; 18" th:text="#{personne.mineure}">Vous êtes mineur</p>
        </div>
    </body>
</html>
  • ligne 17 : l'attribut [th:if] évalue une expression booléenne. Si cette expression est vraie, la balise est affichée sinon elle ne l'est pas. Donc ici si ${personne.age}>=18, le texte [#{personne.majeure}] sera affiché, ç-à-d le message de clé [personne.majeure] dans les fichiers de messages ;
  • ligne 18 : on ne peut pas écrire [*{age} < 18] car le signe < est un caractère réservé. Il faut donc utiliser son équivalent HTML [&lt;] appelé également entité HTML [http://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references];

Les fichiers de messages sont modifiés :

[messages_fr.properties]


title=Les vues dans Spring MVC
personne.nom=Nom :
personne.age=Age :
personne.mineure=Vous êtes mineur
personne.majeure=Vous êtes majeur

[messages_en.properties]


title=Views in Spring MVC
personne.nom=Name:
personne.age=Age:
personne.mineure=You are under 18
personne.majeure=You are over 18

Le résultat est le suivant :

5.7. [/v07] : itération dans une vue Thymeleaf

Considérons l'action [/v07] suivante :


    // création du modèle M d'une vue V - 4
    @RequestMapping(value = "/v07", method = RequestMethod.GET)
    public String v07(Model model) {
        model.addAttribute("liste", new Personne[] { new Personne(7, "martin", 17), new Personne(8, "lucie", 32),
                new Personne(9, "paul", 7) });
        return "vue-07";
}
  • l'action crée une liste de trois personnes, la met dans le modèle associée à la clé [liste] et fait afficher la vue [vue-07] ;

La vue [vue-07.xml] est la suivante :


<!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}">Liste de personnes</h3>
        <ul>
            <li th:each="element : ${liste}" th:text="'['+ ${element.id} + ', ' +${element.nom}+ ', ' + ${element.age} + ']'">[id,nom,age]</li>
        </ul>
    </body>
</html>
  • ligne 10 : l'attribut [th:each] répète la balise dans laquelle elle se trouve, ici une balise <li>. Elle a ici deux paramètres [element : collection] où [collection] est une collection d'objets, ici une liste de personnes. Thymeleaf va parcourir la collection et générer autant de balises <li> qu'il y a d'éléments dans la collection. Pour chaque balise <li> [element] va représenter l'élément de la collection attaché à la balise. Pour cet élément, l'attribut [th:text] va être évalué. Son expression est ici une concaténation de chaînes pour avoir le résultat [id, nom, age] ;
  • ligne 8 : on ajoute la clé [liste.personnes] dans les fichiers de messages ;

Voici le résultat :

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

Nous revenons sur quelque chose que nous avons vu lors de l'étude des actions, le rôle de l'annotation [@ModelAttribute]. Nous ajoutons la nouvelle action suivante :


    // --------------- Binding et ModelAttribute ----------------------------------

    // si le paramètre est un objet, il est instancié et éventuellement modifié par les paramètres de la requête
    // il fera automatiquement partie du modèle de la vue avec la clé [key]
    // pour @ModelAttribute("xx") paramètre, key sera égal à xx
    // pour @ModelAttribute paramètre, key sera égal au nom de la classe du paramètre commençant avec une minuscule
    // si @ModelAttribute est absent, alors tout se passe comme s'il était présent sans clé
    // on notera que cette présence automatique dans le modèle n'est pas effectuée si le paramètre n'est pas un objet

    @RequestMapping(value = "/v08", method = RequestMethod.GET)
    public String v08(@ModelAttribute("someone") Personne p, Model model) {
        System.out.println(String.format("Modèle=%s", model));
        return "vue-08";
}
  • ligne 11 : l'annotation [@ModelAttribute("someone")] va automatiquement ajouter l'objet [Personne p] dans le modèle, associé à la clé [someone] ;
  • ligne 12 : pour vérifier le modèle ;
  • ligne 13 : affiche la vue [vue-08.xml] ;

La vue [vue-08.xml] est la suivante :


<!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="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • ligne 8 : l'objet Thymeleaf est initialisé avec l'objet de clé [someone] ;

Le résultat est le suivant :

 

et dans la console, on a le log suivant :

Modèle={someone=[id=4, nom=x,  age=11], org.springframework.validation.BindingResult.someone=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

Considérons maintenant l'action [/v09] suivante :


    @RequestMapping(value = "/v09", method = RequestMethod.GET)
    public String v09(Personne p, Model model) {
        System.out.println(String.format("Modèle=%s", model));
        return "vue-09";
}
  • ligne 1 : la présence du paramètre [Personne p] va automatiquement mettre la personne [p] dans le modèle. Comme il n'est pas précisé de clé, la clé utilisée est le nom de la classe avec son premier caractère en minuscule. Donc [Personne p] est équivalent à [@ModelAttribute("personne") Personne p] ;

La vue [vue.09.xml] est la suivante :


<!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="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • ligne 8 : la clé de modèle utilisée est [personne] ;

Voici un résultat :

 

et le log dans la console du serveur :

Modèle={personne=[id=4, nom=x,  age=11], org.springframework.validation.BindingResult.personne=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

Maintenant, considérons la nouvelle action [/v10] suivante :


    @ModelAttribute("uneAutrePersonne")
    private Personne getPersonne(){
        return new Personne(24,"pauline",55);
    }

    @RequestMapping(value = "/v10", method = RequestMethod.GET)
    public String v10(Model model) {
        System.out.println(String.format("Modèle=%s", model));
        return "vue-10";
}
  • lignes 1-4 : définissent une méthode créant dans le modèle de chaque requête un élément de clé [uneAutrePersonne] associé à l'objet [new Personne(24,"pauline",55)] ;
  • lignes 6-10 : l'action [/v10] ne fait rien si ce n'est de passer le modèle qu'elle reçoit à la vue [vue-10.xml]. A Noter que le paramètre [Model model] n'a besoin d'être présent que pour l'instruction de la ligne 8. Sans elle, il est inutile ;

La vue [vue-10.xml] est la suivante :


<!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="${uneAutrePersonne}">        
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>

Le résultat est le suivant :

 

et le log console le suivant :

Modèle={uneAutrePersonne=[id=24, nom=pauline,  age=55]}

5.9. [/v11] : @SessionAttributes

Nous revenons sur quelque chose que nous avons vu lors de l'étude des actions, le rôle de l'annotation [@SessionAttributes]. Nous ajoutons la nouvelle action [/v11] suivante :


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

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

Nous avons quelque chose d'analogue à ce qui vient d'être étudié. La différence réside en une annotation [@SessionAttributes] placée sur la classe elle-même :


@Controller
@SessionAttributes("jean")
public class ViewsController {
  • ligne 2 : on indique que la clé [jean] du modèle doit être placé dans la session ;

C'est pourquoi en ligne 7 de l'action, on a injecté la session. Ligne 8, on affiche la valeur de la session associée à la clé [jean].

La vue [vue-11.xml] est la suivante :


<!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="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">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="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>

On affiche deux personnes :

  • lignes 8-21 : la personne de clé [jean] dans le modèle ;
  • lignes 23-36 : la personne de clé [jean] dans la session ;

Les résultats sont les suivants :

  • en [1], la personne de clé [jean] dans le modèle ;
  • en [2], la personne de clé [jean] dans la session ;

Le log console est lui le suivant :


Modèle={uneAutrePersonne=[id=24, nom=pauline,  age=55], jean=[id=33, nom=jean,  age=10]}, Session[jean]=null

Ci-dessus, on voit que la clé [jean] n'est pas dans la session que reçoit l'action. On en déduit, que la clé [jean] a été mise dans la session après l'exécution de l'action et avant l'affichage de la vue.

Maintenant, considérons le cas où une clé est à la fois référencée par [@ModelAttribute] et [@SessionAttributes]. Nous construisons les deux actions suivantes :


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

    // cas où la clé de [@ModelAttribute] est également une clé de [@SessionAttributes]
    // dans ce cas, le paramètre correspondant est initialisé avec la valeur de la session
    @RequestMapping(value = "/v12b", method = RequestMethod.GET)
    public String v12b(Model model, @ModelAttribute("paul") Personne p) {
        System.out.println(String.format("Modèle=%s", model));
        return "vue-12";
}

L'action [/v12a] ne sert qu'à mettre dans la session l'élément ['paul',new Personne(51, "paul", 33)]. Elle ne fait rien d'autre. Le fait qu'elle soit taguée par [@ResponseBody] indique que c'est elle qui génère la réponse au client. Comme son type est [void], aucune réponse n'est générée.

L'action [/v12b] admet comme paramètre [@ModelAttribute("paul") Personne p]. Si on ne fait rien d'autre, un objet [Personne] est instancié puis initialisé avec les paramètres de la requête et cet objet n'a rien à voir avec l'objet de clé [paul] mis dans la session par l'action [/v12a]. Nous allons ajouter la clé [paul] aux attributs de session de la classe :


@Controller
@SessionAttributes({ "jean", "paul" })
public class ViewsController {
  • ligne 2, il y a désormais deux attributs de session ;

Revenons aux paramètres de l'action [/v12b] :


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

Maintenant, l'objet [Personne p] ne va pas être instancié mais va référencer l'objet de clé [paul] dans la session. Ensuite la procédure reste la même. L'objet de clé [paul] va notamment se retrouver dans le modèle de la vue qui sera affichée. C'est ce qu'on veut voir ligne 11 de l'action [/v12b].

La vue [vue-12.xml] sera la suivante :


<!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="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • ligne 8 : on référence la clé [paul] du modèle de la vue ;

Cela donne le résultat suivant (après avoir exécuté l'action [/v12a] qui met la clé [paul] dans la session) :

 

Le log console est le suivant :


Modèle={jean=[id=33, nom=jean,  age=10], uneAutrePersonne=[id=24, nom=pauline,  age=55], paul=[id=51, nom=paul,  age=33], org.springframework.validation.BindingResult.paul=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

La clé [paul] a bien été mise dans le modèle avec pour valeur, la valeur associée à la clé [paul] dans la session.

5.10. [/v13] : générer un formulaire de saisie

Nous abordons maintenant la saisie des formulaires et leur validation. Nous construisons un premier formulaire avec l'action [/v13] suivante :


  // génère un formulaire pour saisir une personne
  @RequestMapping(value = "/v13", method = RequestMethod.GET)
  public String v13() {
    return "vue-13";
}

qui se contente d'afficher la vue [vue-13.xml] suivante :


<!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="#{personne.formulaire.titre}">Entrez les informations suivantes</h2>
            <div th:object="${personne}">
                <table>
                    <thead></thead>
                    <tbody>
                        <tr>
                            <td th:text="#{personne.id}">Id :</td>
                            <td>
                                <input type="text" name="id" value="11" th:value="''" />
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.nom}">Nom :</td>
                            <td>
                                <input type="text" name="nom" value="Tintin" th:value="''" />
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.age}">Age :</td>
                            <td>
                                <input type="text" name="age" value="17" th:value="''" />
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
            <input type="submit" value="Valider" th:value="#{personne.formulaire.valider}" />
        </form>
    </body>
</html>

Si nous mettons cette vue dans le dossier [static] sous le nom [vue-13.html] et que nous demandons l'URL [http://localhost:8080/vue-13.html], nous obtenons la page suivante :

 
  • ligne 8 du formulaire, on trouve la balise <form> avec l'attribut [th:action]. Cet attribut va être évalué par Thymeleaf et sa valeur remplacer le valeur actuelle de l'attribut [action] qui n'est donc là que pour décorer. Ici la valeur de l'attribut [th:action] sera [/v14.html] ;
  • lignes 17, 23 et 29, la valeur de l'attribut [th:value] va remplacer celle de l'attribut [value]. Ici cette valeur sera la chaîne vide ;

Lorsqu'on demande l'URL [/v13.html], on obtient le résultat suivant :

 

Regardons le code source généré par 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="nom" value="" />
                            </td>
                        </tr>
                        <tr>
                            <td>Age:</td>
                            <td>
                                <input type="text" name="age" value="" />
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
            <input type="submit" value="Validate" />
        </form>
    </body>
</html>

Lignes 9, 18, 24 et 30, on voit l'évaluation des attributs [th:action] et [th:value] faite par Thymeleaf.

5.11. [/v14] : gérer les valeurs postées par un formulaire

L'action [/v14] est l'action qui reçoit les valeurs postées. C'est la suivante :


  // traite les valeurs du formulaire
  @RequestMapping(value = "/v14", method = RequestMethod.POST)
  public String v14(Personne p) {
    return "vue-14";
}
  • ligne 3 : les valeurs postées sont encapsulées dans un objet [Personne p]. On sait que cet objet fait automatiquement partie du modèle M de la vue V qui sera affichée par l'action, associé à la clé [personne] ;
  • ligne 4, la vue affichée est la vue [vue-14.xml] ;

La vue [vue-14.xml] est la suivante :


<!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="#{personne.formulaire.saisies}">Voici vos saisies</h2>
        <div th:object="${personne}">        
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>
  • ligne 9 : on récupère dans le modèle l'objet associé à la clé [personne] ;
  • lignes 12, 16 et 20 : on affiche les caractéristiques de cet objet ;

Cela donne le résultat suivant :

5.12. [/v15-/v16] : validation d'un modèle

Avec l'exemple précédent, regardons la séquence suivante :

  • en [1], on rentre des valeurs erronées pour les champs [id] et [age] de type [int] ;
  • en [2], la réponse du serveur nous indique qu'il y a eu deux erreurs ;

Nous allons utiliser le même formulaire mais en cas d'erreurs de validation, nous allons renvoyer une page signalant ces erreurs afin que l'utilisateur puisse les corriger.

L'action [/v15] est la suivante :


    // ---------------------- affichage d'un formulaire
    @RequestMapping(value = "/v15", method = RequestMethod.GET)
    public String v15(SecuredPerson p) {
        return "vue-15";
}

Elle reçoit en paramètre un type [SecuredPerson] suivant :

  

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 nom;
    
    @Range(min = 8, max = 14)
    private int age;

    // constructeurs
    public SecuredPerson() {

    }

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

    // getters et setters
...
}

Les champs [id, nom, age] ont été annotés avec des contraintes de validation. La vue [vue-15.xml] affichée par l'action [/v15] est la suivante :


<!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="#{personne.formulaire.titre}">Entrez les informations suivantes</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">Identifiant erroné</span>
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.nom}">Nom :</td>
                            <td>
                                <input type="text" name="nom" value="Tintin" th:value="*{nom}" />
                            </td>
                            <td>
                                <span th:if="${#fields.hasErrors('nom')}" th:errors="*{nom}" style="color: red">Nom erroné</span>
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.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">Âge erroné</span>
                            </td>
                        </tr>
                    </tbody>
                </table>
                <input type="submit" value="Valider" th:value="#{personne.formulaire.valider}" />
                <ul>
                    <li th:each="err : ${#fields.errors('*')}" th:text="${err}" style="color: red" />
                </ul>
            </div>
        </form>
    </body>
</html>
  • lignes 10-47 : l'objet du modèle de la page attaché à la clé [securedPerson] est récupéré. A l'issue du GET, on a un objet avec sa valeur d'instanciation [id=0, nom=null, age=0] ;
  • ligne 17 : la valeur du champ [securedPerson.id] ;
  • ligne 20 : l'expression [${#fields.hasErrors('id')}] permet de savoir s'il y a eu des erreurs de validation sur le champ [securedPerson.id]. Si c'est le cas, l'attribut [th:errors="*{id}"] affiche le message d'erreur associé ;
  • ce scénario se répète aux lignes 29 pour le champ [nom] et 38 pour le champ [age] ;
  • ligne 45 : l'expression [${#fields.errors('*')}] désigne l'ensemble des erreurs sur les champs de l'objet [securedPerson]. Ainsi, c'est l'ensemble de ces erreurs qui va être affiché par les lignes 44-46 ;
  • ligne 16 : on voit que les valeurs du formulaire vont être postées à l'action [/v16]. Celle-ci est la suivante :

    // -------------------- validation d'un modèle------------------
    @RequestMapping(value = "/v16", method = RequestMethod.POST)
    public String v16(@Valid SecuredPerson p, BindingResult result) {
        // erreurs ?
        if (result.hasErrors()) {
            return "vue-15";
        } else {
            return "vue-16";
        }
}
  • ligne 3, l'annotation [@Valid SecuredPerson p] force la validation des valeurs postées ;
  • ligne 5 : teste si le modèle de l'action est erroné ou non ;
  • ligne 6 : s'il est erroné, on retourne le formulaire [vue-15.xml]. Comme celui-ci affiche les messages d'erreur, on va voir ceux-ci ;
  • ligne 8 : si le modèle de l'action est validé, alors on affiche la vue [vue-16.xml] suivante :

<!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="#{personne.formulaire.saisies}">Voici vos saisies</h2>
        <div th:object="${securedPerson}">        
            <p>
                <span th:text="#{personne.id}">Id :</span>
                <span th:text="*{id}">14</span>
            </p>
            <p>
                <span th:text="#{personne.nom}">Nom :</span>
                <span th:text="*{nom}">Bill</span>
            </p>
            <p>
                <span th:text="#{personne.age}">Age :</span>
                <span th:text="*{age}">56</span>
            </p>
        </div>
    </body>
</html>

Voici des exemples d'exécution :

5.13. [/v17-/v18] : contrôle des messages d'erreur

Lorsqu'on demande la première fois l'action [/v15], on obtient le résultat suivant :

 

On pourrait vouloir un formulaire vide plutôt que des zéros dans les champs [Identifiant, Age]. Pour obtenir cela, nous faisons évoluer le modèle de l'action de la façon suivante :


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 nom;

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

    // constructeurs
    public StringSecuredPerson() {

    }

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

    // getters et setters
...

}
  • lignes 12 et 19 : les champs [id] et [age] sont passés en type [String] ;
  • ligne 11 : on indique que le champ [id] doit être un nombre d'au plus quatre chiffres, sans décimales ;
  • ligne 18 : idem pour le champ [age] qui doit être un nombre entier d'au plus deux chiffres ;

L'action [/v17] devient la suivante :


    // ---------------------- affichage d'un formulaire
    @RequestMapping(value = "/v17", method = RequestMethod.GET)
    public String v17(StringSecuredPerson p) {
        return "vue-17";
}

La vue [vue-17.xml] affichée par l'action [/v17] est la suivante :


<!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}">Entrez les informations suivantes</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">
                                    Identifiant erroné
                                </span>
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.nom}">Nom :</td>
                            <td>
                                <input type="text" name="nom" value="Tintin" th:value="*{nom}" />
                            </td>
                            <td>
                                <span th:if="${#fields.hasErrors('nom')}" th:errors="*{nom}" style="color: red">Nom erroné</span>
                            </td>
                        </tr>
                        <tr>
                            <td th:text="#{personne.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">Âge erroné</span>
                            </td>
                        </tr>
                    </tbody>
                </table>
                <input type="submit" value="Valider" th:value="#{personne.formulaire.valider}" />
                <ul>
                    <li th:each="err : ${#fields.errors('*')}" th:text="${err}" style="color: red" />
                </ul>
            </div>
        </form>
    </body>
</html>

Les changements ont lieu aux lignes suivantes :

  • ligne 10 : on travaille désormais avec l'objet du modèle de clé [stringSecuredPerson] ;
  • ligne 20 : on parcourt la liste des erreurs du champ [id]. Dans la syntaxe [th:each="err,status : ${#fields.errors('id')}"], c'est la variable [err] qui parcourt la liste. La variable [status] donne des informations sur chaque itération. C'est un objet [index, count, size, current] où :
    • index : est le n° de l'élément courant,
    • current : la valeur de cet élément courant,
    • count, size : la taille de la liste parcourue ;
  • ligne 20 : on n'affiche que le 1er élément de la liste [th:if="${status.index}==0"] ;

L'action [/v18] qui traite le POST de l'action [/v17] est la suivante :


    // -------------------- validation d'un modèle------------------
    @RequestMapping(value = "/v18", method = RequestMethod.POST)
    public String v18(@Valid StringSecuredPerson p, BindingResult result) {
        // erreurs ?
        if (result.hasErrors()) {
            return "vue-17";
        } else {
            return "vue-18";
        }
}

Les fichiers de messages évoluent de la façon suivante :

[messages_fr.properties]


title=Les vues dans Spring MVC
personne.nom=Nom :
personne.age=Age :
personne.id=Identifiant :
personne.mineure=Vous êtes mineur
personne.majeure=Vous êtes majeur
liste.personnes=Liste de personnes
personne.formulaire.titre=Entrez les informations suivantes et validez
personne.formulaire.valider=Valider
personne.formulaire.saisies=Voici vos saisies
notNull=La donnée est obligatoire
Range.securedPerson.id=L''identifiant doit être un nombre entier >=1
Range.securedPerson.age=Seules les personnes entre 8 et 14 ans sont autorisées sur ce site
Length.securedPerson.nom=Le nom doit avoir entre 1 et 4 caractères
typeMismatch=Donnée invalide
Range.stringSecuredPerson.id=L''identifiant doit être un nombre entier >=1
Range.stringSecuredPerson.age=Seules les personnes entre 8 et 14 ans sont autorisées sur ce site
Length.stringSecuredPerson.nom=Le nom doit avoir entre 1 et 4 caractères
Digits.stringSecuredPerson.id=Tapez un nombre entier de 4 chiffres au plus
Digits.stringSecuredPerson.age=Tapez un nombre entier de 2 chiffres au plus

[messages_en.properties]


title=Views in Spring MVC
personne.nom=Name:
personne.age=Age:
personne.id=Identifier:
personne.mineure=You are under 18
personne.majeure=You are over 18
liste.personnes=Persons' list
personne.formulaire.titre=Please, enter information and validate
personne.formulaire.valider=Validate
personne.formulaire.saisies=Here are your inputs
NotNull=Data is required
Range.securedPerson.id=Identifier must be an integer >=1
Range.securedPerson.age=Only kids who are 8 to 14 years old are allowed on this site
Length.securedPerson.nom=Name must be 4 to 10 characters long
typeMismatch=Invalid format
Range.stringSecuredPerson.id=Identifier must be an integer >=1
Range.stringSecuredPerson.age=Only kids who are 8 to 14 years old are allowed on this site
Length.stringSecuredPerson.nom=Name must be 4 to 10 characters long
Digits.stringSecuredPerson.id=Should be an integer with at most four digits
Digits.stringSecuredPerson.age=Should be an integer with at most two digits

Voyons quelques exemples :

 

On voit en [1], que les deux validateurs du champ [age] ont été exécutés :


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

Y-a-t-il un ordre des messages d'erreur ? Pour le champ [age], il semble que les validateurs se soient exécutés dans l'ordre [Digits, Range]. Mais si on fait plusieurs requêtes, on peut constater que cet ordre peut changer. Donc, on ne peut se fier à l'ordre des validateurs. En [2], on n'affiche qu'un message sur les deux du champ [id]. En [3], on voit l'ensemble des messages d'erreur.

5.14. [/v19-/v20] : usage de différents validateurs

Considérons le nouveau modèle d'action suivant :

  

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 et setters
...
}

Il sera affiché par l'action [/v19] suivante :


    // ------------------ affichage d'un formulaire
    @RequestMapping(value = "/v19", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String v19(Form19 formulaire) {
        return "vue-19";
}
  • ligne 3 : l'action reçoit comme paramètre un objet [Form19 formulaire]. Si le GET ne reçoit pas de paramètres, cet objet sera initialisé avec les valeurs par défaut du Java ;
  • ligne 4 : la vue [vue-19.xml] est affichée. Celle-ci est la suivante :

<!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>Formulaire - Validations côté serveur</h3>
        <form action="/someURL" th:action="@{/v20.html}" method="post" th:object="${form19}">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Contrainte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Erreur</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">Donnée erronée</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">Donnée erronée</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">Donnée erronée</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">Donnée erronée</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">Donnée erronée</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">Donnée erronée</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">Donnée erronée</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">Donnée erronée</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">Donnée erronée</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">Donnée erronée</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">Donnée erronée</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">Donnée erronée</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">Donnée erronée</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">Donnée erronée</span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>

Ce code affiche la vue suivante :

 

La page présente un tableau à trois colonnes :

  • colonne 1 : le validateur du champ de saisie ;
  • colonne 2 : le champ de saisie ;
  • colonne 3 : les messages d'erreur sur le champ de saisie ;

Examinons par exemple le code de la vue [/v19.html] pour le validateur [@Pattern] :


                    <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">Donnée erronée</span>
                        </td>
</tr>

On retrouve du code que nous venons d'étudier avec les formulaires de type [Personne] :

  • ligne 2 : la 1ère colonne : le nom du validateur testé ;
  • ligne 4 : l'attribut Thymeleaf [th:field="*{hhmmss}] va générer les attributs HTML [id="hhmmss"] et [name="hhmmss"]. L'attribut Thymeleaf [th:value="*{hhmmss}"] va générer l'attribut HTML [value="valeur de [form19.hhmmss]]" ;
  • ligne 7 : si la valeur saisie pour le champ [form19.hhmmss] est erroné, alors la ligne 7 affiche les messages d'erreur associés à ce champ ;

Les valeurs postées sont traitées par l'action [/v20] suivante :


    // ----------------- validation du modèle du formulaire
    @RequestMapping(value = "/v20", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String v20(@Valid Form19 formulaire, BindingResult result, RedirectAttributes redirectAttributes) {
        if (result.hasErrors()) {
            return "vue-19";
        } else {
            // redirection vers [vue-19]
            redirectAttributes.addFlashAttribute("form19", formulaire);
            return "redirect:/v19.html";
        }
}
  • ligne 3 : les valeurs postées vont remplir les champs de l'objet [Form19 formulaire] si elles sont valides ;
  • ligne 4-6 : si les valeurs postées ne sont pas valides, alors on réaffiche le formulaire [vue-19] avec les messages d'erreur ;
  • lignes 6-10 : si les valeurs postées sont valides, alors l'objet [Form19 formulaire] construit avec ces valeurs est mis à la disposition de la requête suivante, ici celle de la redirection. Il est détruit ensuite ;
  • ligne 9 : on redirige le client vers l'action [/v19.html]. Celle-ci va réafficher le formulaire [vue-19] qui contient du code tel que :

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

L'attribut [th:object="${form19}"] va alors récupérer l'objet associé à l'attribut Flash [form19] et ainsi réafficher le formulaire tel qu'il a été saisi.

Le code du formulaire mérite encore quelques explications. Considérons le code suivant :


                    <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">Donnée erronée</span>
                        </td>
</tr>

Cela génère le code HTML suivant :


<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>

Dans le 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>

les attributs Thymeleaf des lignes 1 et 3 [th:field="*{assertFalse}"] posent un problème. On a dit que cet attribut générait les attributs HTML [id=assertFalse] et [name=assertFalse]. La difficulté vient du fait que cela étant généré aux lignes 1 et 3 on a deux attributs [name] identiques et deux attributs [id] identiques. Si c'est possible avec l'attribut [name], cela ne l'est pas avec l'attribut [id]. Comme on le voit dans le code HTML généré, Thymeleaf a généré deux attributs [id] différents [id=asserFalse1] et [id=assertFalse2]. Ce qui est une bonne chose. Le problème est qu'on ne connaît pas ces identifiants et qu'on peut en avoir besoin. C'est le cas pour la balise [label] de la ligne 2. L'attribut [for] d'une balise HTML [label] doit référencer un attribut [id], en l'occurrence celui généré pour la balise [input] de la ligne 1. La documentation Thymeleaf indique que l'expression [${#ids.prev('assertFalse')}"] permet d'obtenir le dernier attribut [id] généré pour le champ [assertFalse].

Maintenant considérons le code de la liste déroulante du formulaire :


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

Ce code génère le code HTML d'une liste déroulante :

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

La valeur postée le sera avec le nom [name="assertTrue"].

La vue [vue-19.xml] utilise une feuille de style :


    <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>

Ligne 4, la feuille de style utilisée doit être placée dans le dossier [static] du projet :

  

Son contenu est le suivant :


@CHARSET "UTF-8";

.col1 {
    background: lightblue;
}

.col2 {
    background: Cornsilk;
}

.col3 {
    background: #e2d31d;
}

.error {
    color: red;
}

Maintenant, examinons les dates :


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

L'examen des échanges réseau dans l'outil de développement de Chrome (Ctrl-Maj-I) montrent que les dates sont postées au format (aaaa-mm-dd) :

 

C'est la raison pour laquelle les dates ont été annotées avec le validateur :


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

qui fixe le format attendu pour la valeur postée des dates.

Pour terminer, le fichier des messages français [messages_fr.properties] :


title=Les vues dans Spring MVC
personne.nom=Nom :
personne.age=Age :
personne.id=Identifiant :
personne.mineure=Vous êtes mineur
personne.majeure=Vous êtes majeur
liste.personnes=Liste de personnes
personne.formulaire.titre=Entrez les informations suivantes et validez
personne.formulaire.valider=Valider
personne.formulaire.saisies=Voici vos saisies
NotNull=La donnée est obligatoire
Range.securedPerson.id=L''identifiant doit être un nombre entier >=1
Range.securedPerson.age=Seules les personnes entre 8 et 14 ans sont autorisées sur ce site
Length.securedPerson.nom=Le nom doit avoir entre 1 et 4 caractères
typeMismatch=Donnée invalide
Range.stringSecuredPerson.id=L''identifiant doit être un nombre entier >=1
Range.stringSecuredPerson.age=Seules les personnes entre 8 et 14 ans sont autorisées sur ce site
Length.stringSecuredPerson.nom=Le nom doit avoir entre 1 et 4 caractères
Digits.stringSecuredPerson.id=Tapez un nombre entier de 4 chiffres au plus
Digits.stringSecuredPerson.age=Tapez un nombre entier de 2 chiffres au plus
Future.form19.dateInFuture=La date doit être postérieure à celle d''aujourd'hui
Past.form19.dateInPast=La date doit être antérieure à celle d''aujourd'hui
Size.form19.strBetween4and6=la chaîne doit avoir entre 4 et 6 caractères
Min.form19.intMin10=La valeur doit être supérieure ou égale à 10
Max.form19.intMax100=La valeur doit être inférieure ou égale à 100
Length.form19.str4=La chaîne doit avoir quatre caractères exactement
Email.form19.email=Adresse mail invalide
URL.form19.url=URL invalide
Range.form19.int1014=La valeur doit être dans l''intervalle [10,14]
AssertTrue=Seule la valeur True est acceptée
AssertFalse=Seule la valeur False est acceptée
Pattern.form19.hhmmss=Tapez l''heure sous la forme hh:mm:ss
NotEmpty=La donnée ne peut être vide
NotBlank=La donnée ne peut être vide

Voyons quelques exemples d'exécution :

 
 

Ci-dessus, entre [1] et [2], on a l'impression qu'il ne s'est rien passé. Si on regarde les échanges réseau (Ctrl-Maj-I), on voit pourtant qu'il y a eu deux échanges réseau avec le serveur :

  • en [1], le POST initial vers [/v20] ;
  • en [2], la réponse de cette action est une redirection ;
  • en [3], la seconde requête vers [/v19] cette fois ;

L'action [/v19] est alors exécutée :


    // ------------------ affichage d'un formulaire
    @RequestMapping(value = "/v19", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String v19(Form19 formulaire) {
        return "vue-19";
}
  • ligne 3, le paramètre [Form19 formulaire] est initialisé avec l'attribut Flash de clé [form19] qui avait été créé par l'action précédente [/v19] et qui était un objet de type [Form19] avec pour valeurs, les valeurs postées à l'action [/v19] ;
  • ligne 4 : la vue [vue-19.xml] va être affichée avec dans son modèle un objet [Form19 formulaire] initialisé avec les valeurs postées. C'est pourquoi, l'utilisateur retrouve le formulaire tel qu'il l'a posté ;

Pourquoi une redirection ? Pourquoi n'a-t-on pas posté simplement à l'action [/v19] ci-dessus ? On aurait eu le même le résultat. A quelques différences près :

  • le navigateur aurait mis dans son champ d'adresse [http://localhost:8080/v20.html] au lieu de [http://localhost:8080/v19.html] comme il l'a fait ici, car il affiche la dernière URL appelée ;
  • si l'utilisateur fait un rafraîchissement de la page (F5), on n'a pas du tout le même résultat :
    • dans le cas de la redirection, l'URL affichée est [http://localhost:8080/v19.html] obtenue avec un GET. Le navigateur rejouera cette dernière commande et il obtiendra alors un formulaire tout neuf (l'attribut Flash n'est utilisé qu'une fois),
    • dans le cas de la non redirection, l'URL affichée est [http://localhost:8080/v20.html] obtenue avec un POST. Le navigateur rejouera cette dernière commande et donc fera de nouveau un POST avec les mêmes valeurs postées que précédemment. Ici ça ne porte pas à conséquence mais c'est souvent indésirable et donc on préfèrera en général la redirection ;

5.15. [/v21-/v22] : gérer des boutons radio

Considérons le composant Spring [Listes] suivant :

  

package istia.st.springmvc.models;

import org.springframework.stereotype.Component;

@Component
public class Listes {

    private String[] deplacements = new String[] { "0", "1", "2", "3", "4" };
    private String[] libellesDeplacements = new String[] { "vélo", "marche", "train", "avion", "autre" };
    private String[] libellesBijoux = new String[] { "émeraude", "rubis", "diamant", "opaline" };

    // getters et setters
  ...

}
  • ligne 5 : la classe [Listes] sera un composant Spring ;
  • lignes 8-10 : des listes utilisées pour alimenter des boutons radio, des cases à cocher et des listes déroulantes ;

Dans la classe de configuration [Config], il est écrit :


@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
  • ligne 2 : le package [models] où se trouve le composant [Listes] sera bien exploré par Spring ;

Nous créons les nouvelles actions suivantes :


    // ------------------ formulaire avec boutons radio
    @Autowired
    private Listes listes;

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

    @RequestMapping(value = "/v22", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String v22(@ModelAttribute("form") Form21 formulaire, RedirectAttributes redirectAttributes) {
        redirectAttributes.addFlashAttribute("form", formulaire);
        return "redirect:/v21.html";
}
  • lignes 2-3 : le composant [Listes] est injecté dans le contrôleur ;
  • ligne 6 : nous gérons un formulaire de type [Form21] que nous allons décrire. A noter qu'on a précisé sa clé [form] dans le modèle de la vue. On rappelle que par défaut, cela aurait été [form21] ;
  • ligne 7 : on injecte le composant [Listes] dans le modèle. La vue va en avoir besoin ;
  • ligne 8 : on affiche la vue [vue-21.xml]. Cette vue va afficher le formulaire [Form21] et les valeurs postées le seront à l'action [/v22] des lignes 12-15 ;
  • lignes 12-15 : l'action [/v22] se contente d'une redirection vers l'action [/v21] en mettant les valeurs postées qu'elle a reçues dans un attribut Flash de clé [form]. Il est important que cette clé soit la même que celle utilisée ligne 6 ;

Le modèle [Form21] est le suivant :

  

package istia.st.springmvc.models;

public class Form21 {

    // valeurs postées
    private String marie = "non";
    private String deplacement = "4";
    private String[] couleurs;
    private String strCouleurs;
    private String[] bijoux;
    private String strBijoux;
    private int couleur2;
    private int[] bijoux2;
    private String strBijoux2;

    // getters et setters
    ...
}

La vue [vue-21.xml] est la suivante :


<!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>Formulaire - Boutons radio</h3>
        <form action="/someURL" th:action="@{/v22.html}" method="post" th:object="${form}">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Etes-vous marié(e)</td>
                        <td class="col2">
                            <input type="radio" th:field="*{marie}" value="oui" />
                            <label th:for="${#ids.prev('marie')}">Oui</label>
                            <input type="radio" th:field="*{marie}" value="non" />
                            <label th:for="${#ids.prev('marie')}">Non</label>
                        </td>
                        <td class="col3">
                            <span th:text="*{marie}"></span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Mode de déplacement</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]}">Autre</label>
                            </span>
                        </td>
                        <td class="col3">
                            <span th:text="*{deplacement}"></span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>
  • lignes 36-40 : on notera l'exploitation du composant [Listes] mis dans le modèle, pour générer les libellés des cases à cocher ;
  • la colonne 3 permet de connaître la valeur postée pour un POST, ou la valeur initiale du formulaire lors du GET initial ;

Ce code affiche la page suivante :

 

correspondant au code HTML suivant :


<!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>Formulaire - Boutons radio</h3>
        <form action="/v22.html" method="post">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Etes-vous marié(e)</td>
                        <td class="col2">
                            <input type="radio" value="oui" id="marie1" name="marie" />
                            <label for="marie1">Oui</label>
                            <input type="radio" value="non" id="marie2" name="marie" checked="checked" />
                            <label for="marie2">Non</label>
                        </td>
                        <td class="col3">
                            <span>non</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Mode de déplacement</td>
                        <td class="col2">
                            <span>
                                <input type="radio" value="0" id="deplacement1" name="deplacement" />
                                <label for="deplacement1">vélo</label>
                            </span>
                            <span>
                                <input type="radio" value="1" id="deplacement2" name="deplacement" />
                                <label for="deplacement2">marche</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">avion</label>
                            </span>
                            <span>
                                <input type="radio" value="4" id="deplacement5" name="deplacement" checked="checked" />
                                <label for="deplacement5">autre</label>
                            </span>
                        </td>
                        <td class="col3">
                            <span>4</span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>

On voit que les valeurs postées (attributs name) le sont dans les champs suivants du modèle [Form21] :


    private String marie = "non";
    private String deplacement = "4";

Le lecteur est invité à faire des tests. On notera bien que c'est l'attribut [value] des boutons radio qui est posté.

5.16. [/v23-/v24] : gérer des cases à cocher

Nous ajoutons la nouvelle action suivante :


    // ------------------ formulaire avec cases à cocher
    @RequestMapping(value = "/v23", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String av20(@ModelAttribute("form") Form21 formulaire, Model model) {
        model.addAttribute("listes", listes);
        return "vue-23";
}
  • ligne 3 : nous continuons à utiliser le modèle [Form21] ;

La vue [vue-23.xml] est la suivante :


<!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>Formulaire - Cases à cocher</h3>
        <form action="/someURL" th:action="@{/v24.html}" method="post" th:object="${form}">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Vos couleurs préférées</td>
                        <td class="col2">
                            <input type="checkbox" th:field="*{couleurs}" value="0" />
                            <label th:for="${#ids.prev('couleurs')}">rouge</label>
                            <input type="checkbox" th:field="*{couleurs}" value="1" />
                            <label th:for="${#ids.prev('couleurs')}">vert</label>
                            <input type="checkbox" th:field="*{couleurs}" value="2" />
                            <label th:for="${#ids.prev('couleurs')}">bleu</label>
                        </td>
                        <td class="col3">
                            <span th:text="*{strCouleurs}"></span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Pierres préférées</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}">Autre</label>
                            </span>
                        </td>
                        <td class="col3">
                            <span th:text="*{strBijoux}"></span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>
  • lignes 37-41 : on notera l'utilisation du composant [Listes] pour générer les libellés des cases à cocher ;

Ce code affiche la page suivante :

 

issue du code HTML suivant :


<!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>Formulaire - Cases à cocher</h3>
        <form action="/v24.html" method="post">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Vos couleurs préférées</td>
                        <td class="col2">
                            <input type="checkbox" value="0" id="couleurs1" name="couleurs" /><input type="hidden" name="_couleurs" value="on" />
                            <label for="couleurs1">rouge</label>
                            <input type="checkbox" value="1" id="couleurs2" name="couleurs" /><input type="hidden" name="_couleurs" value="on" />
                            <label for="couleurs2">vert</label>
                            <input type="checkbox" value="2" id="couleurs3" name="couleurs" /><input type="hidden" name="_couleurs" value="on" />
                            <label for="couleurs3">bleu</label>
                        </td>
                        <td class="col3">
                            <span></span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Pierres préférées</td>
                        <td class="col2">
                            <span>
                                <input type="checkbox" value="0" id="bijoux1" name="bijoux" /><input type="hidden" name="_bijoux" value="on" />
                                <label for="bijoux1">émeraude</label>
                            </span>
                            <span>
                                <input type="checkbox" value="1" id="bijoux2" name="bijoux" /><input type="hidden" name="_bijoux" value="on" />
                                <label for="bijoux2">rubis</label>
                            </span>
                            <span>
                                <input type="checkbox" value="2" id="bijoux3" name="bijoux" /><input type="hidden" name="_bijoux" value="on" />
                                <label for="bijoux3">diamant</label>
                            </span>
                            <span>
                                <input type="checkbox" value="3" id="bijoux4" name="bijoux" /><input type="hidden" name="_bijoux" value="on" />
                                <label for="bijoux4">opaline</label>
                            </span>
                        </td>
                        <td class="col3">
                            <span></span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>

On notera que les valeurs postées (attributs name) le sont dans les champs suivants de [Form21] :


    private String[] couleurs;
    private String[] bijoux;

Ce sont des tableaux car pour chaque champ, il existe plusieurs cases à cocher portant le nom du champ. Il est donc possible que plusieurs valeurs postées arrivent avec le même nom (attribut name du formulaire). Il faut donc un tableau pour les récupérer.

Revenons au code Thymeleaf de la colonne 3 de la page :


  <td class="col3">
    <span th:text="*{strCouleurs}"></span>
  </td>
</tr>
<tr>
  <td class="col1">Pierres préférées</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}">Autre</label>
    </span>
  </td>
  <td class="col3">
    <span th:text="*{strBijoux}"></span>
  </td>
</tr>

Les champs référencés lignes 2 et 14 sont les suivants :


    private String strCouleurs;
    private String strBijoux;

Ils sont calculés par l'action [/v24] qui gère le POST :


    // mappeur Jackson / jSON
    private ObjectMapper mapper = new ObjectMapper();

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

Il faut se rappeler ici que la bibliothèque jackson / jSON est dans les dépendances du projet.

  • ligne 2 : on crée un type [ObjectMapper] qui permet de sérialiser / désérialiser des objets en jSON,
  • ligne 7 : on sérialise en jSON le tableau des couleurs. Le résultat est placé dans le champ [strCouleurs] ;
  • ligne 8 : on sérialise en jSON le tableau des bijoux. Le résultat est placé dans le champ [strBijoux] ;

Voici un exemple d'exécution :

On notera bien que c'est l'attribut [value] des cases à cocher qui est posté.

5.17. [/25-/v26] : gérer des listes

Nous ajoutons l'action suivante [/v25] :


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

La vue [vue-25.xml] est la suivante :


<!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>Formulaire - Listes</h3>
        <form action="/someURL" th:action="@{/v26.html}" method="post"
            th:object="${form}">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Votre couleur préférée</td>
                        <td class="col2">
                            <select th:field="*{couleur2}">
                                <option value="0">rouge</option>
                                <option value="1">bleu</option>
                                <option value="2">vert</option>
                            </select>
                        </td>
                        <td class="col3">
                            <span th:text="*{couleur2}"></span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Pierres préférées (choix multiple)</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="Valider" />
        </form>
    </body>
</html>
  • lignes 38-42 : génération d'une liste à choix multiple où les libellés sont pris dans le composant [Listes] que nous avons déjà utilisé ;

La page affichée est la suivante :

 

générée par le code HTML suivant :


<!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>Formulaire - Listes</h3>
        <form action="/v26.html" method="post">
            <table>
                <thead>
                    <tr>
                        <th class="col1">Texte</th>
                        <th class="col2">Saisie</th>
                        <th class="col3">Valeur</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td class="col1">Votre couleur préférée</td>
                        <td class="col2">
                            <select id="couleur2" name="couleur2">
                                <option value="0" selected="selected">rouge</option>
                                <option value="1">bleu</option>
                                <option value="2">vert</option>
                            </select>
                        </td>
                        <td class="col3">
                            <span>0</span>
                        </td>
                    </tr>
                    <tr>
                        <td class="col1">Pierres préférées (choix multiple)</td>
                        <td class="col2">
                            <select multiple="multiple" size="3" id="bijoux2" name="bijoux2">
                                <option value="0">émeraude</option>
                                <option value="1">rubis</option>
                                <option value="2">diamant</option>
                                <option value="3">opaline</option>
                            </select>
                            <input type="hidden" name="_bijoux2" value="1" />
                        </td>
                        <td class="col3">
                            <span></span>
                        </td>
                    </tr>
                </tbody>
            </table>
            <p>
                <input type="submit" value="Valider" />
            </p>
        </form>
    </body>
</html>
  • ligne 44 : on peut remarquer que Thymeleaf a créé un champ caché. Je n'ai pas compris son rôle :
  • les valeurs postées (attributs value des balises option) le seront dans les champs suivants (attributs name) de [Form21] :

    private int couleur2;
    private int[] bijoux2;
  • ligne 38 : la liste [bijoux2] est à choix multiple. Donc plusieurs valeurs peuvent être postées associées au nom [bijoux2]. Pour les récupérer, le champ [bijoux2] doit être un tableau. On remarquera que c'est un tableau d'entiers. C'est possible puisque les valeurs postées peuvent être converties dans ce type ;

Les valeurs sont postées à l'action [/v26] suivante :


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

Il n'y a là rien qu'on n'ait déjà vu. Voici un exemple d'exécution :

5.18. [/v27] : paramétrage des messages

Considérons l'action [/v27] suivante :


  // ------------------ messages paramétrés
  @RequestMapping(value = "/v27", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
  public String v27(Model model) {
        model.addAttribute("param1","paramètre un");
        model.addAttribute("param2","paramètre deux");
        model.addAttribute("param3","paramètre trois");
        model.addAttribute("param4","messages.param4");        
        return "vue-27";
}

L'action se contente de mettre quatre valeurs dans le modèle et fait afficher la vue [vue-27.xml] suivante :


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{messages.titre}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <h2 th:text="#{messages.titre}">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>
  • ligne 8 : un message sans paramètres ;
  • ligne 9 : un message avec un paramètre [$param1] pris dans le modèle ;
  • ligne 10 : un message avec deux paramètres [$param2, $param3] pris dans le modèle ;
  • ligne 11 : un message avec un paramètre. Ce paramètre est lui-même une clé de message (présence de #). La clé est fournie par [$param4] ;

Le fichier des messages français est le suivant :

[messages_fr.properties]


messages.titre=Messages paramétrés
messages.msg1=Un message avec un paramètre : {0}
messages.msg2=Un message avec deux paramètres : {0}, {1}
messages.msg3=Un message avec une clé de message comme paramètre : {0}
messages.param4=paramètre quatre

Pour indiquer la présence de paramètres dans le message, on utilise les symboles {0}, {1}, ...

La fusion du modèle construit par l'action [/v27] avec la vue [vue-27] va produire le code HTML suivant :


<!DOCTYPE html>

<html>
    <head>
        <title>Messages paramétrés</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <h2>Messages paramétrés</h2>
        <p>Un message avec un paramètre : paramètre un</p>
        <p>Un message avec deux paramètre : paramètre deux, paramètre trois</p>
        <p>Un message avec une clé de message comme paramètre : paramètre quatre</p>
    </body>
</html>

ce qui donne la vue suivante :

 

Le fichier des messages anglais est le suivant :

[messages_fr.properties]


messages.titre=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

La fusion du modèle construit par l'action [/v27] avec la vue [vue-27] va produire le code HTML suivant :


<!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: paramètre un</p>
        <p>Message with two parameters: paramètre deux, paramètre trois</p>
        <p>Message with a message key as a parameter: parameter four</p>
    </body>
</html>

ce qui donne la vue suivante :

 

On voit que le dernier message a été internationalisé de bout en bout, ce qui n'est pas le cas des deux précédents.

5.19. Utilisation d'une page maître

Dans une application web, il est fréquent que les vues partagent un certain nombre d'éléments qu'on peut factoriser dans une page maître. Voici un exemple :

Ci-dessus, on a deux pages semblables où le fragment [1] a été remplacé par le fragment [2]. La vue est celle d'une page maître ayant trois fragments fixes [3-5] et un fragment variable [6].

5.19.1. Le projet

Nous construisons un projet [springmvc-masterpage] en suivant la démarche du paragraphe 5.1.

  

Le fichier [pom.xml] est le suivant :


<?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>Page maître</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>

L'une des dépendances amenées par ce fichier est nécessaire pour la page maître :

 

Les packages [config] et [main] sont identique à ceux de mêmes noms du projet précédent.

5.19.2. La page maître

  

La page maître est la vue [layout.xml] suivante :


<!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="entete" />
                </td>
            </tr>
            <tr style="height: 200px">
                <td bgcolor="#ffcccc">
                    <div th:include="menu" />
                </td>
                <td>
                    <section layout:fragment="contenu">
                        <h2>Contenu</h2>
                    </section>
                </td>
            </tr>
            <tr bgcolor="#ffcc66">
                <td colspan="2">
                    <div th:include="basdepage" />
                </td>
            </tr>
        </table>
    </body>
</html>
  • ligne 2 : la page maître doit définir l'espace de noms [xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"] dont un élément est utilisé ligne 19 ;
  • lignes 10-12 : génèrent la zone [1] ci-dessous. La balise Thymeleaf [th:include] permet d'inclure dans la vue courante un fragment défini dans un autre fichier. Cela permet de factoriser les fragments utilisés dans plusieurs vues ;
  • lignes 15-17 : génèrent la zone [2] ci-dessous ;
  • lignes 19-20 : génèrent la zone [3] ci-dessous. L'attribut [layout:fragment] est un attribut de l'espace de noms [xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"]. Il indique une zone qui à l'exécution peut être remplacée par une autre ;
  • lignes 24-28 : génèrent la zone [4] ci-dessous ;

5.19.3. Les fragments

Les fragments [entete.xml], [menu.xml] et [basdepage.xml] sont les suivants :

[entete.xml]


<!DOCTYPE html>
<html>
    <h2>entête</h2>
</html>

[menu.xml]


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

[basdepage.xml]


<!DOCTYPE html>
<html>
    <h2>bas de page</h2>
</html>

Le fragment [page1.xml] est le suivant :


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorator="layout">
    <section layout:fragment="contenu">
        <h2>Page 1</h2>
        <form action="/someURL" th:action="@{/page2.html}" method="post">
            <input type="submit" value="Page 2" />
        </form>
    </section>
</html>
  • ligne 2 : l'attribut [layout:decorator="layout"] indique que la page courante [page1.xml] est 'décorée', ç-à-d. qu'elle appartient à une page maître. Celle-ci est la valeur de l'attribut, ici la vue [layout.xml] ;
  • ligne 3 : on indique dans quel fragment de la page maître va venir s'insérer [page1.xml]. L'attribut [layout:fragment="contenu"] indique que [page1.xml] va s'insérer dans le fragment appelé [contenu], ç-à-d. la zone [3] de la page maître ;
  • lignes 5-7 : le contenu du fragment est un formulaire qui offre un bouton de POST vers l'action [/page2.html] ;

Le fragment [page2.xml] est analogue :


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
    layout:decorator="layout">
    <section layout:fragment="contenu">
        <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. Les actions

 

Le contrôleur [Layout.java] est le suivant :


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";
    }
}
  • lignes 10-12 : l'action [/page1] se contente de faire afficher la vue [page1.xml] ;
  • lignes 15-17 : idem pour l'action [/page2] qui fait afficher la vue [page2.xml] ;