Aller au contenu
Bekaglaçon, un Pokémon de la huitième génération

(Pour Rudy et David)

Projet Pokedex

Etapes pour construire votre Projet

  1. Construire le ou les Uses Cases pour identifier les Rôles et les Fonctionnalités qui seront mises en place dans les classes Services et/ou Contrôleurs
  2. Construite le MCD (avec Looping) ou le diagramme de classe pour générer les Entités et aussi les tables en Base de données.
  3. Mise en place des entités avec les annotations de mapping pour utiliser Hibernate
  4. Création des Repositories pour chaque entités
  5. Création des classes DTO (pas une obligation)
  6. Création des contrôleurs spécifiques à chaque entités avec les CRUD pour certaines entités et donc les POST, GET, PUT, DELETE,…
  7. Tests les différentes requêtes avec Postman ou Swagger
  8. Dans votre projet Spring Boot pensez à ajouter les dépendences pour Thymeleaf (très utilisé en entreprise et facile dans Spring Boot)
  9. Pour la mise en page avec Thymeleaf prévoir des fragment de template pour le menu, le bas de page, etc…
  10. Choisir un framework CSS ou faire soi-même les classes CSS
  11. Créer les pages HTML avec intégration des balises Thymeleaf pour les Formulaires d’ajout, de modification, d’affichage de liste et de recherches multicritères…
  12. Configurer la partie sécurité avec Spring Boot si prévue avec connexion et authentification
  13. Ecrire quelques tests unitaires si vous avez le temps…

Thymeleaf : un framework facile à mettre en place

C’est quoi ?

Thymeleaf est un framework de templating très facile à utiliser avec Spring Boot, un autre framework très populaire pour le développement d’applications web Java. Thymeleaf est apprécié pour la simplicité de sa syntaxe intuitive pour la création de modèles HTML dynamiques.

C’est une solution open source avec une communauté active de développeurs. Je vous conseille de l’utiliser plutôt que de poursuivre avec les JSP (Java Server Pages).

Est-il utilisé dans les entreprises ?

Oui, Thymeleaf est très utilisé dans les entreprises pour développer des applications web Java.

Thymeleaf est utilisé dans des entreprises de toutes tailles, des start-ups aux grandes entreprises. De nombreuses entreprises ont adopté Spring Boot et Thymeleaf pour leur développement web, car ils offrent une solution complète pour le développement d’applications web Java, de la gestion des dépendances à la gestion de la couche de présentation.

Avantages

Inconvénients

Mise en pratique

Intégration à Spring Boot

Pour intégrer Thymeleaf dans un projet Spring Boot, c’est plutôt simple. Vous devez ajouter la dépendance Thymeleaf dans votre fichier pom.xml ou build.gradle selon vos préférences.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    <version>3.0.5</version>
</dependency>

implementation group: 'org.springframework.boot', name: 'spring-boot-starter-thymeleaf', version: '3.0.5'

Configuration dans le fichier application.properties

spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

Ces propriétés indiquent à Spring Boot où se trouvent nos fichiers HTML et comment les traiter avec Thymeleaf. On peut préciser l’extension des vues HTML de Thymeleaf avec .html. On pourrait indiquer une autre extension, ça fonctionne aussi.

Utilisez les balises Thymeleaf

Thymeleaf utilise des balises spéciales appelées attributs. Elles commencent toujours par th: et servent à associer des données et des comportements à des éléments HTML.

Exemples d’attributs utilisés :

Il y en a bien d’autres que nous verrons ultérieurement.

Voici un exemple de page HTML utilisant Thymeleaf :

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Exemple simple d'une page avec Thymeleaf</title>
</head>
<body>
    <h1 th:text="${message}"></h1>
    <ul>
        <li th:each="pokemon : ${pokemons}" th:text="${pokemon.nom}"></li>
    </ul>
</body>
</html>

Comme vous pouvez le constater,les balises Thymeleaf sont des balises HTML standard avec des attributs spéciaux.

Les attributs spéciaux Thymeleaf contiennent souvent des expressions Thymeleaf pour ajouter de la dynamique à une page HTML.

Dans l’exemple ci-dessus :

Nous pourrions ajouter d’autres balises avec des if et des else :

th:if et th:else : permettent de conditionner l’affichage d’une balise ou d’un bloc de balises en fonction d’une expression.

Par exemple : <p th:if="${nom != null}" th:text="${'Salut ' + nom}"></p> affichera le message Salut nomDuPokemon si la variable nom n’est pas nulle !

Comment un objet java est transmis à Thymeleaf depuis un contrôleur Spring Boot ?

Pour transmettre un objet Java à Thymeleaf depuis un contrôleur Spring Boot, vous pouvez utiliser la méthode addAttribute() de l’objet Model. Cette méthode permet d’ajouter des objets ou des valeurs à un modèle qui sera ensuite utilisé pour générer notre vue.

Voici un exemple :

@GetMapping("/pokemon/{id}")
public String getPokemon(@PathVariable("id") int id, Model model) {
    Pokemon pokemon = pokemonService.getPokemon(id);
    model.addAttribute("pokemon", pokemon);
    return "pokemonDetails";
}

Dans ce bout de code, on utilise l’annotation habituelle @GetMapping(“/pokemon/{id}”) pour définir une méthode qui répond à une requête GET :

Comment Spring Boot récupére des données d’un formulaire Thymeleaf pour ajouter un nouveau Pokémon ?

Pour récupérer des données d’un formulaire, vous pouvez utiliser la méthode avec l’annotation @PostMapping() de Spring Boot pour traiter les données soumises par le formulaire.

L’objet @ModelAttribute vous permet de récupérer les données saisies dans le formulaire. Cette annotation permet de lier les données du formulaire à un objet Java. C’est bien pratique et simplissime !

Voici un exemple pour ajouter un nouveau Pokemon à partir d’un formulaire Thymeleaf :

@PostMapping("/pokemon/add")
public String addPokemon(@ModelAttribute("pokemon") Pokemon pokemon) {
    pokemonService.addPokemon(pokemon); // notre service ajoute le pokémon quelque part en BD ou autre
    return "redirect:/pokemon"; // on redirige vers la vue qui affichera quelque chose comme le Pokémon ajouté
}

Dans la vue Thymeleaf, on peut utiliser la balise th:object pour lier le formulaire à l’objet Pokemon :

<form th:object="${pokemon}" method="post" action="/pokemon/add">
    <label for="nom">Nom :</label>
    <input type="text" id="nom" th:field="*{nom}" />

    <label for="type">Type :</label>
    <input type="text" id="type" th:field="*{type}" />

    <button type="submit">Ajouter un Pokemon</button>
</form>

Dans cet exemple, la propriété nom de l’objet Pokemon est liée au champ de formulaire name avec l’expression {nom} de la balise th:field, et de même pour la propriété type. Vous avez compris que l’attribut th:object de la balise form est utilisé pour lier l’ensemble du formulaire à l’objet Pokemon !

Voici un autre exemple pour transmettre cette fois-ci, une liste de Pokémon :


@GetMapping("/pokemon")
public String getPokemonList(Model model) {
    List<Pokemon> pokemonList = pokemonService.getAllPokemon(); // on récupère les Pokémon de notre base de données
    model.addAttribute("pokemonList", pokemonList); // on l'ajoute au model pour que la vue "vue-pokemonListe" puisse utiliser cet attribut.
    return "vue-pokemonListe"; // nom de la vue qui est toujours une chaîne de caractère
}

La vue Thymeleaf avec la balise th:each pour itérer sur la liste de Pokemon :

<table>
    <tr th:each="pokemon : ${pokemonList}">
        <td th:text="${pokemon.nom}"></td>
        <td th:text="${pokemon.type}"></td>
    </tr>
</table>

Pour transmettre des données d’un contrôleur Spring Boot à une vue Thymeleaf, c’est tout simple, il suffit de stocker les données dans un objet de type Model ou ModelAndView. La méthode addAttribute() de l’objet Model peut être utilisée pour ajouter des données à transmettre à la vue !

Quelles sont les méthodes de l’interface Model ?

Cette interface permet de stocker des attributs dans le modèle de données qui pourront ensuite être utilisés dans la vue.

Voici quelques méthodes :

Remarque : Les méthodes des contrôleurs avec Thymeleaf renvoient une chaîne de caractères qui correspond à une vue (template) à afficher dans le navigateur. Cependant, il est possible de renvoyer d’autres types de réponse HTTP, tels que des fichiers JSON, XML ou autres, en utilisant des annotations spécifiques telles que @ResponseBody que vous connaissez déjà. Cela permet de renvoyer des données formatées sans avoir besoin d’une vue HTML.

Quelle est la différence avec ModelAndView ?

Cette classe de Spring combine les informations de modèle (fournies par l’interface Model) avec le nom de la vue à rendre. Elle permet de spécifier à la fois les données du modèle et la vue à afficher, ce qui simplifie la gestion des deux éléments en une seule entité.

Contrairement à Model qui renvoie simplement une instance de Model, ModelAndView renvoie une instance contenant à la fois le modèle et le nom de la vue à afficher.

L’utilisation de ModelAndView peut être plus pratique pour les cas où l’on souhaite spécifier la vue à afficher de manière explicite plutôt que de simplement renvoyer un nom de vue et laisser Spring Boot déterminer lui-même comment l’afficher.

un Exemple de code :

@GetMapping("/")
public ModelAndView accueil() {
    ModelAndView mav = new ModelAndView("accueil");
    mav.addObject("message", "Salut Rudy et David !");
    return mav;
}

Ici, on crée une instance de ModelAndView en passant le nom de la vue accueil.html comme argument du constructeur. Ensuite, on ajoute un attribut “message” avec la méthode addObject(“message”, “Salut Rudy et David !”) qui prend 2 arguments : le nom de l’attribut et sa valeur.

Ensuite, on renvoie l’objet ModelAndView en tant que valeur de retour de la méthode. Spring Boot va alors utiliser la vue spécifiée et inclure l’attribut “message” dans notre page HTML.

Comment valider notre objet Pokemon avant d’être ajouté ?

Vous pouvez ajouter l’annotation @Valid à son paramètre de méthode dans le contrôleur :

@PostMapping("/pokemon/add")
public String addPokemon(@Valid @ModelAttribute("pokemon") Pokemon pokemon, BindingResult result) {
    if (result.hasErrors()) {
        return "addPokemon";
    }
    pokemonService.addPokemon(pokemon);
    return "redirect:/pokemon";
}

Ci-dessus, l’annotation @Valid indique à Spring de valider l’objet Pokemon en utilisant les contraintes de validation définies dans sa classe, et le paramètre BindingResult permet de récupérer les erreurs de validation éventuelles.

Dans la vue Thymeleaf, on peut utiliser la balise th:errors pour afficher les erreurs de validation associées à chaque champ :

<form th:object="${pokemon}" method="post" action="/pokemon/add">
    <label for="nom">Nom :</label>
    <input type="text" id="nom" th:field="*{nom}" />
    <span th:errors="*{nom}"></span>

    <label for="type">Type:</label>
    <input type="text" id="type" th:field="*{type}" />
    <span th:errors="*{type}"></span>

    <button type="submit">Ajouter un Pokémon</button>
</form>

Ci-dessus, la balise th:errors est utilisée avec l’expression {nom} et {type} pour afficher les erreurs de validation associées au champ nom et type du formulaire.

Vous pouvez utiliser l’attribut th:errors avec n’importe quelle expression d’attribut th:field qui correspond à un champ.

Remarque : En utilisant ces techniques, il est possible de construire des formulaires avec une validation de données côté serveur. Lorsqu’un formulaire est soumis, les données sont automatiquement liées à un objet Java, ici notre Entity.

Si la validation échoue, les erreurs de validation peuvent être affichées dans la vue Thymeleaf en utilisant la balise th:errors.

Comment récupérer les données d’un formulaire pour ajouter un nouveau Pokémon ?

Vous pouvez utiliser la méthode @PostMapping() avec un objet PokemonForm en paramètre :

@PostMapping("/addPokemon")
public String addPokemon(@Valid PokemonForm pokemonForm, BindingResult bindingResult, Model model) {
    // on vérifie s'il y a des erreurs 
    if (bindingResult.hasErrors()) {
        // Si les erreurs de validation sont présentes, les afficher dans la vue Thymeleaf déjà utilisée
        return "addPokemon";
    }
    // sinon, on convertie les données du formulaire en objet Pokemon
    Pokemon pokemon = new Pokemon();
    pokemon.setNom(pokemonForm.getNom());
    pokemon.setType(pokemonForm.getType());
    // On enregistre le nouveau Pokémon dans la base de données ou autres
    pokemonService.savePokemon(pokemon);
    
    // On redirige vers la page de liste des Pokémons
    return "redirect:/pokemonList";
}

Dans ce code, la validation des données du formulaire est effectuée en utilisant l’annotation @Valid pour l’objet PokemonForm et en utilisant l’objet BindingResult pour récupérer les erreurs de validation. Si la validation est réussie, les données du formulaire sont converties en objet Pokemon, puis enregistrées dans la base de données à l’aide du service PokemonService.

Enfin, le contrôleur redirige l’utilisateur vers la page de liste des Pokémon.

A quoi ressemble le formulaire correspondant pour ajouter un Pokémon ?

Ici, nous utilisons th:object="${pokemonForm}" pour lier le formulaire à l’objet PokemonForm qui contient les données de notre formulaire :

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Ajouter un Pokémon</title>
</head>
<body>
    <h1>Ajouter un Pokémon</h1>
    
    <form th:object="${pokemonForm}" th:action="@{/addPokemon}" method="post">
        <p>
            <label>Nom :</label>
            <input type="text" th:field="*{nom}" />
        </p>
        <p>
            <label>Type :</label>
            <input type="text" th:field="*{type}" />
        </p>
        <button type="submit">Ajouter un Pokémon</button>
    </form>
    
</body>
</html>

Comment faire une recherche Asynchrone ?

Nous ne sommes pas dans un framework Angular, donc, il va falloir gérer la partie asyncrhone avec Ajax, JQuery ou la méthode fetch() de javascript.

Vous pouvez utiliser JavaScript pour envoyer une requête AJAX au serveur en fonction de ce que l’utilisateur saisit dans une zone de saisie. Le serveur peut renvoyer les résultats de recherche sous forme de JSON, que vous pouvez ensuite utiliser pour mettre à jour dynamiquement la vue sans recharger la totalité de la page !

Dans notre fichier search.js :

<script>
    $(document).ready(function() {
        $("#searchForm").submit(function(event) {
            event.preventDefault(); // Empêcher la soumission du formulaire
            var searchNom = $("#searchNom").val();
            var searchType = $("#searchType").val();
            $.ajax({
                url: "/search",
                data: {name: searchNom, type: searchType},
                success: function(data) {
                    $("#pokemonList").html(data); // Mettre à jour la liste des Pokémons
                }
            });
        });
    });
</script>

Dans ce code, nous utilisons la fonction $.ajax de jQuery pour envoyer une requête POST au serveur avec les paramètres de recherche.

Nous utilisons ensuite la fonction .html(data) pour mettre à jour la liste des Pokémons affichée sur notre page.

Dans notre fichier search.js :

document.addEventListener("DOMContentLoaded", function() {
    const searchForm = document.getElementById("searchForm");
    searchForm.addEventListener("submit", function(event) {
        event.preventDefault();
        const searchNom = document.getElementById("searchNom").value;
        const searchType = document.getElementById("searchType").value;
        fetch(`/search?name=${searchName}&type=${searchType}`)
            .then(response => response.text())
            .then(html => {
                const pokemonList = document.getElementById("pokemonList");
                pokemonList.innerHTML = html;
            });
    });
});

A quoi peut ressembler la méthode de recherche dans notre contrôleur ?

Voici un exemple de code pour traiter la requête POST et renvoyer la liste des Pokémons correspondante :

@Controller
public class PokemonController {

    @Autowired
    private PokemonService pokemonService;

    @PostMapping("/search")
    public String searchPokemon(@RequestParam("nom") String name, @RequestParam("type") String type, Model model) {
        List<Pokemon> pokemonList = pokemonService.searchPokemon(nom, type);
        model.addAttribute("pokemonList", pokemonList);
        return "pokemonList :: pokemonListFragment"; // Renvoyer le fragment de code HTML plutôt que recharger toute la page
    }

Dans ce code ci-dessus, on renvoie le fragment de code HTML correspondant à la liste de Pokémon mise à jour qui sera inséré dans la page sans recharger la totalité de la page.

En fait, la syntaxe return "pokemonList :: pokemonListFragment" renvoie le fragment de page nommé pokemonListFragment défini dans la vue pokemonList. Cette syntaxe permet de réutiliser des parties communes de code HTML sans avoir à les répéter dans chaque vue !

Du coup, c’est bien pratique car on indique à Spring Boot de retourner uniquement le fragment pokemonListFragment de la vue pokemonList, plutôt que de renvoyer la vue entière !

Mais au fait, c’est quoi un fragment ?

Un fragment de page est simplement une section de code HTML qui peut être incluse dans d’autres pages.

Il existe une syntaxe pour déclarer un fragment de page dans une vue Thymeleaf, il faut utiliser la directive th:fragment comme ci-dessous :

<div th:fragment="pokemonListFragment">
    <!-- Contenu du fragment -->
</div>

Dans notre exemple précédent, pokemonListFragment est le nom donné au fragment de page. On peut ensuite inclure ce fragment dans une autre page en utilisant la syntaxe ci-dessous déjà vue plus haut dans la page :

<div th:replace="pokemonList :: pokemonListFragment"></div>

pokemonList est le nom de la page qui contient le fragment et pokemonListFragment est le nom du fragment de page que l’on veut inclure. Le résultat sera l’inclusion du contenu du fragment de page dans la balise div !

Page HTML Thymeleaf de recherche correspondante

Ajout d’une zone de saisie de recherche :

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Liste des Pokémons</title>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script th:src="@{/js/search.js}"></script>
</head>
<body>
    <h1>Liste des Pokémons</h1>
    <form id="searchForm">
        <label>Nom : <input type="text" id="searchNom"></label>
        <label>Type : <input type="text" id="searchType"></label>
        <button type="submit">Rechercher</button>
    </form>
    <div id="pokemonList" th:replace="pokemonList :: pokemonListFragment"></div>
</body>
</html>

Dans ce code, nous incluons les fichiers JavaScript nécessaires pour la recherche Asynchrone. Voyez pour prendre des versions plus récentes.

La balise th:replace est utilisée pour inclure le fragment de code HTML correspondant à la liste des Pokémons mise à jour.

Notre formulaire utilise une méthode POST et la fonction de recherche asynchrone est déclenchée par jQuery lorsque le formulaire est envoyé !

Exemple de traitement d’une relation ManyToMany avec Thymeleaf (TP11)

relation plusieurs à plusieurs

Voici un exemple de formulaire Thymeleaf avec un contrôleur Spring Boot pour la relation ManyToMany entre les entités client et reservation.

Formulaire

Dans ce formulaire, nous avons un champ clients qui utilise une liste déroulante multiselect pour permettre à l’utilisateur de sélectionner plusieurs clients.

Ici, j’utilise le framework Bootstrap pour faire joli !

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Réserver une croisière</title>
    <!-- Ajout du lien vers le framework Bootstrap -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
          integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
          crossorigin="anonymous">
</head>
<body>
<div class="container">
    <h1>Réserver une croisière</h1>
    <form th:action="@{/reservations}" th:object="${reservation}" method="post">
        <div class="form-group">
            <label for="duree">Durée de la croisière :</label>
            <input type="number" id="duree" name="duree" th:field="*{croisiere.duree}" class="form-control"/>
        </div>
        <div class="form-group">
            <label for="nom">Nom de la croisière :</label>
            <input type="text" id="nom" name="nom" th:field="*{croisiere.nom}" class="form-control"/>
        </div>
        <div class="form-group">
            <label for="montant">Montant de la réservation :</label>
            <input type="number" id="montant" name="montant" th:field="*{montant}" class="form-control"/>
        </div>
        <div class="form-group">
    <label for="clients">Clients :</label>
    <select id="clients" name="clients" multiple="multiple" th:field="*{clients}" class="form-control">
        <option th:each="client : ${allClients}" th:value="${client.id}" th:text="${client.nom}+' '+${client.prenom}"></option>
    </select>
</div>

        <input type="submit" value="Réserver" class="btn btn-primary"/>
    </form>
</div>
<!-- Ajout du lien vers les scripts JavaScript de Bootstrap -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
        integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
        crossorigin="anonymous"></script>
</body>
</html>

Contrôleur

Dans le contrôleur, nous récupérons les IDs de clients sélectionnés et on les associe à la réservation en utilisant la méthode setClients() de notre classe Reservation.

@Controller
public class ReservationController {
    
    @Autowired
    private ReservationRepository reservationRepository;
    
    @Autowired
    private ClientRepository clientRepository;
    
    @Autowired
    private CroisiereRepository croisiereRepository;
    
    @GetMapping("/reservations/new")
    public String newReservation(Model model) {
        model.addAttribute("reservation", new Reservation());
        model.addAttribute("allClients", clientRepository.findAll());
        return "newReservation";
    }
    
    @PostMapping("/reservations")
    public String saveReservation(@ModelAttribute("reservation") Reservation reservation, @RequestParam("clients") List<Long> clientIds) {
        Croisiere croisiere = croisiereRepository.findById(reservation.getCroisiere().getId()).orElseThrow(() -> new IllegalArgumentException("Croisière invalide"));
        reservation.setCroisiere(croisiere);
        Set<Client> clients = clientRepository.findAllById(clientIds).stream().collect(Collectors.toSet());
        reservation.setClients(clients);
        reservationRepository.save(reservation);
        return "redirect:/";
    }
}

Repositories

@Repository
public interface ReservationRepository extends JpaRepository<Reservation, Long> {
}

@Repository
public interface ClientRepository extends JpaRepository<Client, Long> {
}

@Repository
public interface CroisiereRepository extends JpaRepository<Croisiere, Long> {
}

Conclusion

Lien vers une API Pokebuild

A suivre…