Skip to content

5. Widoki Thymeleaf

Wróćmy do architektury aplikacji Spring MVC.

W dwóch poprzednich rozdziałach omówiono różne aspekty bloku [1], czyli akcji. Teraz zajmiemy się:

  • bloku [2] widoków V;
  • blok [3] modelu M wyświetlanego przez te widoki;

Od momentu powstania Spring MVC technologia generowania stron HTML wysyłanych do przeglądarek klienckich opierała się na stronach JSP (Java Server Pages). Od kilku lat można również korzystać z technologii [Thymeleaf] [http://www.thymeleaf.org/]. To właśnie tę technologię przedstawiamy teraz.

5.1. Projekt STS

Tworzymy nowy projekt:

  • w projekcie [3] należy wskazać, że projekt wymaga zależności [Thymeleaf]. Spowoduje to dodanie, oprócz zależności [Spring MVC] z poprzedniego projektu, również zależności frameworka [Thymeleaf] i [5];

Teraz zmodyfikujmy ten projekt w następujący sposób:

  

Czerpiemy inspirację z poprzedniego projektu:

  • [istia.st.springmvc.controllers] będzie zawierał kontrolery;
  • [istia.st.springmvc.models] będzie zawierał modele akcji i widoków;
  • [istia.st.springmvc.main] to pakiet klasy wykonywalnej Spring Boot;
  • [templates] będzie zawierał widoki Thymeleaf;
  • [i18n] będzie zawierał zinternacjonalizowane komunikaty wyświetlane przez widoki;

Klasa [Application] ma następującą postać:


package istia.st.springmvc.main;

import org.springframework.boot.SpringApplication;

public class Application {

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

Klasa [Config] ma następującą postać:


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

Ta konfiguracja umożliwia na razie obsługę ustawień regionalnych.

Kontroler [ViewController] ma następujący numer:


package istia.st.springmvc.actions;

import org.springframework.stereotype.Controller;

@Controller
public class ViewsController {

}
  • w wierszu 5 adnotacja [@Controller] zastąpiła adnotację [@RestController], ponieważ od tej pory akcje nie będą generować odpowiedzi dla klienta. Będą one:
    • utworzyć model M
    • zwrócić typ [String], który będzie nazwą widoku [Thymeleaf] odpowiedzialnego za wyświetlenie tego modelu. To połączenie tego widoku V i tego modelu M wygeneruje strumień HTML wysyłany do klienta;

Plik [messages.properties] jest na razie pusty.

5.2. [/v01]: podstawy Thymeleaf

Rozważmy następującą pierwszą akcję w pliku [ViewsController]:


    // Podstawy Thymeleaf – 1
    @RequestMapping(value = "/v01", method = RequestMethod.GET)
    public String v01() {
        return "v01";
}
  • wiersz 3: akcja zwraca typ [String]. Będzie to nazwa akcji;
  • wiersz 4: ten widok będzie miał nazwę [v01]. Domyślnie powinien znajdować się w folderze [templates] i nosić nazwę [v01.html];

Widok [v01.html] wygląda następująco:


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

Jest to plik o nazwie HTML. Obecność Thymeleaf widać:

  • w przestrzeni nazw [th] w wierszu 2;
  • w atrybutach [th:text] w wierszach 4 i 8;

Mamy tu poprawny plik HTML, który można wyświetlić. Umieszczamy go w folderze [static] [2] pod nazwą [vue-01.html] i wywołujemy go bezpośrednio w przeglądarce:

Jeśli przyjrzymy się kodowi źródłowemu strony [2], zauważymy, że atrybuty [th:text] zostały przesłane przez serwer, ale zignorowane przez przeglądarkę. Gdy widok jest wynikiem akcji, uruchamia się Thymeleaf i interpretuje atrybuty [th] przed wysłaniem odpowiedzi do klienta.

Tag HTML:


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

jest przetwarzana przez Thymeleaf w następujący sposób:

  • th:text ma składnię th:text="wyrażenie”, gdzie wyrażenie jest wyrażeniem do obliczenia. Gdy wyrażenie to jest ciągiem znaków, jak w tym przypadku, należy je ująć w apostrofy;
  • wartość [expression] zastępuje tekst znacznika HTML, w tym przypadku tekst znacznika [title];

Po przetworzeniu powyższy tag przybrał następującą postać:


<title>Les vues</title>

Wywołajmy akcję [/v01]:

  • w [2] widać, jak Thymeleaf dokonał zamiany;

Teraz wywołajmy akcję URL z [http://localhost:8080/v01.html]:

 

Jak należy to interpretować? Czy widok [templates/v01.html] został wygenerowany bezpośrednio, bez przechodzenia przez akcję? Aby wyjaśnić tę kwestię, tworzymy następującą akcję [/v02]:


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

Widok [vue-02.html] jest kopią widoku [v01.html]:

  

Teraz wywołajmy widok URL z widoku [http://localhost:8080/vue-02.html]:

 

Nie znaleziono pliku URL. Teraz poprośmy o pliki URL i [http://localhost:8080/v02.html]

  • w logach konsoli przy [1] widać, że wywołano akcję [/v02], a ta spowodowała wyświetlenie widoku [vue-02.html] w [2];

Wiemy już, że nazwa URL [http://localhost:8080/v02.html] może również odnosić się do pliku [/v02.html] znajdującego się w folderze [static]. Co się stanie, jeśli ten plik istnieje? Spróbujmy. W folderze [static] tworzymy następujący plik [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>

następnie żądamy plików URL i [http://localhost:8080/v02.html]:

[1] oraz [2] wskazują, że wywołano akcję [/v02]. Należy zatem pamiętać, że gdy żądana akcja URL ma postać [/x.html], Spring / Thymeleaf:

  • wykonuje akcję [/x], jeśli istnieje;
  • wyświetla stronę [/static/x.html], jeśli istnieje;
  • w przeciwnym razie zgłasza wyjątek 404 Not found;

Aby uniknąć nieporozumień, od tej pory akcje i widoki nie będą miały tych samych nazw.

5.3. [/v03]: internacjonalizacja widoków

Integracja Spring / Thymeleaf umożliwia Thymeleaf korzystanie z plików komunikatów Spring. Rozważmy następującą nową akcję [/v03]:


    // internacjonalizacja widoków
    @RequestMapping(value = "/v03", method = RequestMethod.GET)
    public String v03() {
        return "vue-03";
}

Powoduje ona wyświetlenie następującego widoku [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>

W wierszach 4 i 8 wyrażeniem atrybutu [th:text] jest #{title}, którego wartością jest komunikat o kluczu [title]. Tworzymy następujące pliki: [messages_fr.properties] i [messages_en.properties]:

[messages_fr.properties]


title=Les vues dans Spring MVC

[messages_en.properties]


title=Views in Spring MVC

Poprośmy o pliki URL, [http://localhost:8080/v03.html?lang=fr] i [http://localhost:8080/v03.html?lang=en]:

Zauważmy, że wykorzystaliśmy to, czego niedawno się nauczyliśmy. Zamiast oznaczyć akcję [v03] jako [/v03], oznaczyliśmy ją jako [/v03.html].

5.4. [/v04]: utworzenie szablonu M dla widoku V

Rozważmy następującą nową akcję [/v04]:


    // tworzenie szablonu M dla widoku 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";
}
  • wiersz 4: szablon widoku jest wstrzykiwany do parametrów akcji. Domyślnie ten początkowy szablon jest pusty. Zobaczymy, że można go wstępnie wypełnić;
  • wiersz 4: szablon typu [Model] jest rodzajem słownika zawierającego elementy typu <String, Object>. W wierszu 4 dodajemy wpis do tego słownika z kluczem [personne] powiązanym z wartością typu [Personne];
  • wiersz 5: wyświetlamy model na konsoli, aby zobaczyć, jak wygląda;
  • w wierszu 6 wyświetlamy widok [vue-04.html];

Klasa [Personne] to ta, która została wykorzystana w poprzednim rozdziale:

  

package istia.st.springmvc.models;

public class Personne {

    // identyfikator
    private Integer id;
    // nazwa
    private String nom;
    // wiek
    private int age;

    // konstruktory
    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);
    }

    // metody pobierające i ustawiające
...
}

Widok [vue-04.html] wygląda następująco:

  

<!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>
  • w wierszu 10 wprowadzono nowy typ wyrażenia Thymeleaf ${var}, gdzie var jest kluczem modelu M widoku. Przypomnijmy, że akcja [/v04] umieściła w szablonie klucz [personne] powiązany z typem Personne[id, nom, age];
  • wiersz 10: wyświetla imię osoby obecnej w modelu;
  • wiersz 14: wyświetla jej wiek;

Pliki komunikatów zostały zmodyfikowane w celu dodania kluczy [personne.nom] i [personne.age] z wierszy 9 i 13. Wynik jest następujący:

a typ szablonu M można znaleźć w logach konsoli [2].

Można się zastanawiać, dlaczego nie zapisuje się widoku [vue-04] w następujący sposób:


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

Ten widok jest całkowicie poprawny i da ten sam wynik, co poprzednio. Jednym z celów Thymeleaf jest to, aby strona Thymeleaf mogła zostać wyświetlona, nawet jeśli nie przechodzi przez Thymeleaf. Stwórzmy więc dwie nowe strony statyczne:

  

Widok [vue-04b.html] jest kopią widoku [vue-04.html]. To samo dotyczy widoku [vue-04a.html], z którego usunięto jednak teksty statyczne. Jeśli wyświetlimy obie strony, otrzymamy następujące wyniki:

W przypadku widoku [1] struktura strony nie jest widoczna, podczas gdy w przypadku widoku [2] jest ona wyraźnie widoczna. Na tym polega zaleta umieszczania tekstów statycznych w widoku Thymeleaf, nawet jeśli podczas wykonywania zostaną one zastąpione innymi tekstami.

Przyjrzyjmy się teraz szczegółowi technicznemu. W widoku [vue-04.html] formatujemy kod za pomocą [ctrl-Maj-F]. Otrzymujemy następujący wynik:


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

Tagi są źle wyrównane, co utrudnia czytanie kodu. Jeśli zmienimy nazwę pliku z [vue-04.html] na [vue-04.xml] i ponownie sformatujemy kod, tagi znów będą wyrównane. Zatem rozszerzenie [xml] byłoby bardziej praktyczne. Praca z tym rozszerzeniem jest możliwa. W tym celu należy skonfigurować Thymeleaf. Aby nie cofać tego, co już zrobiliśmy, duplikujemy omawiany projekt [springmvc-vues], tworząc projekt [springmvc-vues-xml]

  

Modyfikujemy plik [pom.xml] w następujący sposób:


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

Nazwa projektu została zmieniona w wierszach 2 i 6. Ponadto zmieniamy rozszerzenie plików widoków znajdujących się w folderze [templates]:

  

Dokument [http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html] zawiera listę właściwości konfiguracyjnych Spring Boot, które można wykorzystać w pliku [application.properties]:

  

Niniejszy dokument przedstawia właściwości, z których korzysta Spring Boot podczas autokonfiguracji i które można zmodyfikować, wprowadzając odmienną konfigurację w pliku [application.properties]. W przypadku Thymeleaf właściwości autokonfiguracji są następujące:


# 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 # ;dodano charset=<kodowanie>
spring.thymeleaf.cache=true # ustawiono na false w celu umożliwienia odświeżania na gorąco

Wystarczyłoby więc po prostu dodać wiersz


spring.thymeleaf.suffix=.xml

w pliku [application.properties]. Pójdziemy jednak inną drogą, czyli konfiguracją programową. Skonfigurujemy Thymeleaf w klasie [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;
    }

}
  • wiersze 16–24 konfigurują obiekt [TemplateResolver] dla Thymeleaf. To właśnie ten obiekt jest ładowany na podstawie nazwy widoku przekazanej przez akcję w celu znalezienia odpowiedniego pliku;
  • wiersze 18 i 19 określają prefiks i sufiks, które należy dodać do nazwy widoku w celu znalezienia pliku. Jeśli więc nazwa widoku to [vue04], poszukiwany plik będzie nosił nazwę [classpath:/templates/vue04.xml]. [classpath:/templates] to składnia Springa, która oznacza folder [/templates] umieszczony w katalogu głównym ścieżki Classpath projektu;
  • wiersz 21: aby w odpowiedzi wysyłanej do klienta pojawił się nagłówek HTTP:

Content-Type:text/html;charset=UTF-8
  • wiersz 20: wskazuje, że widok jest zgodny ze standardem HTML5;
  • wiersz 22: wskazuje, że widoki Thymeleaf mogą być buforowane;
  • wiersze 26–31: ustawia silnik rozpoznawania widoków pary Spring/Thymeleaf na poprzedni silnik rozpoznawania;

Uruchommy plik wykonywalny tego nowego projektu i wywołajmy URL [http://localhost:8080/v04.html?lang=en]:

 

Zauważamy, że w URL akcja [/v04] mogła zostać ponownie zastąpiona przez [v04.html].

5.5. [/v05]: rozkład obiektu w widoku Thymeleaf

Tworzymy następującą akcję [/v05]:


    // utworzenie szablonu M dla widoku V - 2
    @RequestMapping(value = "/v05", method = RequestMethod.GET)
    public String v05(Model model) {
        model.addAttribute("personne", new Personne(7, "martin", 17));
        return "vue-05";
}

Jest ona identyczna z akcją [/v04]. Widok [vue-05.xml] wygląda następująco:

  

<!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>
  • wiersze 8–17: w tych wierszach obiekt Thymeleaf jest definiowany przez atrybut [th:object="${personne}"] (wiersz 8). Obiekt ten jest tutaj obiektem o kluczu [personne], który znajduje się w szablonie:
  • wiersz 11: wyrażenie Thymeleaf [*{nom}] jest równoważne wyrażeniu [${objet.nom}], gdzie [objet] jest bieżącym obiektem Thymeleaf. Zatem w tym przypadku wyrażenie [*{nom}] jest równoważne [${personne.nom}];
  • wiersz 15: to samo;

Wynik:

 

5.6. [/v06]: testy w widoku Thymeleaf

Rozważmy następującą akcję [/v06]:


    // utworzenie modelu M widoku V – 3
    @RequestMapping(value = "/v06", method = RequestMethod.GET)
    public String v06(Model model) {
        model.addAttribute("personne", new Personne(7, "martin", 17));
        return "vue-06";
}

Jest ona identyczna z dwiema poprzednimi akcjami. Wyświetla następujący widok [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>
  • wiersz 17: atrybut [th:if] ocenia wyrażenie boolowskie. Jeśli wyrażenie to jest prawdziwe, tag jest wyświetlany, w przeciwnym razie nie. Zatem w tym przypadku, jeśli ${personne.age}>=18, wyświetlony zostanie tekst [#{personne.majeure}], tj. komunikat o kluczu [personne.majeure] z plików komunikatów;
  • wiersz 18: nie można wpisać [*{age} < 18], ponieważ znak < jest znakiem zastrzeżonym. Należy zatem użyć jego odpowiednika HTML [&lt;], zwanego również encją HTML [http://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references];

Pliki komunikatów zostały zmodyfikowane:

[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

Wynik jest następujący:

5.7. [/v07]: iteracja w widoku Thymeleaf

Rozważmy następującą akcję [/v07]:


    // utworzenie modelu M widoku 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";
}
  • akcja tworzy listę trzech osób, umieszcza ją w szablonie powiązanym z kluczem [liste] i wyświetla widok [vue-07];

Widok [vue-07.xml] wygląda następująco:


<!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>
  • wiersz 10: atrybut [th:each] powtarza tag, w którym się znajduje, w tym przypadku tag <li>. Posiada on tutaj dwa parametry [element : collection], gdzie [collection] jest kolekcją obiektów, w tym przypadku listą osób. Thymeleaf przejrzy tę kolekcję i wygeneruje tyle tagów <li>, ile elementów znajduje się w kolekcji. Dla każdego tagu <li> atrybut [element] będzie reprezentował element kolekcji powiązany z tym tagiem. Dla tego elementu zostanie oceniony atrybut [th:text]. Jego wyrażenie jest tutaj konkatenacją ciągów znaków, dającą wynik [id, nom, age];
  • wiersz 8: dodajemy klucz [liste.personnes] do plików komunikatów;

Oto wynik:

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

Wracamy do kwestii, którą omówiliśmy podczas analizy akcji, a mianowicie do roli adnotacji [@ModelAttribute]. Dodajemy następującą nową akcję:


    // --------------- Powiązanie i ModelAttribute ----------------------------------

    // jeśli parametr jest obiektem, jest on instancjonowany i ewentualnie modyfikowany przez parametry zapytania
    // automatycznie stanie się częścią modelu widoku z kluczem [key]
    // dla parametru @ModelAttribute("xx") klucz będzie równy xx
    // dla parametru @ModelAttribute klucz będzie równy nazwie klasy parametru zaczynającej się od małej litery
    // jeśli @ModelAttribute nie występuje, to wszystko przebiega tak, jakby występował bez klucza
    // należy zauważyć, że to automatyczne uwzględnienie w modelu nie ma miejsca, jeśli parametr nie jest obiektem

    @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";
}
  • wiersz 11: adnotacja [@ModelAttribute("someone")] automatycznie doda obiekt [Personne p] do modelu, powiązany z kluczem [someone];
  • wiersz 12: w celu sprawdzenia modelu;
  • wiersz 13: wyświetla widok [vue-08.xml];

Widok [vue-08.xml] wygląda następująco:


<!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>
  • wiersz 8: obiekt Thymeleaf jest inicjowany przy użyciu obiektu o kluczu [someone];

Wynik jest następujący:

 

a w konsoli pojawia się następujący wpis:

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

Rozważmy teraz następującą akcję [/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";
}
  • wiersz 1: obecność parametru [Personne p] spowoduje automatyczne umieszczenie osoby [p] w szablonie. Ponieważ nie podano klucza, jako klucz wykorzystywana jest nazwa klasy z pierwszą literą pisana małą literą. Zatem [Personne p] jest równoważne [@ModelAttribute("personne") Personne p];

Widok [vue.09.xml] wygląda następująco:


<!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>
  • wiersz 8: używany klucz szablonu to [personne];

Oto wynik:

 

oraz wpis w konsoli serwera:

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

Rozważmy teraz następującą nową akcję [/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";
}
  • wiersze 1–4: definiują metodę, która w szablonie każdego żądania tworzy element klucza [uneAutrePersonne] powiązany z obiektem [new Personne(24,"pauline",55)];
  • wiersze 6–10: akcja [/v10] nie wykonuje żadnych czynności poza przekazaniem otrzymanego modelu do widoku [vue-10.xml]. Należy zauważyć, że parametr [Model model] musi być obecny wyłącznie w instrukcji z wiersza 8. Bez niego jest zbędny;

Widok [vue-10.xml] wygląda następująco:


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

Wynik jest następujący:

 

a wpis w dzienniku konsoli wygląda następująco:

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

5.9. [/v11]: @SessionAttributes

Wracamy do kwestii, którą poruszyliśmy podczas analizy akcji, a mianowicie do roli adnotacji [@SessionAttributes]. Dodajemy następującą nową akcję [/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";
}

Mamy tu do czynienia z sytuacją analogiczną do tej, którą właśnie analizowaliśmy. Różnica polega na adnotacji [@SessionAttributes] umieszczonej na samej klasie:


@Controller
@SessionAttributes("jean")
public class ViewsController {
  • wiersz 2: wskazano, że klucz [jean] z modelu musi zostać umieszczony w sesji;

Dlatego w wierszu 7 akcji wprowadzono sesję. W wierszu 8 wyświetlana jest wartość sesji powiązanej z kluczem [jean].

Widok [vue-11.xml] wygląda następująco:


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

Wyświetlane są dwie osoby:

  • w wierszach 8–21: osoba o kluczu [jean] w modelu;
  • wiersze 23–36: osoba o kluczu [jean] w sesji;

Wyniki są następujące:

  • w [1] – osoba o kluczu [jean] w modelu;
  • na [2], osoba o kluczu [jean] w sesji;

Dziennik konsoli wygląda następująco:


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

Z powyższego wynika, że klucz [jean] nie znajduje się w sesji, do której trafia akcja. Można z tego wywnioskować, że klucz [jean] został umieszczony w sesji po wykonaniu akcji i przed wyświetleniem widoku.

Rozważmy teraz przypadek, w którym klucz jest odwołany zarówno przez [@ModelAttribute], jak i przez [@SessionAttributes]. Tworzymy dwie następujące akcje:


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

    // w przypadku, gdy klucz [@ModelAttribute] jest również kluczem [@SessionAttributes]
    // w takim przypadku odpowiedni parametr jest inicjowany wartością z sesji
    @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";
}

Akcja [/v12a] służy wyłącznie do umieszczenia elementu ['paul',new Personne(51, "paul", 33)] w sesji. Nie wykonuje żadnych innych czynności. Fakt, że jest ona oznaczona tagiem [@ResponseBody], wskazuje, że to właśnie ona generuje odpowiedź dla klienta. Ponieważ jej typ to [void], żadna odpowiedź nie jest generowana.

Akcja [/v12b] przyjmuje jako parametr [@ModelAttribute("paul") Personne p]. Jeśli nie podejmie się żadnych innych działań, obiekt [Personne] zostanie zainicjowany, a następnie zainicjowany z parametrami żądania, a obiekt ten nie ma nic wspólnego z obiektem klucza [paul] umieszczonym w sesji przez akcję [/v12a]. Dodamy klucz [paul] do atrybutów sesji tej klasy:


@Controller
@SessionAttributes({ "jean", "paul" })
public class ViewsController {
  • w wierszu 2 znajdują się teraz dwa atrybuty sesji;

Wróćmy do parametrów akcji [/v12b]:


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

Obiekt [Personne p] nie zostanie teraz zainicjowany, ale będzie odwoływał się do obiektu klucza [paul] w sesji. Dalszy przebieg procedury pozostaje bez zmian. Obiekt o kluczu [paul] znajdzie się w szczególności w szablonie widoku, który zostanie wyświetlony. Właśnie to chcemy zobaczyć w wierszu 11 akcji [/v12b].

Widok [vue-12.xml] będzie wyglądał następująco:


<!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>
  • wiersz 8: odwołujemy się do klucza [paul] z modelu widoku;

Daje to następujący wynik (po wykonaniu akcji [/v12a], która umieszcza klucz [paul] w sesji):

 

Dziennik konsoli wygląda następująco:


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}

Klucz [paul] został prawidłowo umieszczony w szablonie, a jego wartością jest wartość powiązana z kluczem [paul] w sesji.

5.10. [/v13]: wygenerowanie formularza do wprowadzania danych

Przechodzimy teraz do wprowadzania danych w formularzach i ich walidacji. Tworzymy pierwszy formularz za pomocą następującej akcji [/v13]:


  // generuje formularz do wprowadzenia danych osoby
  @RequestMapping(value = "/v13", method = RequestMethod.GET)
  public String v13() {
    return "vue-13";
}

która po prostu wyświetla następujący widok [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>

Jeśli umieścimy ten widok w folderze [static] pod nazwą [vue-13.html] i wywołamy URL [http://localhost:8080/vue-13.html], otrzymamy następującą stronę:

 
  • W wierszu 8 formularza znajduje się tag <form> z atrybutem [th:action]. Atrybut ten zostanie przetworzony przez Thymeleaf, a jego wartość zastąpi aktualną wartość atrybutu [action], który pełni zatem jedynie funkcję dekoracyjną. W tym przypadku wartość atrybutu [th:action] będzie wynosić [/v14.html];
  • w wierszach 17, 23 i 29 wartość atrybutu [th:value] zastąpi wartość atrybutu [value]. W tym przypadku wartością tą będzie pusty ciąg znaków;

Po wysłaniu zapytania o URL [/v13.html] otrzymujemy następujący wynik:

 

Przyjrzyjmy się kodowi źródłowemu wygenerowanemu przez 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>

W wierszach 9, 18, 24 i 30 widać, jak Thymeleaf ocenia atrybuty [th:action] i [th:value].

5.11. [/v14]: obsługa wartości przesłanych przez formularz

Akcja [/v14] jest akcją, która odbiera przesłane wartości. Wygląda ona następująco:


  // przetwarza wartości z formularza
  @RequestMapping(value = "/v14", method = RequestMethod.POST)
  public String v14(Personne p) {
    return "vue-14";
}
  • wiersz 3: przesłane wartości są zawarte w obiekcie [Personne p]. Wiadomo, że obiekt ten automatycznie staje się częścią modelu M widoku V, który zostanie wyświetlony przez akcję, powiązanego z kluczem [personne];
  • wiersz 4: wyświetlany widok to widok [vue-14.xml];

Widok [vue-14.xml] wygląda następująco:


<!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>
  • wiersz 9: z modelu pobierany jest obiekt powiązany z kluczem [personne];
  • wiersze 12, 16 i 20: wyświetlane są cechy tego obiektu;

Daje to następujący wynik:

5.12. [/v15-/v16]: walidacja modelu

W oparciu o poprzedni przykład przyjrzyjmy się następującej sekwencji:

  • w [1] wprowadzono błędne wartości dla pól [id] i [age] typu [int];
  • w polu [2] odpowiedź serwera wskazuje, że wystąpiły dwa błędy;

Będziemy korzystać z tego samego formularza, ale w przypadku błędów walidacji wyświetlimy stronę informującą o tych błędach, aby użytkownik mógł je poprawić.

Akcja [/v15] wygląda następująco:


    // ---------------------- wyświetlenie formularza
    @RequestMapping(value = "/v15", method = RequestMethod.GET)
    public String v15(SecuredPerson p) {
        return "vue-15";
}

Jako parametr otrzymuje typ [SecuredPerson] o następującej treści:

  

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;

    // konstruktory
    public SecuredPerson() {

    }

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

    // metody pobierające i ustawiające
...
}

Pola [id, nom, age] zostały opatrzone adnotacjami dotyczącymi ograniczeń walidacji. Widok [vue-15.xml] wyświetlany przez akcję [/v15] wygląda następująco:


<!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>
  • wiersze 10–47: pobierany jest obiekt modelu strony powiązany z kluczem [securedPerson]. Po zakończeniu operacji GET otrzymujemy obiekt wraz z jego wartością instancji [id=0, nom=null, age=0];
  • wiersz 17: wartość pola [securedPerson.id];
  • wiersz 20: wyrażenie [${#fields.hasErrors('id')}] pozwala ustalić, czy wystąpiły błędy walidacji w polu [securedPerson.id]. Jeśli tak, atrybut [th:errors="*{id}"] wyświetla powiązany komunikat o błędzie;
  • ten scenariusz powtarza się w wierszu 29 dla pola [nom] oraz w wierszu 38 dla pola [age];
  • wiersz 45: wyrażenie [${#fields.errors('*')}] oznacza zbiór wszystkich błędów dotyczących pól obiektu [securedPerson]. Zatem właśnie ten zbiór błędów zostanie wyświetlony w wierszach 44–46;
  • wiersz 16: widać, że wartości z formularza zostaną przesłane do akcji [/v16]. Wygląda ona następująco:

    // -------------------- walidacja modelu------------------
    @RequestMapping(value = "/v16", method = RequestMethod.POST)
    public String v16(@Valid SecuredPerson p, BindingResult result) {
        // błędy?
        if (result.hasErrors()) {
            return "vue-15";
        } else {
            return "vue-16";
        }
}
  • w wierszu 3 adnotacja [@Valid SecuredPerson p] wymusza walidację przesłanych wartości;
  • wiersz 5: sprawdza, czy szablon akcji jest błędny, czy nie;
  • wiersz 6: jeśli jest błędny, zwracany jest formularz [vue-15.xml]. Ponieważ formularz ten wyświetla komunikaty o błędach, zobaczymy je;
  • wiersz 8: jeśli szablon akcji został zatwierdzony, wyświetlany jest następujący widok [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>

Oto przykłady wykonania:

5.13. [/v17-/v18]: kontrola komunikatów o błędach

Przy pierwszym wywołaniu akcji [/v15] otrzymujemy następujący wynik:

 

Być może wolelibyśmy otrzymać pusty formularz zamiast zer w polach [Identifiant, Age]. Aby to osiągnąć, modyfikujemy szablon akcji w następujący sposób:


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;

    // konstruktory
    public StringSecuredPerson() {

    }

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

    // metody pobierające i ustawiające
...

}
  • wiersze 12 i 19: pola [id] i [age] zostały zmienione na typ [String];
  • wiersz 11: określa się, że pole [id] musi być liczbą składającą się z maksymalnie czterech cyfr, bez miejsc po przecinku;
  • wiersz 18: to samo dotyczy pola [age], które musi być liczbą całkowitą składającą się z maksymalnie dwóch cyfr;

Akcja [/v17] przyjmuje następujący kształt:


    // ---------------------- wyświetlanie formularza
    @RequestMapping(value = "/v17", method = RequestMethod.GET)
    public String v17(StringSecuredPerson p) {
        return "vue-17";
}

Widok [vue-17.xml] wyświetlany przez akcję [/v17] wygląda następująco:


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

Zmiany dotyczą następujących wierszy:

  • wiersz 10: obecnie operujemy na obiekcie szablonu klucza [stringSecuredPerson];
  • wiersz 20: przeglądana jest lista błędów pola [id]. W składni [th:each="err,status : ${#fields.errors('id')}"] listę tę przegląda zmienna [err]. Zmienna [status] dostarcza informacji o każdej iteracji. Jest to obiekt [index, count, size, current], w którym:
    • index: to numer bieżącego elementu,
    • current: wartość bieżącego elementu,
    • count, size: rozmiar przeglądanej listy;
  • wiersz 20: wyświetlany jest tylko pierwszy element listy [th:if="${status.index}==0"];

Akcja [/v18], która przetwarza obiekt POST z akcji [/v17], wygląda następująco:


    // -------------------- walidacja modelu------------------
    @RequestMapping(value = "/v18", method = RequestMethod.POST)
    public String v18(@Valid StringSecuredPerson p, BindingResult result) {
        // błędy?
        if (result.hasErrors()) {
            return "vue-17";
        } else {
            return "vue-18";
        }
}

Pliki komunikatów zmieniają się w następujący sposób:

[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

Przyjrzyjmy się kilku przykładom:

 

W przypadku [1] widać, że uruchomiono oba walidatory pola [age]:


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

Czy istnieje określona kolejność komunikatów o błędach? W przypadku pola [age] wydaje się, że walidatory zostały uruchomione w kolejności [Digits, Range]. Jednak po wykonaniu kilku zapytań można zauważyć, że kolejność ta może ulec zmianie. Nie można więc polegać na kolejności walidatorów. W przypadku [2] wyświetlany jest tylko jeden z dwóch komunikatów dotyczących pola [id]. W przypadku [3] widoczne są wszystkie komunikaty o błędach.

5.14. [/v19-/v20]: wykorzystanie różnych walidatorów

Rozważmy następujący nowy szablon akcji:

  

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;

    // metody pobierające i ustawiające
...
}

Zostanie on wyświetlony przez następującą akcję [/v19]:


    // ------------------ wyświetlanie formularza
    @RequestMapping(value = "/v19", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String v19(Form19 formulaire) {
        return "vue-19";
}
  • wiersz 3: akcja otrzymuje jako parametr obiekt [Form19 formulaire]. Jeśli akcja GET nie otrzyma żadnych parametrów, obiekt ten zostanie zainicjowany wartościami domyślnymi języka Java;
  • wiersz 4: wyświetlany jest widok [vue-19.xml]. Wygląda on następująco:

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

Ten kod wyświetla następujący widok:

 

Strona zawiera tabelę z trzema kolumnami:

  • kolumna 1: walidator pola wprowadzania danych;
  • kolumna 2: pole wprowadzania danych;
  • kolumna 3: komunikaty o błędach dotyczące pola wprowadzania danych;

Przyjrzyjmy się na przykład kodowi widoku [/v19.html] dla modułu walidacyjnego [@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>

Znajdujemy tu kod, który właśnie analizowaliśmy w przypadku formularzy typu [Personne]:

  • wiersz 2: pierwsza kolumna: nazwa testowanego walidatora;
  • wiersz 4: atrybut Thymeleaf [th:field="*{hhmmss}] wygeneruje atrybuty HTML, [id="hhmmss"] oraz [name="hhmmss"]. Atrybut Thymeleaf [th:value="*{hhmmss}"] wygeneruje atrybut HTML [value="valeur de [form19.hhmmss]]";
  • wiersz 7: jeśli wartość wprowadzona w polu [form19.hhmmss] jest nieprawidłowa, wówczas w wierszu 7 wyświetlane są komunikaty o błędach związane z tym polem;

Wprowadzone wartości są przetwarzane przez następującą akcję [/v20]:


    // ----------------- walidacja modelu formularza
    @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 {
            // przekierowanie do [vue-19]
            redirectAttributes.addFlashAttribute("form19", formulaire);
            return "redirect:/v19.html";
        }
}
  • wiersz 3: zaksięgowane wartości wypełnią pola obiektu [Form19 formulaire], jeśli są prawidłowe;
  • wiersze 4–6: jeśli zaksięgowane wartości są nieprawidłowe, formularz [vue-19] zostanie ponownie wyświetlony wraz z komunikatami o błędach;
  • wiersze 6–10: jeśli przesłane wartości są prawidłowe, obiekt [Form19 formulaire] utworzony na podstawie tych wartości jest udostępniany dla następnego żądania, w tym przypadku żądania przekierowania. Następnie obiekt ten jest usuwany;
  • wiersz 9: klient jest przekierowywany do akcji [/v19.html]. Akcja ta ponownie wyświetli formularz [vue-19], zawierający kod taki jak:

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

Atrybut [th:object="${form19}"] pobierze wówczas obiekt powiązany z atrybutem Flash [form19] i w ten sposób ponownie wyświetli formularz w stanie, w jakim został wypełniony.

Kod formularza wymaga jeszcze kilku wyjaśnień. Rozważmy następujący kod:


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

Generuje to następujący kod 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>

W kodzie


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

atrybuty Thymeleaf w wierszach 1 i 3 [th:field="*{assertFalse}"] stanowią problem. Stwierdzono, że ten atrybut generuje atrybuty HTML, [id=assertFalse] oraz [name=assertFalse]. Trudność wynika z faktu, że ponieważ są one generowane w wierszach 1 i 3, mamy dwa identyczne atrybuty [name] oraz dwa identyczne atrybuty [id]. Chociaż jest to możliwe w przypadku atrybutu [name], nie jest to możliwe w przypadku atrybutu [id]. Jak widać w wygenerowanym kodzie HTML, Thymeleaf wygenerował dwa różne atrybuty [id]: [id=asserFalse1] i [id=assertFalse2]. To dobrze. Problem polega na tym, że nie znamy tych identyfikatorów, a mogą nam się one przydać. Tak jest w przypadku tagu [label] w wierszu 2. Atrybut [for] tagu HTML [label] musi odwoływać się do atrybutu [id], w tym przypadku ten wygenerowany dla tagu [input] w wierszu 1. Dokumentacja Thymeleaf wskazuje, że wyrażenie [${#ids.prev('assertFalse')}"] pozwala uzyskać ostatni atrybut [id] wygenerowany dla pola [assertFalse].

Rozważmy teraz kod listy rozwijanej w formularzu:


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

Ten kod generuje kod HTML listy rozwijanej:

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

Wartość zostanie przesłana pod nazwą [name="assertTrue"].

Widok [vue-19.xml] korzysta z arkusza stylów:


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

W wierszu 4 arkusze stylów, z których korzysta widok, należy umieścić w folderze [static] projektu:

  

Jej zawartość jest następująca:


@CHARSET "UTF-8";

.col1 {
    background: lightblue;
}

.col2 {
    background: Cornsilk;
}

.col3 {
    background: #e2d31d;
}

.error {
    color: red;
}

Przyjrzyjmy się teraz datom:


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

Analiza ruchu sieciowego w narzędziu deweloperskim przeglądarki Chrome (Ctrl-Shift-I) pokazuje, że daty są wysyłane w formacie (rrrr-mm-dd):

 

To właśnie dlatego daty zostały opatrzone adnotacjami w walidatorze:


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

który określa oczekiwany format wartości wysyłanych dat.

Na koniec plik z komunikatami w języku francuskim [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

Przyjrzyjmy się kilku przykładom wykonania:

 
 

Powyżej, między [1] a [2], wydaje się, że nic się nie wydarzyło. Jeśli jednak przyjrzymy się komunikacji sieciowej (Ctrl-Shift-I), widać, że miały miejsce dwie wymiany danych z serwerem:

  • w przypadku [1] – początkowa wymiana POST do [/v20];
  • do [2] – odpowiedź na tę akcję to przekierowanie;
  • w [3], drugie żądanie, tym razem do [/v19];

Następnie wykonywana jest akcja [/v19]:


    // ------------------ wyświetlenie formularza
    @RequestMapping(value = "/v19", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String v19(Form19 formulaire) {
        return "vue-19";
}
  • wiersz 3, parametr [Form19 formulaire] jest inicjowany za pomocą atrybutu Flash klucza [form19], który został utworzony przez poprzednią akcję [/v19] i który był obiektem typu [Form19] o wartościach wartościami przesłanymi do akcji [/v19];
  • wiersz 4: widok [vue-19.xml] zostanie wyświetlony z obiektem [Form19 formulaire] w swoim szablonie, zainicjowanym wartościami przesłanymi w akcji. Dlatego użytkownik widzi formularz dokładnie taki, jaki go przesłał;

Dlaczego następuje przekierowanie? Dlaczego po prostu nie wysłano danych do powyższej akcji [/v19]? Uzyskano by ten sam wynik. Z kilkoma różnicami:

  • przeglądarka umieściłaby w polu adresu adres [http://localhost:8080/v20.html] zamiast [http://localhost:8080/v19.html], jak to miało miejsce w tym przypadku, ponieważ wyświetla ostatni wywołany adres URL;
  • jeśli użytkownik odświeży stronę (F5), wynik będzie zupełnie inny:
    • w przypadku przekierowania wyświetlany URL to [http://localhost:8080/v19.html] uzyskany na podstawie GET. Przeglądarka ponownie wykona to ostatnie polecenie i otrzyma wtedy zupełnie nowy formularz (atrybut Flash jest używany tylko raz),
    • w przypadku braku przekierowania wyświetlany kod URL to [http://localhost:8080/v20.html] uzyskany na podstawie kodu POST. Przeglądarka ponownie wykona to ostatnie polecenie i w ten sposób utworzy ponownie POST z tymi samymi wartościami przesłanymi w danych POST, co poprzednio. W tym przypadku nie ma to większego znaczenia, ale często jest to niepożądane, dlatego zazwyczaj preferuje się przekierowanie;

5.15. [/v21-/v22]: obsługa przycisków opcji

Rozważmy następujący komponent 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" };

    // metody pobierające i ustawiające
  ...

}
  • wiersz 5: klasa [Listes] będzie komponentem Spring;
  • wiersze 8–10: listy wykorzystywane do wypełniania przycisków opcji, pól wyboru i list rozwijanych;

W klasie konfiguracyjnej [Config] zapisano:


@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
  • wiersz 2: pakiet [models], w którym znajduje się komponent [Listes], zostanie prawidłowo zindeksowany przez Spring;

Tworzymy następujące nowe akcje:


    // ------------------ formularz z przyciskami opcji
    @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";
}
  • wiersze 2–3: komponent [Listes] jest wstrzykiwany do kontrolera;
  • wiersz 6: obsługujemy formularz typu [Form21], który opiszemy poniżej. Należy zauważyć, że w modelu widoku określono jego klucz [form]. Przypominamy, że domyślnie byłby to [form21];
  • wiersz 7: wstawiamy komponent [Listes] do modelu. Widok będzie go potrzebował;
  • wiersz 8: wyświetlamy widok [vue-21.xml]. Widok ten wyświetli formularz [Form21], a przesłane wartości zostaną przekazane do akcji [/v22] z wierszy 12–15;
  • wiersze 12–15: akcja [/v22] ogranicza się do przekierowania do akcji [/v21], umieszczając otrzymane wartości POST w atrybucie Flash o kluczu [form]. Ważne jest, aby ten klucz był taki sam jak ten użyty w wierszu 6;

Szablon [Form21] wygląda następująco:

  

package istia.st.springmvc.models;

public class Form21 {

    // wartości przesłane
    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;

    // metody pobierające i ustawiające
    ...
}

Widok [vue-21.xml] wygląda następująco:


<!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>
  • wiersze 36–40: należy zwrócić uwagę na wykorzystanie komponentu [Listes] umieszczonego w szablonie w celu wygenerowania etykiet pól wyboru;
  • kolumna 3 pozwala sprawdzić wartość wprowadzoną dla POST lub wartość początkową formularza podczas pierwotnego GET;

Ten kod wyświetla następującą stronę:

 

odpowiadającą następującemu kodowi 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>

Widać, że wartości przesłane (atrybuty „name”) znajdują się w następujących polach szablonu [Form21]:


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

Zachęcamy czytelnika do przeprowadzenia testów. Należy zwrócić uwagę, że wysyłany jest atrybut [value] przycisków opcji.

5.16. [/v23-/v24]: zarządzanie polami wyboru

Dodajemy następującą nową akcję:


    // ------------------ formularz z polami wyboru
    @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";
}
  • wiersz 3: nadal korzystamy z szablonu [Form21];

Widok [vue-23.xml] wygląda następująco:


<!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>
  • wiersze 37–41: należy zwrócić uwagę na wykorzystanie komponentu [Listes] do generowania etykiet pól wyboru;

Kod ten wyświetla następującą stronę:

 

pochodząca z następującego kodu 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>

Należy zauważyć, że wartości przesyłane (atrybuty name) są umieszczane w następujących polach komponentu [Form21]:


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

Są to tabele, ponieważ dla każdego pola istnieje kilka pól wyboru o nazwie odpowiadającej nazwie pola. Możliwe jest zatem, że kilka przesłanych wartości będzie miało tę samą nazwę (atrybut name formularza). Aby je pobrać, potrzebna jest więc tabela.

Wróćmy do kodu Thymeleaf w kolumnie 3 strony:


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

Pola, do których odwołują się wiersze 2 i 14, są następujące:


    private String strCouleurs;
    private String strBijoux;

Są one obliczane przez akcję [/v24], która obsługuje akcję POST:


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

Należy pamiętać, że biblioteka jackson / jSON znajduje się wśród zależności projektu.

  • wiersz 2: tworzymy typ [ObjectMapper], który umożliwia serializację i deserializację obiektów w formacie jSON,
  • wiersz 7: serializujemy tablicę kolorów do formatu jSON. Wynik umieszczamy w polu [strCouleurs];
  • wiersz 8: tablica biżuterii jest serializowana do formatu jSON. Wynik jest umieszczany w polu [strBijoux];

Oto przykładowe wykonanie:

Należy zwrócić uwagę, że wysyłany jest atrybut [value] pól wyboru.

5.17. [/25-/v26]: zarządzanie listami

Dodajemy następującą akcję [/v25]:


  // ------------------ formularz z listami
  @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";
}

Widok [vue-25.xml] wygląda następująco:


<!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>
  • wiersze 38–42: generowanie listy wielokrotnego wyboru, w której etykiety pochodzą z komponentu [Listes], z którego już korzystaliśmy;

Wyświetlana strona wygląda następująco:

 

wygenerowana przez następujący kod 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 - 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>
  • wiersz 44: można zauważyć, że Thymeleaf utworzył ukryte pole. Nie zrozumiałem jego roli:
  • wartości przesłane (atrybuty value tagów option) zostaną umieszczone w następujących polach (atrybuty name) elementu [Form21]:

    private int couleur2;
    private int[] bijoux2;
  • wiersz 38: lista [bijoux2] jest listą wielokrotnego wyboru. W związku z tym z nazwą [bijoux2] może być powiązanych kilka wartości. Aby je pobrać, pole [bijoux2] musi być tablicą. Należy zauważyć, że jest to tablica liczb całkowitych. Jest to możliwe, ponieważ zapisane wartości można przekonwertować na ten typ;

Wartości są zapisywane w następującej akcji [/v26]:


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

Nie ma tu nic, czego byśmy już nie widzieli. Oto przykład wykonania:

5.18. [/v27]: konfiguracja komunikatów

Rozważmy następującą akcję [/v27]:


  // ------------------ komunikaty z parametrami
  @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";
}

Akcja ta polega jedynie na umieszczeniu czterech wartości w szablonie i wyświetleniu następującego widoku [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>
  • wiersz 8: komunikat bez parametrów;
  • wiersz 9: komunikat z jednym parametrem [$param1] pobranym z szablonu;
  • wiersz 10: komunikat z dwoma parametrami [$param2, $param3] pobranymi z szablonu;
  • wiersz 11: komunikat z jednym parametrem. Parametr ten sam w sobie jest kluczem komunikatu (obecność znaku #). Klucz ten jest dostarczany przez [$param4];

Plik z komunikatami w języku francuskim wygląda następująco:

[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

Aby wskazać obecność parametrów w komunikacie, stosuje się symbole {0}, {1}, ...

Połączenie szablonu utworzonego przez akcję [/v27] z widokiem [vue-27] da w wyniku następujący kod 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>

co daje następujący widok:

 

Plik z komunikatami w języku angielskim wygląda następująco:

[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

Połączenie szablonu utworzonego przez akcję [/v27] z widokiem [vue-27] da następujący kod 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>

co daje następujący widok:

 

Widać, że ostatni komunikat został w pełni zinternacjonalizowany, co nie miało miejsca w przypadku dwóch poprzednich.

5.19. Korzystanie ze strony szablonowej

W aplikacji internetowej często zdarza się, że widoki mają wspólne elementy, które można wyodrębnić do strony szablonowej. Oto przykład:

Powyżej mamy dwie podobne strony, na których fragment [1] został zastąpiony fragmentem [2]. Widok przedstawia stronę szablonową zawierającą trzy stałe fragmenty [3-5] oraz jeden fragment zmienny [6].

5.19.1. Projekt

Tworzymy projekt [springmvc-masterpage], postępując zgodnie z procedurą opisaną w paragrafie 5.1.

  

Plik [pom.xml] ma następującą treść:


<?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/> <!-- wyszukiwanie elementu nadrzędnego z repozytorium -->
    </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>

Jedna z zależności wynikających z tego pliku jest niezbędna dla strony głównej:

 

Pakiety [config] i [main] są identyczne z pakietami o tych samych nazwach z poprzedniego projektu.

5.19.2. Strona główna

  

Szablon to następujący widok [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>
  • wiersz 2: strona szablonowa musi zdefiniować przestrzeń nazw [xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"], której element jest używany w wierszu 19;
  • wiersze 10–12: generują poniższy obszar [1]. Tag Thymeleaf [th:include] pozwala na włączenie do bieżącego widoku fragmentu zdefiniowanego w innym pliku. Umożliwia to wyodrębnienie fragmentów wykorzystywanych w wielu widokach;
  • wiersze 15–17: generują poniższy obszar [2];
  • wiersze 19–20: generują obszar [3] poniżej. Atrybut [layout:fragment] jest atrybutem przestrzeni nazw [xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"]. Wskazuje on obszar, który podczas wykonywania może zostać zastąpiony innym;
  • wiersze 24–28: generują poniższe pole [4];

5.19.3. Fragmenty

Fragmenty [entete.xml], [menu.xml] i [basdepage.xml] są następujące:

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

Fragment [page1.xml] wygląda następująco:


<!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>
  • wiersz 2: atrybut [layout:decorator="layout"] wskazuje, że bieżąca strona [page1.xml] jest „ozdobiona”, tzn. należy do strony wzorcowej. Jest to wartość atrybutu, w tym przypadku widok [layout.xml];
  • wiersz 3: określa się, w którym fragmencie strony głównej zostanie wstawiona strona [page1.xml]. Atrybut [layout:fragment="contenu"] wskazuje, że [page1.xml] zostanie wstawiony do fragmentu o nazwie [contenu], tj. do obszaru [3] strony wzorcowej;
  • wiersze 5–7: zawartością fragmentu jest formularz zawierający przycisk POST prowadzący do akcji [/page2.html];

Fragment [page2.xml] jest analogiczny:


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

 

Kontroler [Layout.java] wygląda następująco:


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";
    }
}
  • wiersze 10–12: akcja [/page1] powoduje jedynie wyświetlenie widoku [page1.xml];
  • wiersze 15–17: to samo dotyczy akcji [/page2], która powoduje wyświetlenie widoku [page2.xml];