Skip to content

5. As vistas do Thymeleaf

Voltemos à arquitetura de uma aplicação Spring MVC.

Os dois capítulos anteriores descreveram vários aspetos do bloco [1], as ações. Abordamos agora:

  • o bloco [2] das vistas V;
  • o bloco [3] do modelo M apresentado por estas vistas;

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). Desde há alguns anos, a tecnologia [Thymeleaf] [http://www.thymeleaf.org/] também pode ser utilizada. É essa tecnologia que apresentamos agora.

5.1. O projeto STS

Criamos um novo projeto:

  • no [3], indicar que o projeto necessita das dependências [Thymeleaf]. Isto irá adicionar, além das dependências [Spring MVC] do projeto anterior, as do framework [Thymeleaf] e [5];

Agora, vamos desenvolver este projeto da seguinte forma:

  

Inspiramo-nos no projeto anterior:

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

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

Esta configuração permite, por enquanto, a gestão 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], uma vez que, doravante, as ações não irão gerar a resposta para o cliente. Em vez disso, irão:
    • construir um modelo M
    • retornar um tipo [String], que será o nome da vista [Thymeleaf] responsável por apresentar esse modelo. É a combinação desta vista V e deste modelo M que irá gerar o fluxo HTML enviado ao cliente;

O ficheiro [messages.properties] está, por enquanto, vazio.

5.2. [/v01]: os fundamentos do Thymeleaf

Analisamos a primeira ação seguinte 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 devolve um tipo [String]. Este será o nome da ação;
  • linha 4: esta vista será [v01]. Por predefinição, deve estar localizada na pasta [templates] e chamar-se [v01.html];

A vista [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 ficheiro 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 ficheiro HTML válido que pode ser visualizado. Colocamo-lo na pasta [static] [2] com o nome [vue-01.html] e acedemos a ele diretamente através de um navegador:

Se analisarmos 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 vista é 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 forma pelo Thymeleaf:

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

Após o processamento, a baliza acima passou a ser:


<title>Les vues</title>

Solicitemos a ação [/v01]:

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

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

 

Como se deve interpretar isto? A vista [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 vista [vue-02.html] é uma cópia da [v01.html]:

  

Agora, vamos solicitar a URL e a [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 registos da consola, no [1], vemos que a ação [/v02] foi chamada e que esta fez com que a vista [vue-02.html] fosse apresentada no [2];

Agora sabemos que o URL [http://localhost:8080/v02.html] também pode referir-se a um ficheiro [/v02.html] na pasta [static]. O que acontece se esse ficheiro existir? Vamos experimentar. Criamos na pasta [static] o seguinte ficheiro [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>

depois solicitamos o URL [http://localhost:8080/v02.html]:

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

  • executa a ação [/x], se esta existir;
  • apresenta a página [/static/x.html], caso 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 vistas

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


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

Esta ação 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-chave [title]. Criamos os seguintes ficheiros [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]:

Repare que utilizámos o que aprendemos recentemente. Em vez de designar a ação [v03] como [/v03], designámo-la como [/v03.html].

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

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


    // criação do modelo M de uma vista 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 vista é inserido nos parâmetros da ação. Por predefinição, este 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 a este dicionário com a chave [personne] associada a um valor do tipo [Personne];
  • linha 5: exibimos o modelo na consola para ver como fica;
  • na linha 6: exibimos a vista [vue-04.html];

A classe [Personne] é a 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, introduz-se um novo tipo de expressão Thymeleaf ${var}, em que var é uma chave do modelo M da vista. Recorde-se que a ação [/v04] inseriu no modelo uma chave [personne] associada a um tipo Pessoa[id, nom, age];
  • linha 10: apresenta o nome da pessoa presente no modelo;
  • linha 14: apresenta a sua idade;

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

e a natureza do modelo M encontra-se nos registos da consola [2].

Podemos questionar-nos por que razão não se escreve a vista [vue-04] da seguinte forma:


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

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

  

A vista [vue-04b.html] é uma cópia da vista [vue-04.html]. O mesmo se aplica à vista [vue-04a.html], mas retirámos 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. É por isso que faz sentido colocar textos estáticos numa vista Thymeleaf, mesmo que, na execução, eles venham a ser substituídos por outros textos.

Agora, vamos analisar um pormenor técnico. Na vista [vue-04.html], formatamos o código através 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 balizas estão mal alinhadas e o código torna-se mais difícil de ler. Se renomearmos [vue-04.html] para [vue-04.xml] e reformatarmos o código, as balizas voltam a ficar alinhadas. Portanto, o sufixo [xml] seria mais prático. É possível trabalhar com este sufixo. Para tal, é necessário configurar o Thymeleaf. Para não desfazer o que fizemos, duplicamos o projeto [springmvc-vues] analisado num projeto [springmvc-vues-xml]

  

Alteramos o ficheiro [pom.xml] da seguinte forma:


    <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 vistas presentes na pasta [templates]:

  

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

  

Este documento apresenta as propriedades que o Spring Boot utiliza durante a autoconfiguração e que podem ser alteradas através de uma configuração diferente no ficheiro [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> é adicionado
spring.thymeleaf.cache=true # definido como «false» para atualização instantânea

Assim, bastaria inserir a linha


spring.thymeleaf.suffix=.xml

no ficheiro [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-24 configuram um [TemplateResolver] para o Thymeleaf. É este objeto que é carregado a partir de um nome de vista fornecido por uma ação, para encontrar o ficheiro correspondente;
  • as linhas 18 e 19 definem o prefixo e o sufixo a adicionar ao nome da vista para localizar o ficheiro. Assim, se o nome da vista for [vue04], o ficheiro 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, apareça o cabeçalho HTTP:

Content-Type:text/html;charset=UTF-8
  • linha 20: indica que a vista cumpre a norma HTML5;
  • linha 22: indica que as vistas Thymeleaf podem ser armazenadas em cache;
  • linhas 26-31: define o motor de resolução de vistas do conjunto Spring/Thymeleaf com o motor de resolução anterior;

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

 

Notamos 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 numa vista Thymeleaf

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


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

É idêntica à ação [/v04]. A vista [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: dentro destas linhas, um objeto Thymeleaf é definido pelo atributo [th:object="${personne}"] (linha 8). Este objeto é, neste caso, o objeto de chave [personne] que se encontra no modelo:
  • linha 11: a expressão Thymeleaf [*{nom}] é equivalente a [${objet.nom}], sendo que [objet] é o objeto Thymeleaf atual. Portanto, aqui a expressão [*{nom}] é equivalente a [${personne.nom}];
  • linha 15: o mesmo;

O resultado:

 

5.6. [/v06]: os testes numa vista Thymeleaf

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


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

É idêntica às duas ações anteriores. Apresenta a seguinte vista [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 baliza é apresentada; caso contrário, não é. Assim, neste caso, se ${personne.age}>=18, o texto [#{personne.majeure}] será exibido, ou seja, a mensagem com a chave [personne.majeure] nos ficheiros de mensagens;
  • linha 18: não é possível escrever [*{age} < 18], pois o sinal < é um carácter reservado. Por isso, é necessário utilizar o seu equivalente HTML [&lt;], também designado por entidade HTML [http://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references];

Os ficheiros de mensagens são alterados:

[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 numa vista Thymeleaf

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


    // criação do modelo M de uma vista 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 vista [vue-07];

A vista [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 baliza em que se encontra, neste caso uma baliza <li>. Tem aqui dois parâmetros [element : collection], em que [collection] é uma coleção de objetos, neste caso uma lista de pessoas. O Thymeleaf irá 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 este elemento, o atributo [th:text] será avaliado. A 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] aos ficheiros de mensagens;

Eis o resultado:

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

Voltamos a abordar algo que vimos ao analisar 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, este é instanciado e, eventualmente, modificado pelos parâmetros da consulta
    // 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 por uma letra minúscula
    // se @ModelAttribute estiver ausente, então tudo decorre como se estivesse presente sem chave
    // note-se que esta 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")] irá adicionar automaticamente o objeto [Personne p] ao modelo, associado à chave [someone];
  • linha 12: para verificar o modelo;
  • linha 13: apresenta a vista [vue-08.xml];

A vista [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, na consola, temos o seguinte registo:

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] irá automaticamente inserir a pessoa [p] no modelo. Como não é especificada nenhuma chave, a chave utilizada é o nome da classe com o primeiro carácter em minúscula. Assim, [Personne p] é equivalente a [@ModelAttribute("personne") Personne p];

A vista [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 do modelo utilizada é [personne];

Eis um resultado:

 

e o registo na consola do servidor:

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

Agora, consideremos 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 pedido, 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 vista [vue-10.xml]. Note-se que o parâmetro [Model model] só precisa de estar presente para a instrução da linha 8. Sem ele, é desnecessário;

A vista [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 registo da consola é o seguinte:

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

5.9. [/v11]: @SessionAttributes

Voltamos a abordar algo que vimos ao analisar 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 acabámos de analisar. A diferença reside numa 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 colocada na sessão;

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

A vista [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 apresentadas 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 registo da consola é o seguinte:


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

No exemplo acima, verifica-se que a chave [jean] não se encontra 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 vista.

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


    @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] seja também uma chave de [@SessionAttributes]
    // neste 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. Não faz mais nada. O facto de estar marcada por [@ResponseBody] indica que é ela que gera a resposta para o cliente. Como o seu tipo é [void], não é gerada qualquer resposta.

A ação [/v12b] aceita como parâmetro [@ModelAttribute("paul") Personne p]. Se não for feita qualquer outra ação, é instanciado um objeto [Personne], que é posteriormente inicializado com os parâmetros da solicitação; este objeto não tem qualquer 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, existem agora 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 irá referenciar o objeto-chave [paul] na sessão. A seguir, o procedimento mantém-se o mesmo. O objeto-chave [paul] irá, nomeadamente, constar no modelo da vista que será apresentada. É isso que queremos ver na linha 11 da ação [/v12b].

A vista [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 vista;

Isto dá o seguinte resultado (após executar a ação [/v12a], que coloca a chave [paul] na sessão):

 

O registo da consola é 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 introdução de dados

Passamos agora à introdução 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 introduzir os dados de uma pessoa
  @RequestMapping(value = "/v13", method = RequestMethod.GET)
  public String v13() {
    return "vue-13";
}

que se limita a apresentar a seguinte vista [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 esta vista na pasta [static] com o nome [vue-13.html] e solicitarmos o URL [http://localhost:8080/vue-13.html], obtemos a seguinte página:

 
  • Na linha 8 do formulário, encontra-se a baliza <form> com o atributo [th:action]. Este atributo será avaliado pelo Thymeleaf e o seu valor substituirá o valor atual do atributo [action], que, portanto, está lá apenas para fins decorativos. Neste caso, 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 cadeia vazia;

Quando se solicita 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] efetuada pelo Thymeleaf.

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

A ação [/v14] é a ação que recebe os valores enviados. É 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 num objeto [Personne p]. Sabe-se que este objeto faz automaticamente parte do modelo M da vista V que será apresentada pela ação, associado à chave [personne];
  • linha 4: a vista apresentada é a vista [vue-14.xml];

A vista [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: apresentam-se as características deste objeto;

O resultado é o seguinte:

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

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

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

Vamos utilizar o mesmo formulário, mas, em caso de erros de validação, vamos redirecionar para uma página que indique esses erros, para que o utilizador os possa corrigir.

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

Recebe como parâmetro um tipo [SecuredPerson], conforme se 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 vista [vue-15.xml] apresentada 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. No final da ação GET, obtém-se um objeto com o 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 determinar se ocorreram erros de validação no campo [securedPerson.id]. Se for esse o caso, o atributo [th:errors="*{id}"] exibe a mensagem de erro associada;
  • este cenário repete-se 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 destes erros que será apresentado nas linhas 44-46;
  • linha 16: verifica-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";
        }
}
  • na linha 3, a anotação [@Valid SecuredPerson p] obriga à validação dos valores enviados;
  • linha 5: verifica se o modelo da ação está incorreto ou não;
  • linha 6: se estiver incorreto, é devolvido o formulário [vue-15.xml]. Como este apresenta as mensagens de erro, vamos ver essas mensagens;
  • linha 8: se o modelo da ação for validado, exibe-se a seguinte vista [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>

Eis alguns exemplos de execução:

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

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

 

Pode ser preferível ter um formulário vazio em vez de zeros nos campos [Identifiant, Age]. Para o conseguir, alteramos o modelo da ação da seguinte forma:


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] passam a ter o tipo [String];
  • linha 11: indica-se que o campo [id] deve ser um número com, no máximo, quatro dígitos, sem decimais;
  • linha 18: o mesmo se aplica ao campo [age], que deve ser um número inteiro com, no máximo, dois algarismos;

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 vista [vue-17.xml] apresentada 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: passa-se agora a trabalhar 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:
    • index: é o número do elemento atual,
    • current: o valor desse elemento atual,
    • count, size: o tamanho da lista percorrida;
  • linha 20: apenas é apresentado 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 ficheiros 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:

 

No [1], verifica-se 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? No caso do campo [age], parece que os validadores foram executados na ordem [Digits, Range]. No entanto, se forem efetuadas várias consultas, verifica-se que essa ordem pode variar. Por conseguinte, não se pode confiar na ordem dos validadores. No [2], é apresentada apenas uma das duas mensagens do campo [id]. No [3], são apresentadas todas as mensagens de erro.

5.14. [/v19-/v20]: utilização 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
...
}

Será apresentado pela seguinte ação [/v19]:


    // ------------------ 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, este objeto será inicializado com os valores por defeito do Java;
  • linha 4: a vista [vue-19.xml] é apresentada. Esta é 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 apresenta a seguinte vista:

 

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

  • coluna 1: o validador do campo de introdução de dados;
  • coluna 2: o campo de introdução de dados;
  • coluna 3: as mensagens de erro relativas ao campo de introdução de dados;

Analisemos, por exemplo, o código da vista [/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 acabámos de analisar 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 introduzido no campo [form19.hhmmss] estiver incorreto, a linha 7 exibe as mensagens de erro associadas a esse campo;

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


    // ----------------- 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] é novamente apresentado 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, é eliminado;
  • linha 9: redireciona-se o cliente para a ação [/v19.html]. Esta irá voltar a apresentar 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, voltar a apresentar o formulário tal como foi preenchido.

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


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

Isto 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}"], levantam um problema. Foi referido que este atributo gerava os atributos HTML, [id=assertFalse] e [name=assertFalse]. A dificuldade reside no facto de, ao serem gerados nas linhas 1 e 3, termos dois atributos [name] idênticos e dois atributos [id] idênticos. Embora isso seja possível com o atributo [name], não o é com o atributo [id]. Como se pode ver no código HTML gerado, o Thymeleaf gerou dois atributos [id] diferentes: [id=asserFalse1] e [id=assertFalse2]. O que é positivo. O problema é que não conhecemos esses identificadores e podemos precisar deles. É o caso da baliza [label] da linha 2. O atributo [for] de uma baliza HTML [label] deve referenciar um atributo [id], neste caso, aquele gerado para a baliza [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].

Consideremos agora o código da lista suspensa do formulário:


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

Este 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á publicado com o nome [name="assertTrue"].

A vista [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:

  

O seu conteúdo é o seguinte:


@CHARSET "UTF-8";

.col1 {
    background: lightblue;
}

.col2 {
    background: Cornsilk;
}

.col3 {
    background: #e2d31d;
}

.error {
    color: red;
}

Agora, analisemos 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 com o validador:


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

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

Por fim, o ficheiro 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], sendo que a resposta a esta 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 tinha sido criado pela ação anterior [/v19] e que era um objeto do tipo [Form19] com, como valores, os valores introduzidos na ação [/v19];
  • linha 4: a vista [vue-19.xml] será apresentada com, no seu modelo, um objeto [Form19 formulaire] inicializado com os valores enviados. É por isso que o utilizador encontra o formulário tal como o enviou;

Porquê um redirecionamento? Por que razão não se enviou simplesmente para a ação [/v19] acima referida? Ter-se-ia obtido o mesmo resultado. Com algumas diferenças:

  • o navegador teria colocado no seu campo de endereço [http://localhost:8080/v20.html] em vez de [http://localhost:8080/v19.html], como fez aqui, pois exibe a última ação URL chamada;
  • se o utilizador atualizar a página (F5), o resultado não é de todo o mesmo:
    • no caso do redirecionamento, o URL apresentado é o [http://localhost:8080/v19.html] obtido a partir de um GET. O navegador irá executar novamente este último comando e obterá então um formulário totalmente novo (o atributo Flash é utilizado apenas uma vez),
    • no caso de não haver redirecionamento, o URL apresentado é o [http://localhost:8080/v20.html], obtido a partir de um POST. O navegador irá executar novamente este último comando e, por conseguinte, irá criar novamente um POST com os mesmos valores enviados anteriormente. Neste caso, isso não tem consequências, mas é frequentemente indesejável e, por isso, em geral, prefere-se a redireção;

5.15. [/v21-/v22]: gerir 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: gerimos um formulário do tipo [Form21] que iremos descrever. Note-se que especificámos a sua chave [form] no modelo da vista. Recorde-se que, por predefinição, esta teria sido [form21];
  • linha 7: injetamos o componente [Listes] no modelo. A vista vai precisar dele;
  • linha 8: exibimos a vista [vue-21.xml]. Esta vista irá exibir o formulário [Form21] e os valores enviados serão encaminhados para a ação [/v22] das linhas 12-15;
  • linhas 12-15: a ação [/v22] limita-se a redirecionar para a ação [/v21], colocando os valores enviados que recebeu num atributo Flash com a chave [form]. É importante que esta chave seja a mesma que a 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 vista [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: note-se a utilização do componente [Listes] incluído no modelo, para gerar os textos das caixas 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 apresenta a página seguinte:

 

correspondente ao seguinte código HTML:


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

Verifica-se que os valores enviados (atributos «name») são inseridos nos seguintes campos do modelo [Form21]:


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

Convidamos o leitor a realizar alguns testes. Note-se que é o atributo [value] dos botões de opção que é enviado.

5.16. [/v23-/v24]: gerir 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 utilizar o modelo [Form21];

A vista [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: note-se a utilização do componente [Listes] para gerar os rótulos das caixas de seleção;

Este código apresenta a seguinte página:

 

resultante do seguinte código HTML:


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

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


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

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

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 referenciados nas linhas 2 e 14 são os seguintes:


    private String strCouleurs;
    private String strBijoux;

São calculados pela ação [/v24], que gere 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 que a biblioteca jackson / jSON faz parte das dependências do projeto.

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

Eis um exemplo de execução:

Note-se que é o atributo [value] das caixas de seleção que é enviado.

5.17. [/25-/v26]: gerir 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 vista [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 escolha múltipla em que os rótulos são retirados do componente [Listes] que já utilizámos;

A página apresentada é 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 compreendi a sua função:
  • os valores enviados (atributos value das balizas option) serão colocados nos seguintes campos (atributos name) de [Form21]:

    private int couleur2;
    private int[] bijoux2;
  • linha 38: a lista [bijoux2] é de escolha múltipla. Assim, podem ser enviados vários valores associados ao nome [bijoux2]. Para os recuperar, o campo [bijoux2] deve ser um tabela. Note-se que se trata de uma matriz de inteiros. Isto é possível, uma vez que os valores lançados podem ser convertidos para este tipo;

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


  @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á aqui nada que já não tenhamos visto. Eis um exemplo de execução:

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

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


  // ------------------ mensagens parametrizadas
  @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 apresenta a seguinte vista [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] retirado do modelo;
  • linha 10: uma mensagem com dois parâmetros [$param2, $param3] retirados do modelo;
  • linha 11: uma mensagem com um parâmetro. Este parâmetro é, por sua vez, uma chave de mensagem (presença de #). A chave é fornecida por [$param4];

O ficheiro 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 vista [vue-27] irá 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 vista:

 

O ficheiro 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 vista [vue-27] irá 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 vista:

 

Vê-se que a última mensagem foi internacionalizada na íntegra, o que não acontece com as duas anteriores.

5.19. Utilização de uma página-mestre

Numa aplicação web, é frequente que as vistas partilhem um certo número de elementos que podem ser agrupados numa página mestre. Eis um exemplo:

Acima, temos duas páginas semelhantes em que o fragmento [1] foi substituído pelo fragmento [2]. A vista 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 a construir um projeto [springmvc-masterpage] seguindo o procedimento descrito no parágrafo 5.1.

  

O ficheiro [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 elemento 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 introduzidas por este ficheiro é necessária para a página mestre:

 

Os pacotes [config] e [main] são idênticos aos que têm os mesmos nomes no projeto anterior.

5.19.2. A página mestre

  

A página mestre é a seguinte vista [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 a área [1] abaixo. A baliza Thymeleaf [th:include] permite incluir na vista atual um fragmento definido noutro ficheiro. Isto permite reutilizar os fragmentos utilizados em várias vistas;
  • 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"]. Indica uma zona que, durante a execução, pode ser substituída por outra;
  • linhas 24-28: geram a zona [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, que pertence a uma página-mãe. Esta é o valor do atributo, neste caso a vista [layout.xml];
  • linha 3: indica-se em que fragmento da página-mãe será inserida a [page1.xml]. 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 apresenta 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 apresentar a vista [page1.xml];
  • linhas 15-17: o mesmo se aplica à ação [/page2], que exibe a vista [page2.xml];