Skip to content

5. As visualizações do Thymeleaf

Voltemos à arquitetura de um aplicativo Spring MVC.

Os dois capítulos anteriores descreveram diversos aspectos do bloco [1], as ações. Abordaremos agora:

  • o bloco [2] das visualizações V;
  • o bloco [3] do modelo M exibido por essas visualizações;

Desde a criação do Spring MVC, a tecnologia utilizada para gerar as páginas HTML enviadas aos navegadores dos clientes era a das páginas JSP (Java Server Pages). Há alguns anos, a tecnologia [Thymeleaf] [http://www.thymeleaf.org/] também passou a ser utilizada. É essa tecnologia que apresentamos agora.

5.1. O projeto STS

Criamos um novo projeto:

  • no [3], indique que o projeto precisa das dependências [Thymeleaf]. Isso fará com que, além das dependências [Spring MVC] do projeto anterior, sejam incluídas as do framework [Thymeleaf] e [5];

Agora, vamos atualizar esse projeto da seguinte maneira:

  

Vamos nos inspirar no projeto anterior:

  • [istia.st.springmvc.controllers] conterá os controladores;
  • [istia.st.springmvc.models] conterá os modelos das ações e das visualizações;
  • [istia.st.springmvc.main] é o pacote da classe executável do Spring Boot;
  • [templates] conterá as visualizações Thymeleaf;
  • [i18n] conterá as mensagens internacionalizadas exibidas pelas visualizações;

A classe [Application] é a seguinte:


package istia.st.springmvc.main;

import org.springframework.boot.SpringApplication;

public class Application {

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

A classe [Config] é a seguinte:


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

Essa configuração permite, por enquanto, o gerenciamento das configurações regionais.

O controlador [ViewController] é o seguinte:


package istia.st.springmvc.actions;

import org.springframework.stereotype.Controller;

@Controller
public class ViewsController {

}
  • na linha 5, a anotação [@Controller] substituiu a anotação [@RestController], pois, a partir de agora, as ações não gerarão a resposta para o cliente. Elas irão:
    • construir um modelo M
    • retornar um tipo [String], que será o nome da visualização [Thymeleaf] responsável por exibir esse modelo. É a combinação dessa visualização V e desse modelo M que irá gerar o fluxo HTML enviado ao cliente;

O arquivo [messages.properties] está vazio por enquanto.

5.2. [/v01]: noções básicas do Thymeleaf

Consideremos a primeira ação a seguir em [ViewsController]:


    // Noções básicas do Thymeleaf - 1
    @RequestMapping(value = "/v01", method = RequestMethod.GET)
    public String v01() {
        return "v01";
}
  • linha 3: a ação retorna um tipo [String]. Esse será o nome da ação;
  • linha 4: essa visualização será [v01]. Por padrão, ela deve estar na pasta [templates] e se chamar [v01.html];

A visualização [v01.html] é a seguinte:


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

Trata-se de um arquivo HTML. A presença do Thymeleaf é visível:

  • no espaço de nomes [th] da linha 2;
  • nos atributos [th:text] das linhas 4 e 8;

Temos aqui um arquivo HTML válido que pode ser visualizado. Nós o colocamos na pasta [static] [2] com o nome [vue-01.html] e o acessamos diretamente com um navegador:

Se examinarmos o código-fonte da página em [2], podemos constatar que os atributos [th:text] foram enviados pelo servidor e ignorados pelo navegador. Quando uma visualização é resultado de uma ação, o Thymeleaf entra em ação e interpreta os atributos [th] antes de enviar a resposta ao cliente.

A tag HTML:


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

é processada da seguinte maneira pelo Thymeleaf:

  • th:text tem a sintaxe th:text="expressão", em que expressão é uma expressão a ser avaliada. Quando essa expressão é uma sequência de caracteres, como neste caso, ela deve ser colocada entre apóstrofos;
  • o valor de [expression] substitui o texto da tag HTML, neste caso, o texto da tag [title];

Após o processamento, a tag acima ficou assim:


<title>Les vues</title>

Vamos solicitar a ação [/v01]:

  • em [2], vemos o trabalho de substituição realizado pelo Thymeleaf;

Agora, vamos chamar a ação URL a partir de [http://localhost:8080/v01.html]:

 

Como devemos interpretar isso? A visualização [templates/v01.html] foi servida diretamente, sem passar por uma ação? Para esclarecer as coisas, criamos a seguinte ação [/v02]:


    // Noções básicas do Thymeleaf - 2
    @RequestMapping(value = "/v02", method = RequestMethod.GET)
    public String v02() {
        System.out.println("action v02");
        return "vue-02";
}

A visualização [vue-02.html] é uma cópia da [v01.html]:

  

Agora, vamos solicitar a ação URL a partir da ação [http://localhost:8080/vue-02.html]:

 

O URL não foi encontrado. Agora, vamos solicitar o URL e o [http://localhost:8080/v02.html]

  • nos logs do console, em [1], vemos que a ação [/v02] foi chamada, e esta fez com que a visualização [vue-02.html] fosse exibida em [2];

Agora sabemos que o URL [http://localhost:8080/v02.html] também pode se referir a um arquivo [/v02.html] na pasta [static]. O que acontece se esse arquivo existir? Vamos tentar. Criamos na pasta [static] o seguinte arquivo [v02.html]:

  

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

e, em seguida, solicitamos o URL e o [http://localhost:8080/v02.html]:

[1] e [2] mostram que foi a ação [/v02] que foi chamada. Portanto, deve-se observar que, quando a ação URL solicitada tem o formato [/x.html], o Spring/Thymeleaf:

  • executa a ação [/x], se ela existir;
  • exibe a página [/static/x.html], caso ela exista;
  • lança uma exceção 404 Not found caso contrário;

Para evitar confusões, a partir de agora, as ações e as visualizações não terão os mesmos nomes.

5.3. [/v03]: internacionalização das visualizações

A integração Spring/Thymeleaf permite que o Thymeleaf utilize os arquivos de mensagens do Spring. Consideremos a seguinte nova ação [/v03]:


    // internacionalização das visualizações
    @RequestMapping(value = "/v03", method = RequestMethod.GET)
    public String v03() {
        return "vue-03";
}

Ela exibe a seguinte vista [vue-03.html]:

  

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

Nas linhas 4 e 8, a expressão do atributo [th:text] é #{title}, cujo valor é a mensagem de chave [title]. Criamos os seguintes arquivos [messages_fr.properties] e [messages_en.properties]:

[messages_fr.properties]


title=Les vues dans Spring MVC

[messages_en.properties]


title=Views in Spring MVC

Vamos solicitar os URL, [http://localhost:8080/v03.html?lang=fr] e [http://localhost:8080/v03.html?lang=en]:

Observe que utilizamos o que aprendemos recentemente. Em vez de designar a ação [v03] como [/v03], nós a designamos como [/v03.html].

5.4. [/v04]: criação do modelo M de uma visualização V

Consideremos a seguinte nova ação [/v04]:


    // criação do modelo M de uma visualização 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";
}
  • linha 4: o modelo da visualização é injetado nos parâmetros da ação. Por padrão, esse modelo inicial está vazio. Veremos que é possível preenchê-lo previamente;
  • linha 4: um modelo do tipo [Model] é uma espécie de dicionário de elementos do tipo <String, Object>. Na linha 4, adicionamos uma entrada nesse dicionário com a chave [personne] associada a um valor do tipo [Personne];
  • linha 5: exibimos o modelo no console para ver como ele se parece;
  • linha 6: exibimos a vista [vue-04.html];

A classe [Personne] é a mesma utilizada no capítulo anterior:

  

package istia.st.springmvc.models;

public class Personne {

    // identificador
    private Integer id;
    // nome
    private String nom;
    // idade
    private int age;

    // construtores
    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 e setters
...
}

A vista [vue-04.html] é a seguinte:

  

<!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>
  • na linha 10, é introduzido um novo tipo de expressão Thymeleaf ${var}, em que var é uma chave do modelo M da visualização. Lembramos que a ação [/v04] inseriu no modelo uma chave [personne] associada a um tipo Pessoa[id, nom, age];
  • linha 10: exibe o nome da pessoa presente no modelo;
  • linha 14: exibe a idade dela;

Os arquivos de mensagens são modificados para adicionar as chaves [personne.nom] e [personne.age] das linhas 9 e 13. O resultado é o seguinte:

e a natureza do modelo M pode ser encontrada nos logs do console [2].

Pode-se questionar por que não se escreve a visão [vue-04] da seguinte maneira:


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

Essa visão é perfeitamente válida e produzirá o mesmo resultado que anteriormente. Um dos objetivos do Thymeleaf é que a página Thymeleaf possa ser exibida mesmo que não passe pelo processamento do Thymeleaf. Assim, vamos criar duas novas páginas estáticas:

  

A visualização [vue-04b.html] é uma cópia da visualização [vue-04.html]. O mesmo se aplica à visualização [vue-04a.html], mas removemos os textos estáticos da página. Se visualizarmos as duas páginas, obtemos os seguintes resultados:

No caso da [1], a estrutura da página não aparece, enquanto que no caso da [2] ela está bem visível. Esse é o benefício de inserir textos estáticos em uma visualização Thymeleaf, mesmo que, na execução, eles sejam substituídos por outros textos.

Agora, vamos examinar um detalhe técnico. Na visualização [vue-04.html], formatamos o código por meio de [ctrl-Maj-F]. Obtemos o seguinte resultado:


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

As tags estão desalinhadas e o código fica mais difícil de ler. Se renomearmos [vue-04.html] para [vue-04.xml] e reformatarmos o código, as tags voltam a ficar alinhadas. Portanto, o sufixo [xml] seria mais prático. É possível trabalhar com esse sufixo. Para isso, é preciso configurar o Thymeleaf. Para não desfazer o que fizemos, duplicamos o projeto [springmvc-vues] analisado, criando um projeto [springmvc-vues-xml]

  

Alteramos o arquivo [pom.xml] da seguinte maneira:


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

O nome do projeto é alterado nas linhas 2 e 6. Além disso, alteramos o sufixo das visualizações presentes na pasta [templates]:

  

O documento [http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html] lista as propriedades de configuração do Spring Boot que podem ser utilizadas no arquivo [application.properties]:

  

Este documento apresenta as propriedades que o Spring Boot utiliza durante a autoconfiguração e que podem ser alteradas por meio de uma configuração diferente no arquivo [application.properties]. Para o Thymeleaf, as propriedades de autoconfiguração são as seguintes:


# 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> foi adicionado
spring.thymeleaf.cache=true # definido como false para atualização dinâmica

Portanto, bastaria inserir a linha


spring.thymeleaf.suffix=.xml

no [application.properties]. Vamos seguir outro caminho: a configuração por programação. Vamos configurar o Thymeleaf na 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;
    }

}
  • as linhas 16 a 24 configuram um [TemplateResolver] para o Thymeleaf. É esse objeto que é carregado a partir de um nome de visualização fornecido por uma ação, para localizar o arquivo correspondente;
  • as linhas 18 e 19 definem o prefixo e o sufixo a serem adicionados ao nome da vista para localizar o arquivo. Assim, se o nome da vista for [vue04], o arquivo procurado será [classpath:/templates/vue04.xml]. [classpath:/templates] é uma sintaxe do Spring que designa uma pasta [/templates] localizada na raiz do Classpath do projeto;
  • linha 21: para que, na resposta enviada ao cliente, conste o cabeçalho HTTP:

Content-Type:text/html;charset=UTF-8
  • linha 20: indica que a visualização está em conformidade com a norma HTML5;
  • linha 22: indica que as visualizações do Thymeleaf podem ser armazenadas em cache;
  • linhas 26-31: define o mecanismo de resolução de visualizações do par Spring/Thymeleaf com o mecanismo de resolução anterior;

Vamos executar o executável deste novo projeto e solicitar o URL [http://localhost:8080/v04.html?lang=en]:

 

Observamos que, no URL, a ação [/v04] pôde ser substituída, mais uma vez, por [v04.html].

5.5. [/v05]: fatoração de um objeto em uma visualização Thymeleaf

Criamos a seguinte ação [/v05]:


    // criação do modelo M de uma visualização V - 2
    @RequestMapping(value = "/v05", method = RequestMethod.GET)
    public String v05(Model model) {
        model.addAttribute("personne", new Personne(7, "martin", 17));
        return "vue-05";
}

Ela é idêntica à ação [/v04]. A visualização [vue-05.xml] é a seguinte:

  

<!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>
  • linhas 8-17: nessas linhas, um objeto Thymeleaf é definido pelo atributo [th:object="${personne}"] (linha 8). Esse objeto é, neste caso, o objeto de chave [personne] que está no modelo:
  • linha 11: a expressão Thymeleaf [*{nom}] é equivalente a [${objet.nom}], onde [objet] é o objeto Thymeleaf atual. Portanto, aqui a expressão [*{nom}] é equivalente a [${personne.nom}];
  • linha 15: o mesmo;

O resultado:

 

5.6. [/v06]: testes em uma visualização Thymeleaf

Consideremos a seguinte ação [/v06]:


    // criação do modelo M de uma visualização V - 3
    @RequestMapping(value = "/v06", method = RequestMethod.GET)
    public String v06(Model model) {
        model.addAttribute("personne", new Personne(7, "martin", 17));
        return "vue-06";
}

Ela é idêntica às duas ações anteriores. Ela exibe a seguinte visualização [vue-06.xml]:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:object="${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>
  • linha 17: o atributo [th:if] avalia uma expressão booleana. Se essa expressão for verdadeira, a tag é exibida; caso contrário, não é. Portanto, neste caso, se ${personne.age}>=18, o texto [#{personne.majeure}] será exibido, ou seja, a mensagem de chave [personne.majeure] nos arquivos de mensagens;
  • linha 18: não é possível escrever [*{age} < 18], pois o sinal < é um caractere reservado. Portanto, deve-se usar seu equivalente HTML [&lt;], também chamado de entidade HTML [http://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references];

Os arquivos de mensagens são modificados:

[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

O resultado é o seguinte:

5.7. [/v07]: iteração em uma visualização Thymeleaf

Consideremos a seguinte ação [/v07]:


    // criação do modelo M de uma visualização 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";
}
  • a ação cria uma lista de três pessoas, insere-a no modelo associado à chave [liste] e exibe a visualização [vue-07];

A visualização [vue-07.xml] é a seguinte:


<!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>
  • linha 10: o atributo [th:each] repete a tag na qual está inserido, neste caso uma tag <li>. Ele possui aqui dois parâmetros [element : collection], sendo que [collection] é uma coleção de objetos, neste caso uma lista de pessoas. O Thymeleaf percorrerá a coleção e gerará tantas tags <li> quantos forem os elementos da coleção. Para cada tag <li>, [element] representará o elemento da coleção associado à tag. Para esse elemento, o atributo [th:text] será avaliado. Sua expressão, neste caso, é uma concatenação de cadeias de caracteres para obter o resultado [id, nom, age];
  • linha 8: adiciona-se a chave [liste.personnes] nos arquivos de mensagens;

Eis o resultado:

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

Voltamos a abordar algo que vimos ao estudar as ações: o papel da anotação [@ModelAttribute]. Adicionamos a seguinte nova ação:


    // --------------- Binding e ModelAttribute ----------------------------------

    // se o parâmetro for um objeto, ele é instanciado e, eventualmente, modificado pelos parâmetros da consulta
    // ele passará automaticamente a fazer parte do modelo da vista com a chave [key]
    // para o parâmetro @ModelAttribute("xx"), a chave será igual a xx
    // para o parâmetro @ModelAttribute, a chave será igual ao nome da classe do parâmetro, começando com uma letra minúscula
    // se @ModelAttribute estiver ausente, então tudo ocorre como se ele estivesse presente sem chave
    // vale ressaltar que essa inclusão automática no modelo não ocorre se o parâmetro não for um objeto

    @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";
}
  • linha 11: a anotação [@ModelAttribute("someone")] adicionará automaticamente o objeto [Personne p] ao modelo, associado à chave [someone];
  • linha 12: para verificar o modelo;
  • linha 13: exibe a visualização [vue-08.xml];

A visualização [vue-08.xml] é a seguinte:


<!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>
  • linha 8: o objeto Thymeleaf é inicializado com o objeto-chave [someone];

O resultado é o seguinte:

 

e, no console, temos o seguinte registro:

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

Consideremos agora a seguinte ação [/v09]:


    @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";
}
  • linha 1: a presença do parâmetro [Personne p] colocará automaticamente a pessoa [p] no modelo. Como não há especificação de chave, a chave utilizada é o nome da classe com o primeiro caractere em minúscula. Portanto, [Personne p] é equivalente a [@ModelAttribute("personne") Personne p];

A visualização [vue.09.xml] é a seguinte:


<!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>
  • linha 8: a chave de modelo utilizada é [personne];

Aqui está um resultado:

 

e o registro no console do servidor:

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

Agora, vamos considerar a seguinte nova ação [/v10]:


    @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";
}
  • linhas 1-4: definem um método que cria, no modelo de cada solicitação, um elemento-chave [uneAutrePersonne] associado ao objeto [new Personne(24,"pauline",55)];
  • linhas 6-10: a ação [/v10] não faz nada além de passar o modelo que recebe para a visualização [vue-10.xml]. Observe que o parâmetro [Model model] só precisa estar presente para a instrução da linha 8. Sem ele, é desnecessário;

A visualização [vue-10.xml] é a seguinte:


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

O resultado é o seguinte:

 

e o log da console é o seguinte:

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

5.9. [/v11]: @SessionAttributes

Voltamos a abordar algo que vimos ao estudar as ações: o papel da anotação [@SessionAttributes]. Adicionamos a seguinte nova ação [/v11]:


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

Temos algo semelhante ao que acabamos de estudar. A diferença está em uma anotação [@SessionAttributes] colocada na própria classe:


@Controller
@SessionAttributes("jean")
public class ViewsController {
  • linha 2: indica-se que a chave [jean] do modelo deve ser inserida na sessão;

É por isso que, na linha 7 da ação, injetamos a sessão. Na linha 8, exibimos o valor da sessão associada à chave [jean].

A visualização [vue-11.xml] é a seguinte:


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

São exibidas duas pessoas:

  • linhas 8-21: a pessoa com a chave [jean] no modelo;
  • linhas 23-36: a pessoa com a chave [jean] na sessão;

Os resultados são os seguintes:

  • em [1], a pessoa com a chave [jean] no modelo;
  • em [2], a pessoa com a chave [jean] na sessão;

O log da console é o seguinte:


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

Acima, vemos que a chave [jean] não está na sessão que recebe a ação. Deduz-se, portanto, que a chave [jean] foi inserida na sessão após a execução da ação e antes da exibição da visualização.

Agora, consideremos o caso em que uma chave é referenciada tanto por [@ModelAttribute] quanto por [@SessionAttributes]. Criamos as duas ações a seguir:


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

    // caso em que a chave de [@ModelAttribute] também seja uma chave de [@SessionAttributes]
    // nesse caso, o parâmetro correspondente é inicializado com o valor da sessão
    @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";
}

A ação [/v12a] serve apenas para inserir o elemento ['paul',new Personne(51, "paul", 33)] na sessão. Ela não faz mais nada. O fato de ela estar marcada por [@ResponseBody] indica que é ela quem gera a resposta para o cliente. Como seu tipo é [void], nenhuma resposta é gerada.

A ação [/v12b] aceita como parâmetro [@ModelAttribute("paul") Personne p]. Se nada mais for feito, um objeto [Personne] é instanciado e, em seguida, inicializado com os parâmetros da solicitação, e esse objeto não tem nenhuma relação com o objeto-chave [paul] inserido na sessão pela ação [/v12a]. Vamos adicionar a chave [paul] aos atributos de sessão da classe:


@Controller
@SessionAttributes({ "jean", "paul" })
public class ViewsController {
  • na linha 2, agora há dois atributos de sessão;

Voltemos aos parâmetros da ação [/v12b]:


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

Agora, o objeto [Personne p] não será instanciado, mas fará referência ao objeto-chave [paul] na sessão. Em seguida, o procedimento permanece o mesmo. O objeto-chave [paul] aparecerá, notadamente, no modelo da visualização que será exibida. É isso que queremos ver na linha 11 da ação [/v12b].

A visualização [vue-12.xml] será a seguinte:


<!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>
  • linha 8: faz-se referência à chave [paul] do modelo da visualização;

Isso resulta no seguinte (após executar a ação [/v12a], que insere a chave [paul] na sessão):

 

O log da console é o seguinte:


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}

A chave [paul] foi efetivamente inserida no modelo com o valor associado à chave [paul] na sessão.

5.10. [/v13]: gerar um formulário de preenchimento

Abordamos agora a entrada de dados em formulários e sua validação. Criamos um primeiro formulário com a seguinte ação [/v13]:


  // gera um formulário para inserir dados de uma pessoa
  @RequestMapping(value = "/v13", method = RequestMethod.GET)
  public String v13() {
    return "vue-13";
}

que se limita a exibir a seguinte visualização [vue-13.xml]:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{title}">Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <form action="/someURL" th:action="@{/v14.html}" method="post">
            <h2 th:text="#{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>

Se colocarmos essa visualização na pasta [static] com o nome [vue-13.html] e solicitarmos o URL [http://localhost:8080/vue-13.html], obteremos a seguinte página:

 
  • Na linha 8 do formulário, encontramos a tag <form> com o atributo [th:action]. Esse atributo será avaliado pelo Thymeleaf e seu valor substituirá o valor atual do atributo [action], que, portanto, está lá apenas para fins decorativos. Aqui, o valor do atributo [th:action] será [/v14.html];
  • nas linhas 17, 23 e 29, o valor do atributo [th:value] substituirá o do atributo [value]. Aqui, esse valor será a string vazia;

Ao consultar o URL [/v13.html], obtém-se o seguinte resultado:

 

Vejamos o código-fonte gerado pelo 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>

Nas linhas 9, 18, 24 e 30, vemos a avaliação dos atributos [th:action] e [th:value] feita pelo Thymeleaf.

5.11. [/v14]: gerenciar os valores enviados por um formulário

A ação [/v14] é a ação que recebe os valores enviados. Ela é a seguinte:


  // processa os valores do formulário
  @RequestMapping(value = "/v14", method = RequestMethod.POST)
  public String v14(Personne p) {
    return "vue-14";
}
  • linha 3: os valores enviados são encapsulados em um objeto [Personne p]. Sabe-se que esse objeto faz parte automaticamente do modelo M da visualização V que será exibida pela ação, associado à chave [personne];
  • linha 4: a visualização exibida é a visualização [vue-14.xml];

A visualização [vue-14.xml] é a seguinte:


<!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>
  • linha 9: recupera-se no modelo o objeto associado à chave [personne];
  • linhas 12, 16 e 20: exibem-se as características desse objeto;

Isso resulta no seguinte:

5.12. [/v15-/v16]: validação de um modelo

Com base no exemplo anterior, vejamos a sequência a seguir:

  • em [1], inserimos valores incorretos nos campos [id] e [age], do tipo [int];
  • em [2], a resposta do servidor indica que ocorreram dois erros;

Vamos usar o mesmo formulário, mas, em caso de erros de validação, redirecionaremos para uma página que sinalize esses erros para que o usuário possa corrigi-los.

A ação [/v15] é a seguinte:


    // ---------------------- exibição de um formulário
    @RequestMapping(value = "/v15", method = RequestMethod.GET)
    public String v15(SecuredPerson p) {
        return "vue-15";
}

Ela recebe como parâmetro um tipo [SecuredPerson], conforme segue:

  

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;

    // construtores
    public SecuredPerson() {

    }

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

    // getters e setters
...
}

Os campos [id, nom, age] foram anotados com restrições de validação. A visualização [vue-15.xml] exibida pela ação [/v15] é a seguinte:


<!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>
  • linhas 10-47: o objeto do modelo da página associado à chave [securedPerson] é recuperado. Após a execução de GET, obtém-se um objeto com seu valor de instanciação [id=0, nom=null, age=0];
  • linha 17: o valor do campo [securedPerson.id];
  • linha 20: a expressão [${#fields.hasErrors('id')}] permite verificar se houve erros de validação no campo [securedPerson.id]. Se for o caso, o atributo [th:errors="*{id}"] exibe a mensagem de erro associada;
  • esse cenário se repete na linha 29 para o campo [nom] e na linha 38 para o campo [age];
  • linha 45: a expressão [${#fields.errors('*')}] designa o conjunto de erros nos campos do objeto [securedPerson]. Assim, é o conjunto desses erros que será exibido nas linhas 44 a 46;
  • linha 16: observa-se que os valores do formulário serão enviados para a ação [/v16]. Esta ação é a seguinte:

    // -------------------- validação de um modelo------------------
    @RequestMapping(value = "/v16", method = RequestMethod.POST)
    public String v16(@Valid SecuredPerson p, BindingResult result) {
        // erros?
        if (result.hasErrors()) {
            return "vue-15";
        } else {
            return "vue-16";
        }
}
  • linha 3: a anotação [@Valid SecuredPerson p] força a validação dos valores enviados;
  • linha 5: verifica se o modelo da ação está incorreto ou não;
  • linha 6: se estiver incorreto, retorna-se o formulário [vue-15.xml]. Como este exibe as mensagens de erro, vamos verificá-las;
  • linha 8: se o modelo da ação for validado, exibe-se a seguinte visualização [vue-16.xml]:

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

Aqui estão alguns exemplos de execução:

5.13. [/v17-/v18]: verificação de mensagens de erro

Ao solicitar a ação [/v15] pela primeira vez, obtém-se o seguinte resultado:

 

Talvez seja preferível ter um formulário vazio em vez de zeros nos campos [Identifiant, Age]. Para isso, modificamos o modelo da ação da seguinte maneira:


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;

    // construtores
    public StringSecuredPerson() {

    }

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

    // getters e setters
...

}
  • linhas 12 e 19: os campos [id] e [age] foram alterados para o tipo [String];
  • linha 11: indica-se que o campo [id] deve ser um número de no máximo quatro dígitos, sem decimais;
  • linha 18: o mesmo vale para o campo [age], que deve ser um número inteiro de no máximo dois dígitos;

A ação [/v17] passa a ser a seguinte:


    // ---------------------- exibição de um formulário
    @RequestMapping(value = "/v17", method = RequestMethod.GET)
    public String v17(StringSecuredPerson p) {
        return "vue-17";
}

A visualização [vue-17.xml] exibida pela ação [/v17] é a seguinte:


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

As alterações ocorrem nas seguintes linhas:

  • linha 10: agora se trabalha com o objeto do modelo de chave [stringSecuredPerson];
  • linha 20: percorre-se a lista de erros do campo [id]. Na sintaxe [th:each="err,status : ${#fields.errors('id')}"], é a variável [err] que percorre a lista. A variável [status] fornece informações sobre cada iteração. Trata-se de um objeto [index, count, size, current], em que:
    • índice: é o número do elemento atual,
    • current: o valor desse elemento atual,
    • count, size: o tamanho da lista percorrida;
  • linha 20: exibe-se apenas o primeiro elemento da lista [th:if="${status.index}==0"];

A ação [/v18], que processa o POST da ação [/v17], é a seguinte:


    // -------------------- validação de um modelo------------------
    @RequestMapping(value = "/v18", method = RequestMethod.POST)
    public String v18(@Valid StringSecuredPerson p, BindingResult result) {
        // erros?
        if (result.hasErrors()) {
            return "vue-17";
        } else {
            return "vue-18";
        }
}

Os arquivos de mensagens evoluem da seguinte forma:

[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

Vejamos alguns exemplos:

 

Pode-se observar em [1] que os dois validadores do campo [age] foram executados:


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

Existe uma ordem definida para as mensagens de erro? Para o campo [age], parece que os validadores foram executados na ordem [Digits, Range]. No entanto, ao realizar várias consultas, percebe-se que essa ordem pode mudar. Portanto, não se pode confiar na ordem dos validadores. No [2], é exibida apenas uma das duas mensagens do campo [id]. No [3], são exibidas todas as mensagens de erro.

5.14. [/v19-/v20]: uso de diferentes validadores

Consideremos o seguinte novo modelo de ação:

  

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

Ele será exibido pela ação [/v19] a seguir:


    // ------------------ exibição de um formulário
    @RequestMapping(value = "/v19", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String v19(Form19 formulaire) {
        return "vue-19";
}
  • linha 3: a ação recebe como parâmetro um objeto [Form19 formulaire]. Se a ação GET não receber parâmetros, esse objeto será inicializado com os valores padrão do Java;
  • linha 4: a visualização [vue-19.xml] é exibida. Ela é a seguinte:

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

Este código exibe a seguinte visualização:

 

A página apresenta uma tabela com três colunas:

  • coluna 1: o validador do campo de entrada;
  • coluna 2: o campo de entrada;
  • coluna 3: as mensagens de erro no campo de entrada;

Vejamos, por exemplo, o código da visualização [/v19.html] para o validador [@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>

Encontramos aqui o código que acabamos de estudar com os formulários do tipo [Personne]:

  • linha 2: a primeira coluna: o nome do validador testado;
  • linha 4: o atributo Thymeleaf [th:field="*{hhmmss}] irá gerar os atributos HTML, [id="hhmmss"] e [name="hhmmss"]. O atributo Thymeleaf [th:value="*{hhmmss}"] irá gerar o atributo HTML [value="valeur de [form19.hhmmss]]";
  • linha 7: se o valor inserido no campo [form19.hhmmss] estiver incorreto, a linha 7 exibirá as mensagens de erro associadas a esse campo;

Os valores lançados são processados pela ação [/v20] a seguir:


    // ----------------- validação do modelo do formulário
    @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 {
            // redirecionamento para [vue-19]
            redirectAttributes.addFlashAttribute("form19", formulaire);
            return "redirect:/v19.html";
        }
}
  • linha 3: os valores lançados preencherão os campos do objeto [Form19 formulaire], caso sejam válidos;
  • linhas 4-6: se os valores lançados não forem válidos, o formulário [vue-19] é exibido novamente com as mensagens de erro;
  • linhas 6-10: se os valores enviados forem válidos, o objeto [Form19 formulaire], criado com esses valores, é disponibilizado para a próxima solicitação, neste caso, a de redirecionamento. Em seguida, ele é destruído;
  • linha 9: o cliente é redirecionado para a ação [/v19.html]. Essa ação exibirá novamente o formulário [vue-19], que contém código como:

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

O atributo [th:object="${form19}"] irá, então, recuperar o objeto associado ao atributo Flash [form19] e, assim, exibir novamente o formulário exatamente como foi preenchido.

O código do formulário ainda merece algumas explicações. Consideremos o código a seguir:


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

Isso gera o seguinte código HTML:


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

No código


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

os atributos Thymeleaf das linhas 1 e 3, [th:field="*{assertFalse}"], apresentam um problema. Foi mencionado que esse atributo gera os atributos HTML, [id=assertFalse] e [name=assertFalse]. A dificuldade decorre do fato de que, como isso é gerado nas linhas 1 e 3, temos dois atributos [name] idênticos e dois atributos [id] idênticos. Embora isso seja possível com o atributo [name], não é possível com o atributo [id]. Como podemos ver no código HTML gerado, o Thymeleaf gerou dois atributos [id] diferentes: [id=asserFalse1] e [id=assertFalse2]. O que é algo positivo. O problema é que não conhecemos esses identificadores e podemos precisar deles. Esse é o caso da tag [label] da linha 2. O atributo [for] de uma baliza HTML [label] deve fazer referência a um atributo [id], neste caso, aquele gerado para a tag [input] da linha 1. A documentação do Thymeleaf indica que a expressão [${#ids.prev('assertFalse')}"] permite obter o último atributo [id] gerado para o campo [assertFalse].

Agora, vamos analisar o código da lista suspensa do formulário:


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

Esse código gera o código HTML de uma lista suspensa:

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

O valor enviado será registrado com o nome [name="assertTrue"].

A visualização [vue-19.xml] utiliza uma folha de estilo:


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

Na linha 4, a folha de estilo utilizada deve ser colocada na pasta [static] do projeto:

  

Seu conteúdo é o seguinte:


@CHARSET "UTF-8";

.col1 {
    background: lightblue;
}

.col2 {
    background: Cornsilk;
}

.col3 {
    background: #e2d31d;
}

.error {
    color: red;
}

Agora, vamos examinar as datas:


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

A análise do tráfego de rede na ferramenta de desenvolvimento do Chrome (Ctrl+Shift+I) mostra que as datas são enviadas no formato (aaaa-mm-dd):

 

É por isso que as datas foram anotadas no validador:


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

que define o formato esperado para o valor enviado das datas.

Por fim, o arquivo de mensagens em francês [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

Vejamos alguns exemplos de execução:

 
 

Acima, entre [1] e [2], parece que nada aconteceu. No entanto, se observarmos as trocas de dados de rede (Ctrl-Shift-I), vemos que houve duas trocas de dados de rede com o servidor:

  • em [1], o POST inicial para [/v20];
  • para [2]; a resposta a essa ação é um redirecionamento;
  • em [3], a segunda solicitação, desta vez para [/v19];

A ação [/v19] é então executada:


    // ------------------ exibição de um formulário
    @RequestMapping(value = "/v19", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String v19(Form19 formulaire) {
        return "vue-19";
}
  • linha 3, o parâmetro [Form19 formulaire] é inicializado com o atributo Flash da chave [form19], que havia sido criado pela ação anterior [/v19] e que era um objeto do tipo [Form19] com os seguintes valores: os valores lançados na ação [/v19];
  • linha 4: a visualização [vue-19.xml] será exibida com, em seu modelo, um objeto [Form19 formulaire] inicializado com os valores enviados. É por isso que o usuário encontra o formulário exatamente como o enviou;

Por que um redirecionamento? Por que não enviamos simplesmente para a ação [/v19] acima? Teríamos obtido o mesmo resultado. Com algumas diferenças:

  • o navegador teria exibido no campo de endereço [http://localhost:8080/v20.html] em vez de [http://localhost:8080/v19.html], como fez aqui, pois ele exibe a última ação URL chamada;
  • se o usuário atualizar a página (F5), o resultado não é o mesmo:
    • no caso do redirecionamento, o URL exibido é o [http://localhost:8080/v19.html] obtido a partir de um GET. O navegador executará novamente esse último comando e, assim, obterá um formulário totalmente novo (o atributo Flash é usado apenas uma vez),
    • no caso de não haver redirecionamento, o URL exibido é o [http://localhost:8080/v20.html], obtido a partir de um POST. O navegador executará novamente esse último comando e, portanto, criará novamente um POST com os mesmos valores enviados anteriormente. Aqui isso não traz consequências, mas costuma ser indesejável e, por isso, geralmente se prefere o redirecionamento;

5.15. [/v21-/v22]: gerenciar botões de opção

Consideremos o seguinte componente Spring [Listes]:

  

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 e setters
  ...

}
  • linha 5: a classe [Listes] será um componente Spring;
  • linhas 8-10: listas utilizadas para preencher botões de opção, caixas de seleção e listas suspensas;

Na classe de configuração [Config], está escrito:


@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
  • linha 2: o pacote [models], onde se encontra o componente [Listes], será devidamente explorado pelo Spring;

Criamos as seguintes novas ações:


    // ------------------ formulário com botões de opção
    @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";
}
  • linhas 2-3: o componente [Listes] é injetado no controlador;
  • linha 6: gerenciamos um formulário do tipo [Form21], que descreveremos a seguir. Observe que especificamos sua chave, [form], no modelo da vista. Vale lembrar que, por padrão, ela seria [form21];
  • linha 7: injetamos o componente [Listes] no modelo. A vista precisará dele;
  • linha 8: exibimos a vista [vue-21.xml]. Essa vista exibirá o formulário [Form21], e os valores enviados serão direcionados para a ação [/v22] nas linhas 12 a 15;
  • linhas 12 a 15: a ação [/v22] limita-se a redirecionar para a ação [/v21], colocando os valores enviados que recebeu em um atributo Flash com a chave [form]. É importante que essa chave seja a mesma utilizada na linha 6;

O modelo [Form21] é o seguinte:

  

package istia.st.springmvc.models;

public class Form21 {

    // valores enviados
    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 e setters
    ...
}

A visualização [vue-21.xml] é a seguinte:


<!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>
  • linhas 36-40: observe-se a utilização do componente [Listes] incluído no modelo para gerar os textos dos campos de seleção;
  • a coluna 3 permite conhecer o valor lançado para um POST, ou o valor inicial do formulário no GET inicial;

Este código exibe a seguinte página:

 

correspondente ao código HTML a seguir:


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

Observa-se que os valores lançados (atributos name) são inseridos nos seguintes campos do modelo [Form21]:


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

Recomenda-se ao leitor que faça alguns testes. Observe-se que é o atributo [value] dos botões de opção que é enviado.

5.16. [/v23-/v24]: gerenciar caixas de seleção

Adicionamos a seguinte nova ação:


    // ------------------ formulário com caixas de seleção
    @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";
}
  • linha 3: continuamos a usar o modelo [Form21];

A visualização [vue-23.xml] é a seguinte:


<!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>
  • linhas 37-41: observe-se o uso do componente [Listes] para gerar os rótulos das caixas de seleção;

Este código exibe a seguinte página:

 

gerada a partir do código HTML a seguir:


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

Observe-se que os valores lançados (atributos name) são inseridos nos seguintes campos do [Form21]:


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

Trata-se de tabelas, pois, para cada campo, há várias caixas de seleção com o nome do campo. Portanto, é possível que vários valores enviados cheguem com o mesmo nome (atributo name do formulário). Por isso, é necessária uma tabela para recuperá-los.

Voltemos ao código Thymeleaf da coluna 3 da página:


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

Os campos mencionados nas linhas 2 e 14 são os seguintes:


    private String strCouleurs;
    private String strBijoux;

Eles são calculados pela ação [/v24], que gerencia a ação POST:


    // mapeador 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";
}

É importante lembrar aqui que a biblioteca jackson / jSON está entre as dependências do projeto.

  • linha 2: cria-se um tipo [ObjectMapper] que permite serializar/desserializar objetos em jSON,
  • linha 7: serializa-se a matriz de cores em jSON. O resultado é colocado no campo [strCouleurs];
  • linha 8: serializa-se a tabela de joias em jSON. O resultado é colocado no campo [strBijoux];

Veja um exemplo de execução:

Observe que é o atributo [value] das caixas de seleção que é enviado.

5.17. [/25-/v26]: gerenciar listas

Adicionamos a seguinte ação [/v25]:


  // ------------------ formulário com listas
  @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";
}

A visualização [vue-25.xml] é a seguinte:


<!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>
  • linhas 38-42: geração de uma lista de múltipla escolha cujos rótulos são obtidos do componente [Listes], que já utilizamos;

A página exibida é a seguinte:

 

gerada pelo código HTML a seguir:


<!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>
  • linha 44: é possível observar que o Thymeleaf criou um campo oculto. Não entendi qual é a função dele:
  • os valores enviados (atributos value das tags option) serão inseridos nos seguintes campos (atributos name) de [Form21]:

    private int couleur2;
    private int[] bijoux2;
  • linha 38: a lista [bijoux2] é de múltipla escolha. Portanto, vários valores podem ser enviados associados ao nome [bijoux2]. Para recuperá-los, o campo [bijoux2] deve ser uma matriz. Observe-se que se trata de uma matriz de inteiros. Isso é possível, pois os valores postados podem ser convertidos para esse tipo;

Os valores são lançados na ação [/v26] a seguir:


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

Não há nada aqui que já não tenhamos visto. Aqui está um exemplo de execução:

5.18. [/v27]: configuração das mensagens

Consideremos a seguinte ação [/v27]:


  // ------------------ mensagens configuradas
  @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";
}

A ação limita-se a inserir quatro valores no modelo e exibe a seguinte visualização [vue-27.xml]:


<!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>
  • linha 8: uma mensagem sem parâmetros;
  • linha 9: uma mensagem com um parâmetro [$param1] extraído do modelo;
  • linha 10: uma mensagem com dois parâmetros [$param2, $param3] retirados do modelo;
  • linha 11: uma mensagem com um parâmetro. Esse parâmetro é, por sua vez, uma chave de mensagem (presença do símbolo #). A chave é fornecida por [$param4];

O arquivo de mensagens em francês é o seguinte:

[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

Para indicar a presença de parâmetros na mensagem, utilizam-se os símbolos {0}, {1}, ...

A fusão do modelo criado pela ação [/v27] com a visualização [vue-27] produzirá o seguinte código HTML:


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

o que resulta na seguinte visualização:

 

O arquivo de mensagens em inglês é o seguinte:

[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

A fusão do modelo criado pela ação [/v27] com a visualização [vue-27] produzirá o seguinte código HTML:


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

o que resulta na seguinte visualização:

 

Percebe-se que a última mensagem foi internacionalizada de ponta a ponta, o que não é o caso das duas anteriores.

5.19. Uso de uma página mestre

Em um aplicativo web, é comum que as visualizações compartilhem alguns elementos que podem ser agrupados em uma página mestre. Veja um exemplo:

Acima, temos duas páginas semelhantes nas quais o fragmento [1] foi substituído pelo fragmento [2]. A visualização corresponde a uma página-mestre com três fragmentos fixos [3-5] e um fragmento variável [6].

5.19.1. O projeto

Estamos construindo um projeto [springmvc-masterpage] seguindo a abordagem descrita no parágrafo 5.1.

  

O arquivo [pom.xml] é o seguinte:


<?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/> <!-- pesquisa de pai no repositório -->
    </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>

Uma das dependências fornecidas por esse arquivo é necessária para a página mestre:

 

Os pacotes [config] e [main] são idênticos aos de nomes iguais do projeto anterior.

5.19.2. A página mestre

  

A página mestre é a seguinte visualização [layout.xml]:


<!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>
  • linha 2: a página mestre deve definir o espaço de nomes [xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"], cujo elemento é utilizado na linha 19;
  • linhas 10-12: geram o campo [1] abaixo. A tag Thymeleaf [th:include] permite incluir na visualização atual um fragmento definido em outro arquivo. Isso permite reusar fragmentos utilizados em várias visualizações;
  • linhas 15-17: geram a área [2] abaixo;
  • linhas 19-20: geram a área [3] abaixo. O atributo [layout:fragment] é um atributo do espaço de nomes [xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"]. Ele indica uma área que, durante a execução, pode ser substituída por outra;
  • linhas 24-28: geram o campo [4] abaixo;

5.19.3. Os fragmentos

Os fragmentos [entete.xml], [menu.xml] e [basdepage.xml] são os seguintes:

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

O fragmento [page1.xml] é o seguinte:


<!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>
  • linha 2: o atributo [layout:decorator="layout"] indica que a página atual [page1.xml] está “decorada”, ou seja, pertence a uma página-mestre. Este é o valor do atributo, neste caso a visualização [layout.xml];
  • linha 3: indica-se em qual fragmento da página mestre [page1.xml] será inserido. O atributo [layout:fragment="contenu"] indica que [page1.xml] será inserido no fragmento denominado [contenu], ou seja, na área [3] da página mestre;
  • linhas 5-7: o conteúdo do fragmento é um formulário que oferece um botão de POST para a ação [/page2.html];

O fragmento [page2.xml] é semelhante:


<!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. As ações

 

O controlador [Layout.java] é o seguinte:


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";
    }
}
  • linhas 10-12: a ação [/page1] limita-se a exibir a visualização [page1.xml];
  • linhas 15-17: o mesmo vale para a ação [/page2], que exibe a visualização [page2.xml];