# [RAPIDE] Guide Complet Spring Boot — Projet Fil Rouge : **TaskFlow**

> **Pour grand débutant · De zéro à la production · 10 modules progressifs**

---

## [GUIDE] À propos de ce guide

Ce guide vous accompagne pas à pas dans la maîtrise de **Spring Boot**, le framework Java le plus utilisé en entreprise.  
Vous construirez **TaskFlow**, une API de gestion de tâches professionnelle, du premier endpoint jusqu'au déploiement Docker.

Chaque module correspond à un chapitre du fichier `spring_boot_ultra_detaille.txt` et ajoute une couche fonctionnelle à votre projet.

---

## [OBJECTIF] Ce que vous allez construire

**TaskFlow** est une API REST complète de gestion de tâches, similaire à Trello :

- Gestion de tâches avec statuts, priorités et assignations
- Authentification sécurisée avec JWT
- Relations entre utilisateurs, projets et tâches
- Validation des données, gestion d'erreurs professionnelle
- Documentation API interactive (Swagger)
- Tests unitaires et d'intégration
- Déploiement Docker en production

---

## [DOCS] Structure du Guide

| Module | Fichier | Contenu | Durée estimée |
|--------|---------|---------|---------------|
| 00 | `00_introduction.md` | Spring Boot, architecture, premier projet | 3–4h |
| 01 | `01_crud_rest_api.md` | CRUD complet, REST, HTTP, ResponseEntity | 4–5h |
| 02 | `02_services_architecture.md` | Services, injection de dépendances, architecture | 3–4h |
| 03 | `03_jpa_database.md` | JPA, Hibernate, entités, repositories | 5–6h |
| 04 | `04_validation.md` | Bean Validation, gestion d'erreurs | 3–4h |
| 05 | `05_relations_jpa.md` | @OneToMany, @ManyToMany, relations | 5–6h |
| 06 | `06_spring_security_jwt.md` | Authentification, JWT, autorisation | 5–6h |
| 07 | `07_tests.md` | JUnit 5, Mockito, tests d'intégration | 4–5h |
| 08 | `08_swagger_documentation.md` | OpenAPI, Swagger UI, documentation | 2–3h |
| 09 | `09_docker_deploiement.md` | Docker, CI/CD, mise en production | 4–5h |

**Total estimé : 38–48 heures** (à votre rythme)

---

## [OUTILS] Prérequis

### Logiciels à installer

| Outil | Version | Lien |
|-------|---------|------|
| **JDK** | 17 ou 21 | [adoptium.net](https://adoptium.net) |
| **IntelliJ IDEA** | Community (gratuit) | [jetbrains.com](https://www.jetbrains.com/idea/) |
| **Maven** | 3.9+ | Inclus dans IntelliJ |
| **Docker Desktop** | Dernière | [docker.com](https://www.docker.com/get-started) |
| **Postman** | Dernière | [postman.com](https://www.postman.com/downloads/) |

### Connaissances requises

- [OK] Bases de Java (variables, classes, méthodes, héritage)
- [OK] Notions de base des API (qu'est-ce qu'une requête HTTP)
- [OK] Concepts de base SQL (SELECT, INSERT, tables)
- [X] Pas besoin d'expérience avec Spring

---

## [RAPIDE] Démarrage rapide

### 1. Créer le projet Spring Boot

Rendez-vous sur **[start.spring.io](https://start.spring.io)** et configurez :

```
Project     : Maven
Language    : Java
Spring Boot : 3.2.x
Group       : com.taskflow
Artifact    : taskflow
Java        : 17
```

**Dépendances à ajouter :**
- Spring Web
- Spring Data JPA
- H2 Database
- Spring Boot DevTools
- Lombok
- Validation

### 2. Télécharger et ouvrir

1. Cliquez **Generate** -> téléchargez le ZIP
2. Décompressez dans votre dossier de travail
3. Ouvrez dans IntelliJ : `File -> Open -> sélectionnez le dossier`
4. Attendez que Maven télécharge les dépendances

### 3. Lancer l'application

```bash
# Dans IntelliJ : clic droit sur TaskflowApplication -> Run
# Ou en terminal :
mvn spring-boot:run
```

Vous devriez voir dans la console :
```
Started TaskflowApplication in 2.345 seconds
```

---

## [DOSSIER] Structure finale du projet

```
taskflow/
├── src/
│   ├── main/
│   │   ├── java/com/taskflow/
│   │   │   ├── TaskflowApplication.java       <- Point d'entrée
│   │   │   ├── config/                        <- Configurations
│   │   │   │   ├── SecurityConfig.java
│   │   │   │   └── OpenAPIConfig.java
│   │   │   ├── controller/                    <- Endpoints HTTP
│   │   │   │   ├── AuthController.java
│   │   │   │   ├── TaskController.java
│   │   │   │   └── UserController.java
│   │   │   ├── dto/                           <- Objets de transfert
│   │   │   │   ├── CreateTaskDTO.java
│   │   │   │   ├── UpdateTaskDTO.java
│   │   │   │   └── AuthenticationRequest.java
│   │   │   ├── entity/                        <- Entités JPA
│   │   │   │   ├── Task.java
│   │   │   │   ├── User.java
│   │   │   │   └── Role.java
│   │   │   ├── exception/                     <- Gestion d'erreurs
│   │   │   │   ├── GlobalExceptionHandler.java
│   │   │   │   └── TaskNotFoundException.java
│   │   │   ├── repository/                    <- Accès données
│   │   │   │   ├── TaskRepository.java
│   │   │   │   └── UserRepository.java
│   │   │   ├── security/                      <- JWT & sécurité
│   │   │   │   ├── JwtService.java
│   │   │   │   └── JwtAuthenticationFilter.java
│   │   │   └── service/                       <- Logique métier
│   │   │       ├── TaskService.java
│   │   │       └── AuthService.java
│   │   └── resources/
│   │       ├── application.properties
│   │       ├── application-dev.properties
│   │       └── application-prod.properties
│   └── test/
│       └── java/com/taskflow/
│           ├── controller/
│           ├── service/
│           └── repository/
├── Dockerfile
├── docker-compose.yml
└── pom.xml
```

---

## [IDEE] Conseils pour bien apprendre

> **Ne copiez-collez pas le code !** Tapez-le vous-même. La mémoire musculaire aide à mémoriser.

1. **Lisez** d'abord toute la section avant de coder
2. **Codez** chaque exemple en le tapant (pas de copier-coller)
3. **Testez** immédiatement dans Postman ou le navigateur
4. **Cassez** le code volontairement pour voir les erreurs
5. **Cherchez** les erreurs dans les logs avant de demander de l'aide

---

## [LIEN] Ressources complémentaires

- [Documentation officielle Spring Boot](https://docs.spring.io/spring-boot/docs/current/reference/html/)
- [Spring Initializr](https://start.spring.io)
- [Baeldung](https://www.baeldung.com) — Tutoriels Spring en anglais
- [Stack Overflow](https://stackoverflow.com/questions/tagged/spring-boot)

---

*Commencez par le module `00_introduction.md` -> Bonne chance ! [BRAVO]*

# Module 00 — Introduction à Spring Boot

> **Objectif :** Comprendre ce qu'est Spring Boot, créer votre premier projet et écrire votre premier endpoint HTTP.

---

## [LISTE] Sommaire

1. [Qu'est-ce que Spring Boot ?](#1-quest-ce-que-spring-boot)
2. [L'architecture en couches](#2-larchitecture-en-couches)
3. [Créer votre premier projet](#3-créer-votre-premier-projet)
4. [Anatomie du projet généré](#4-anatomie-du-projet-généré)
5. [Votre premier Controller REST](#5-votre-premier-controller-rest)
6. [Configuration avec application.properties](#6-configuration-avec-applicationproperties)
7. [Les logs avec SLF4J](#7-les-logs-avec-slf4j)
8. [Exercices pratiques](#8-exercices-pratiques)

---

## 1. Qu'est-ce que Spring Boot ?

### Le problème avant Spring Boot

Avant Spring Boot (avant 2014), créer une application Java web nécessitait :

- Configurer manuellement des dizaines de fichiers XML
- Installer et configurer un serveur Tomcat séparé
- Gérer manuellement la compatibilité des versions de librairies
- Écrire des centaines de lignes de configuration

Résultat : il fallait plusieurs jours juste pour démarrer un projet vide.

### La solution : Spring Boot

Spring Boot apporte trois idées révolutionnaires :

**1. Auto-configuration** : Spring Boot détecte automatiquement ce dont vous avez besoin.
> Vous ajoutez `spring-boot-starter-web` -> Spring Boot configure automatiquement Tomcat, Spring MVC, Jackson (pour le JSON), etc.

**2. Serveur embarqué** : Tomcat est inclus dans votre JAR.
> Plus besoin d'installer un serveur séparé. Votre application est un simple `.jar` qu'on lance avec `java -jar`.

**3. Starters (dépendances pré-packagées)** : Au lieu de chercher et assembler 10 librairies compatibles, vous ajoutez un seul starter.

```xml
<!-- Au lieu de gérer 10 dépendances séparées... -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <!-- ...cette unique ligne inclut tout ! -->
</dependency>
```

### Quand utiliser Spring Boot ?

| Cas d'usage | Spring Boot approprié ? |
|------------|------------------------|
| API REST (web services) | [OK] Idéal |
| Application microservices | [OK] Idéal |
| Application monolithique | [OK] Très bien |
| Script batch de traitement | [OK] Possible |
| Application mobile Android | [X] Non |
| Site web front-end (React/Vue) | [X] Non |

---

## 2. L'architecture en couches

Spring Boot encourage une **architecture en 3 couches** (ou 4 avec la sécurité). C'est LE concept fondamental à comprendre avant d'écrire une seule ligne de code.

```
┌──────────────────────────────────────────────────────────┐
│                    CLIENT (Postman, navigateur, app)     │
└──────────────────────────────┬───────────────────────────┘
                               │  Requête HTTP
                               [BLACK_DOWN-POINTING_TRIANGLE]
┌──────────────────────────────────────────────────────────┐
│  COUCHE CONTROLLER (@RestController)                     │
│  • Reçoit les requêtes HTTP                              │
│  • Vérifie le format des données (pas la logique)        │
│  • Délègue au service                                    │
│  • Retourne la réponse HTTP                              │
└──────────────────────────────┬───────────────────────────┘
                               │  Appel Java normal
                               [BLACK_DOWN-POINTING_TRIANGLE]
┌──────────────────────────────────────────────────────────┐
│  COUCHE SERVICE (@Service)                               │
│  • Contient TOUTE la logique métier                      │
│  • Règles de gestion (ex: "une tâche complète           │
│    ne peut pas être réassignée")                         │
│  • Coordonne plusieurs repositories si nécessaire        │
└──────────────────────────────┬───────────────────────────┘
                               │  Appel Java normal
                               [BLACK_DOWN-POINTING_TRIANGLE]
┌──────────────────────────────────────────────────────────┐
│  COUCHE REPOSITORY (@Repository)                         │
│  • Accès à la base de données UNIQUEMENT                 │
│  • CRUD : Create, Read, Update, Delete                   │
│  • Pas de logique métier ici                             │
└──────────────────────────────┬───────────────────────────┘
                               │  SQL
                               [BLACK_DOWN-POINTING_TRIANGLE]
┌──────────────────────────────────────────────────────────┐
│  BASE DE DONNÉES (PostgreSQL, MySQL, H2...)               │
└──────────────────────────────────────────────────────────┘
```

### Pourquoi cette séparation ?

| Sans séparation | Avec séparation |
|----------------|----------------|
| Logique éparpillée partout | Logique centralisée |
| Difficile à tester | Testable indépendamment |
| Modification = risque de tout casser | Modification limitée à une couche |
| Difficile à comprendre | Facile à naviguer |

> **Règle d'or :** Le Controller ne fait QUE parler HTTP. Le Service ne sait pas qu'il y a du HTTP. Le Repository ne sait pas qu'il y a de la logique.

---

## 3. Créer votre premier projet

### Via Spring Initializr (recommandé)

1. Ouvrez **[start.spring.io](https://start.spring.io)**
2. Configurez comme suit :

```
Project     : Maven Project
Language    : Java
Spring Boot : 3.2.x (choisissez la dernière version stable)

Project Metadata
  Group    : com.taskflow
  Artifact : taskflow
  Name     : taskflow
  Package  : com.taskflow
  Packaging: Jar
  Java     : 17
```

3. Cliquez **"ADD DEPENDENCIES"** et ajoutez :
   - `Spring Web` — Pour créer des API REST
   - `Spring Boot DevTools` — Pour le rechargement automatique
   - `Lombok` — Pour éviter les getters/setters répétitifs

4. Cliquez **"GENERATE"** -> Décompressez le ZIP

5. Ouvrez dans IntelliJ : `File -> Open -> sélectionnez le dossier taskflow`

### Via IntelliJ IDEA (alternative)

```
File -> New -> Project -> Spring Initializr
```
Mêmes paramètres qu'au-dessus.

---

## 4. Anatomie du projet généré

```
taskflow/
│
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/taskflow/
│   │   │       └── TaskflowApplication.java   <- POINT D'ENTRÉE
│   │   └── resources/
│   │       └── application.properties         <- CONFIGURATION
│   │
│   └── test/
│       └── java/
│           └── com/taskflow/
│               └── TaskflowApplicationTests.java
│
└── pom.xml                                    <- DÉPENDANCES MAVEN
```

### Le fichier pom.xml

C'est le "fichier de recette" de votre projet. Maven s'en sert pour télécharger toutes les librairies nécessaires.

```xml
<?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
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!--
        PARENT POM : On hérite de spring-boot-starter-parent
        Avantage : toutes les versions de librairies sont déjà
        choisies et compatibles entre elles. Plus besoin de gérer
        les versions manuellement pour la plupart des dépendances.
    -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>

    <!-- Identité de VOTRE projet -->
    <groupId>com.taskflow</groupId>
    <artifactId>taskflow</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>taskflow</name>
    <description>API de gestion de tâches - Projet fil rouge Spring Boot</description>

    <!-- Version de Java à utiliser -->
    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <!--
            STARTER WEB : Inclut automatiquement :
            - Spring MVC (routing des requêtes HTTP)
            - Tomcat embarqué (serveur web)
            - Jackson (conversion Java <-> JSON)
        -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--
            DEVTOOLS : Outils de développement
            - Hot reload : redémarre l'app quand vous modifiez le code
            - scope=runtime : uniquement en développement, pas en prod
            - optional=true : n'est pas transitif (ne se propage pas)
        -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <!--
            LOMBOK : Génère automatiquement le code répétitif
            - @Getter, @Setter : génère getters/setters
            - @AllArgsConstructor : génère un constructeur
            - @Data : @Getter + @Setter + @ToString + @EqualsAndHashCode
        -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!--
            STARTER TEST : Framework de tests
            - JUnit 5 : moteur de test
            - Mockito : créer des faux objets
            - AssertJ : assertions expressives
        -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!--
                PLUGIN SPRING BOOT MAVEN : Permet de faire
                mvn spring-boot:run pour lancer l'app
                et mvn clean package pour créer le JAR final
            -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <!-- Exclure Lombok du JAR final -->
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
```

### Le point d'entrée : TaskflowApplication.java

```java
package com.taskflow;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @SpringBootApplication est une annotation "composite" qui combine 3 annotations :
 *
 * 1. @Configuration
 *    -> Indique que cette classe peut déclarer des beans Spring (des objets gérés par Spring)
 *
 * 2. @EnableAutoConfiguration
 *    -> Active la "magie" de Spring Boot : il détecte vos dépendances et configure
 *      automatiquement ce dont vous avez besoin.
 *      Exemple : vous avez ajouté spring-boot-starter-web ? Spring configure Tomcat.
 *
 * 3. @ComponentScan
 *    -> Spring va scanner le package "com.taskflow" ET tous ses sous-packages pour
 *      trouver vos classes annotées (@RestController, @Service, @Repository, etc.)
 *      IMPORTANT : C'est pour ça que toutes vos classes doivent être dans
 *      des sous-packages de com.taskflow !
 */
@SpringBootApplication
public class TaskflowApplication {

    public static void main(String[] args) {
        /*
         * SpringApplication.run() fait plusieurs choses :
         * 1. Crée le contexte Spring (le "conteneur" qui gère tous vos objets)
         * 2. Lance l'auto-configuration
         * 3. Scanne les composants (@RestController, @Service, etc.)
         * 4. Démarre le serveur Tomcat embarqué sur le port 8080
         * 5. Affiche le banner ASCII art dans la console
         * 6. L'app est prête !
         */
        SpringApplication.run(TaskflowApplication.class, args);
    }
}
```

---

## 5. Votre premier Controller REST

Créez le fichier `src/main/java/com/taskflow/controller/HelloController.java` :

```java
package com.taskflow.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

/**
 * @RestController = @Controller + @ResponseBody
 *
 * @Controller : Marque cette classe comme un contrôleur Spring MVC.
 *               Spring va l'instancier et l'enregistrer dans son conteneur.
 *
 * @ResponseBody : Les valeurs retournées par les méthodes sont
 *                 automatiquement converties en JSON et écrites dans
 *                 le corps de la réponse HTTP. Sans ça, Spring
 *                 chercherait des fichiers HTML à afficher.
 *
 * @RequestMapping("/api") : Préfixe commun à toutes les URLs de ce controller.
 *                           Chaque méthode sera accessible sous /api/...
 */
@RestController
@RequestMapping("/api")
public class HelloController {

    // Logger SLF4J — on explique son utilisation dans la section suivante
    private static final Logger logger = LoggerFactory.getLogger(HelloController.class);

    /**
     * ENDPOINT 1 : Message simple
     *
     * @GetMapping("/hello") : Cette méthode répond aux requêtes GET sur /api/hello
     *
     * HTTP GET = "Donne-moi une ressource"
     * On ne modifie rien, on récupère juste une information.
     *
     * Testez dans votre navigateur : http://localhost:8080/api/hello
     */
    @GetMapping("/hello")
    public String sayHello() {
        logger.info("Quelqu'un a appelé GET /api/hello");
        return "Bienvenue dans TaskFlow ! [RAPIDE]";
    }

    /**
     * ENDPOINT 2 : Path Variable — variable dans l'URL
     *
     * @PathVariable extrait une partie de l'URL.
     * {name} dans l'URL = paramètre name dans la méthode.
     *
     * Exemple :
     *   URL : GET /api/hello/Alice
     *   name = "Alice"
     *   Retour : "Bonjour, Alice !"
     *
     * Pourquoi utiliser les Path Variables ?
     * -> Pour identifier une ressource précise : /users/42, /tasks/7, etc.
     */
    @GetMapping("/hello/{name}")
    public String sayHelloTo(@PathVariable String name) {
        logger.debug("Salutation pour : {}", name);
        return "Bonjour, " + name + " !";
    }

    /**
     * ENDPOINT 3 : Query Parameters — paramètres de requête
     *
     * @RequestParam extrait les paramètres après le "?" dans l'URL.
     * required=false : le paramètre est optionnel
     * defaultValue="World" : valeur par défaut si absent
     *
     * Exemples :
     *   GET /api/greet?name=Bob   -> "Salut, Bob !"
     *   GET /api/greet            -> "Salut, World !"
     *   GET /api/greet?name=Alice&formal=true -> "Bonjour, Alice !"
     *
     * Pourquoi utiliser les Query Params ?
     * -> Pour filtrer, trier, paginer : /tasks?status=DONE&page=2
     */
    @GetMapping("/greet")
    public String greet(
            @RequestParam(required = false, defaultValue = "World") String name,
            @RequestParam(required = false, defaultValue = "false") boolean formal) {

        if (formal) {
            return "Bonjour, " + name + " !";
        }
        return "Salut, " + name + " !";
    }

    /**
     * ENDPOINT 4 : Retourner un objet JSON
     *
     * Spring Boot utilise Jackson pour convertir automatiquement
     * les objets Java en JSON.
     *
     * Map<String, Object> est pratique pour des réponses ad-hoc.
     * En pratique, on crée des classes dédiées (DTOs) — Module 01.
     *
     * Retour JSON :
     * {
     *   "message": "API opérationnelle",
     *   "version": "1.0.0",
     *   "timestamp": "2026-01-26T10:30:00"
     * }
     */
    @GetMapping("/status")
    public Map<String, Object> getStatus() {
        logger.info("Vérification du statut de l'API");

        Map<String, Object> status = new HashMap<>();
        status.put("message", "API TaskFlow opérationnelle");
        status.put("version", "1.0.0");
        status.put("timestamp", LocalDateTime.now());

        return status;
    }
}
```

### Tester vos endpoints

**Option 1 : Navigateur web** (uniquement pour les GET simples)
```
http://localhost:8080/api/hello
http://localhost:8080/api/hello/Alice
http://localhost:8080/api/greet?name=Bob
http://localhost:8080/api/status
```

**Option 2 : Curl (terminal)**
```bash
curl http://localhost:8080/api/hello
curl http://localhost:8080/api/hello/Alice
curl "http://localhost:8080/api/greet?name=Bob&formal=true"
curl http://localhost:8080/api/status
```

**Option 3 : Postman** (recommandé pour la suite)
1. Ouvrez Postman
2. Nouvelle requête -> GET -> `http://localhost:8080/api/hello`
3. Cliquez **Send**

---

## 6. Configuration avec application.properties

Le fichier `src/main/resources/application.properties` est le fichier de configuration central de votre application. Tout ce que vous y mettez peut être modifié sans recompiler le code.

```properties
# ==================================================
#   CONFIGURATION TASKFLOW - MODULE 00
# ==================================================

# PORT DU SERVEUR
# Par défaut c'est 8080. Changez si ce port est déjà utilisé.
# 8080 est la convention pour les API en développement.
server.port=8080

# NOM DE L'APPLICATION
# Apparaît dans les logs et les métriques
spring.application.name=taskflow

# ==================================================
#   LOGS
# ==================================================

# Niveau de log global :
#   TRACE   -> Tout (très verbeux, évitez en prod)
#   DEBUG   -> Détails de débogage
#   INFO    -> Informations générales (recommandé en dev)
#   WARN    -> Avertissements
#   ERROR   -> Erreurs uniquement
logging.level.root=INFO

# Niveau pour votre code uniquement (plus verbeux que le reste)
logging.level.com.taskflow=DEBUG

# Format des logs dans la console
# %d = date/heure, %p = niveau, %c = classe, %m = message, %n = nouvelle ligne
logging.pattern.console=%d{HH:mm:ss} %p [%c{1}] %m%n

# ==================================================
#   PROPRIÉTÉS PERSONNALISÉES
# ==================================================

# Vous pouvez définir vos propres clés/valeurs
# Elles seront injectées dans vos classes avec @Value
app.name=TaskFlow API
app.version=1.0.0
app.description=API de gestion de tâches professionnelle
```

### Injecter les propriétés avec @Value

```java
package com.taskflow.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("/api")
public class InfoController {

    /**
     * @Value("${app.name}") injecte la valeur de la propriété app.name
     * depuis application.properties.
     *
     * Spring fait la substitution automatiquement au démarrage de l'app.
     * Si la propriété n'existe pas, l'application ne démarre pas (sauf si
     * on définit une valeur par défaut avec @Value("${app.name:Défaut}"))
     */
    @Value("${app.name}")
    private String appName;

    @Value("${app.version}")
    private String appVersion;

    @Value("${server.port}")
    private int serverPort;

    /**
     * Endpoint pour afficher les informations de l'application.
     * Utile pour vérifier rapidement quelle version est déployée.
     *
     * URL : GET /api/info
     */
    @GetMapping("/info")
    public Map<String, Object> getInfo() {
        return Map.of(
            "name", appName,
            "version", appVersion,
            "port", serverPort
        );
    }
}
```

---

## 7. Les logs avec SLF4J

SLF4J (Simple Logging Facade for Java) est l'API de logging standard en Java. Spring Boot l'utilise par défaut avec Logback.

### Pourquoi logger ?

Sans logs, votre application est une boîte noire. Quand quelque chose se passe mal en production, les logs sont souvent votre seule source d'information.

### Les niveaux de log

```
TRACE -> le plus détaillé (rarement utilisé)
  v
DEBUG -> informations de débogage
  v
INFO  -> événements normaux importants (démarrage, requêtes reçues)
  v
WARN  -> situations anormales mais non bloquantes
  v
ERROR -> erreurs qui nécessitent attention
  v
FATAL -> erreurs critiques (application inutilisable)
```

### Utilisation dans votre code

```java
package com.taskflow.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class LoggingDemoController {

    /**
     * Convention : le logger est static final et prend le nom de la classe.
     * static : partagé par toutes les instances de la classe
     * final : ne peut pas être modifié
     *
     * LoggerFactory.getLogger(LoggingDemoController.class) crée un logger
     * associé à cette classe spécifique, ce qui permet d'afficher le nom
     * de la classe dans les logs et de configurer le niveau par classe.
     */
    private static final Logger logger = LoggerFactory.getLogger(LoggingDemoController.class);

    @GetMapping("/demo-logs")
    public String demonstrateLogs(@RequestParam Long userId) {

        // Utilisez {} pour les paramètres (plus performant que la concaténation)
        // Si le niveau INFO n'est pas activé, le toString() de userId n'est même pas appelé
        logger.info("Traitement de la requête pour userId={}", userId);

        // DEBUG : informations utiles pendant le développement
        logger.debug("Début du traitement - Paramètres reçus : userId={}", userId);

        try {
            if (userId <= 0) {
                // WARN : situation anormale mais qu'on peut gérer
                logger.warn("userId invalide reçu : {}. Doit être positif.", userId);
                return "ID invalide";
            }

            // Simulation d'un traitement
            String result = "Traitement réussi pour l'utilisateur " + userId;

            // INFO : résultat normal
            logger.info("Traitement terminé avec succès pour userId={}", userId);

            return result;

        } catch (Exception e) {
            // ERROR : avec le stack trace complet (passez l'exception en dernier paramètre)
            logger.error("Erreur lors du traitement pour userId={}", userId, e);
            return "Erreur";
        }
    }
}
```

### Avec Lombok (recommandé)

Lombok fournit l'annotation `@Slf4j` qui génère automatiquement le logger :

```java
package com.taskflow.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Slf4j génère automatiquement :
 * private static final Logger log = LoggerFactory.getLogger(MonController.class);
 *
 * Vous utilisez la variable "log" (pas "logger") avec Lombok.
 */
@Slf4j
@RestController
@RequestMapping("/api")
public class SimpleController {

    @GetMapping("/ping")
    public String ping() {
        log.info("Ping reçu !");
        return "pong";
    }
}
```

---

## 8. Exercices pratiques

### Exercice 1 : Calculatrice REST (facile)

Créez `src/main/java/com/taskflow/controller/CalculatriceController.java` avec les endpoints :

```
GET /api/calc/add?a=5&b=3        -> 8
GET /api/calc/subtract?a=10&b=3  -> 7
GET /api/calc/multiply?a=4&b=5   -> 20
GET /api/calc/divide?a=15&b=3    -> 5.0
GET /api/calc/divide?a=10&b=0    -> 400 Bad Request avec message d'erreur
```

**Bonus** : Retournez un objet JSON avec `{operation, a, b, result}`.

---

### Exercice 2 : Profil utilisateur (moyen)

Créez une classe `UserProfile` avec les champs : `id`, `firstName`, `lastName`, `email`, `age`.

Créez `UserProfileController` avec :
- `GET /api/profiles/me` -> retourne un profil fictif statique
- `GET /api/profiles/{id}` -> retourne un profil selon l'ID (fictif)
- `GET /api/profiles?minAge=18` -> retourne une liste de profils fictifs

---

### Exercice 3 : Configuration avancée (difficile)

1. Ajoutez dans `application.properties` :
```properties
app.welcome-message=Bienvenue dans TaskFlow
app.max-tasks-per-user=50
app.contact-email=support@taskflow.com
```

2. Créez un endpoint `GET /api/config` qui affiche toutes ces valeurs.

3. Testez en changeant les valeurs dans properties SANS modifier le code Java.

---

### Solution de l'Exercice 1

```java
package com.taskflow.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@Slf4j
@RestController
@RequestMapping("/api/calc")
public class CalculatriceController {

    @GetMapping("/add")
    public Map<String, Object> add(@RequestParam double a, @RequestParam double b) {
        log.info("Addition : {} + {}", a, b);
        double result = a + b;
        return Map.of("operation", "addition", "a", a, "b", b, "result", result);
    }

    @GetMapping("/subtract")
    public Map<String, Object> subtract(@RequestParam double a, @RequestParam double b) {
        return Map.of("operation", "soustraction", "a", a, "b", b, "result", a - b);
    }

    @GetMapping("/multiply")
    public Map<String, Object> multiply(@RequestParam double a, @RequestParam double b) {
        return Map.of("operation", "multiplication", "a", a, "b", b, "result", a * b);
    }

    @GetMapping("/divide")
    public ResponseEntity<?> divide(@RequestParam double a, @RequestParam double b) {
        if (b == 0) {
            // ResponseEntity<?> nous permet de retourner différents types selon le cas
            return ResponseEntity.badRequest()
                .body(Map.of("error", "Division par zéro impossible"));
        }
        return ResponseEntity.ok(
            Map.of("operation", "division", "a", a, "b", b, "result", a / b)
        );
    }
}
```

---

## [OK] Récapitulatif du Module 00

Vous avez appris :

- Ce qu'est Spring Boot et ses avantages (auto-config, serveur embarqué, starters)
- L'architecture en 3 couches : Controller -> Service -> Repository
- Comment créer un projet avec Spring Initializr
- La structure d'un projet Spring Boot et le rôle de chaque fichier
- Les annotations de base : `@SpringBootApplication`, `@RestController`, `@RequestMapping`
- Les mappings HTTP : `@GetMapping`, `@PathVariable`, `@RequestParam`
- La configuration avec `application.properties` et `@Value`
- Le logging avec SLF4J et l'annotation Lombok `@Slf4j`

---

**-> Prochain module : `01_crud_rest_api.md` — Opérations CRUD complètes**

# Module 01 — CRUD REST API Complet

> **Objectif :** Implémenter les 5 opérations CRUD sur les tâches TaskFlow, maîtriser ResponseEntity et gérer les erreurs proprement.

---

## [LISTE] Sommaire

1. [CRUD et les méthodes HTTP](#1-crud-et-les-méthodes-http)
2. [Le modèle Task](#2-le-modèle-task)
3. [Controller CRUD complet](#3-controller-crud-complet)
4. [ResponseEntity — contrôler les réponses HTTP](#4-responseentity--contrôler-les-réponses-http)
5. [Gestion des erreurs](#5-gestion-des-erreurs)
6. [Le pattern DTO](#6-le-pattern-dto)
7. [Exercices pratiques](#7-exercices-pratiques)

---

## 1. CRUD et les méthodes HTTP

**CRUD** (Create, Read, Update, Delete) représente les 4 opérations de base sur des données. En REST, chaque opération correspond à une méthode HTTP précise.

```
Opération │ Méthode HTTP │ URL type          │ Description
──────────┼──────────────┼───────────────────┼────────────────────────
CREATE    │ POST         │ /api/tasks        │ Créer une nouvelle tâche
READ      │ GET          │ /api/tasks        │ Lister toutes les tâches
READ      │ GET          │ /api/tasks/{id}   │ Récupérer une tâche précise
UPDATE    │ PUT          │ /api/tasks/{id}   │ Remplacer une tâche
UPDATE    │ PATCH        │ /api/tasks/{id}   │ Modifier partiellement
DELETE    │ DELETE       │ /api/tasks/{id}   │ Supprimer une tâche
```

### PUT vs PATCH : quelle différence ?

```json
// Tâche actuelle en base de données
{
  "id": 1,
  "title": "Apprendre Spring Boot",
  "description": "Faire les modules 1 à 10",
  "completed": false,
  "priority": 3
}
```

**Requête PUT** -> remplace TOUT l'objet (les champs omis deviennent null)
```json
// Body PUT /api/tasks/1
{ "title": "Spring Boot maîtrisé" }

// Résultat : description et priority perdus !
{ "id": 1, "title": "Spring Boot maîtrisé", "description": null, "priority": null }
```

**Requête PATCH** -> modifie SEULEMENT les champs fournis
```json
// Body PATCH /api/tasks/1
{ "title": "Spring Boot maîtrisé" }

// Résultat : seul le title est modifié
{ "id": 1, "title": "Spring Boot maîtrisé", "description": "Faire les modules 1 à 10", "priority": 3 }
```

> **Convention TaskFlow** : On utilisera PUT pour les mises à jour complètes et PATCH pour les mises à jour partielles (comme marquer une tâche comme terminée).

---

## 2. Le modèle Task

Créez `src/main/java/com/taskflow/model/Task.java` :

> **Attention :** On crée d'abord une classe Java simple (POJO). La persistance en base de données sera ajoutée au Module 03 (JPA).

```java
package com.taskflow.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

/**
 * MODÈLE TASK — Représente une tâche dans TaskFlow
 *
 * Annotations Lombok utilisées :
 *
 * @Data : Génère automatiquement :
 *   - getters pour tous les champs
 *   - setters pour tous les champs non-final
 *   - toString()
 *   - equals() et hashCode() basés sur tous les champs
 *
 * @NoArgsConstructor : Génère le constructeur sans paramètres.
 *   Obligatoire pour Jackson (la lib JSON de Spring) qui doit
 *   pouvoir créer des instances vides puis appeler les setters.
 *
 * @AllArgsConstructor : Génère un constructeur avec tous les paramètres.
 *   Pratique pour créer des instances rapidement dans les tests.
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Task {

    /**
     * Identifiant unique — généré côté serveur.
     * Le client ne doit PAS fournir cet ID lors de la création.
     * Lors d'un PUT/PATCH, l'ID vient de l'URL, pas du body.
     */
    private Long id;

    /**
     * Titre de la tâche — obligatoire.
     * Doit être court et descriptif (max 100 caractères).
     */
    private String title;

    /**
     * Description détaillée — optionnelle.
     * Peut contenir des informations supplémentaires.
     */
    private String description;

    /**
     * Statut de complétion.
     * false = en cours / à faire
     * true  = terminée
     */
    private boolean completed;

    /**
     * Priorité de 1 (basse) à 5 (critique).
     * Permet de trier les tâches par importance.
     */
    private Integer priority;

    /**
     * Statut détaillé du workflow.
     * Enum défini juste après cette classe.
     */
    private Status status;

    /**
     * Date de création — définie automatiquement côté serveur.
     * updatable=false dans JPA (sera ajouté au Module 03).
     */
    private LocalDateTime createdAt;

    /**
     * Date de dernière modification — mise à jour automatiquement.
     */
    private LocalDateTime updatedAt;

    /**
     * ENUM : Statuts possibles pour une tâche
     *
     * On utilise un enum au lieu de String pour :
     * - Limiter les valeurs possibles (pas n'importe quelle chaîne)
     * - Avoir la complétion automatique dans l'IDE
     * - Éviter les erreurs de frappe (typos)
     */
    public enum Status {
        PENDING,      // En attente (état initial)
        IN_PROGRESS,  // En cours de réalisation
        IN_REVIEW,    // En attente de validation
        COMPLETED,    // Terminée et validée
        CANCELLED     // Annulée
    }

    /**
     * Constructeur pratique pour créer une tâche rapidement.
     * Les timestamps sont définis ici, pas par le client.
     */
    public Task(String title, String description, Integer priority) {
        this.title = title;
        this.description = description;
        this.priority = priority != null ? priority : 1;
        this.completed = false;
        this.status = Status.PENDING;
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }
}
```

---

## 3. Controller CRUD complet

Créez `src/main/java/com/taskflow/controller/TaskController.java` :

```java
package com.taskflow.controller;

import com.taskflow.model.Task;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;

/**
 * CONTROLLER CRUD POUR LES TÂCHES
 *
 * Ce controller gère toutes les opérations sur les tâches.
 * Pour l'instant, on stocke les données EN MÉMOIRE dans une List.
 * Au Module 02, on déplacera la logique dans un Service.
 * Au Module 03, on persistera en base de données avec JPA.
 *
 * ENDPOINTS DISPONIBLES :
 *   GET    /api/tasks           -> Toutes les tâches
 *   GET    /api/tasks/{id}      -> Une tâche par ID
 *   GET    /api/tasks/search    -> Recherche par mot-clé
 *   POST   /api/tasks           -> Créer une tâche
 *   PUT    /api/tasks/{id}      -> Remplacer une tâche
 *   PATCH  /api/tasks/{id}/done -> Marquer comme terminée
 *   DELETE /api/tasks/{id}      -> Supprimer une tâche
 */
@Slf4j
@RestController
@RequestMapping("/api/tasks")
public class TaskController {

    /**
     * BASE DE DONNÉES EN MÉMOIRE (provisoire)
     *
     * En production, cette liste sera remplacée par un vrai Repository JPA.
     * Pour l'instant, c'est suffisant pour apprendre les concepts HTTP.
     *
     * AtomicLong : compteur thread-safe pour générer les IDs
     * (garantit qu'on n'a pas deux tâches avec le même ID même si
     *  deux requêtes arrivent en même temps)
     */
    private final List<Task> tasks = new ArrayList<>();
    private final AtomicLong idGenerator = new AtomicLong(1);

    /**
     * Constructeur : initialise quelques tâches de démonstration
     * pour ne pas démarrer avec une liste vide
     */
    public TaskController() {
        tasks.add(new Task("Lire la documentation Spring Boot",
            "Commencer par le guide officiel", 3));
        tasks.get(0).setId(idGenerator.getAndIncrement()); // ID = 1

        tasks.add(new Task("Implémenter le CRUD",
            "Suivre le Module 01 du guide", 4));
        tasks.get(1).setId(idGenerator.getAndIncrement()); // ID = 2
    }

    // =========================================================
    //  READ — Récupérer des données (GET)
    // =========================================================

    /**
     * RÉCUPÉRER TOUTES LES TÂCHES
     *
     * GET /api/tasks
     * GET /api/tasks?completed=true    -> filtrer par statut
     * GET /api/tasks?status=IN_PROGRESS -> filtrer par statut détaillé
     *
     * @param completed filtre optionnel sur le champ completed
     * @param status    filtre optionnel sur le statut
     * @return 200 OK avec la liste des tâches (peut être vide [])
     */
    @GetMapping
    public ResponseEntity<List<Task>> getAllTasks(
            @RequestParam(required = false) Boolean completed,
            @RequestParam(required = false) Task.Status status) {

        log.info("GET /api/tasks - completed={}, status={}", completed, status);

        List<Task> result = tasks;

        // Appliquer les filtres si présents
        if (completed != null) {
            result = result.stream()
                .filter(t -> t.isCompleted() == completed)
                .collect(Collectors.toList());
        }

        if (status != null) {
            result = result.stream()
                .filter(t -> status.equals(t.getStatus()))
                .collect(Collectors.toList());
        }

        log.debug("{} tâche(s) retournée(s)", result.size());

        /*
         * ResponseEntity.ok(result) :
         * - Status HTTP : 200 OK
         * - Body : la liste en JSON
         * - Content-Type : application/json (automatique)
         *
         * On retourne TOUJOURS 200 même si la liste est vide.
         * 404 serait incorrect ici : la ressource "liste de tâches"
         * existe, elle est juste vide.
         */
        return ResponseEntity.ok(result);
    }

    /**
     * RÉCUPÉRER UNE TÂCHE PAR ID
     *
     * GET /api/tasks/1
     *
     * @param id l'identifiant de la tâche (extrait de l'URL)
     * @return 200 OK avec la tâche, ou 404 si elle n'existe pas
     */
    @GetMapping("/{id}")
    public ResponseEntity<Task> getTaskById(@PathVariable Long id) {
        log.info("GET /api/tasks/{}", id);

        /*
         * findTaskById() retourne un Optional<Task>.
         *
         * Optional est un container qui peut contenir
         * une valeur ou être vide. Il force à gérer
         * explicitement le cas "pas trouvé" et évite
         * les NullPointerException.
         */
        Optional<Task> taskOptional = findTaskById(id);

        if (taskOptional.isPresent()) {
            return ResponseEntity.ok(taskOptional.get());
        } else {
            log.warn("Tâche non trouvée : id={}", id);
            /*
             * ResponseEntity.notFound().build() :
             * - Status HTTP : 404 Not Found
             * - Body : vide (pas de contenu)
             *
             * On retourne 404 car la ressource /api/tasks/999
             * n'existe pas dans notre système.
             */
            return ResponseEntity.notFound().build();
        }

        // Version compacte avec Optional (même résultat) :
        // return findTaskById(id)
        //     .map(ResponseEntity::ok)
        //     .orElse(ResponseEntity.notFound().build());
    }

    /**
     * RECHERCHER DES TÂCHES PAR MOT-CLÉ
     *
     * GET /api/tasks/search?q=spring
     * -> Retourne les tâches dont le titre ou la description contient "spring"
     *
     * @param q le mot-clé de recherche (obligatoire)
     * @return 200 OK avec les tâches correspondantes
     */
    @GetMapping("/search")
    public ResponseEntity<List<Task>> searchTasks(@RequestParam String q) {
        log.info("GET /api/tasks/search?q={}", q);

        if (q == null || q.trim().isEmpty()) {
            return ResponseEntity.badRequest().build();
        }

        String keyword = q.toLowerCase().trim();

        List<Task> results = tasks.stream()
            .filter(task ->
                (task.getTitle() != null && task.getTitle().toLowerCase().contains(keyword))
                || (task.getDescription() != null && task.getDescription().toLowerCase().contains(keyword))
            )
            .collect(Collectors.toList());

        log.debug("Recherche '{}' : {} résultat(s)", q, results.size());
        return ResponseEntity.ok(results);
    }

    // =========================================================
    //  CREATE — Créer une ressource (POST)
    // =========================================================

    /**
     * CRÉER UNE NOUVELLE TÂCHE
     *
     * POST /api/tasks
     * Content-Type: application/json
     * Body:
     * {
     *   "title": "Ma nouvelle tâche",
     *   "description": "Description optionnelle",
     *   "priority": 3
     * }
     *
     * @param task les données de la tâche à créer (depuis le body JSON)
     * @return 201 Created avec la tâche créée (incluant son ID généré)
     */
    @PostMapping
    public ResponseEntity<Task> createTask(@RequestBody Task task) {
        log.info("POST /api/tasks - title='{}'", task.getTitle());

        // Validation basique (la validation avancée arrive au Module 04)
        if (task.getTitle() == null || task.getTitle().trim().isEmpty()) {
            log.warn("Tentative de création d'une tâche sans titre");
            return ResponseEntity.badRequest().build();
        }

        // Initialisation des champs gérés côté serveur
        task.setId(idGenerator.getAndIncrement());
        task.setCompleted(false);
        task.setStatus(Task.Status.PENDING);
        task.setCreatedAt(LocalDateTime.now());
        task.setUpdatedAt(LocalDateTime.now());

        // Priorité par défaut si non fournie
        if (task.getPriority() == null) {
            task.setPriority(1);
        }

        tasks.add(task);
        log.info("Tâche créée avec succès : id={}, title='{}'", task.getId(), task.getTitle());

        /*
         * ResponseEntity.status(HttpStatus.CREATED).body(task) :
         * - Status HTTP : 201 Created (PAS 200 OK !)
         * - Body : la tâche créée avec son ID
         *
         * La convention REST dit d'utiliser 201 pour une création.
         * Cela permet au client de distinguer "j'ai reçu des données"
         * (200) de "j'ai créé une nouvelle ressource" (201).
         *
         * Bonne pratique : on pourrait aussi ajouter le header "Location"
         * avec l'URL de la ressource créée :
         * .header("Location", "/api/tasks/" + task.getId())
         */
        return ResponseEntity.status(HttpStatus.CREATED).body(task);
    }

    // =========================================================
    //  UPDATE — Modifier une ressource (PUT / PATCH)
    // =========================================================

    /**
     * REMPLACER UNE TÂCHE (Mise à jour complète)
     *
     * PUT /api/tasks/1
     * Body: l'objet tâche COMPLET avec tous les champs
     *
     * PUT = "Remplace entièrement la ressource par le body fourni"
     * Tous les champs doivent être fournis, sinon ils deviennent null.
     *
     * @param id         ID de la tâche à modifier
     * @param updatedTask nouvelles données complètes
     * @return 200 OK avec la tâche mise à jour, ou 404 si inexistante
     */
    @PutMapping("/{id}")
    public ResponseEntity<Task> updateTask(
            @PathVariable Long id,
            @RequestBody Task updatedTask) {

        log.info("PUT /api/tasks/{}", id);

        Optional<Task> existingOpt = findTaskById(id);

        if (existingOpt.isEmpty()) {
            return ResponseEntity.notFound().build();
        }

        Task existing = existingOpt.get();

        // Mise à jour de TOUS les champs modifiables
        existing.setTitle(updatedTask.getTitle());
        existing.setDescription(updatedTask.getDescription());
        existing.setCompleted(updatedTask.isCompleted());
        existing.setPriority(updatedTask.getPriority());
        existing.setStatus(updatedTask.getStatus());
        existing.setUpdatedAt(LocalDateTime.now());

        // On ne touche PAS à : id, createdAt

        log.info("Tâche mise à jour : id={}", id);
        return ResponseEntity.ok(existing);
    }

    /**
     * MISE À JOUR PARTIELLE — Marquer comme terminée
     *
     * PATCH /api/tasks/1/done
     *
     * C'est un cas concret de PATCH : on ne modifie QUE les champs liés
     * à la complétion de la tâche, pas tout l'objet.
     *
     * @param id ID de la tâche
     * @return 200 OK avec la tâche mise à jour
     */
    @PatchMapping("/{id}/done")
    public ResponseEntity<Task> markAsDone(@PathVariable Long id) {
        log.info("PATCH /api/tasks/{}/done", id);

        return findTaskById(id)
            .map(task -> {
                task.setCompleted(true);
                task.setStatus(Task.Status.COMPLETED);
                task.setUpdatedAt(LocalDateTime.now());
                log.info("Tâche marquée comme terminée : id={}", id);
                return ResponseEntity.ok(task);
            })
            .orElseGet(() -> {
                log.warn("Tâche non trouvée pour complétion : id={}", id);
                return ResponseEntity.notFound().build();
            });
    }

    /**
     * CHANGEMENT DE STATUT
     *
     * PATCH /api/tasks/1/status
     * Body: { "status": "IN_PROGRESS" }
     *
     * @param id     ID de la tâche
     * @param body   Map contenant le nouveau statut
     */
    @PatchMapping("/{id}/status")
    public ResponseEntity<Task> updateStatus(
            @PathVariable Long id,
            @RequestBody java.util.Map<String, String> body) {

        log.info("PATCH /api/tasks/{}/status", id);

        String statusStr = body.get("status");
        if (statusStr == null) {
            return ResponseEntity.badRequest().build();
        }

        Task.Status newStatus;
        try {
            newStatus = Task.Status.valueOf(statusStr.toUpperCase());
        } catch (IllegalArgumentException e) {
            log.warn("Statut invalide reçu : {}", statusStr);
            return ResponseEntity.badRequest().build();
        }

        return findTaskById(id)
            .map(task -> {
                task.setStatus(newStatus);
                task.setUpdatedAt(LocalDateTime.now());
                return ResponseEntity.ok(task);
            })
            .orElse(ResponseEntity.notFound().build());
    }

    // =========================================================
    //  DELETE — Supprimer une ressource (DELETE)
    // =========================================================

    /**
     * SUPPRIMER UNE TÂCHE
     *
     * DELETE /api/tasks/1
     *
     * @param id ID de la tâche à supprimer
     * @return 204 No Content si supprimée, 404 si introuvable
     */
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteTask(@PathVariable Long id) {
        log.info("DELETE /api/tasks/{}", id);

        Optional<Task> taskOpt = findTaskById(id);

        if (taskOpt.isEmpty()) {
            return ResponseEntity.notFound().build();
        }

        tasks.remove(taskOpt.get());
        log.info("Tâche supprimée : id={}", id);

        /*
         * ResponseEntity.noContent().build() :
         * - Status HTTP : 204 No Content
         * - Body : VIDE
         *
         * Convention REST pour DELETE :
         * On retourne 204 (pas 200) car on n'a rien à retourner.
         * La ressource a été supprimée, il n'y a plus rien à montrer.
         *
         * ResponseEntity<Void> : le type Void indique explicitement
         * qu'il n'y a pas de body dans la réponse.
         */
        return ResponseEntity.noContent().build();
    }

    // =========================================================
    //  MÉTHODE UTILITAIRE PRIVÉE
    // =========================================================

    /**
     * Recherche une tâche par son ID dans la liste.
     *
     * @param id l'ID à chercher
     * @return Optional<Task> : contient la tâche si trouvée, vide sinon
     *
     * On retourne Optional<Task> plutôt que Task ou null pour :
     * 1. Rendre explicite que la tâche peut ne pas exister
     * 2. Éviter les NullPointerException
     * 3. Utiliser l'API fonctionnelle (map, orElse, etc.)
     */
    private Optional<Task> findTaskById(Long id) {
        return tasks.stream()
            .filter(task -> task.getId() != null && task.getId().equals(id))
            .findFirst();
    }
}
```

---

## 4. ResponseEntity — contrôler les réponses HTTP

`ResponseEntity<T>` est la classe Spring qui vous donne un contrôle total sur la réponse HTTP : status code, headers, et body.

### Tableau de référence

```java
// [OK] 200 OK — Succès avec données
ResponseEntity.ok(data)
// Équivalent à :
new ResponseEntity<>(data, HttpStatus.OK)

// [OK] 201 Created — Ressource créée
ResponseEntity.status(HttpStatus.CREATED).body(newResource)

// [OK] 204 No Content — Succès sans données à retourner (DELETE)
ResponseEntity.noContent().build()

// [X] 400 Bad Request — Données invalides
ResponseEntity.badRequest().body(errorMessage)
ResponseEntity.badRequest().build()  // sans body

// [X] 404 Not Found — Ressource introuvable
ResponseEntity.notFound().build()

// [X] 409 Conflict — Conflit (ex: email déjà utilisé)
ResponseEntity.status(HttpStatus.CONFLICT).body(errorMessage)

// [X] 500 Internal Server Error — Erreur serveur
ResponseEntity.internalServerError().body(errorMessage)
```

### Ajouter des headers personnalisés

```java
@PostMapping
public ResponseEntity<Task> createTask(@RequestBody Task task) {
    Task created = taskService.create(task);

    // Ajouter le header Location avec l'URL de la ressource créée
    return ResponseEntity
        .status(HttpStatus.CREATED)
        .header("Location", "/api/tasks/" + created.getId())
        .header("X-Task-Id", created.getId().toString())
        .body(created);
}
```

### Codes HTTP importants à connaître

```
2xx — SUCCÈS
  200 OK              -> Requête réussie avec données
  201 Created         -> Ressource créée
  204 No Content      -> Succès sans données

4xx — ERREUR DU CLIENT
  400 Bad Request     -> Données invalides (format, validation)
  401 Unauthorized    -> Non authentifié (pas de token)
  403 Forbidden       -> Authentifié mais pas autorisé
  404 Not Found       -> Ressource inexistante
  409 Conflict        -> Conflit (duplicate, état incorrect)
  422 Unprocessable   -> Données comprises mais invalides sémantiquement

5xx — ERREUR SERVEUR
  500 Internal Error  -> Bug non géré
  503 Unavailable     -> Service momentanément indisponible
```

---

## 5. Gestion des erreurs

### Problème actuel

Si quelqu'un appelle `GET /api/tasks/abc` (avec un texte au lieu d'un Long), Spring génère automatiquement une réponse d'erreur peu claire. Il faut centraliser et personnaliser la gestion des erreurs.

### Exception personnalisée

Créez `src/main/java/com/taskflow/exception/TaskNotFoundException.java` :

```java
package com.taskflow.exception;

/**
 * Exception lancée quand une tâche n'est pas trouvée.
 *
 * On extends RuntimeException (pas checked Exception) pour :
 * - Ne pas forcer les try/catch partout
 * - Spring les intercepte automatiquement avec @ExceptionHandler
 *
 * Le message d'erreur est clair et inclut l'ID pour faciliter
 * le débogage.
 */
public class TaskNotFoundException extends RuntimeException {

    private final Long taskId;

    public TaskNotFoundException(Long id) {
        super("Tâche non trouvée avec l'ID : " + id);
        this.taskId = id;
    }

    public Long getTaskId() {
        return taskId;
    }
}
```

### Classe de réponse d'erreur

Créez `src/main/java/com/taskflow/exception/ErrorResponse.java` :

```java
package com.taskflow.exception;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * Structure STANDARDISÉE pour toutes les réponses d'erreur.
 *
 * Pourquoi standardiser ?
 * Les clients de l'API (apps mobiles, frontend React...) ont besoin
 * de traiter les erreurs de manière cohérente. Si chaque endpoint
 * retourne une structure différente, le client doit gérer des
 * dizaines de formats d'erreur différents.
 *
 * Format JSON retourné :
 * {
 *   "status": 404,
 *   "error": "Not Found",
 *   "message": "Tâche non trouvée avec l'ID : 99",
 *   "path": "/api/tasks/99",
 *   "timestamp": "2026-01-26T10:30:00"
 * }
 */
@Data
@AllArgsConstructor
public class ErrorResponse {

    /** Code HTTP numérique (400, 404, 500, etc.) */
    private int status;

    /** Libellé du code HTTP ("Bad Request", "Not Found", etc.) */
    private String error;

    /** Message d'erreur lisible par un humain */
    private String message;

    /** URL qui a déclenché l'erreur */
    private String path;

    /** Moment où l'erreur s'est produite */
    private LocalDateTime timestamp;

    /** Constructeur rapide sans timestamp (on le met automatiquement) */
    public ErrorResponse(int status, String error, String message, String path) {
        this.status = status;
        this.error = error;
        this.message = message;
        this.path = path;
        this.timestamp = LocalDateTime.now();
    }
}
```

### Gestionnaire global d'exceptions

Créez `src/main/java/com/taskflow/exception/GlobalExceptionHandler.java` :

```java
package com.taskflow.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;

/**
 * GESTIONNAIRE GLOBAL D'EXCEPTIONS
 *
 * @RestControllerAdvice :
 * - Combine @ControllerAdvice (s'applique à tous les controllers)
 *   et @ResponseBody (les retours sont sérialisés en JSON)
 * - Intercepte les exceptions non gérées dans N'IMPORTE QUEL controller
 * - Centralise toute la gestion d'erreurs en un seul endroit
 *
 * Sans cette classe : Spring retourne des pages d'erreur HTML génériques
 * Avec cette classe : On contrôle exactement ce que le client reçoit
 *
 * Avantages :
 * [OK] Code des controllers épuré (pas de try/catch partout)
 * [OK] Réponses d'erreur cohérentes
 * [OK] Facile à modifier (un seul endroit)
 * [OK] Facile à tester
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * Gère TaskNotFoundException -> 404 Not Found
     *
     * @ExceptionHandler(TaskNotFoundException.class) :
     * Cette méthode est appelée automatiquement par Spring chaque fois
     * qu'un controller (ou un service) lance une TaskNotFoundException.
     * Le controller n'a pas besoin de catch cette exception.
     *
     * @param ex      l'exception capturée
     * @param request la requête qui a déclenché l'erreur (pour récupérer l'URL)
     */
    @ExceptionHandler(TaskNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleTaskNotFound(
            TaskNotFoundException ex,
            WebRequest request) {

        log.warn("Tâche non trouvée : {}", ex.getMessage());

        ErrorResponse error = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            "Not Found",
            ex.getMessage(),
            extractPath(request)
        );

        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }

    /**
     * Gère IllegalArgumentException -> 400 Bad Request
     *
     * Lancée quand les données sont invalides côté logique métier
     * (différent de la validation Bean Validation du Module 04).
     */
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleIllegalArgument(
            IllegalArgumentException ex,
            WebRequest request) {

        log.warn("Argument invalide : {}", ex.getMessage());

        ErrorResponse error = new ErrorResponse(
            HttpStatus.BAD_REQUEST.value(),
            "Bad Request",
            ex.getMessage(),
            extractPath(request)
        );

        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }

    /**
     * Fallback — Gère toutes les autres exceptions -> 500 Internal Server Error
     *
     * C'est le "filet de sécurité" : si une exception non gérée échappe
     * à tous les autres handlers, celle-ci la capture.
     *
     * On ne retourne PAS les détails de l'exception au client
     * (risque de sécurité en production).
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleAllExceptions(
            Exception ex,
            WebRequest request) {

        // On log le stack trace complet pour nous (développeurs)
        log.error("Exception non gérée sur {}", extractPath(request), ex);

        // Mais on retourne un message générique au client
        ErrorResponse error = new ErrorResponse(
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            "Internal Server Error",
            "Une erreur interne s'est produite. Veuillez réessayer plus tard.",
            extractPath(request)
        );

        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    /**
     * Extrait le chemin URL de la requête depuis WebRequest.
     * Nettoie le préfixe "uri=" ajouté par Spring.
     */
    private String extractPath(WebRequest request) {
        return request.getDescription(false).replace("uri=", "");
    }
}
```

---

## 6. Le pattern DTO

### Pourquoi les DTOs ?

**DTO** = Data Transfer Object (Objet de Transfert de Données).

Problème sans DTO :
```java
// Le client envoie cet objet Task pour créer une tâche
// Mais il peut aussi "envoyer" un ID et des timestamps !
// -> Risque de sécurité : le client pourrait falsifier l'ID

@PostMapping
public Task createTask(@RequestBody Task task) {
    task.setId(99L); // <- le client peut forcer l'ID !
    ...
}
```

Solution avec DTO :

```java
// Le client ne peut envoyer QUE les champs autorisés
@PostMapping
public Task createTask(@RequestBody CreateTaskDTO dto) {
    // dto.getId() n'existe pas ! L'ID est généré côté serveur.
}
```

### DTOs pour TaskFlow

Créez `src/main/java/com/taskflow/dto/CreateTaskDTO.java` :

```java
package com.taskflow.dto;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;

/**
 * DTO pour la CRÉATION d'une tâche.
 *
 * Contient SEULEMENT les champs que le client peut fournir.
 * Les champs gérés par le serveur sont absents :
 * - id         -> généré par le serveur
 * - createdAt  -> défini par le serveur
 * - updatedAt  -> défini par le serveur
 * - completed  -> toujours false à la création
 * - status     -> toujours PENDING à la création
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateTaskDTO {

    /** Titre de la tâche — sera validé au Module 04 */
    private String title;

    /** Description optionnelle */
    private String description;

    /** Priorité 1-5 */
    private Integer priority;

    /** Email de la personne assignée */
    private String assignedTo;
}
```

Créez `src/main/java/com/taskflow/dto/UpdateTaskDTO.java` :

```java
package com.taskflow.dto;

import com.taskflow.model.Task;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * DTO pour la MISE À JOUR partielle d'une tâche.
 *
 * Tous les champs sont OPTIONNELS (peuvent être null).
 * On ne met à jour que les champs fournis (non null).
 *
 * Différence avec CreateTaskDTO :
 * - Ici on peut modifier completed et status
 * - Les champs null signifient "ne pas modifier ce champ"
 */
@Data
@NoArgsConstructor
public class UpdateTaskDTO {

    /** Nouveau titre (null = ne pas modifier) */
    private String title;

    /** Nouvelle description (null = ne pas modifier) */
    private String description;

    /** Nouvelle priorité (null = ne pas modifier) */
    private Integer priority;

    /** Nouveau statut completed (null = ne pas modifier) */
    private Boolean completed;

    /** Nouveau statut workflow (null = ne pas modifier) */
    private Task.Status status;

    /** Nouvelle assignation (null = ne pas modifier) */
    private String assignedTo;
}
```

---

## 7. Exercices pratiques

### Exercice 1 : Compléter le CRUD avec les DTOs

Modifiez le `TaskController` pour utiliser `CreateTaskDTO` dans le POST et `UpdateTaskDTO` dans le PATCH.

**Résultat attendu :**
```bash
# Création avec DTO
curl -X POST http://localhost:8080/api/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"Nouvelle tâche","priority":3}'

# Réponse 201
{ "id": 3, "title": "Nouvelle tâche", "priority": 3, "completed": false, "status": "PENDING", ... }
```

### Exercice 2 : Ajouter un endpoint de statistiques

`GET /api/tasks/stats` -> retourne :
```json
{
  "total": 5,
  "completed": 2,
  "pending": 3,
  "byPriority": { "1": 1, "2": 0, "3": 2, "4": 1, "5": 1 }
}
```

### Exercice 3 : Pagination simple

`GET /api/tasks?page=0&size=10` -> retourne seulement les 10 premières tâches.

**Astuce :** Utilisez `tasks.stream().skip(page * size).limit(size).toList()`

---

## [OK] Récapitulatif du Module 01

Vous avez appris :

- Les 5 opérations CRUD et leurs méthodes HTTP (GET, POST, PUT, PATCH, DELETE)
- La différence entre PUT (remplacement total) et PATCH (mise à jour partielle)
- `@RequestBody` pour recevoir des données JSON dans la requête
- `ResponseEntity` pour contrôler le code HTTP de la réponse
- Les codes HTTP importants (200, 201, 204, 400, 404, etc.)
- La gestion centralisée des erreurs avec `@RestControllerAdvice`
- Le pattern DTO pour sécuriser et clarifier les entrées/sorties API
- `Optional<T>` pour éviter les NullPointerException

---

**-> Prochain module : `02_services_architecture.md` — Services et injection de dépendances**

# Module 02 : Services et Injection de Dépendances

## Objectifs du module
- Comprendre pourquoi séparer la logique métier des contrôleurs
- Maîtriser l'injection de dépendances avec Spring
- Créer une couche Service complète pour TaskFlow
- Comprendre le cycle de vie des beans Spring

**Durée estimée :** 4-5 heures  
**Prérequis :** Module 01 complété

---

## 2.1 Pourquoi une couche Service ?

Dans le Module 01, notre `TaskController` faisait **tout** : gérer les requêtes HTTP, contenir la logique métier, stocker les données. C'est un anti-pattern appelé **Fat Controller**.

### Le problème du Fat Controller

```java
// [X] Anti-pattern : logique métier dans le contrôleur
@RestController
public class TaskController {
    private List<Task> tasks = new ArrayList<>();

    @PostMapping("/tasks")
    public ResponseEntity<Task> createTask(@RequestBody CreateTaskDTO dto) {
        // Validation manuelle (devrait être dans le service)
        if (dto.getTitle() == null || dto.getTitle().isBlank()) {
            throw new IllegalArgumentException("Le titre est requis");
        }
        
        // Logique métier (devrait être dans le service)
        Task task = new Task();
        task.setId(UUID.randomUUID().toString());
        task.setTitle(dto.getTitle().trim());
        task.setCreatedAt(LocalDateTime.now());
        
        // Persistance (devrait être dans le repository)
        tasks.add(task);
        
        return ResponseEntity.status(201).body(task);
    }
}
```

**Problèmes :**
- Impossible de tester la logique métier sans lancer le serveur HTTP
- Si on change de base de données, on modifie le contrôleur
- Code dupliqué si plusieurs contrôleurs ont besoin des mêmes opérations
- Violation du **Single Responsibility Principle**

### La solution : Architecture en couches

```
┌─────────────────────────────────────────┐
│           CLIENT (Postman, Browser)      │
└─────────────────┬───────────────────────┘
                  │ HTTP Request/Response
┌─────────────────[BLACK_DOWN-POINTING_TRIANGLE]───────────────────────┐
│         CONTROLLER (couche web)          │
│  • Reçoit les requêtes HTTP              │
│  • Valide le format des données          │
│  • Délègue au Service                    │
│  • Retourne la réponse HTTP              │
└─────────────────┬───────────────────────┘
                  │ Appel de méthodes Java
┌─────────────────[BLACK_DOWN-POINTING_TRIANGLE]───────────────────────┐
│          SERVICE (couche métier)         │
│  • Contient la logique métier            │
│  • Orchestre les opérations              │
│  • Gère les transactions                 │
│  • Lance les exceptions métier           │
└─────────────────┬───────────────────────┘
                  │ Appel de méthodes Java
┌─────────────────[BLACK_DOWN-POINTING_TRIANGLE]───────────────────────┐
│        REPOSITORY (couche données)       │
│  • Accès à la base de données            │
│  • CRUD operations                       │
│  • Requêtes personnalisées               │
└─────────────────────────────────────────┘
```

---

## 2.2 L'Injection de Dépendances (DI)

### Qu'est-ce que c'est ?

L'injection de dépendances est un pattern où les objets **reçoivent** leurs dépendances plutôt que de les **créer** eux-mêmes.

```java
// [X] Sans injection de dépendances
public class TaskController {
    // Le contrôleur crée lui-même son service
    private TaskService taskService = new TaskService(); // PROBLÈME !
    
    // Si TaskService change de constructeur, on modifie TaskController
    // Impossible de remplacer TaskService par un mock dans les tests
}

// [OK] Avec injection de dépendances
@RestController
public class TaskController {
    private final TaskService taskService;
    
    // Spring fournit le TaskService
    @Autowired // Optionnel si un seul constructeur
    public TaskController(TaskService taskService) {
        this.taskService = taskService;
    }
}
```

### Le conteneur IoC (Inversion of Control)

Spring maintient un **conteneur** qui :
1. Scanne les classes annotées (@Component, @Service, @Repository, @Controller...)
2. Les instancie dans le bon ordre
3. Injecte les dépendances automatiquement

```java
// Spring crée ces objets et gère leurs dépendances
@Repository
public class InMemoryTaskRepository { ... }      // 1. Créé en premier

@Service  
public class TaskService {
    // Spring injecte automatiquement le repository
    public TaskService(InMemoryTaskRepository repo) { ... }  // 2. Créé après
}

@RestController
public class TaskController {
    // Spring injecte automatiquement le service
    public TaskController(TaskService service) { ... }  // 3. Créé en dernier
}
```

### Les annotations de stéréotype

```java
@Component      // Bean générique
@Service        // Couche service (même chose que @Component, plus sémantique)
@Repository     // Couche données (ajoute la gestion des exceptions JPA)
@Controller     // Contrôleur web MVC
@RestController // Contrôleur REST (@Controller + @ResponseBody)
```

---

## 2.3 Les modes d'injection

### 1. Injection par constructeur (RECOMMANDÉE)

```java
@Service
public class TaskService {
    private final TaskRepository taskRepository;
    private final NotificationService notificationService;
    
    // @Autowired optionnel si un seul constructeur
    public TaskService(TaskRepository taskRepository,
                       NotificationService notificationService) {
        this.taskRepository = taskRepository;
        this.notificationService = notificationService;
    }
}
```

**Avantages :**
- Les dépendances sont **immuables** (final)
- Facilite les **tests** (on peut passer des mocks dans le constructeur)
- Détecte les dépendances circulaires au **démarrage**
- Communique clairement les **dépendances requises**

### 2. Injection par champ (déconseillée)

```java
@Service
public class TaskService {
    @Autowired
    private TaskRepository taskRepository; // [X] Pas de final, difficile à tester
}
```

### 3. Injection par setter (cas spéciaux)

```java
@Service
public class TaskService {
    private NotificationService notificationService;
    
    // Pour les dépendances optionnelles
    @Autowired(required = false)
    public void setNotificationService(NotificationService service) {
        this.notificationService = service;
    }
}
```

---

## 2.4 Refactoring de TaskFlow

### Étape 1 : Créer l'interface TaskRepository

```java
// src/main/java/com/taskflow/repository/TaskRepository.java
package com.taskflow.repository;

import com.taskflow.model.Task;
import java.util.List;
import java.util.Optional;

/**
 * Interface définissant les opérations de persistance pour les tâches.
 * 
 * Pourquoi une interface ?
 * - Permet d'avoir plusieurs implémentations (in-memory, PostgreSQL, Redis...)
 * - Facilite les tests (on peut créer une implémentation mock)
 * - Respecte le principe d'inversion de dépendances (DIP de SOLID)
 */
public interface TaskRepository {
    
    List<Task> findAll();
    
    Optional<Task> findById(String id);
    
    List<Task> findByCompleted(boolean completed);
    
    List<Task> findByPriority(String priority);
    
    List<Task> searchByTitle(String keyword);
    
    Task save(Task task);
    
    void deleteById(String id);
    
    boolean existsById(String id);
    
    long count();
}
```

### Étape 2 : Implémentation In-Memory

```java
// src/main/java/com/taskflow/repository/InMemoryTaskRepository.java
package com.taskflow.repository;

import com.taskflow.model.Task;
import org.springframework.stereotype.Repository;

import java.util.*;
import java.util.stream.Collectors;

/**
 * Implémentation en mémoire du repository.
 * 
 * @Repository indique à Spring que c'est un composant de la couche données.
 * Les données sont perdues au redémarrage de l'application.
 * Dans le Module 03, on remplacera cette classe par Spring Data JPA.
 */
@Repository
public class InMemoryTaskRepository implements TaskRepository {
    
    // ConcurrentHashMap pour la thread-safety (plusieurs requêtes simultanées)
    private final Map<String, Task> storage = new LinkedHashMap<>();
    
    @Override
    public List<Task> findAll() {
        // Retourne une copie pour éviter les modifications externes
        return new ArrayList<>(storage.values());
    }
    
    @Override
    public Optional<Task> findById(String id) {
        // Optional évite les NullPointerException
        return Optional.ofNullable(storage.get(id));
    }
    
    @Override
    public List<Task> findByCompleted(boolean completed) {
        return storage.values().stream()
                .filter(task -> task.isCompleted() == completed)
                .collect(Collectors.toList());
    }
    
    @Override
    public List<Task> findByPriority(String priority) {
        return storage.values().stream()
                .filter(task -> priority.equalsIgnoreCase(
                    task.getPriority() != null ? task.getPriority().toString() : ""))
                .collect(Collectors.toList());
    }
    
    @Override
    public List<Task> searchByTitle(String keyword) {
        String lowerKeyword = keyword.toLowerCase();
        return storage.values().stream()
                .filter(task -> task.getTitle().toLowerCase().contains(lowerKeyword))
                .collect(Collectors.toList());
    }
    
    @Override
    public Task save(Task task) {
        storage.put(task.getId(), task);
        return task;
    }
    
    @Override
    public void deleteById(String id) {
        storage.remove(id);
    }
    
    @Override
    public boolean existsById(String id) {
        return storage.containsKey(id);
    }
    
    @Override
    public long count() {
        return storage.size();
    }
}
```

### Étape 3 : Créer le TaskService

```java
// src/main/java/com/taskflow/service/TaskService.java
package com.taskflow.service;

import com.taskflow.dto.CreateTaskDTO;
import com.taskflow.dto.UpdateTaskDTO;
import com.taskflow.exception.TaskNotFoundException;
import com.taskflow.model.Task;
import com.taskflow.model.Task.Priority;
import com.taskflow.model.Task.Status;
import com.taskflow.repository.TaskRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;

/**
 * Service gérant la logique métier des tâches.
 * 
 * @Service : Marque cette classe comme un bean de la couche service.
 *            Spring la détectera automatiquement lors du component scanning.
 * 
 * @Slf4j : Lombok crée un logger SLF4J (log.info, log.error, etc.)
 *
 * Ce service :
 * - Orchestre les opérations CRUD
 * - Applique les règles métier (ex: une tâche complétée ne peut pas être réouverte)
 * - Transforme les DTOs en entités et vice-versa
 * - Délègue la persistance au TaskRepository
 */
@Slf4j
@Service
public class TaskService {
    
    // Injection par constructeur (pattern recommandé)
    private final TaskRepository taskRepository;
    
    public TaskService(TaskRepository taskRepository) {
        this.taskRepository = taskRepository;
        log.info("TaskService initialisé avec {}", taskRepository.getClass().getSimpleName());
    }
    
    // ==================== LECTURE ====================
    
    /**
     * Récupère toutes les tâches avec filtres optionnels.
     * 
     * @param completed  Filtrer par statut de complétion (null = pas de filtre)
     * @param priority   Filtrer par priorité (null = pas de filtre)
     * @return Liste des tâches correspondant aux critères
     */
    public List<Task> getAllTasks(Boolean completed, String priority) {
        log.debug("Récupération des tâches - completed: {}, priority: {}", completed, priority);
        
        List<Task> tasks;
        
        if (completed != null) {
            tasks = taskRepository.findByCompleted(completed);
        } else if (priority != null) {
            tasks = taskRepository.findByPriority(priority);
        } else {
            tasks = taskRepository.findAll();
        }
        
        log.info("Retour de {} tâche(s)", tasks.size());
        return tasks;
    }
    
    /**
     * Récupère une tâche par son ID.
     * 
     * @throws TaskNotFoundException si aucune tâche trouvée avec cet ID
     */
    public Task getTaskById(String id) {
        log.debug("Recherche de la tâche avec l'ID: {}", id);
        
        return taskRepository.findById(id)
                .orElseThrow(() -> {
                    log.warn("Tâche introuvable avec l'ID: {}", id);
                    return new TaskNotFoundException("Tâche introuvable avec l'ID: " + id);
                });
    }
    
    /**
     * Recherche des tâches par mot-clé dans le titre.
     */
    public List<Task> searchTasks(String keyword) {
        log.debug("Recherche de tâches avec le mot-clé: '{}'", keyword);
        
        if (keyword == null || keyword.isBlank()) {
            return taskRepository.findAll();
        }
        
        List<Task> results = taskRepository.searchByTitle(keyword.trim());
        log.info("Recherche '{}' : {} résultat(s)", keyword, results.size());
        return results;
    }
    
    // ==================== CRÉATION ====================
    
    /**
     * Crée une nouvelle tâche à partir d'un DTO.
     * 
     * @param dto Les données de création (validées par le contrôleur)
     * @return La tâche créée avec son ID et ses timestamps
     */
    public Task createTask(CreateTaskDTO dto) {
        log.info("Création d'une nouvelle tâche: '{}'", dto.getTitle());
        
        // Règle métier : le titre est nettoyé des espaces superflus
        Task task = new Task();
        task.setId(UUID.randomUUID().toString());
        task.setTitle(dto.getTitle().trim());
        task.setDescription(dto.getDescription());
        task.setCompleted(false); // Une nouvelle tâche est toujours non complétée
        task.setStatus(Status.TODO); // Statut initial
        task.setCreatedAt(LocalDateTime.now());
        task.setUpdatedAt(LocalDateTime.now());
        
        // Priorité par défaut si non spécifiée
        if (dto.getPriority() != null) {
            task.setPriority(dto.getPriority());
        } else {
            task.setPriority(Priority.MEDIUM);
        }
        
        task.setAssignedTo(dto.getAssignedTo());
        
        Task savedTask = taskRepository.save(task);
        log.info("Tâche créée avec l'ID: {}", savedTask.getId());
        
        return savedTask;
    }
    
    // ==================== MISE À JOUR ====================
    
    /**
     * Met à jour complètement une tâche (remplacement total).
     * Les champs non fournis prennent leur valeur par défaut.
     * 
     * @throws TaskNotFoundException si la tâche n'existe pas
     */
    public Task updateTask(String id, UpdateTaskDTO dto) {
        log.info("Mise à jour complète de la tâche: {}", id);
        
        // Vérifie que la tâche existe
        Task existingTask = getTaskById(id);
        
        // Remplacement complet : tous les champs sont mis à jour
        existingTask.setTitle(dto.getTitle() != null ? dto.getTitle().trim() : existingTask.getTitle());
        existingTask.setDescription(dto.getDescription());
        existingTask.setCompleted(dto.isCompleted());
        existingTask.setPriority(dto.getPriority());
        existingTask.setStatus(dto.getStatus());
        existingTask.setAssignedTo(dto.getAssignedTo());
        existingTask.setUpdatedAt(LocalDateTime.now());
        
        return taskRepository.save(existingTask);
    }
    
    /**
     * Met à jour partiellement une tâche (PATCH).
     * Seuls les champs non-null du DTO sont mis à jour.
     * 
     * @throws TaskNotFoundException si la tâche n'existe pas
     */
    public Task patchTask(String id, UpdateTaskDTO dto) {
        log.info("Mise à jour partielle de la tâche: {}", id);
        
        Task existingTask = getTaskById(id);
        
        // PATCH : on ne modifie que les champs fournis (non-null)
        if (dto.getTitle() != null) {
            existingTask.setTitle(dto.getTitle().trim());
        }
        if (dto.getDescription() != null) {
            existingTask.setDescription(dto.getDescription());
        }
        if (dto.getPriority() != null) {
            existingTask.setPriority(dto.getPriority());
        }
        if (dto.getStatus() != null) {
            // Règle métier : la mise à jour du statut gère aussi le champ completed
            existingTask.setStatus(dto.getStatus());
            if (dto.getStatus() == Status.DONE) {
                existingTask.setCompleted(true);
            }
        }
        if (dto.getAssignedTo() != null) {
            existingTask.setAssignedTo(dto.getAssignedTo());
        }
        
        existingTask.setUpdatedAt(LocalDateTime.now());
        
        return taskRepository.save(existingTask);
    }
    
    /**
     * Marque une tâche comme terminée.
     * 
     * Règle métier : une tâche déjà terminée ne peut pas être complétée à nouveau.
     * 
     * @throws TaskNotFoundException si la tâche n'existe pas
     * @throws IllegalStateException si la tâche est déjà terminée
     */
    public Task markTaskAsDone(String id) {
        log.info("Marquage de la tâche {} comme terminée", id);
        
        Task task = getTaskById(id);
        
        // Règle métier : vérification du statut
        if (task.isCompleted()) {
            throw new IllegalStateException("La tâche '" + task.getTitle() + "' est déjà terminée");
        }
        
        task.setCompleted(true);
        task.setStatus(Status.DONE);
        task.setUpdatedAt(LocalDateTime.now());
        
        log.info("Tâche '{}' marquée comme terminée", task.getTitle());
        return taskRepository.save(task);
    }
    
    /**
     * Change le statut d'une tâche.
     * 
     * @throws TaskNotFoundException si la tâche n'existe pas
     * @throws IllegalArgumentException si le statut est invalide
     */
    public Task updateTaskStatus(String id, String statusStr) {
        log.info("Changement de statut de la tâche {} vers {}", id, statusStr);
        
        Task task = getTaskById(id);
        
        Status newStatus;
        try {
            newStatus = Status.valueOf(statusStr.toUpperCase());
        } catch (IllegalArgumentException e) {
            throw new IllegalArgumentException(
                "Statut invalide: '" + statusStr + "'. Valeurs acceptées: TODO, IN_PROGRESS, DONE, CANCELLED"
            );
        }
        
        task.setStatus(newStatus);
        task.setCompleted(newStatus == Status.DONE);
        task.setUpdatedAt(LocalDateTime.now());
        
        return taskRepository.save(task);
    }
    
    // ==================== SUPPRESSION ====================
    
    /**
     * Supprime une tâche par son ID.
     * 
     * @throws TaskNotFoundException si la tâche n'existe pas
     */
    public void deleteTask(String id) {
        log.info("Suppression de la tâche: {}", id);
        
        // Vérifie l'existence avant suppression (pour retourner une 404 cohérente)
        if (!taskRepository.existsById(id)) {
            throw new TaskNotFoundException("Tâche introuvable avec l'ID: " + id);
        }
        
        taskRepository.deleteById(id);
        log.info("Tâche {} supprimée avec succès", id);
    }
    
    // ==================== STATISTIQUES ====================
    
    /**
     * Retourne des statistiques sur les tâches.
     */
    public TaskStats getStats() {
        List<Task> allTasks = taskRepository.findAll();
        
        long total = allTasks.size();
        long completed = allTasks.stream().filter(Task::isCompleted).count();
        long inProgress = allTasks.stream()
                .filter(t -> t.getStatus() == Status.IN_PROGRESS).count();
        long todo = allTasks.stream()
                .filter(t -> t.getStatus() == Status.TODO).count();
        
        return new TaskStats(total, completed, inProgress, todo);
    }
    
    /**
     * Classe interne pour les statistiques.
     * 
     * Record Java (Java 16+) : génère automatiquement constructeur, getters, equals, hashCode, toString
     */
    public record TaskStats(long total, long completed, long inProgress, long todo) {
        public double completionRate() {
            return total == 0 ? 0 : (double) completed / total * 100;
        }
    }
}
```

### Étape 4 : Simplifier le TaskController

```java
// src/main/java/com/taskflow/controller/TaskController.java
package com.taskflow.controller;

import com.taskflow.dto.CreateTaskDTO;
import com.taskflow.dto.UpdateTaskDTO;
import com.taskflow.model.Task;
import com.taskflow.service.TaskService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

/**
 * Contrôleur REST pour les tâches.
 * 
 * RESPONSABILITÉ UNIQUE :
 * - Gérer le protocole HTTP (méthodes, codes de statut, headers)
 * - Valider le format des requêtes
 * - Déléguer TOUTE la logique au TaskService
 * 
 * Le contrôleur ne contient AUCUNE logique métier.
 */
@Slf4j
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
    
    private final TaskService taskService;
    
    // Spring injecte automatiquement le TaskService
    public TaskController(TaskService taskService) {
        this.taskService = taskService;
    }
    
    // GET /api/tasks?completed=true&priority=HIGH
    @GetMapping
    public ResponseEntity<List<Task>> getAllTasks(
            @RequestParam(required = false) Boolean completed,
            @RequestParam(required = false) String priority) {
        
        List<Task> tasks = taskService.getAllTasks(completed, priority);
        return ResponseEntity.ok(tasks);
    }
    
    // GET /api/tasks/{id}
    @GetMapping("/{id}")
    public ResponseEntity<Task> getTaskById(@PathVariable String id) {
        Task task = taskService.getTaskById(id); // Lance TaskNotFoundException si absent
        return ResponseEntity.ok(task);
    }
    
    // GET /api/tasks/search?keyword=spring
    @GetMapping("/search")
    public ResponseEntity<List<Task>> searchTasks(@RequestParam String keyword) {
        List<Task> tasks = taskService.searchTasks(keyword);
        return ResponseEntity.ok(tasks);
    }
    
    // POST /api/tasks
    @PostMapping
    public ResponseEntity<Task> createTask(@RequestBody CreateTaskDTO dto) {
        Task createdTask = taskService.createTask(dto);
        return ResponseEntity.status(201).body(createdTask);
    }
    
    // PUT /api/tasks/{id}
    @PutMapping("/{id}")
    public ResponseEntity<Task> updateTask(@PathVariable String id, 
                                           @RequestBody UpdateTaskDTO dto) {
        Task updatedTask = taskService.updateTask(id, dto);
        return ResponseEntity.ok(updatedTask);
    }
    
    // PATCH /api/tasks/{id}
    @PatchMapping("/{id}")
    public ResponseEntity<Task> patchTask(@PathVariable String id, 
                                          @RequestBody UpdateTaskDTO dto) {
        Task patchedTask = taskService.patchTask(id, dto);
        return ResponseEntity.ok(patchedTask);
    }
    
    // PATCH /api/tasks/{id}/done
    @PatchMapping("/{id}/done")
    public ResponseEntity<Task> markAsDone(@PathVariable String id) {
        Task task = taskService.markTaskAsDone(id);
        return ResponseEntity.ok(task);
    }
    
    // PATCH /api/tasks/{id}/status
    @PatchMapping("/{id}/status")
    public ResponseEntity<Task> updateStatus(@PathVariable String id,
                                             @RequestBody Map<String, String> body) {
        String status = body.get("status");
        if (status == null || status.isBlank()) {
            return ResponseEntity.badRequest().build();
        }
        Task task = taskService.updateTaskStatus(id, status);
        return ResponseEntity.ok(task);
    }
    
    // DELETE /api/tasks/{id}
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteTask(@PathVariable String id) {
        taskService.deleteTask(id);
        return ResponseEntity.noContent().build();
    }
    
    // GET /api/tasks/stats
    @GetMapping("/stats")
    public ResponseEntity<TaskService.TaskStats> getStats() {
        TaskService.TaskStats stats = taskService.getStats();
        return ResponseEntity.ok(stats);
    }
}
```

---

## 2.5 Le cycle de vie des beans

### Les scopes

```java
// Singleton (par défaut) : une seule instance partagée
@Service // @Scope("singleton") implicite
public class TaskService { ... }

// Prototype : nouvelle instance à chaque injection
@Component
@Scope("prototype")
public class TaskBuilder { ... }

// Request : une instance par requête HTTP (dans un contexte web)
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, 
       proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext { ... }
```

### Hooks de cycle de vie

```java
@Service
public class TaskService {
    
    // Appelé APRÈS que Spring ait injecté toutes les dépendances
    @PostConstruct
    public void init() {
        log.info("TaskService prêt ! Repository: {}", 
                 taskRepository.getClass().getSimpleName());
        
        // Bon endroit pour initialiser des données de démo
        createDemoTasks();
    }
    
    // Appelé AVANT que Spring détruise le bean
    @PreDestroy
    public void cleanup() {
        log.info("TaskService en cours de fermeture...");
        // Bon endroit pour libérer des ressources
    }
    
    private void createDemoTasks() {
        CreateTaskDTO demo1 = new CreateTaskDTO();
        demo1.setTitle("Apprendre Spring Boot");
        demo1.setDescription("Suivre le guide TaskFlow complet");
        demo1.setPriority(Task.Priority.HIGH);
        createTask(demo1);
        
        CreateTaskDTO demo2 = new CreateTaskDTO();
        demo2.setTitle("Créer un projet personnel");
        demo2.setPriority(Task.Priority.MEDIUM);
        createTask(demo2);
        
        log.info("Données de démo créées avec succès");
    }
}
```

---

## 2.6 Profils Spring

Les profils permettent d'avoir des configurations différentes selon l'environnement.

### Définir les profils

```properties
# application.properties (configuration commune)
spring.application.name=taskflow

# application-dev.properties (environnement développement)
app.debug=true
app.demo-data=true
logging.level.com.taskflow=DEBUG

# application-prod.properties (environnement production)
app.debug=false
app.demo-data=false
logging.level.com.taskflow=WARN
```

### Activer un profil

```properties
# application.properties
spring.profiles.active=dev
```

```bash
# Via la ligne de commande
java -jar taskflow.jar --spring.profiles.active=prod

# Via variable d'environnement
SPRING_PROFILES_ACTIVE=prod java -jar taskflow.jar
```

### Beans conditionnels selon le profil

```java
/**
 * Service de notification désactivé en développement.
 */
@Service
@Profile("prod") // N'est créé que si le profil "prod" est actif
public class EmailNotificationService implements NotificationService {
    public void notify(String message) {
        // Envoie un vrai email en production
    }
}

@Service
@Profile("dev") // N'est créé que si le profil "dev" est actif
public class LogNotificationService implements NotificationService {
    public void notify(String message) {
        log.info("[NOTIF DEV] {}", message); // Juste un log en dev
    }
}
```

---

## 2.7 Tester la couche Service

L'un des grands avantages des services découplés : ils sont **faciles à tester** !

```java
// src/test/java/com/taskflow/service/TaskServiceTest.java
package com.taskflow.service;

import com.taskflow.dto.CreateTaskDTO;
import com.taskflow.exception.TaskNotFoundException;
import com.taskflow.model.Task;
import com.taskflow.repository.InMemoryTaskRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;

/**
 * Tests unitaires pour TaskService.
 * 
 * Pas besoin de démarrer Spring ! On teste la logique pure.
 * On utilise directement InMemoryTaskRepository (pas de mock nécessaire).
 */
class TaskServiceTest {
    
    private TaskService taskService;
    
    @BeforeEach
    void setUp() {
        // Création manuelle des dépendances
        InMemoryTaskRepository repository = new InMemoryTaskRepository();
        taskService = new TaskService(repository);
    }
    
    @Test
    void createTask_shouldSetDefaultValues() {
        // GIVEN
        CreateTaskDTO dto = new CreateTaskDTO();
        dto.setTitle("  Ma tâche  "); // Espaces superflus
        
        // WHEN
        Task result = taskService.createTask(dto);
        
        // THEN
        assertThat(result.getId()).isNotNull();
        assertThat(result.getTitle()).isEqualTo("Ma tâche"); // Trimmed
        assertThat(result.isCompleted()).isFalse();
        assertThat(result.getStatus()).isEqualTo(Task.Status.TODO);
        assertThat(result.getPriority()).isEqualTo(Task.Priority.MEDIUM); // Défaut
        assertThat(result.getCreatedAt()).isNotNull();
    }
    
    @Test
    void getTaskById_shouldThrowWhenNotFound() {
        // WHEN & THEN
        assertThatThrownBy(() -> taskService.getTaskById("id-inexistant"))
                .isInstanceOf(TaskNotFoundException.class)
                .hasMessageContaining("id-inexistant");
    }
    
    @Test
    void markTaskAsDone_shouldThrowWhenAlreadyCompleted() {
        // GIVEN
        CreateTaskDTO dto = new CreateTaskDTO();
        dto.setTitle("Tâche déjà terminée");
        Task task = taskService.createTask(dto);
        taskService.markTaskAsDone(task.getId()); // Première complétion
        
        // WHEN & THEN
        assertThatThrownBy(() -> taskService.markTaskAsDone(task.getId()))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("déjà terminée");
    }
    
    @Test
    void getStats_shouldReturnCorrectCounts() {
        // GIVEN
        CreateTaskDTO dto1 = new CreateTaskDTO();
        dto1.setTitle("Tâche 1");
        Task task1 = taskService.createTask(dto1);
        
        CreateTaskDTO dto2 = new CreateTaskDTO();
        dto2.setTitle("Tâche 2");
        Task task2 = taskService.createTask(dto2);
        
        taskService.markTaskAsDone(task1.getId());
        
        // WHEN
        TaskService.TaskStats stats = taskService.getStats();
        
        // THEN
        assertThat(stats.total()).isEqualTo(2);
        assertThat(stats.completed()).isEqualTo(1);
        assertThat(stats.completionRate()).isEqualTo(50.0);
    }
}
```

Pour lancer les tests :
```bash
./mvnw test
# Ou spécifiquement :
./mvnw test -Dtest=TaskServiceTest
```

---

## 2.8 Structure finale du projet

```
src/main/java/com/taskflow/
├── TaskflowApplication.java
├── controller/
│   └── TaskController.java       <- Délègue au Service
├── service/
│   └── TaskService.java          <- Logique métier
├── repository/
│   ├── TaskRepository.java       <- Interface
│   └── InMemoryTaskRepository.java <- Implémentation
├── model/
│   └── Task.java                 <- Entité
├── dto/
│   ├── CreateTaskDTO.java        <- Données de création
│   └── UpdateTaskDTO.java        <- Données de mise à jour
└── exception/
    ├── TaskNotFoundException.java
    ├── ErrorResponse.java
    └── GlobalExceptionHandler.java
```

---

## 2.9 Exercices pratiques

### Exercice 1 : Service de priorité

Ajoutez au `TaskService` une méthode `getHighPriorityTasks()` qui retourne toutes les tâches avec une priorité HIGH ou URGENT, triées par date de création (les plus récentes en premier).

**Indices :**
- Utilisez `Comparator.comparing()` avec `.reversed()`
- Utilisez l'enum `Task.Priority` pour comparer

### Exercice 2 : Règle métier de réouverture

Ajoutez une méthode `reopenTask(String id)` dans le service qui :
- Marque une tâche comme non complétée
- Change son statut en `IN_PROGRESS`
- Lève une `IllegalStateException` si la tâche n'est pas encore complétée

Exposez cette méthode via `PATCH /api/tasks/{id}/reopen` dans le contrôleur.

### Exercice 3 : Service de rapport

Créez un `TaskReportService` distinct qui génère un rapport textuel des tâches :

```
=== RAPPORT TASKFLOW ===
Date: 2024-01-15
Total: 10 tâches
  - À faire: 5
  - En cours: 3
  - Terminées: 2
Taux de complétion: 20%

TÂCHES EN COURS:
  [HIGH] Implémenter l'auth JWT (assignée à: alice)
  [MEDIUM] Écrire les tests (assignée à: bob)
  [LOW] Mettre à jour le README (assignée à: Non assignée)
```

Injectez le `TaskReportService` dans un nouveau `ReportController` exposant `GET /api/reports`.

---

## Récapitulatif du Module 02

[OK] **Architecture en couches** : Controller -> Service -> Repository  
[OK] **Injection de dépendances** : Spring gère la création et l'injection des beans  
[OK] **Injection par constructeur** : pattern recommandé (final, testable)  
[OK] **@Service, @Repository** : stéréotypes pour identifier les composants  
[OK] **Interface + implémentation** : permet de changer l'implémentation sans modifier le code client  
[OK] **Règles métier dans le Service** : pas dans le Controller  
[OK] **Cycle de vie** : @PostConstruct, @PreDestroy  
[OK] **Profils Spring** : configurations différentes par environnement  
[OK] **Tests unitaires** : testez le Service sans démarrer Spring  

### Prochaine étape

Dans le **Module 03**, vous connecterez TaskFlow à une vraie base de données avec **Spring Data JPA et PostgreSQL**. Notre `InMemoryTaskRepository` sera remplacé par un vrai repository JPA en quelques lignes de code — et vous verrez pourquoi l'interface `TaskRepository` était si importante !

# Module 03 : JPA, Hibernate et Base de Données

## Objectifs du module
- Comprendre JPA et Hibernate
- Connecter TaskFlow à PostgreSQL
- Utiliser Spring Data JPA pour remplacer le repository in-memory
- Maîtriser les requêtes JPA (JPQL, méthodes dérivées, @Query)

**Durée estimée :** 5-6 heures  
**Prérequis :** Module 02 complété, Docker installé

---

## 3.1 JPA, Hibernate, Spring Data JPA : les différences

Avant de coder, clarifions ces termes souvent confondus.

```
┌────────────────────────────────────────────────────┐
│              VOTRE CODE JAVA                        │
├────────────────────────────────────────────────────┤
│           SPRING DATA JPA                           │
│  Génère automatiquement les implémentations        │
│  Ajoute findById, findAll, save, delete...          │
├────────────────────────────────────────────────────┤
│                    JPA                              │
│  Spécification Java (interface standardisée)        │
│  Définit @Entity, @Id, @Column, EntityManager...    │
├────────────────────────────────────────────────────┤
│                 HIBERNATE                           │
│  Implémentation de JPA (le plus populaire)          │
│  Traduit vos objets Java en SQL                     │
├────────────────────────────────────────────────────┤
│                  JDBC                               │
│  Connexion bas niveau à la base de données          │
├────────────────────────────────────────────────────┤
│        BASE DE DONNÉES (PostgreSQL, MySQL...)       │
└────────────────────────────────────────────────────┘
```

**Résumé :**
- **JPA** : les règles du jeu (annotations, interfaces)
- **Hibernate** : le joueur qui applique les règles (génère le SQL)
- **Spring Data JPA** : votre assistant qui fait tout automatiquement

---

## 3.2 Mise en place de PostgreSQL avec Docker

### Option A : Docker (recommandé pour le développement)

```bash
# Lancer PostgreSQL en arrière-plan
docker run --name taskflow-db \
  -e POSTGRES_DB=taskflow \
  -e POSTGRES_USER=taskflow_user \
  -e POSTGRES_PASSWORD=taskflow_pass \
  -p 5432:5432 \
  -d postgres:16

# Vérifier que le conteneur tourne
docker ps

# Se connecter à la base (optionnel)
docker exec -it taskflow-db psql -U taskflow_user -d taskflow
```

### Option B : docker-compose.yml (encore mieux !)

```yaml
# docker-compose.yml (à la racine du projet)
version: '3.8'

services:
  db:
    image: postgres:16
    container_name: taskflow-db
    environment:
      POSTGRES_DB: taskflow
      POSTGRES_USER: taskflow_user
      POSTGRES_PASSWORD: taskflow_pass
    ports:
      - "5432:5432"
    volumes:
      # Persistance des données entre les redémarrages
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U taskflow_user -d taskflow"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  postgres_data:
```

```bash
# Démarrer
docker compose up -d

# Arrêter
docker compose down

# Arrêter ET supprimer les données
docker compose down -v
```

---

## 3.3 Dépendances Maven

Mettez à jour votre `pom.xml` :

```xml
<dependencies>
    <!-- Spring Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- Spring Data JPA + Hibernate -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    
    <!-- Driver PostgreSQL -->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>
    
    <!-- H2 pour les tests (base de données en mémoire) -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>test</scope>
    </dependency>
    
    <!-- Lombok, DevTools, etc. -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
</dependencies>
```

---

## 3.4 Configuration de la base de données

```properties
# application.properties

# ========================
# BASE DE DONNÉES
# ========================
spring.datasource.url=jdbc:postgresql://localhost:5432/taskflow
spring.datasource.username=taskflow_user
spring.datasource.password=taskflow_pass
spring.datasource.driver-class-name=org.postgresql.Driver

# ========================
# JPA / HIBERNATE
# ========================

# Stratégie de création des tables :
# - none : Hibernate ne touche pas au schéma
# - validate : vérifie que le schéma correspond aux entités
# - update : met à jour le schéma (ajoute colonnes manquantes)
# - create : recrée les tables au démarrage (PERD LES DONNÉES !)
# - create-drop : recrée et supprime à la fermeture
spring.jpa.hibernate.ddl-auto=update

# Affiche le SQL généré par Hibernate (utile pour débugger)
spring.jpa.show-sql=true

# Formate le SQL (plus lisible)
spring.jpa.properties.hibernate.format_sql=true

# Dialecte PostgreSQL
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect

# ========================
# LOGGING
# ========================
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
```

```properties
# application-test.properties (utilisé pendant les tests)
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
```

---

## 3.5 Transformer Task en entité JPA

```java
// src/main/java/com/taskflow/model/Task.java
package com.taskflow.model;

import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;

import java.time.LocalDateTime;

/**
 * Entité JPA représentant une tâche.
 * 
 * @Entity : Marque cette classe comme une entité JPA.
 *           Hibernate créera une table correspondante.
 * 
 * @Table : Configure le nom de la table (optionnel, par défaut = nom de la classe en minuscules).
 *          Permet aussi d'ajouter des index et contraintes.
 * 
 * @Data : Lombok génère getters, setters, equals, hashCode, toString.
 *         ATTENTION : equals/hashCode basés sur TOUS les champs peuvent poser problème
 *         avec JPA (utiliser @EqualsAndHashCode(of = "id") est plus sûr).
 */
@Entity
@Table(name = "tasks", indexes = {
        @Index(name = "idx_task_status", columnList = "status"),
        @Index(name = "idx_task_priority", columnList = "priority"),
        @Index(name = "idx_task_completed", columnList = "completed")
})
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Task {
    
    /**
     * @Id : Clé primaire.
     * 
     * @GeneratedValue : Stratégie de génération de l'ID.
     *   - IDENTITY : Délègue à la base de données (auto_increment, SERIAL...)
     *   - SEQUENCE : Utilise une séquence de la BDD (plus efficace pour PostgreSQL)
     *   - UUID : Génère un UUID (unique mais plus de stockage)
     *   - AUTO : Spring choisit selon la base de données
     * 
     * Avec SEQUENCE, Hibernate peut pré-allouer des IDs en batch (meilleure performance).
     */
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "task_seq")
    @SequenceGenerator(name = "task_seq", sequenceName = "task_sequence", 
                       allocationSize = 50) // Pré-alloue 50 IDs à la fois
    private Long id;
    
    /**
     * @Column : Configure le mapping vers la colonne de la table.
     * nullable = false -> contrainte NOT NULL dans la base
     * length = 255 -> longueur max pour VARCHAR
     */
    @Column(nullable = false, length = 255)
    private String title;
    
    /**
     * @Column(columnDefinition = "TEXT") -> type TEXT (longueur illimitée)
     * vs VARCHAR(length) qui est limité
     */
    @Column(columnDefinition = "TEXT")
    private String description;
    
    @Column(nullable = false)
    private boolean completed = false;
    
    /**
     * @Enumerated(EnumType.STRING) : Stocke le nom de l'enum en base (ex: "HIGH")
     * vs EnumType.ORDINAL qui stocke l'index numérique (0, 1, 2...) — DÉCONSEILLÉ
     * car l'ajout d'une valeur peut décaler les index !
     */
    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 20)
    private Priority priority = Priority.MEDIUM;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 20)
    private Status status = Status.TODO;
    
    @Column(name = "assigned_to", length = 100)
    private String assignedTo;
    
    /**
     * @Column(updatable = false) : Cette colonne ne sera jamais modifiée par Hibernate.
     * Parfait pour un timestamp de création.
     */
    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;
    
    @Column(name = "updated_at", nullable = false)
    private LocalDateTime updatedAt;
    
    /**
     * @PrePersist : Callback appelé juste AVANT l'insertion en base.
     * Permet d'initialiser automatiquement les timestamps.
     */
    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
    }
    
    /**
     * @PreUpdate : Callback appelé juste AVANT la mise à jour en base.
     * Met à jour le timestamp automatiquement.
     */
    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }
    
    // Enums
    public enum Priority {
        LOW, MEDIUM, HIGH, URGENT
    }
    
    public enum Status {
        TODO, IN_PROGRESS, DONE, CANCELLED
    }
}
```

---

## 3.6 Repository Spring Data JPA

Voici la **magie** de Spring Data JPA : notre repository in-memory de ~80 lignes devient ceci :

```java
// src/main/java/com/taskflow/repository/TaskJpaRepository.java
package com.taskflow.repository;

import com.taskflow.model.Task;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

/**
 * Repository Spring Data JPA.
 * 
 * JpaRepository<Task, Long> fournit automatiquement :
 * - findAll(), findAll(Sort), findAll(Pageable)
 * - findById(id), existsById(id)
 * - save(entity), saveAll(entities)
 * - delete(entity), deleteById(id), deleteAll()
 * - count()
 * - Et bien plus...
 * 
 * Spring génère l'implémentation à la compilation — pas besoin de code !
 */
@Repository
public interface TaskJpaRepository extends JpaRepository<Task, Long> {
    
    // ========================
    // MÉTHODES DÉRIVÉES
    // Spring analyse le nom de la méthode et génère automatiquement le SQL !
    // ========================
    
    /**
     * findBy{Champ}
     * -> SELECT * FROM tasks WHERE completed = ?
     */
    List<Task> findByCompleted(boolean completed);
    
    /**
     * findBy{Champ}OrderBy{Champ}{Direction}
     * -> SELECT * FROM tasks WHERE priority = ? ORDER BY created_at DESC
     */
    List<Task> findByPriorityOrderByCreatedAtDesc(Task.Priority priority);
    
    /**
     * findBy{Champ}And{Champ}
     * -> SELECT * FROM tasks WHERE completed = ? AND priority = ?
     */
    List<Task> findByCompletedAndPriority(boolean completed, Task.Priority priority);
    
    /**
     * findBy{Champ}Containing (LIKE %keyword%)
     * -> SELECT * FROM tasks WHERE LOWER(title) LIKE LOWER('%keyword%')
     */
    List<Task> findByTitleContainingIgnoreCase(String keyword);
    
    /**
     * countBy{Champ}
     * -> SELECT COUNT(*) FROM tasks WHERE completed = ?
     */
    long countByCompleted(boolean completed);
    
    /**
     * countBy{Champ}
     * -> SELECT COUNT(*) FROM tasks WHERE status = ?
     */
    long countByStatus(Task.Status status);
    
    /**
     * findBy{Champ}In : filtre avec une liste de valeurs
     * -> SELECT * FROM tasks WHERE status IN (?, ?, ...)
     */
    List<Task> findByStatusIn(List<Task.Status> statuses);
    
    /**
     * findBy{Champ}IsNull
     * -> SELECT * FROM tasks WHERE assigned_to IS NULL
     */
    List<Task> findByAssignedToIsNull();
    
    // ========================
    // REQUÊTES JPQL (@Query)
    // Pour des requêtes plus complexes
    // ========================
    
    /**
     * JPQL : Java Persistence Query Language
     * Similaire au SQL mais avec des noms de classes/champs Java (pas de tables/colonnes).
     * 
     * "t" est un alias pour Task.
     * :keyword est un paramètre nommé.
     */
    @Query("SELECT t FROM Task t WHERE LOWER(t.title) LIKE LOWER(CONCAT('%', :keyword, '%')) " +
           "OR LOWER(t.description) LIKE LOWER(CONCAT('%', :keyword, '%'))")
    List<Task> searchInTitleAndDescription(@Param("keyword") String keyword);
    
    /**
     * Requête native SQL (quand JPQL ne suffit pas).
     * 
     * nativeQuery = true : Hibernate envoie ce SQL directement à PostgreSQL.
     * Utile pour les fonctionnalités spécifiques à la base de données.
     */
    @Query(value = "SELECT * FROM tasks WHERE created_at >= NOW() - INTERVAL '7 days' " +
                   "ORDER BY created_at DESC", 
           nativeQuery = true)
    List<Task> findTasksCreatedThisWeek();
    
    /**
     * Mise à jour en masse avec @Modifying.
     * 
     * @Modifying : Indique que cette requête modifie des données (INSERT/UPDATE/DELETE).
     *              Doit être utilisé avec @Transactional dans le service.
     */
    @Modifying
    @Query("UPDATE Task t SET t.completed = true, t.status = 'DONE' WHERE t.id IN :ids")
    int markMultipleAsDone(@Param("ids") List<Long> ids);
    
    /**
     * Requête de statistiques personnalisée.
     * 
     * On peut projeter sur un record/DTO au lieu de l'entité complète.
     */
    @Query("SELECT t.status, COUNT(t) FROM Task t GROUP BY t.status")
    List<Object[]> countByStatus();
}
```

### Comparaison : Before / After

```java
// AVANT (Module 02) : InMemoryTaskRepository - 80 lignes de code
@Repository
public class InMemoryTaskRepository implements TaskRepository {
    private final Map<String, Task> storage = new LinkedHashMap<>();
    
    @Override
    public List<Task> findAll() { return new ArrayList<>(storage.values()); }
    
    @Override
    public Optional<Task> findById(String id) { return Optional.ofNullable(storage.get(id)); }
    
    // ... 70+ lignes supplémentaires
}

// APRÈS (Module 03) : Spring Data JPA - 0 ligne d'implémentation !
@Repository
public interface TaskJpaRepository extends JpaRepository<Task, Long> {
    // Spring génère tout automatiquement
    List<Task> findByCompleted(boolean completed);
    List<Task> findByTitleContainingIgnoreCase(String keyword);
    long countByCompleted(boolean completed);
}
```

---

## 3.7 Mettre à jour le TaskService

```java
// src/main/java/com/taskflow/service/TaskService.java
package com.taskflow.service;

import com.taskflow.dto.CreateTaskDTO;
import com.taskflow.dto.UpdateTaskDTO;
import com.taskflow.exception.TaskNotFoundException;
import com.taskflow.model.Task;
import com.taskflow.repository.TaskJpaRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Map;

@Slf4j
@Service
@Transactional(readOnly = true) // Par défaut, toutes les méthodes sont en lecture seule
public class TaskService {
    
    private final TaskJpaRepository taskRepository;
    
    public TaskService(TaskJpaRepository taskRepository) {
        this.taskRepository = taskRepository;
    }
    
    // ==================== LECTURE ====================
    
    // Pas besoin de @Transactional : readOnly = true est hérité de la classe
    public List<Task> getAllTasks(Boolean completed, String priority) {
        if (completed != null) {
            return taskRepository.findByCompleted(completed);
        }
        if (priority != null) {
            Task.Priority p = Task.Priority.valueOf(priority.toUpperCase());
            return taskRepository.findByPriorityOrderByCreatedAtDesc(p);
        }
        // findAll avec tri par date de création décroissante
        return taskRepository.findAll(Sort.by(Sort.Direction.DESC, "createdAt"));
    }
    
    public Task getTaskById(Long id) {
        return taskRepository.findById(id)
                .orElseThrow(() -> new TaskNotFoundException("Tâche introuvable avec l'ID: " + id));
    }
    
    public List<Task> searchTasks(String keyword) {
        if (keyword == null || keyword.isBlank()) {
            return taskRepository.findAll();
        }
        return taskRepository.searchInTitleAndDescription(keyword.trim());
    }
    
    /**
     * Pagination : récupère les tâches page par page.
     * 
     * @param page   Numéro de page (commence à 0)
     * @param size   Nombre d'éléments par page
     * @param sortBy Champ de tri
     * @return Page contenant les tâches et les infos de pagination
     */
    public Page<Task> getTasksPaginated(int page, int size, String sortBy) {
        PageRequest pageRequest = PageRequest.of(
                page, 
                size, 
                Sort.by(Sort.Direction.DESC, sortBy)
        );
        return taskRepository.findAll(pageRequest);
    }
    
    // ==================== ÉCRITURE ====================
    
    @Transactional // Surcharge la politique readOnly de la classe
    public Task createTask(CreateTaskDTO dto) {
        log.info("Création d'une nouvelle tâche: '{}'", dto.getTitle());
        
        Task task = new Task();
        task.setTitle(dto.getTitle().trim());
        task.setDescription(dto.getDescription());
        task.setPriority(dto.getPriority() != null ? dto.getPriority() : Task.Priority.MEDIUM);
        task.setAssignedTo(dto.getAssignedTo());
        // Les autres champs ont des valeurs par défaut ou sont gérés par @PrePersist
        
        // save() = INSERT si nouveau, UPDATE si existant (selon l'ID)
        Task savedTask = taskRepository.save(task);
        log.info("Tâche créée avec l'ID: {}", savedTask.getId());
        
        return savedTask;
    }
    
    @Transactional
    public Task updateTask(Long id, UpdateTaskDTO dto) {
        Task existingTask = getTaskById(id);
        
        if (dto.getTitle() != null) existingTask.setTitle(dto.getTitle().trim());
        if (dto.getDescription() != null) existingTask.setDescription(dto.getDescription());
        if (dto.getPriority() != null) existingTask.setPriority(dto.getPriority());
        if (dto.getStatus() != null) {
            existingTask.setStatus(dto.getStatus());
            if (dto.getStatus() == Task.Status.DONE) {
                existingTask.setCompleted(true);
            }
        }
        if (dto.getAssignedTo() != null) existingTask.setAssignedTo(dto.getAssignedTo());
        
        // save() déclenche @PreUpdate qui met à jour updatedAt
        return taskRepository.save(existingTask);
    }
    
    @Transactional
    public Task markTaskAsDone(Long id) {
        Task task = getTaskById(id);
        
        if (task.isCompleted()) {
            throw new IllegalStateException("La tâche est déjà terminée");
        }
        
        task.setCompleted(true);
        task.setStatus(Task.Status.DONE);
        
        return taskRepository.save(task);
    }
    
    @Transactional
    public void deleteTask(Long id) {
        if (!taskRepository.existsById(id)) {
            throw new TaskNotFoundException("Tâche introuvable avec l'ID: " + id);
        }
        taskRepository.deleteById(id);
        log.info("Tâche {} supprimée", id);
    }
    
    // ==================== STATISTIQUES ====================
    
    public TaskStats getStats() {
        long total = taskRepository.count();
        long completed = taskRepository.countByCompleted(true);
        long inProgress = taskRepository.countByStatus(Task.Status.IN_PROGRESS);
        long todo = taskRepository.countByStatus(Task.Status.TODO);
        
        return new TaskStats(total, completed, inProgress, todo);
    }
    
    public record TaskStats(long total, long completed, long inProgress, long todo) {
        public double completionRate() {
            return total == 0 ? 0 : (double) completed / total * 100;
        }
    }
}
```

---

## 3.8 Les transactions

### Qu'est-ce qu'une transaction ?

Une transaction garantit que plusieurs opérations s'exécutent **tout ou rien** :

```java
@Transactional
public void transferTask(Long taskId, String newAssignee) {
    // Opération 1 : récupérer la tâche
    Task task = taskRepository.findById(taskId).orElseThrow(...);
    
    // Opération 2 : libérer l'assignation précédente
    notifyPreviousAssignee(task.getAssignedTo());
    
    // Opération 3 : assigner à la nouvelle personne
    task.setAssignedTo(newAssignee);
    taskRepository.save(task);
    
    // Opération 4 : notifier la nouvelle personne
    notifyNewAssignee(newAssignee);
    
    // Si une exception se produit N'IMPORTE OÙ ici,
    // toutes les opérations sont annulées (rollback)
}
```

### Propriétés importantes

```java
// Lecture seule : optimisation (Hibernate ne traque pas les changements)
@Transactional(readOnly = true)
public List<Task> getAllTasks() { ... }

// Rollback sur RuntimeException uniquement par défaut
// Pour rollback sur Exception vérifiée :
@Transactional(rollbackFor = Exception.class)
public void riskyOperation() { ... }

// Propagation : que faire si une transaction est déjà active ?
@Transactional(propagation = Propagation.REQUIRES_NEW)
// Crée une nouvelle transaction même si une est déjà active
```

---

## 3.9 Pagination et tri

```java
// GET /api/tasks?page=0&size=10&sort=createdAt,desc
@GetMapping
public ResponseEntity<Map<String, Object>> getAllTasks(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size,
        @RequestParam(defaultValue = "createdAt") String sortBy,
        @RequestParam(defaultValue = "desc") String direction) {
    
    Sort.Direction sortDirection = direction.equalsIgnoreCase("asc") 
            ? Sort.Direction.ASC : Sort.Direction.DESC;
    
    PageRequest pageRequest = PageRequest.of(page, size, 
                                             Sort.by(sortDirection, sortBy));
    Page<Task> taskPage = taskRepository.findAll(pageRequest);
    
    // Retourner les métadonnées de pagination
    Map<String, Object> response = new HashMap<>();
    response.put("tasks", taskPage.getContent());
    response.put("currentPage", taskPage.getNumber());
    response.put("totalItems", taskPage.getTotalElements());
    response.put("totalPages", taskPage.getTotalPages());
    response.put("hasNext", taskPage.hasNext());
    response.put("hasPrevious", taskPage.hasPrevious());
    
    return ResponseEntity.ok(response);
}
```

Réponse :
```json
{
  "tasks": [
    { "id": 10, "title": "Tâche 10", ... },
    { "id": 9, "title": "Tâche 9", ... }
  ],
  "currentPage": 0,
  "totalItems": 25,
  "totalPages": 3,
  "hasNext": true,
  "hasPrevious": false
}
```

---

## 3.10 Données initiales avec data.sql

```sql
-- src/main/resources/data.sql
-- Exécuté au démarrage si spring.jpa.defer-datasource-initialization=true

INSERT INTO tasks (title, description, priority, status, completed, assigned_to, created_at, updated_at)
VALUES 
('Apprendre Spring Boot', 'Suivre le guide TaskFlow complet', 'HIGH', 'IN_PROGRESS', false, 'alice', NOW(), NOW()),
('Configurer PostgreSQL', 'Mettre en place la base de données', 'HIGH', 'DONE', true, 'bob', NOW(), NOW()),
('Écrire les tests', 'Tests unitaires et intégration', 'MEDIUM', 'TODO', false, null, NOW(), NOW()),
('Déployer sur Docker', 'Conteneuriser l''application', 'LOW', 'TODO', false, 'alice', NOW(), NOW());
```

```properties
# application.properties
spring.jpa.defer-datasource-initialization=true
spring.sql.init.mode=always # always, embedded (H2 only), never
```

---

## 3.11 Tests d'intégration avec H2

```java
// src/test/java/com/taskflow/repository/TaskRepositoryTest.java
package com.taskflow.repository;

import com.taskflow.model.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.ActiveProfiles;

import java.util.List;

import static org.assertj.core.api.Assertions.*;

/**
 * @DataJpaTest : Configure un contexte Spring minimal pour JPA.
 *               - Lance une base H2 en mémoire
 *               - N'instancie que les beans JPA (@Repository, @Entity...)
 *               - Enroule chaque test dans une transaction qui sera annulée
 * 
 * Plus rapide que @SpringBootTest qui charge tout le contexte.
 */
@DataJpaTest
@ActiveProfiles("test")
class TaskRepositoryTest {
    
    @Autowired
    private TaskJpaRepository taskRepository;
    
    private Task createAndSaveTask(String title, Task.Priority priority, boolean completed) {
        Task task = new Task();
        task.setTitle(title);
        task.setPriority(priority);
        task.setCompleted(completed);
        task.setStatus(completed ? Task.Status.DONE : Task.Status.TODO);
        return taskRepository.save(task);
    }
    
    @Test
    void findByCompleted_shouldReturnOnlyCompletedTasks() {
        // GIVEN
        createAndSaveTask("Tâche active", Task.Priority.HIGH, false);
        createAndSaveTask("Tâche terminée", Task.Priority.LOW, true);
        createAndSaveTask("Autre tâche terminée", Task.Priority.MEDIUM, true);
        
        // WHEN
        List<Task> completedTasks = taskRepository.findByCompleted(true);
        
        // THEN
        assertThat(completedTasks).hasSize(2);
        assertThat(completedTasks).allMatch(Task::isCompleted);
    }
    
    @Test
    void findByTitleContainingIgnoreCase_shouldBeCaseInsensitive() {
        // GIVEN
        createAndSaveTask("Apprendre Spring Boot", Task.Priority.HIGH, false);
        createAndSaveTask("Maîtriser SPRING DATA", Task.Priority.MEDIUM, false);
        createAndSaveTask("Configurer Docker", Task.Priority.LOW, false);
        
        // WHEN
        List<Task> results = taskRepository.findByTitleContainingIgnoreCase("spring");
        
        // THEN
        assertThat(results).hasSize(2);
        assertThat(results).extracting(Task::getTitle)
                .containsExactlyInAnyOrder("Apprendre Spring Boot", "Maîtriser SPRING DATA");
    }
    
    @Test
    void countByCompleted_shouldReturnCorrectCount() {
        // GIVEN
        createAndSaveTask("T1", Task.Priority.HIGH, true);
        createAndSaveTask("T2", Task.Priority.MEDIUM, true);
        createAndSaveTask("T3", Task.Priority.LOW, false);
        
        // WHEN & THEN
        assertThat(taskRepository.countByCompleted(true)).isEqualTo(2);
        assertThat(taskRepository.countByCompleted(false)).isEqualTo(1);
    }
    
    @Test
    void save_shouldSetTimestampsAutomatically() {
        // GIVEN
        Task task = new Task();
        task.setTitle("Nouvelle tâche");
        task.setPriority(Task.Priority.MEDIUM);
        
        // WHEN
        Task saved = taskRepository.save(task);
        
        // THEN
        assertThat(saved.getId()).isNotNull();
        assertThat(saved.getCreatedAt()).isNotNull();
        assertThat(saved.getUpdatedAt()).isNotNull();
    }
}
```

---

## 3.12 Exercices pratiques

### Exercice 1 : Requêtes avancées

Ajoutez dans `TaskJpaRepository` :

1. `findByAssignedToOrderByPriorityDesc(String assignedTo)` — tâches d'une personne triées par priorité
2. Une requête JPQL qui retourne les tâches créées aujourd'hui
3. `countByPriority(Task.Priority priority)` — compte par priorité

### Exercice 2 : Endpoint de statistiques détaillées

Créez un endpoint `GET /api/stats` qui retourne :

```json
{
  "total": 25,
  "byStatus": {
    "TODO": 10,
    "IN_PROGRESS": 8,
    "DONE": 6,
    "CANCELLED": 1
  },
  "byPriority": {
    "URGENT": 2,
    "HIGH": 8,
    "MEDIUM": 12,
    "LOW": 3
  },
  "completionRate": 28.0,
  "unassigned": 7
}
```

### Exercice 3 : Recherche avancée

Créez un endpoint `POST /api/tasks/search` avec un body de critères :

```json
{
  "keyword": "spring",
  "priority": "HIGH",
  "completed": false,
  "assignedTo": "alice"
}
```

**Indice :** Utilisez `Specification<Task>` de Spring Data JPA pour construire des requêtes dynamiques, ou `@Query` avec des paramètres optionnels.

---

## Récapitulatif du Module 03

[OK] **JPA vs Hibernate vs Spring Data JPA** : chacun a son rôle  
[OK] **@Entity, @Table, @Column, @Id** : mapper les classes Java en tables  
[OK] **@Enumerated(STRING)** : toujours utiliser STRING, jamais ORDINAL  
[OK] **@PrePersist, @PreUpdate** : callbacks automatiques  
[OK] **Spring Data JPA** : repositories générés automatiquement  
[OK] **Méthodes dérivées** : Spring comprend findByXxxOrderByYyy  
[OK] **@Query JPQL et SQL natif** : pour les requêtes complexes  
[OK] **@Transactional** : garantit l'atomicité des opérations  
[OK] **Pagination** : Page<T> et PageRequest  
[OK] **@DataJpaTest** : tests du repository avec H2  

### Prochaine étape

Dans le **Module 04**, vous ajouterez la **validation des données** avec Bean Validation : `@NotBlank`, `@Size`, `@Email`, `@Pattern`... Plus jamais de données invalides dans votre base !

# Module 04 : Validation des Données avec Bean Validation

## Objectifs du module
- Valider les données entrantes avec les annotations Bean Validation
- Créer des messages d'erreur clairs et localisés
- Implémenter des validations personnalisées
- Gérer les erreurs de validation de façon cohérente

**Durée estimée :** 3-4 heures  
**Prérequis :** Module 03 complété

---

## 4.1 Pourquoi valider les données ?

Sans validation, votre API accepte n'importe quoi :

```bash
# Requête malformée — sans validation, ça passe !
curl -X POST http://localhost:8080/api/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "", "priority": "SUPER_URGENT"}'
```

Les données invalides peuvent :
- Corrompre la base de données
- Déclencher des exceptions non maîtrisées (NullPointerException, etc.)
- Renvoyer des messages d'erreur cryptiques aux utilisateurs

### La règle d'or : valider à la frontière

```
CLIENT -> [VALIDATION] -> Controller -> Service -> Repository -> DB
                ^
          Rejeter ici avec
          un message clair
```

---

## 4.2 Dépendance Maven

Spring Boot inclut Bean Validation via ce starter :

```xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
```

---

## 4.3 Annoter les DTOs

```java
// src/main/java/com/taskflow/dto/CreateTaskDTO.java
package com.taskflow.dto;

import com.taskflow.model.Task;
import jakarta.validation.constraints.*;
import lombok.Data;

/**
 * DTO de création de tâche avec validations Bean Validation.
 * 
 * Les annotations de validation définissent des CONTRATS :
 * - Ce que le client DOIT fournir
 * - Les contraintes sur chaque champ
 * - Les messages d'erreur à retourner
 */
@Data
public class CreateTaskDTO {
    
    /**
     * @NotBlank : Interdit null, "", "  " (espaces uniquement)
     * Différence avec :
     * - @NotNull : Interdit null, accepte ""
     * - @NotEmpty : Interdit null et "", accepte "  "
     * 
     * Pour les strings, @NotBlank est presque toujours le bon choix.
     */
    @NotBlank(message = "Le titre est obligatoire")
    @Size(min = 3, max = 100, 
          message = "Le titre doit contenir entre {min} et {max} caractères")
    private String title;
    
    /**
     * Description optionnelle mais avec longueur max.
     * Pas de @NotBlank car null est accepté.
     */
    @Size(max = 1000, message = "La description ne peut pas dépasser {max} caractères")
    private String description;
    
    /**
     * Priorité optionnelle (défaut géré dans le Service).
     * Pas besoin de @NotNull : null est accepté.
     */
    private Task.Priority priority;
    
    /**
     * Assignation optionnelle.
     * @Email vérifie le format si non null.
     * Utilisé ici pour illustrer, mais "assignedTo" pourrait être un username plutôt qu'un email.
     */
    @Size(max = 100, message = "L'assignation ne peut pas dépasser {max} caractères")
    private String assignedTo;
}
```

```java
// src/main/java/com/taskflow/dto/UpdateTaskDTO.java
package com.taskflow.dto;

import com.taskflow.model.Task;
import jakarta.validation.constraints.*;
import lombok.Data;

@Data
public class UpdateTaskDTO {
    
    // Pour PATCH, tous les champs sont optionnels (null = ne pas modifier)
    // Mais SI fourni, les contraintes s'appliquent
    
    @Size(min = 3, max = 100, message = "Le titre doit faire entre {min} et {max} caractères")
    private String title; // null = ne pas modifier
    
    @Size(max = 1000, message = "La description ne peut pas dépasser {max} caractères")
    private String description;
    
    private Task.Priority priority;
    
    private Task.Status status;
    
    @Size(max = 100)
    private String assignedTo;
    
    private boolean completed;
}
```

---

## 4.4 Activer la validation dans le Controller

```java
// src/main/java/com/taskflow/controller/TaskController.java

@RestController
@RequestMapping("/api/tasks")
@Validated // Active la validation au niveau de la classe (pour @PathVariable, @RequestParam)
public class TaskController {
    
    private final TaskService taskService;
    
    public TaskController(TaskService taskService) {
        this.taskService = taskService;
    }
    
    /**
     * @Valid : Déclenche la validation du DTO avant l'exécution de la méthode.
     * 
     * Si la validation échoue -> MethodArgumentNotValidException est levée
     * Notre GlobalExceptionHandler la capte et retourne une 400 claire.
     */
    @PostMapping
    public ResponseEntity<Task> createTask(@Valid @RequestBody CreateTaskDTO dto) {
        Task createdTask = taskService.createTask(dto);
        return ResponseEntity.status(201).body(createdTask);
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<Task> updateTask(
            @PathVariable @Positive(message = "L'ID doit être positif") Long id,
            @Valid @RequestBody UpdateTaskDTO dto) {
        Task updatedTask = taskService.updateTask(id, dto);
        return ResponseEntity.ok(updatedTask);
    }
    
    @PatchMapping("/{id}")
    public ResponseEntity<Task> patchTask(
            @PathVariable @Positive Long id,
            @Valid @RequestBody UpdateTaskDTO dto) {
        Task patchedTask = taskService.patchTask(id, dto);
        return ResponseEntity.ok(patchedTask);
    }
    
    // Les autres méthodes restent identiques...
}
```

---

## 4.5 Gérer les erreurs de validation

```java
// src/main/java/com/taskflow/exception/GlobalExceptionHandler.java
package com.taskflow.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import jakarta.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    /**
     * Gère les erreurs de validation (@Valid).
     * 
     * Retourne une réponse structurée avec tous les champs invalides.
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ValidationErrorResponse> handleValidationErrors(
            MethodArgumentNotValidException ex,
            HttpServletRequest request) {
        
        // Collecter toutes les erreurs de champs
        Map<String, String> fieldErrors = new HashMap<>();
        List<FieldError> errors = ex.getBindingResult().getFieldErrors();
        
        for (FieldError error : errors) {
            // Si plusieurs erreurs sur le même champ, garder la première
            fieldErrors.putIfAbsent(error.getField(), error.getDefaultMessage());
        }
        
        ValidationErrorResponse response = new ValidationErrorResponse(
                HttpStatus.BAD_REQUEST.value(),
                "Validation échouée",
                fieldErrors,
                request.getRequestURI(),
                LocalDateTime.now()
        );
        
        log.warn("Erreur de validation sur {}: {}", request.getRequestURI(), fieldErrors);
        
        return ResponseEntity.badRequest().body(response);
    }
    
    /**
     * Gère les erreurs de contrainte sur les @PathVariable et @RequestParam (@Validated).
     */
    @ExceptionHandler(jakarta.validation.ConstraintViolationException.class)
    public ResponseEntity<ErrorResponse> handleConstraintViolation(
            jakarta.validation.ConstraintViolationException ex,
            HttpServletRequest request) {
        
        String message = ex.getConstraintViolations().stream()
                .map(cv -> cv.getPropertyPath() + ": " + cv.getMessage())
                .reduce("", (a, b) -> a + "; " + b);
        
        ErrorResponse response = new ErrorResponse(
                HttpStatus.BAD_REQUEST.value(),
                "Contrainte violée",
                message,
                request.getRequestURI(),
                LocalDateTime.now()
        );
        
        return ResponseEntity.badRequest().body(response);
    }
    
    @ExceptionHandler(TaskNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleTaskNotFound(
            TaskNotFoundException ex, HttpServletRequest request) {
        
        ErrorResponse response = new ErrorResponse(
                HttpStatus.NOT_FOUND.value(),
                "Ressource introuvable",
                ex.getMessage(),
                request.getRequestURI(),
                LocalDateTime.now()
        );
        
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
    }
    
    @ExceptionHandler(IllegalStateException.class)
    public ResponseEntity<ErrorResponse> handleIllegalState(
            IllegalStateException ex, HttpServletRequest request) {
        
        ErrorResponse response = new ErrorResponse(
                HttpStatus.CONFLICT.value(),
                "Opération invalide",
                ex.getMessage(),
                request.getRequestURI(),
                LocalDateTime.now()
        );
        
        return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericException(
            Exception ex, HttpServletRequest request) {
        
        log.error("Erreur inattendue sur {}", request.getRequestURI(), ex);
        
        ErrorResponse response = new ErrorResponse(
                HttpStatus.INTERNAL_SERVER_ERROR.value(),
                "Erreur interne",
                "Une erreur inattendue s'est produite",
                request.getRequestURI(),
                LocalDateTime.now()
        );
        
        return ResponseEntity.internalServerError().body(response);
    }
}
```

```java
// src/main/java/com/taskflow/exception/ValidationErrorResponse.java
package com.taskflow.exception;

import java.time.LocalDateTime;
import java.util.Map;

/**
 * Réponse d'erreur enrichie pour les erreurs de validation.
 * Contient le détail des champs invalides.
 */
public record ValidationErrorResponse(
        int status,
        String error,
        Map<String, String> fieldErrors, // Champ -> message d'erreur
        String path,
        LocalDateTime timestamp
) {}
```

### Exemple de réponse d'erreur

```json
// POST /api/tasks
// Body: {"title": "", "description": "..."}

// Réponse 400 Bad Request :
{
  "status": 400,
  "error": "Validation échouée",
  "fieldErrors": {
    "title": "Le titre est obligatoire"
  },
  "path": "/api/tasks",
  "timestamp": "2024-01-15T10:30:00"
}
```

```json
// POST /api/tasks
// Body: {"title": "AB", "description": "...a..."}  (titre trop court)

{
  "status": 400,
  "error": "Validation échouée",
  "fieldErrors": {
    "title": "Le titre doit contenir entre 3 et 100 caractères"
  },
  "path": "/api/tasks",
  "timestamp": "2024-01-15T10:30:00"
}
```

---

## 4.6 Annotations de validation courantes

```java
// Strings
@NotNull         // Pas null
@NotEmpty        // Pas null, pas ""
@NotBlank        // Pas null, pas "", pas "  "
@Size(min=2, max=100)  // Longueur entre min et max
@Pattern(regexp = "^[A-Z]{2}-\\d{4}$", message = "Format: XX-0000")
@Email           // Format email valide

// Nombres
@Positive        // > 0
@PositiveOrZero  // >= 0
@Negative        // < 0
@NegativeOrZero  // <= 0
@Min(value = 1)  // >= 1
@Max(value = 100) // <= 100
@DecimalMin("0.01")
@DecimalMax("999.99")
@Digits(integer = 5, fraction = 2) // Max 5 chiffres entiers, 2 décimaux

// Collections
@Size(min=1, max=10) // Taille de la collection
@NotEmpty            // Collection non vide

// Dates
@Past              // Date dans le passé
@PastOrPresent     // Date passée ou maintenant
@Future            // Date dans le futur
@FutureOrPresent   // Date future ou maintenant

// Booléens
@AssertTrue(message = "Doit être accepté")
@AssertFalse

// Combinaisons
@NotBlank
@Size(max = 255)
@URL(message = "Doit être une URL valide")
private String websiteUrl;
```

---

## 4.7 Validations personnalisées

### Cas d'usage : valider qu'une date de fin est après la date de début

```java
// Étape 1 : Créer l'annotation
// src/main/java/com/taskflow/validation/ValidDateRange.java
package com.taskflow.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

/**
 * Annotation de validation personnalisée.
 * 
 * @Constraint : Lie cette annotation à son implémentation (ValidDateRangeValidator).
 * @Target : Où cette annotation peut être utilisée.
 * @Retention : L'annotation est disponible à l'exécution (nécessaire pour la reflection).
 */
@Documented
@Constraint(validatedBy = ValidDateRangeValidator.class)
@Target({ElementType.TYPE}) // Annotation de classe (pas de champ)
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidDateRange {
    String message() default "La date de fin doit être après la date de début";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    
    String startField() default "startDate";
    String endField() default "endDate";
}
```

```java
// Étape 2 : Implémenter le validateur
// src/main/java/com/taskflow/validation/ValidDateRangeValidator.java
package com.taskflow.validation;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.springframework.beans.BeanWrapperImpl;

import java.time.LocalDate;

public class ValidDateRangeValidator 
        implements ConstraintValidator<ValidDateRange, Object> {
    
    private String startField;
    private String endField;
    
    @Override
    public void initialize(ValidDateRange annotation) {
        this.startField = annotation.startField();
        this.endField = annotation.endField();
    }
    
    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        if (value == null) return true; // null géré par @NotNull
        
        // Utilise la reflection pour accéder aux champs par nom
        BeanWrapperImpl wrapper = new BeanWrapperImpl(value);
        
        Object startValue = wrapper.getPropertyValue(startField);
        Object endValue = wrapper.getPropertyValue(endField);
        
        // Si l'un est null, pas de validation de plage (d'autres annotations gèrent ça)
        if (startValue == null || endValue == null) return true;
        
        if (startValue instanceof LocalDate start && endValue instanceof LocalDate end) {
            boolean valid = !end.isBefore(start);
            
            if (!valid) {
                // Personnaliser le message d'erreur pour pointer vers le bon champ
                context.disableDefaultConstraintViolation();
                context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
                       .addPropertyNode(endField)
                       .addConstraintViolation();
            }
            
            return valid;
        }
        
        return true;
    }
}
```

```java
// Étape 3 : Utiliser l'annotation
// src/main/java/com/taskflow/dto/TaskWithDeadlineDTO.java

@Data
@ValidDateRange(startField = "startDate", endField = "deadline",
                message = "La deadline doit être après la date de début")
public class TaskWithDeadlineDTO {
    
    @NotBlank
    private String title;
    
    @NotNull
    private LocalDate startDate;
    
    @NotNull
    @Future(message = "La deadline doit être dans le futur")
    private LocalDate deadline;
}
```

### Annotation sur un champ : validation de format personnalisé

```java
// Valide un username : lettres, chiffres, tirets, 3-20 caractères
@Documented
@Constraint(validatedBy = UsernameValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidUsername {
    String message() default "Username invalide (3-20 caractères, lettres/chiffres/tiret)";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

public class UsernameValidator implements ConstraintValidator<ValidUsername, String> {
    
    private static final java.util.regex.Pattern USERNAME_PATTERN = 
        java.util.regex.Pattern.compile("^[a-zA-Z0-9-]{3,20}$");
    
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) return true; // @NotNull gère le cas null
        return USERNAME_PATTERN.matcher(value).matches();
    }
}

// Utilisation :
public class CreateTaskDTO {
    @ValidUsername
    private String assignedTo;
}
```

---

## 4.8 Groupes de validation

Parfois, les règles diffèrent selon le contexte (création vs mise à jour) :

```java
// Définir les groupes
public interface OnCreate {}
public interface OnUpdate {}

// DTO avec groupes
@Data
public class TaskDTO {
    
    @NotBlank(groups = OnCreate.class) // Requis à la création
    @Size(min = 3, max = 100)
    private String title;
    
    // Pas de contrainte sur le titre pour les updates (null = ne pas modifier)
}

// Controller avec groupes
@PostMapping
public ResponseEntity<Task> create(
        @Validated(OnCreate.class) @RequestBody TaskDTO dto) {
    // ...
}

@PatchMapping("/{id}")
public ResponseEntity<Task> patch(
        @PathVariable Long id,
        @Validated(OnUpdate.class) @RequestBody TaskDTO dto) {
    // ...
}
```

---

## 4.9 Validation dans le Service

La validation `@Valid` se fait au niveau du Controller (frontière HTTP).
Mais certaines règles métier nécessitent une validation dans le Service :

```java
@Service
public class TaskService {
    
    @Transactional
    public Task assignTask(Long taskId, String username) {
        Task task = getTaskById(taskId);
        
        // Validation métier (pas de Bean Validation possible ici)
        if (task.getStatus() == Task.Status.DONE) {
            throw new IllegalStateException("Impossible d'assigner une tâche terminée");
        }
        
        if (task.getAssignedTo() != null && !task.getAssignedTo().equals(username)) {
            throw new IllegalStateException(
                "La tâche est déjà assignée à " + task.getAssignedTo()
            );
        }
        
        task.setAssignedTo(username);
        return taskRepository.save(task);
    }
}
```

**Règle :**
- **Bean Validation** : format, contraintes structurelles (null, taille, pattern)
- **Validation dans le Service** : règles métier qui dépendent de l'état de l'application

---

## 4.10 Tests de validation

```java
// src/test/java/com/taskflow/controller/TaskControllerValidationTest.java
package com.taskflow.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.taskflow.dto.CreateTaskDTO;
import com.taskflow.service.TaskService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

/**
 * @WebMvcTest : Lance uniquement la couche web (Controller, ExceptionHandler).
 *              Beaucoup plus rapide que @SpringBootTest.
 *              Remplace les autres beans par des mocks.
 */
@WebMvcTest(TaskController.class)
class TaskControllerValidationTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private ObjectMapper objectMapper; // Pour sérialiser en JSON
    
    @MockBean
    private TaskService taskService; // Mock du service (pas besoin d'une vraie implémentation)
    
    @Test
    void createTask_withBlankTitle_shouldReturn400() throws Exception {
        CreateTaskDTO dto = new CreateTaskDTO();
        dto.setTitle(""); // Titre vide -> invalide
        
        mockMvc.perform(post("/api/tasks")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(dto)))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.status").value(400))
                .andExpect(jsonPath("$.fieldErrors.title").value("Le titre est obligatoire"));
    }
    
    @Test
    void createTask_withTitleTooShort_shouldReturn400() throws Exception {
        CreateTaskDTO dto = new CreateTaskDTO();
        dto.setTitle("AB"); // 2 caractères, minimum 3
        
        mockMvc.perform(post("/api/tasks")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(dto)))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.fieldErrors.title").exists());
    }
    
    @Test
    void createTask_withValidData_shouldReturn201() throws Exception {
        CreateTaskDTO dto = new CreateTaskDTO();
        dto.setTitle("Titre valide");
        
        // Mock du service
        Task mockTask = new Task();
        mockTask.setId(1L);
        mockTask.setTitle("Titre valide");
        
        when(taskService.createTask(any())).thenReturn(mockTask);
        
        mockMvc.perform(post("/api/tasks")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(dto)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").value(1));
    }
}
```

---

## Récapitulatif du Module 04

[OK] **@NotBlank vs @NotNull vs @NotEmpty** : comprendre les nuances  
[OK] **@Valid** dans le Controller : déclenche la validation automatique  
[OK] **@Validated** sur la classe : active la validation sur @PathVariable/@RequestParam  
[OK] **MethodArgumentNotValidException** : interceptée par @RestControllerAdvice  
[OK] **Messages d'erreur** : clairs, localisés, avec les valeurs min/max  
[OK] **Validations personnalisées** : @Constraint + ConstraintValidator  
[OK] **Groupes de validation** : règles différentes selon le contexte  
[OK] **@WebMvcTest** : tester la couche Controller sans Spring complet  

### Prochaine étape

Dans le **Module 05**, vous modéliserez des **relations entre entités** : un projet contient plusieurs tâches, une tâche peut avoir des commentaires. Vous découvrirez `@OneToMany`, `@ManyToOne`, `@ManyToMany` et les pièges courants des relations JPA.

# Module 05 : Relations JPA

## Objectifs du module
- Modéliser des relations entre entités (@OneToMany, @ManyToOne, @ManyToMany)
- Comprendre le lazy/eager loading et éviter le problème N+1
- Gérer la sérialisation JSON des entités liées
- Utiliser des DTOs pour exposer les relations

**Durée estimée :** 5-6 heures  
**Prérequis :** Module 04 complété

---

## 5.1 Nouveau modèle de données

On enrichit TaskFlow avec deux nouvelles entités :

```
┌─────────────┐         ┌─────────────┐         ┌─────────────┐
│   Project   │────────>│    Task     │<────────│   Comment   │
│             │  1:N    │             │  1:N    │             │
│ - id        │         │ - id        │         │ - id        │
│ - name      │         │ - title     │         │ - content   │
│ - description│        │ - project   │         │ - author    │
│ - tasks     │         │ - comments  │         │ - task      │
│ - createdAt │         │ - tags      │         │ - createdAt │
└─────────────┘         └──────┬──────┘         └─────────────┘
                               │ N:M
                        ┌──────[BLACK_DOWN-POINTING_TRIANGLE]──────┐
                        │    Tag      │
                        │             │
                        │ - id        │
                        │ - name      │
                        │ - tasks     │
                        └─────────────┘
```

---

## 5.2 @ManyToOne et @OneToMany : Project <-> Task

### L'entité Project

```java
// src/main/java/com/taskflow/model/Project.java
package com.taskflow.model;

import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "projects")
@Data
@NoArgsConstructor
public class Project {
    
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "project_seq")
    @SequenceGenerator(name = "project_seq", sequenceName = "project_sequence", allocationSize = 10)
    private Long id;
    
    @Column(nullable = false, length = 100)
    private String name;
    
    @Column(columnDefinition = "TEXT")
    private String description;
    
    /**
     * @OneToMany : Un projet contient plusieurs tâches.
     * 
     * mappedBy = "project" : 
     *   - La clé étrangère est dans la table "tasks" (champ "project")
     *   - Project est le côté INVERSE de la relation
     *   - Task est le côté PROPRIÉTAIRE (possède la clé étrangère)
     * 
     * cascade = CascadeType.ALL :
     *   - Les opérations sur Project se propagent aux Task
     *   - Si on supprime un Project, toutes ses Task sont supprimées
     *   - CascadeType.PERSIST : si on sauve Project, les nouvelles Task sont sauvées
     *   - CascadeType.MERGE : si on met à jour Project, les Task liées sont mises à jour
     *   - CascadeType.REMOVE : si on supprime Project, les Task sont supprimées
     *   - CascadeType.ALL : toutes les opérations
     *   [ATTENTION] ATTENTION : CascadeType.ALL peut avoir des effets indésirables !
     * 
     * orphanRemoval = true :
     *   - Si on retire une Task de la liste tasks, elle est supprimée de la BDD
     *   - Utile pour "enfants" qui n'ont pas de sens sans leur "parent"
     * 
     * fetch = FetchType.LAZY (défaut pour @OneToMany) :
     *   - Les tasks ne sont PAS chargées avec le projet
     *   - Chargées seulement à l'accès (project.getTasks())
     *   - Évite de charger des milliers de tâches inutilement
     */
    @OneToMany(mappedBy = "project", 
               cascade = CascadeType.ALL,
               orphanRemoval = true,
               fetch = FetchType.LAZY)
    /**
     * @ToString.Exclude : Évite la récursion infinie
     * Project.toString() -> Task.toString() -> Project.toString() -> ...
     */
    @ToString.Exclude
    private List<Task> tasks = new ArrayList<>();
    
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;
    
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
    
    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
    }
    
    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }
    
    // Méthode utilitaire pour maintenir la cohérence bidirectionnelle
    public void addTask(Task task) {
        tasks.add(task);
        task.setProject(this); // Important ! Toujours maintenir les deux côtés
    }
    
    public void removeTask(Task task) {
        tasks.remove(task);
        task.setProject(null);
    }
}
```

### Mise à jour de Task

```java
// Ajouts dans Task.java

@Entity
@Table(name = "tasks")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Task {
    
    // ... champs existants ...
    
    /**
     * @ManyToOne : Plusieurs tâches appartiennent à un projet.
     * 
     * fetch = FetchType.LAZY (recommandé) :
     *   - Le projet n'est PAS chargé automatiquement avec la tâche
     *   - Chargé seulement si on appelle task.getProject()
     *   - FetchType.EAGER chargerait toujours le projet (peut causer des problèmes)
     * 
     * @JoinColumn : Configure la clé étrangère dans la table "tasks"
     *   - name = "project_id" : nom de la colonne dans la table tasks
     *   - nullable = true : une tâche peut ne pas avoir de projet
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "project_id")
    @ToString.Exclude  // Évite la récursion Task -> Project -> Task -> ...
    private Project project;
    
    /**
     * @OneToMany vers Comment.
     */
    @OneToMany(mappedBy = "task", cascade = CascadeType.ALL, orphanRemoval = true)
    @ToString.Exclude
    private List<Comment> comments = new ArrayList<>();
    
    /**
     * @ManyToMany : Une tâche peut avoir plusieurs tags, un tag peut être sur plusieurs tâches.
     * 
     * @JoinTable : Configure la table de jointure (automatiquement créée par JPA)
     *   - name : nom de la table de jointure
     *   - joinColumns : colonne FK vers cette entité (Task)
     *   - inverseJoinColumns : colonne FK vers l'autre entité (Tag)
     */
    @ManyToMany
    @JoinTable(
        name = "task_tags",
        joinColumns = @JoinColumn(name = "task_id"),
        inverseJoinColumns = @JoinColumn(name = "tag_id")
    )
    @ToString.Exclude
    private List<Tag> tags = new ArrayList<>();
    
    // Méthodes utilitaires
    public void addComment(Comment comment) {
        comments.add(comment);
        comment.setTask(this);
    }
    
    public void addTag(Tag tag) {
        tags.add(tag);
        tag.getTasks().add(this);
    }
}
```

### L'entité Comment

```java
// src/main/java/com/taskflow/model/Comment.java
package com.taskflow.model;

import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.time.LocalDateTime;

@Entity
@Table(name = "comments")
@Data
@NoArgsConstructor
public class Comment {
    
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "comment_seq")
    @SequenceGenerator(name = "comment_seq", sequenceName = "comment_sequence", allocationSize = 20)
    private Long id;
    
    @Column(nullable = false, columnDefinition = "TEXT")
    private String content;
    
    @Column(nullable = false, length = 100)
    private String author;
    
    /**
     * Côté propriétaire de la relation Task <-> Comment.
     * La table "comments" contient la colonne "task_id".
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "task_id", nullable = false)
    @ToString.Exclude
    private Task task;
    
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;
    
    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
    }
}
```

### L'entité Tag

```java
// src/main/java/com/taskflow/model/Tag.java
package com.taskflow.model;

import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "tags")
@Data
@NoArgsConstructor
public class Tag {
    
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "tag_seq")
    @SequenceGenerator(name = "tag_seq", sequenceName = "tag_sequence", allocationSize = 10)
    private Long id;
    
    @Column(nullable = false, unique = true, length = 50)
    private String name;
    
    /**
     * Côté inverse de la relation @ManyToMany.
     * mappedBy indique que Tag n'est PAS le propriétaire (Task l'est).
     */
    @ManyToMany(mappedBy = "tags")
    @ToString.Exclude
    private List<Task> tasks = new ArrayList<>();
}
```

---

## 5.3 Le problème N+1 et comment l'éviter

### Le problème

```java
// [X] Requête N+1 : 1 requête pour les projets + N requêtes pour les tâches

List<Project> projects = projectRepository.findAll();
// SQL: SELECT * FROM projects  -> 1 requête

for (Project project : projects) {
    // Pour chaque projet, Hibernate fait une requête pour charger les tâches
    System.out.println(project.getTasks().size());
    // SQL: SELECT * FROM tasks WHERE project_id = ?  -> 1 requête PAR projet
}

// Si 100 projets -> 101 requêtes !
```

### La solution : JOIN FETCH

```java
// [OK] Solution 1 : @Query avec JOIN FETCH
@Repository
public interface ProjectRepository extends JpaRepository<Project, Long> {
    
    /**
     * JOIN FETCH : charge le projet ET ses tâches en une seule requête SQL.
     * SQL généré : SELECT p, t FROM projects p LEFT JOIN tasks t ON t.project_id = p.id
     */
    @Query("SELECT DISTINCT p FROM Project p LEFT JOIN FETCH p.tasks")
    List<Project> findAllWithTasks();
    
    /**
     * Pour une relation plus profonde.
     * Charge projet + tâches + commentaires en une requête.
     */
    @Query("SELECT DISTINCT p FROM Project p " +
           "LEFT JOIN FETCH p.tasks t " +
           "LEFT JOIN FETCH t.comments " +
           "WHERE p.id = :id")
    Optional<Project> findByIdWithTasksAndComments(@Param("id") Long id);
}
```

```java
// [OK] Solution 2 : @EntityGraph (plus déclaratif)
@Repository
public interface TaskRepository extends JpaRepository<Task, Long> {
    
    /**
     * @EntityGraph : Définit quelles relations charger en EAGER pour cette requête.
     * Ne change pas le mapping global (qui reste LAZY).
     */
    @EntityGraph(attributePaths = {"project", "tags", "comments"})
    List<Task> findByCompleted(boolean completed);
}
```

### Outil de diagnostic

```properties
# application.properties : Affiche le SQL et les statistiques Hibernate
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.generate_statistics=true
logging.level.org.hibernate.stat=DEBUG
```

---

## 5.4 Sérialisation JSON et DTOs

### Le problème de la récursion infinie

Sans précaution, la sérialisation JSON peut boucler :

```
Project -> tasks -> [Task1, Task2]
              -> Task1 -> project -> Project -> tasks -> [Task1, Task2] -> ...
```

**Solution : utiliser des DTOs**

```java
// src/main/java/com/taskflow/dto/ProjectDTO.java

/**
 * DTO pour exposer un projet.
 * 
 * On choisit exactement ce qu'on expose :
 * - Les tâches sont représentées par des DTOs légers (pas l'entité complète)
 * - Pas de référence circulaire possible
 */
public record ProjectDTO(
        Long id,
        String name,
        String description,
        int taskCount,
        List<TaskSummaryDTO> tasks,
        LocalDateTime createdAt
) {}

/**
 * Résumé léger d'une tâche (pour éviter de charger toutes les données dans une liste).
 */
public record TaskSummaryDTO(
        Long id,
        String title,
        Task.Status status,
        Task.Priority priority,
        boolean completed
) {}

/**
 * DTO complet d'une tâche avec ses relations.
 */
public record TaskDetailDTO(
        Long id,
        String title,
        String description,
        Task.Status status,
        Task.Priority priority,
        boolean completed,
        String assignedTo,
        String projectName,       // Juste le nom du projet, pas tout l'objet
        List<CommentDTO> comments,
        List<String> tagNames,    // Juste les noms des tags
        LocalDateTime createdAt,
        LocalDateTime updatedAt
) {}

public record CommentDTO(
        Long id,
        String content,
        String author,
        LocalDateTime createdAt
) {}
```

### Service de mapping

```java
// src/main/java/com/taskflow/service/TaskMapper.java
package com.taskflow.service;

import com.taskflow.dto.*;
import com.taskflow.model.*;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.stream.Collectors;

/**
 * Composant de mapping entre entités et DTOs.
 * 
 * Alternative : utiliser MapStruct (génère les mappers automatiquement à la compilation).
 * Pour ce guide, on implémente manuellement pour bien comprendre.
 */
@Component
public class TaskMapper {
    
    public TaskSummaryDTO toSummary(Task task) {
        return new TaskSummaryDTO(
                task.getId(),
                task.getTitle(),
                task.getStatus(),
                task.getPriority(),
                task.isCompleted()
        );
    }
    
    public TaskDetailDTO toDetail(Task task) {
        return new TaskDetailDTO(
                task.getId(),
                task.getTitle(),
                task.getDescription(),
                task.getStatus(),
                task.getPriority(),
                task.isCompleted(),
                task.getAssignedTo(),
                task.getProject() != null ? task.getProject().getName() : null,
                task.getComments().stream()
                        .map(this::toCommentDTO)
                        .collect(Collectors.toList()),
                task.getTags().stream()
                        .map(Tag::getName)
                        .collect(Collectors.toList()),
                task.getCreatedAt(),
                task.getUpdatedAt()
        );
    }
    
    public CommentDTO toCommentDTO(Comment comment) {
        return new CommentDTO(
                comment.getId(),
                comment.getContent(),
                comment.getAuthor(),
                comment.getCreatedAt()
        );
    }
    
    public ProjectDTO toProjectDTO(Project project) {
        List<TaskSummaryDTO> taskSummaries = project.getTasks().stream()
                .map(this::toSummary)
                .collect(Collectors.toList());
        
        return new ProjectDTO(
                project.getId(),
                project.getName(),
                project.getDescription(),
                project.getTasks().size(),
                taskSummaries,
                project.getCreatedAt()
        );
    }
}
```

---

## 5.5 Repository pour les relations

```java
// src/main/java/com/taskflow/repository/TaskRepository.java
@Repository
public interface TaskRepository extends JpaRepository<Task, Long> {
    
    // Tâches d'un projet avec leurs tags (JOIN FETCH pour éviter N+1)
    @Query("SELECT DISTINCT t FROM Task t " +
           "LEFT JOIN FETCH t.tags " +
           "WHERE t.project.id = :projectId")
    List<Task> findByProjectIdWithTags(@Param("projectId") Long projectId);
    
    // Tâches avec un tag spécifique
    @Query("SELECT t FROM Task t JOIN t.tags tag WHERE tag.name = :tagName")
    List<Task> findByTagName(@Param("tagName") String tagName);
    
    // Tâches avec leurs commentaires (pour une tâche unique)
    @Query("SELECT t FROM Task t LEFT JOIN FETCH t.comments WHERE t.id = :id")
    Optional<Task> findByIdWithComments(@Param("id") Long id);
}
```

```java
// src/main/java/com/taskflow/repository/ProjectRepository.java
@Repository
public interface ProjectRepository extends JpaRepository<Project, Long> {
    
    @Query("SELECT DISTINCT p FROM Project p LEFT JOIN FETCH p.tasks")
    List<Project> findAllWithTasks();
    
    Optional<Project> findByName(String name);
}
```

---

## 5.6 Service et Controller pour Project

```java
// src/main/java/com/taskflow/service/ProjectService.java
@Slf4j
@Service
@Transactional(readOnly = true)
public class ProjectService {
    
    private final ProjectRepository projectRepository;
    private final TaskRepository taskRepository;
    private final TaskMapper taskMapper;
    
    public ProjectService(ProjectRepository projectRepository,
                          TaskRepository taskRepository,
                          TaskMapper taskMapper) {
        this.projectRepository = projectRepository;
        this.taskRepository = taskRepository;
        this.taskMapper = taskMapper;
    }
    
    public List<ProjectDTO> getAllProjects() {
        return projectRepository.findAllWithTasks()
                .stream()
                .map(taskMapper::toProjectDTO)
                .collect(Collectors.toList());
    }
    
    public ProjectDTO getProjectById(Long id) {
        Project project = projectRepository.findById(id)
                .orElseThrow(() -> new TaskNotFoundException("Projet introuvable: " + id));
        return taskMapper.toProjectDTO(project);
    }
    
    @Transactional
    public ProjectDTO createProject(CreateProjectDTO dto) {
        Project project = new Project();
        project.setName(dto.name());
        project.setDescription(dto.description());
        return taskMapper.toProjectDTO(projectRepository.save(project));
    }
    
    @Transactional
    public TaskDetailDTO addTaskToProject(Long projectId, CreateTaskDTO dto) {
        Project project = projectRepository.findById(projectId)
                .orElseThrow(() -> new TaskNotFoundException("Projet introuvable: " + projectId));
        
        Task task = new Task();
        task.setTitle(dto.getTitle().trim());
        task.setDescription(dto.getDescription());
        task.setPriority(dto.getPriority() != null ? dto.getPriority() : Task.Priority.MEDIUM);
        
        project.addTask(task); // Maintient la relation des deux côtés
        
        taskRepository.save(task);
        
        return taskMapper.toDetail(task);
    }
}
```

```java
// src/main/java/com/taskflow/controller/ProjectController.java
@RestController
@RequestMapping("/api/projects")
public class ProjectController {
    
    private final ProjectService projectService;
    
    public ProjectController(ProjectService projectService) {
        this.projectService = projectService;
    }
    
    @GetMapping
    public ResponseEntity<List<ProjectDTO>> getAllProjects() {
        return ResponseEntity.ok(projectService.getAllProjects());
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<ProjectDTO> getProject(@PathVariable Long id) {
        return ResponseEntity.ok(projectService.getProjectById(id));
    }
    
    @PostMapping
    public ResponseEntity<ProjectDTO> createProject(
            @Valid @RequestBody CreateProjectDTO dto) {
        return ResponseEntity.status(201).body(projectService.createProject(dto));
    }
    
    @PostMapping("/{projectId}/tasks")
    public ResponseEntity<TaskDetailDTO> addTask(
            @PathVariable Long projectId,
            @Valid @RequestBody CreateTaskDTO dto) {
        return ResponseEntity.status(201)
                .body(projectService.addTaskToProject(projectId, dto));
    }
    
    @GetMapping("/{projectId}/tasks")
    public ResponseEntity<List<TaskSummaryDTO>> getProjectTasks(
            @PathVariable Long projectId) {
        List<Task> tasks = taskRepository.findByProjectIdWithTags(projectId);
        return ResponseEntity.ok(tasks.stream()
                .map(taskMapper::toSummary)
                .collect(Collectors.toList()));
    }
}
```

---

## 5.7 Gestion des tags (ManyToMany)

```java
// src/main/java/com/taskflow/service/TagService.java
@Service
@Transactional(readOnly = true)
public class TagService {
    
    private final TagRepository tagRepository;
    private final TaskRepository taskRepository;
    
    // ...
    
    @Transactional
    public Task addTagToTask(Long taskId, String tagName) {
        Task task = taskRepository.findById(taskId)
                .orElseThrow(() -> new TaskNotFoundException("Tâche introuvable: " + taskId));
        
        // Trouver le tag existant ou en créer un nouveau
        Tag tag = tagRepository.findByNameIgnoreCase(tagName)
                .orElseGet(() -> {
                    Tag newTag = new Tag();
                    newTag.setName(tagName.toLowerCase().trim());
                    return tagRepository.save(newTag);
                });
        
        task.addTag(tag);
        return taskRepository.save(task);
    }
    
    @Transactional
    public Task removeTagFromTask(Long taskId, String tagName) {
        Task task = taskRepository.findById(taskId)
                .orElseThrow(() -> new TaskNotFoundException("Tâche introuvable: " + taskId));
        
        task.getTags().removeIf(tag -> tag.getName().equalsIgnoreCase(tagName));
        return taskRepository.save(task);
    }
    
    public List<Task> getTasksByTag(String tagName) {
        return taskRepository.findByTagName(tagName);
    }
}
```

---

## 5.8 Exercices pratiques

### Exercice 1 : Commentaires

Créez un `CommentController` avec les endpoints :
- `POST /api/tasks/{taskId}/comments` — ajouter un commentaire
- `GET /api/tasks/{taskId}/comments` — lister les commentaires d'une tâche
- `DELETE /api/comments/{commentId}` — supprimer un commentaire

### Exercice 2 : Statistiques de projet

Ajoutez un endpoint `GET /api/projects/{id}/stats` qui retourne :

```json
{
  "projectId": 1,
  "projectName": "TaskFlow",
  "totalTasks": 15,
  "completedTasks": 6,
  "inProgressTasks": 5,
  "todoTasks": 4,
  "completionRate": 40.0,
  "topTags": ["backend", "urgent", "refactoring"]
}
```

### Exercice 3 : Recherche multi-critères

Créez un endpoint `GET /api/tasks` avec les filtres :
- `projectId` — tâches d'un projet spécifique
- `tag` — tâches avec un tag donné
- `assignedTo` — tâches d'une personne
- `completed` — filtre par complétion

Implémentez avec `Specification<Task>` (recherche dans la doc Spring Data).

---

## Récapitulatif du Module 05

[OK] **@OneToMany + @ManyToOne** : relation bidirectionnelle avec mappedBy  
[OK] **@ManyToMany + @JoinTable** : table de jointure gérée automatiquement  
[OK] **FetchType.LAZY** (défaut) vs **FetchType.EAGER** : toujours préférer LAZY  
[OK] **Problème N+1** : diagnostiqué avec les statistiques Hibernate  
[OK] **JOIN FETCH** et **@EntityGraph** : charger les relations efficacement  
[OK] **@ToString.Exclude** : éviter la récursion infinie de Lombok  
[OK] **DTOs** : éviter la sérialisation circulaire et contrôler l'exposition  
[OK] **Méthodes utilitaires** : maintenir la cohérence des deux côtés d'une relation  

### Prochaine étape

Dans le **Module 06**, vous sécuriserez TaskFlow avec **Spring Security et JWT** : authentification, autorisation, protection des endpoints selon les rôles.

# Module 06 : Sécurité avec Spring Security et JWT

## Objectifs du module
- Comprendre le fonctionnement de Spring Security
- Implémenter l'authentification JWT (JSON Web Token)
- Protéger les endpoints selon les rôles
- Gérer l'inscription et la connexion des utilisateurs

**Durée estimée :** 6-8 heures  
**Prérequis :** Module 05 complété

---

## 6.1 Concepts fondamentaux

### Authentification vs Autorisation

```
AUTHENTIFICATION : "Qui êtes-vous ?"
  -> Vérifier l'identité (login/password, token...)
  
AUTORISATION : "Avez-vous le droit ?"
  -> Vérifier les permissions (rôles, claims...)
```

### Le flux JWT

```
┌─────────┐  1. POST /auth/login          ┌─────────────┐
│ CLIENT  │ ─────────────────────────────> │   SERVEUR   │
│         │   {email, password}             │             │
│         │                                 │ Vérifie     │
│         │  2. 200 OK                      │ credentials │
│         │ <───────────────────────────── │             │
│         │   {token: "eyJhbG..."}          │ Génère JWT  │
│         │                                 │             │
│         │  3. GET /api/tasks              │             │
│         │ ─────────────────────────────> │             │
│         │   Authorization: Bearer eyJhbG │             │
│         │                                 │ Valide JWT  │
│         │  4. 200 OK                      │             │
│         │ <───────────────────────────── │             │
│         │   [{id: 1, title: "..."}]       │             │
└─────────┘                                 └─────────────┘
```

### Structure d'un JWT

```
eyJhbGciOiJIUzI1NiJ9                          <- Header (algo + type)
.eyJzdWIiOiJhbGljZUBleGFtcGxlLmNvbSIsImlhdCI6MTcwNTMzMTIwMCwiZXhwIjoxNzA1NDE3NjAwfQ==  <- Payload (claims)
.signature                                     <- Signature (Header.Payload signés avec la clé secrète)

Payload décodé :
{
  "sub": "alice@example.com",    // Subject (identifiant)
  "iat": 1705331200,             // Issued At (émis à)
  "exp": 1705417600,             // Expiration (expire à, 24h plus tard)
  "roles": ["ROLE_USER"]         // Claims personnalisés
}
```

---

## 6.2 Dépendances Maven

```xml
<!-- Spring Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- JWT (Java JWT library) -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.5</version>
    <scope>runtime</scope>
</dependency>
```

---

## 6.3 L'entité User

```java
// src/main/java/com/taskflow/model/User.java
package com.taskflow.model;

import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;

/**
 * Entité utilisateur qui implémente UserDetails (interface Spring Security).
 * 
 * UserDetails est l'interface que Spring Security utilise pour représenter un utilisateur.
 * En l'implémentant, notre entité User devient directement utilisable par Spring Security.
 */
@Entity
@Table(name = "users", 
       uniqueConstraints = @UniqueConstraint(columnNames = "email"))
@Data
@NoArgsConstructor
public class User implements UserDetails {
    
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq")
    @SequenceGenerator(name = "user_seq", sequenceName = "user_sequence", allocationSize = 10)
    private Long id;
    
    @Column(nullable = false, length = 100)
    private String firstName;
    
    @Column(nullable = false, length = 100)
    private String lastName;
    
    @Column(nullable = false, unique = true, length = 150)
    private String email;
    
    /**
     * Le mot de passe est TOUJOURS stocké haché (bcrypt).
     * JAMAIS en clair !
     */
    @Column(nullable = false)
    private String password;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 20)
    private Role role = Role.USER;
    
    private boolean enabled = true;
    
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;
    
    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
    }
    
    // ==================== UserDetails ====================
    
    /**
     * Retourne les rôles de l'utilisateur.
     * Spring Security utilise ROLE_ comme préfixe.
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority("ROLE_" + role.name()));
    }
    
    /**
     * getUsername() est utilisé par Spring Security comme identifiant unique.
     * On utilise l'email (qui est unique).
     */
    @Override
    public String getUsername() {
        return email;
    }
    
    // Les méthodes suivantes permettent de désactiver ou verrouiller des comptes
    @Override public boolean isAccountNonExpired() { return true; }
    @Override public boolean isAccountNonLocked() { return true; }
    @Override public boolean isCredentialsNonExpired() { return true; }
    @Override public boolean isEnabled() { return enabled; }
    
    // Enum des rôles
    public enum Role {
        USER,   // Utilisateur standard
        ADMIN   // Administrateur (peut tout faire)
    }
}
```

---

## 6.4 DTOs d'authentification

```java
// src/main/java/com/taskflow/dto/RegisterDTO.java
public record RegisterDTO(
        @NotBlank String firstName,
        @NotBlank String lastName,
        @NotBlank @Email String email,
        @NotBlank @Size(min = 8, message = "Le mot de passe doit faire au moins 8 caractères")
        @Pattern(regexp = ".*[A-Z].*", message = "Doit contenir au moins une majuscule")
        @Pattern(regexp = ".*\\d.*", message = "Doit contenir au moins un chiffre")
        String password
) {}

// src/main/java/com/taskflow/dto/LoginDTO.java
public record LoginDTO(
        @NotBlank @Email String email,
        @NotBlank String password
) {}

// src/main/java/com/taskflow/dto/AuthResponse.java
public record AuthResponse(
        String token,
        String type,     // "Bearer"
        String email,
        String firstName,
        String lastName,
        String role,
        long expiresIn   // Durée en secondes
) {}
```

---

## 6.5 Service JWT

```java
// src/main/java/com/taskflow/security/JwtService.java
package com.taskflow.security;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

/**
 * Service de gestion des tokens JWT.
 */
@Slf4j
@Service
public class JwtService {
    
    @Value("${app.jwt.secret}")
    private String jwtSecret;
    
    @Value("${app.jwt.expiration:86400000}") // 24h par défaut
    private long jwtExpiration;
    
    /**
     * Génère un token JWT pour un utilisateur.
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> extraClaims = new HashMap<>();
        // Ajouter des informations supplémentaires dans le token
        extraClaims.put("roles", userDetails.getAuthorities().stream()
                .map(a -> a.getAuthority())
                .toList());
        
        return generateToken(extraClaims, userDetails);
    }
    
    private String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
        return Jwts.builder()
                .claims(extraClaims)
                .subject(userDetails.getUsername()) // Email de l'utilisateur
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + jwtExpiration))
                .signWith(getSigningKey())
                .compact();
    }
    
    /**
     * Extrait le subject (email) du token.
     */
    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }
    
    /**
     * Vérifie si un token est valide pour un utilisateur donné.
     */
    public boolean isTokenValid(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }
    
    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }
    
    private Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }
    
    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }
    
    private Claims extractAllClaims(String token) {
        return Jwts.parser()
                .verifyWith(getSigningKey())
                .build()
                .parseSignedClaims(token)
                .getPayload();
    }
    
    private SecretKey getSigningKey() {
        byte[] keyBytes = jwtSecret.getBytes(StandardCharsets.UTF_8);
        return Keys.hmacShaKeyFor(keyBytes);
    }
    
    public long getExpirationTime() {
        return jwtExpiration;
    }
}
```

---

## 6.6 UserDetailsService et filtre JWT

```java
// src/main/java/com/taskflow/security/UserDetailsServiceImpl.java
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    
    private final UserRepository userRepository;
    
    public UserDetailsServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    /**
     * Spring Security appelle cette méthode pour charger un utilisateur par son username.
     * Notre "username" est l'email.
     */
    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        return userRepository.findByEmail(email)
                .orElseThrow(() -> new UsernameNotFoundException(
                    "Utilisateur introuvable: " + email));
    }
}
```

```java
// src/main/java/com/taskflow/security/JwtAuthenticationFilter.java
package com.taskflow.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

/**
 * Filtre JWT : intercepte chaque requête HTTP et vérifie le token.
 * 
 * OncePerRequestFilter garantit que le filtre n'est exécuté qu'une fois par requête.
 */
@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;
    
    public JwtAuthenticationFilter(JwtService jwtService, 
                                   UserDetailsService userDetailsService) {
        this.jwtService = jwtService;
        this.userDetailsService = userDetailsService;
    }
    
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        
        // 1. Extraire le header Authorization
        final String authHeader = request.getHeader("Authorization");
        
        // Si pas de header ou pas de Bearer token -> passer au filtre suivant
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }
        
        // 2. Extraire le token (enlever "Bearer ")
        final String jwt = authHeader.substring(7);
        
        try {
            // 3. Extraire l'email du token
            final String userEmail = jwtService.extractUsername(jwt);
            
            // 4. Si on a un email ET l'utilisateur n'est pas déjà authentifié
            if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                
                // 5. Charger l'utilisateur depuis la base de données
                UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail);
                
                // 6. Vérifier la validité du token
                if (jwtService.isTokenValid(jwt, userDetails)) {
                    
                    // 7. Créer l'objet d'authentification
                    UsernamePasswordAuthenticationToken authToken = 
                        new UsernamePasswordAuthenticationToken(
                            userDetails,
                            null,
                            userDetails.getAuthorities()
                        );
                    
                    authToken.setDetails(
                        new WebAuthenticationDetailsSource().buildDetails(request)
                    );
                    
                    // 8. Mettre à jour le contexte de sécurité
                    // Après ça, Spring Security sait que l'utilisateur est authentifié
                    SecurityContextHolder.getContext().setAuthentication(authToken);
                    
                    log.debug("Utilisateur '{}' authentifié via JWT", userEmail);
                }
            }
        } catch (Exception e) {
            log.warn("Token JWT invalide: {}", e.getMessage());
            // Ne pas retourner d'erreur ici, laisser Spring Security gérer
        }
        
        // 9. Continuer la chaîne de filtres
        filterChain.doFilter(request, response);
    }
}
```

---

## 6.7 Configuration de Spring Security

```java
// src/main/java/com/taskflow/security/SecurityConfig.java
package com.taskflow.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity // Active @PreAuthorize, @PostAuthorize sur les méthodes
public class SecurityConfig {
    
    private final JwtAuthenticationFilter jwtAuthFilter;
    private final UserDetailsServiceImpl userDetailsService;
    
    public SecurityConfig(JwtAuthenticationFilter jwtAuthFilter,
                          UserDetailsServiceImpl userDetailsService) {
        this.jwtAuthFilter = jwtAuthFilter;
        this.userDetailsService = userDetailsService;
    }
    
    /**
     * Chaîne de filtres de sécurité principale.
     * Définit quels endpoints sont publics et lesquels nécessitent une authentification.
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // Désactiver CSRF (inutile pour une API REST stateless)
            .csrf(AbstractHttpConfigurer::disable)
            
            // Configuration des autorisations
            .authorizeHttpRequests(auth -> auth
                // Endpoints publics
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/actuator/health").permitAll()
                .requestMatchers("/v3/api-docs/**", "/swagger-ui/**").permitAll()
                
                // Endpoints admin uniquement
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                
                // Lecture autorisée à tous les utilisateurs connectés
                .requestMatchers(HttpMethod.GET, "/api/tasks/**").hasAnyRole("USER", "ADMIN")
                .requestMatchers(HttpMethod.GET, "/api/projects/**").hasAnyRole("USER", "ADMIN")
                
                // Écriture : USER et ADMIN
                .requestMatchers("/api/tasks/**").hasAnyRole("USER", "ADMIN")
                .requestMatchers("/api/projects/**").hasAnyRole("USER", "ADMIN")
                
                // Toutes les autres requêtes nécessitent une authentification
                .anyRequest().authenticated()
            )
            
            // API REST = stateless, pas de session
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            
            // Utiliser notre provider d'authentification
            .authenticationProvider(authenticationProvider())
            
            // Ajouter notre filtre JWT avant le filtre d'authentification par défaut
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
    
    /**
     * Provider d'authentification : compare le mot de passe fourni avec le hash en base.
     */
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }
    
    /**
     * BCrypt : algorithme de hachage recommandé pour les mots de passe.
     * Le "strength" (ici 12) détermine le coût du calcul (plus élevé = plus sécurisé mais plus lent).
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }
    
    /**
     * AuthenticationManager : orchestre l'authentification.
     * Injecté dans le AuthController.
     */
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}
```

---

## 6.8 Service et Controller d'authentification

```java
// src/main/java/com/taskflow/service/AuthService.java
@Slf4j
@Service
@Transactional
public class AuthService {
    
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final JwtService jwtService;
    private final AuthenticationManager authenticationManager;
    
    // Constructeur...
    
    public AuthResponse register(RegisterDTO dto) {
        // Vérifier que l'email n'est pas déjà pris
        if (userRepository.existsByEmail(dto.email())) {
            throw new EmailAlreadyExistsException("Email déjà utilisé: " + dto.email());
        }
        
        User user = new User();
        user.setFirstName(dto.firstName());
        user.setLastName(dto.lastName());
        user.setEmail(dto.email());
        // Hacher le mot de passe AVANT de sauvegarder !
        user.setPassword(passwordEncoder.encode(dto.password()));
        user.setRole(User.Role.USER);
        
        User savedUser = userRepository.save(user);
        log.info("Nouvel utilisateur enregistré: {}", savedUser.getEmail());
        
        String token = jwtService.generateToken(savedUser);
        return buildAuthResponse(savedUser, token);
    }
    
    public AuthResponse login(LoginDTO dto) {
        // AuthenticationManager vérifie email + password et lance une exception si invalide
        authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(dto.email(), dto.password())
        );
        
        // Si on arrive ici, les credentials sont valides
        User user = userRepository.findByEmail(dto.email())
                .orElseThrow(() -> new UsernameNotFoundException("Utilisateur introuvable"));
        
        String token = jwtService.generateToken(user);
        log.info("Connexion réussie pour: {}", user.getEmail());
        
        return buildAuthResponse(user, token);
    }
    
    private AuthResponse buildAuthResponse(User user, String token) {
        return new AuthResponse(
                token,
                "Bearer",
                user.getEmail(),
                user.getFirstName(),
                user.getLastName(),
                user.getRole().name(),
                jwtService.getExpirationTime() / 1000
        );
    }
}
```

```java
// src/main/java/com/taskflow/controller/AuthController.java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    
    private final AuthService authService;
    
    public AuthController(AuthService authService) {
        this.authService = authService;
    }
    
    @PostMapping("/register")
    public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterDTO dto) {
        return ResponseEntity.status(201).body(authService.register(dto));
    }
    
    @PostMapping("/login")
    public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginDTO dto) {
        return ResponseEntity.ok(authService.login(dto));
    }
    
    /**
     * Endpoint pour récupérer le profil de l'utilisateur connecté.
     * 
     * @AuthenticationPrincipal injecte l'utilisateur actuellement connecté.
     */
    @GetMapping("/me")
    public ResponseEntity<UserProfileDTO> getProfile(
            @AuthenticationPrincipal User currentUser) {
        return ResponseEntity.ok(new UserProfileDTO(
                currentUser.getId(),
                currentUser.getFirstName(),
                currentUser.getLastName(),
                currentUser.getEmail(),
                currentUser.getRole().name()
        ));
    }
}
```

---

## 6.9 Autorisation au niveau des méthodes

Avec `@EnableMethodSecurity`, on peut contrôler finement les accès :

```java
@Service
public class TaskService {
    
    /**
     * @PreAuthorize : Vérifié AVANT l'exécution de la méthode.
     * 
     * hasRole('ADMIN') : L'utilisateur doit avoir ROLE_ADMIN
     * principal.username == ... : L'email doit correspondre
     * '#id == authentication.principal.id' : L'ID doit correspondre à l'utilisateur connecté
     */
    @PreAuthorize("hasRole('ADMIN')")
    public void deleteAllTasks() {
        taskRepository.deleteAll();
    }
    
    /**
     * Un utilisateur peut voir ses propres tâches, un admin peut voir toutes.
     */
    @PreAuthorize("hasRole('ADMIN') or #assignedTo == authentication.name")
    public List<Task> getTasksForUser(String assignedTo) {
        return taskRepository.findByAssignedTo(assignedTo);
    }
    
    /**
     * @PostAuthorize : Vérifié APRÈS l'exécution.
     * Utile pour vérifier si le résultat appartient à l'utilisateur.
     */
    @PostAuthorize("hasRole('ADMIN') or returnObject.assignedTo == authentication.name")
    public Task getTaskById(Long id) {
        return taskRepository.findById(id)
                .orElseThrow(() -> new TaskNotFoundException("Tâche introuvable: " + id));
    }
}
```

---

## 6.10 Configuration application.properties

```properties
# application.properties

# ========================
# SÉCURITÉ JWT
# ========================
# Clé secrète (au moins 256 bits pour HS256 = 32 caractères)
# EN PRODUCTION : utiliser une variable d'environnement !
app.jwt.secret=${JWT_SECRET:MonSecretJWTTresLongEtComplexePourLaDev2024!}
app.jwt.expiration=86400000  # 24 heures en millisecondes
```

```bash
# En production, ne jamais mettre la clé dans application.properties !
# Utiliser une variable d'environnement :
export JWT_SECRET="VotreCleSuperSecrete256BitsMinimum..."
java -jar taskflow.jar
```

---

## 6.11 Tester les endpoints sécurisés

### Avec curl

```bash
# 1. Inscription
curl -X POST http://localhost:8080/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Alice",
    "lastName": "Martin",
    "email": "alice@example.com",
    "password": "Secure123!"
  }'

# Réponse :
# {
#   "token": "eyJhbGciOiJIUzI1NiJ9...",
#   "type": "Bearer",
#   "email": "alice@example.com",
#   ...
# }

# 2. Utiliser le token
TOKEN="eyJhbGciOiJIUzI1NiJ9..."

curl -X GET http://localhost:8080/api/tasks \
  -H "Authorization: Bearer $TOKEN"

# 3. Sans token -> 401 Unauthorized
curl -X GET http://localhost:8080/api/tasks
# {"status": 401, "error": "Unauthorized"}
```

---

## 6.12 Exercices pratiques

### Exercice 1 : Gestion du refresh token

Implémentez un système de refresh token :
- Le token d'accès expire après 15 minutes
- Un refresh token (valide 7 jours) permet d'obtenir un nouveau token d'accès
- Endpoint `POST /api/auth/refresh` avec le refresh token dans le body

### Exercice 2 : Isolation des données par utilisateur

Modifiez le `TaskService` pour que chaque utilisateur ne voie que ses propres tâches. L'admin voit tout. Utilisez `@AuthenticationPrincipal` pour récupérer l'utilisateur connecté.

### Exercice 3 : Rate limiting

Ajoutez une limite de 5 tentatives de connexion échouées avant de verrouiller le compte pendant 15 minutes. Stockez les tentatives en base (ajoutez `loginAttempts` et `lockedUntil` à `User`).

---

## Récapitulatif du Module 06

[OK] **Spring Security** : configuration de la chaîne de filtres  
[OK] **JWT** : génération, validation, extraction des claims  
[OK] **BCrypt** : hachage des mots de passe (jamais en clair !)  
[OK] **Filtre JWT** : intercepte et valide chaque requête  
[OK] **UserDetails** : interface implémentée par notre entité User  
[OK] **@PreAuthorize** : contrôle d'accès au niveau des méthodes  
[OK] **SessionCreationPolicy.STATELESS** : API REST sans session  
[OK] **Variables d'environnement** : ne jamais hardcoder les secrets  

### Prochaine étape

Dans le **Module 07**, vous écrirez des **tests complets** : tests unitaires avec Mockito, tests d'intégration avec @SpringBootTest, et tests de sécurité pour vérifier que les endpoints sont bien protégés.

# Module 07 : Tests Complets

## Objectifs du module
- Tests unitaires avec JUnit 5 et Mockito
- Tests d'intégration avec @SpringBootTest
- Tests de la couche web avec MockMvc
- Tests de sécurité

**Durée estimée :** 5-6 heures

---

## 7.1 La pyramide des tests

```
        /\
       /  \    Tests E2E (bout en bout)
      /    \   -> Lents, fragiles, peu nombreux
     /──────\
    /        \ Tests d'intégration
   /          \ -> Moyens, testent plusieurs composants
  /────────────\
 /              \ Tests unitaires
/                \ -> Rapides, isolés, nombreux
└────────────────┘
```

---

## 7.2 Tests unitaires avec Mockito

```java
// src/test/java/com/taskflow/service/TaskServiceTest.java
package com.taskflow.service;

import com.taskflow.dto.CreateTaskDTO;
import com.taskflow.exception.TaskNotFoundException;
import com.taskflow.model.Task;
import com.taskflow.repository.TaskJpaRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

/**
 * @ExtendWith(MockitoExtension.class) : Initialise les mocks automatiquement.
 * Remplace @RunWith(MockitoJUnitRunner.class) de JUnit 4.
 */
@ExtendWith(MockitoExtension.class)
class TaskServiceTest {
    
    /**
     * @Mock : Crée un faux objet (mock) qui remplace la vraie implémentation.
     * Toutes les méthodes retournent des valeurs par défaut (null, 0, false, etc.)
     * sauf si on les configure avec when().
     */
    @Mock
    private TaskJpaRepository taskRepository;
    
    /**
     * @InjectMocks : Crée une vraie instance de TaskService et injecte les mocks.
     */
    @InjectMocks
    private TaskService taskService;
    
    private Task sampleTask;
    
    @BeforeEach
    void setUp() {
        sampleTask = new Task();
        sampleTask.setId(1L);
        sampleTask.setTitle("Apprendre Spring Boot");
        sampleTask.setCompleted(false);
        sampleTask.setStatus(Task.Status.TODO);
        sampleTask.setPriority(Task.Priority.HIGH);
    }
    
    // ==================== Tests de getAllTasks ====================
    
    @Test
    void getAllTasks_withNoFilters_shouldReturnAllTasks() {
        // GIVEN
        List<Task> tasks = List.of(sampleTask);
        when(taskRepository.findAll(any(org.springframework.data.domain.Sort.class)))
                .thenReturn(tasks);
        
        // WHEN
        List<Task> result = taskService.getAllTasks(null, null);
        
        // THEN
        assertThat(result).hasSize(1);
        assertThat(result.get(0).getTitle()).isEqualTo("Apprendre Spring Boot");
        
        // Vérifier que le repository a bien été appelé
        verify(taskRepository, times(1)).findAll(any(org.springframework.data.domain.Sort.class));
        verifyNoMoreInteractions(taskRepository);
    }
    
    @Test
    void getAllTasks_withCompletedFilter_shouldCallFindByCompleted() {
        // GIVEN
        when(taskRepository.findByCompleted(true)).thenReturn(List.of());
        
        // WHEN
        taskService.getAllTasks(true, null);
        
        // THEN
        verify(taskRepository).findByCompleted(true);
        verify(taskRepository, never()).findAll(any(org.springframework.data.domain.Sort.class));
    }
    
    // ==================== Tests de getTaskById ====================
    
    @Test
    void getTaskById_whenTaskExists_shouldReturnTask() {
        // GIVEN
        when(taskRepository.findById(1L)).thenReturn(Optional.of(sampleTask));
        
        // WHEN
        Task result = taskService.getTaskById(1L);
        
        // THEN
        assertThat(result).isNotNull();
        assertThat(result.getId()).isEqualTo(1L);
        assertThat(result.getTitle()).isEqualTo("Apprendre Spring Boot");
    }
    
    @Test
    void getTaskById_whenTaskNotFound_shouldThrowTaskNotFoundException() {
        // GIVEN
        when(taskRepository.findById(999L)).thenReturn(Optional.empty());
        
        // WHEN & THEN
        assertThatThrownBy(() -> taskService.getTaskById(999L))
                .isInstanceOf(TaskNotFoundException.class)
                .hasMessageContaining("999");
        
        verify(taskRepository).findById(999L);
    }
    
    // ==================== Tests de createTask ====================
    
    @Test
    void createTask_shouldSaveTaskWithCorrectValues() {
        // GIVEN
        CreateTaskDTO dto = new CreateTaskDTO();
        dto.setTitle("  Ma nouvelle tâche  "); // Titre avec espaces
        dto.setPriority(Task.Priority.HIGH);
        
        // Configurer le mock pour retourner la tâche avec un ID
        when(taskRepository.save(any(Task.class))).thenAnswer(invocation -> {
            Task task = invocation.getArgument(0);
            task.setId(42L); // Simuler la génération d'ID
            return task;
        });
        
        // WHEN
        Task result = taskService.createTask(dto);
        
        // THEN
        assertThat(result.getId()).isEqualTo(42L);
        assertThat(result.getTitle()).isEqualTo("Ma nouvelle tâche"); // Trimmed !
        assertThat(result.getPriority()).isEqualTo(Task.Priority.HIGH);
        assertThat(result.isCompleted()).isFalse();
        assertThat(result.getStatus()).isEqualTo(Task.Status.TODO);
        
        // Vérifier que save a été appelé avec une tâche correctement configurée
        verify(taskRepository).save(argThat(task ->
                task.getTitle().equals("Ma nouvelle tâche") &&
                !task.isCompleted() &&
                task.getStatus() == Task.Status.TODO
        ));
    }
    
    @Test
    void createTask_withoutPriority_shouldDefaultToMedium() {
        // GIVEN
        CreateTaskDTO dto = new CreateTaskDTO();
        dto.setTitle("Tâche sans priorité");
        // Pas de priorité définie
        
        when(taskRepository.save(any())).thenAnswer(i -> i.getArgument(0));
        
        // WHEN
        Task result = taskService.createTask(dto);
        
        // THEN
        assertThat(result.getPriority()).isEqualTo(Task.Priority.MEDIUM);
    }
    
    // ==================== Tests de markTaskAsDone ====================
    
    @Test
    void markTaskAsDone_whenTaskNotCompleted_shouldMarkAsCompleted() {
        // GIVEN
        sampleTask.setCompleted(false);
        when(taskRepository.findById(1L)).thenReturn(Optional.of(sampleTask));
        when(taskRepository.save(any())).thenAnswer(i -> i.getArgument(0));
        
        // WHEN
        Task result = taskService.markTaskAsDone(1L);
        
        // THEN
        assertThat(result.isCompleted()).isTrue();
        assertThat(result.getStatus()).isEqualTo(Task.Status.DONE);
    }
    
    @Test
    void markTaskAsDone_whenAlreadyCompleted_shouldThrowIllegalStateException() {
        // GIVEN
        sampleTask.setCompleted(true); // Déjà terminée
        when(taskRepository.findById(1L)).thenReturn(Optional.of(sampleTask));
        
        // WHEN & THEN
        assertThatThrownBy(() -> taskService.markTaskAsDone(1L))
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining("déjà terminée");
        
        // Vérifier que save n'a PAS été appelé (la tâche n'a pas été modifiée)
        verify(taskRepository, never()).save(any());
    }
    
    // ==================== Tests de deleteTask ====================
    
    @Test
    void deleteTask_whenTaskExists_shouldDeleteSuccessfully() {
        // GIVEN
        when(taskRepository.existsById(1L)).thenReturn(true);
        
        // WHEN
        taskService.deleteTask(1L);
        
        // THEN
        verify(taskRepository).deleteById(1L);
    }
    
    @Test
    void deleteTask_whenTaskNotFound_shouldThrowTaskNotFoundException() {
        // GIVEN
        when(taskRepository.existsById(999L)).thenReturn(false);
        
        // WHEN & THEN
        assertThatThrownBy(() -> taskService.deleteTask(999L))
                .isInstanceOf(TaskNotFoundException.class);
        
        verify(taskRepository, never()).deleteById(any());
    }
}
```

---

## 7.3 Tests de la couche Controller (MockMvc)

```java
// src/test/java/com/taskflow/controller/TaskControllerTest.java
@WebMvcTest(TaskController.class)
@Import(SecurityConfig.class) // Inclure la config de sécurité
class TaskControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @MockBean
    private TaskService taskService;
    
    @MockBean
    private JwtService jwtService;
    
    @MockBean
    private UserDetailsServiceImpl userDetailsService;
    
    // Token de test simulé
    private static final String TEST_TOKEN = "Bearer test-token";
    
    @BeforeEach
    void setUp() {
        // Configurer le mock JWT pour accepter le token de test
        when(jwtService.extractUsername(anyString())).thenReturn("alice@example.com");
        when(jwtService.isTokenValid(anyString(), any())).thenReturn(true);
        
        User testUser = new User();
        testUser.setEmail("alice@example.com");
        testUser.setRole(User.Role.USER);
        when(userDetailsService.loadUserByUsername("alice@example.com")).thenReturn(testUser);
    }
    
    @Test
    void getAllTasks_shouldReturn200WithTasks() throws Exception {
        // GIVEN
        List<Task> tasks = List.of(
                createTask(1L, "Tâche 1"),
                createTask(2L, "Tâche 2")
        );
        when(taskService.getAllTasks(null, null)).thenReturn(tasks);
        
        // WHEN & THEN
        mockMvc.perform(get("/api/tasks")
                        .header("Authorization", TEST_TOKEN))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$").isArray())
                .andExpect(jsonPath("$.length()").value(2))
                .andExpect(jsonPath("$[0].title").value("Tâche 1"))
                .andExpect(jsonPath("$[1].title").value("Tâche 2"));
    }
    
    @Test
    void getAllTasks_withoutToken_shouldReturn401() throws Exception {
        mockMvc.perform(get("/api/tasks"))
                .andExpect(status().isUnauthorized());
    }
    
    @Test
    void createTask_withValidBody_shouldReturn201() throws Exception {
        // GIVEN
        CreateTaskDTO dto = new CreateTaskDTO();
        dto.setTitle("Nouvelle tâche");
        
        Task createdTask = createTask(1L, "Nouvelle tâche");
        when(taskService.createTask(any())).thenReturn(createdTask);
        
        // WHEN & THEN
        mockMvc.perform(post("/api/tasks")
                        .header("Authorization", TEST_TOKEN)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(dto)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.title").value("Nouvelle tâche"));
    }
    
    @Test
    void createTask_withBlankTitle_shouldReturn400() throws Exception {
        // GIVEN
        CreateTaskDTO dto = new CreateTaskDTO();
        dto.setTitle(""); // Invalide
        
        // WHEN & THEN
        mockMvc.perform(post("/api/tasks")
                        .header("Authorization", TEST_TOKEN)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(dto)))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.fieldErrors.title").exists());
    }
    
    @Test
    void getTaskById_whenNotFound_shouldReturn404() throws Exception {
        // GIVEN
        when(taskService.getTaskById(999L))
                .thenThrow(new TaskNotFoundException("Tâche introuvable avec l'ID: 999"));
        
        // WHEN & THEN
        mockMvc.perform(get("/api/tasks/999")
                        .header("Authorization", TEST_TOKEN))
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("$.status").value(404))
                .andExpect(jsonPath("$.message").value("Tâche introuvable avec l'ID: 999"));
    }
    
    private Task createTask(Long id, String title) {
        Task task = new Task();
        task.setId(id);
        task.setTitle(title);
        task.setStatus(Task.Status.TODO);
        task.setPriority(Task.Priority.MEDIUM);
        return task;
    }
}
```

---

## 7.4 Tests d'intégration

```java
// src/test/java/com/taskflow/integration/TaskIntegrationTest.java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@Transactional // Rollback après chaque test
class TaskIntegrationTest {
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Autowired
    private TaskJpaRepository taskRepository;
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private AuthService authService;
    
    private String userToken;
    
    @BeforeEach
    void setUp() {
        // Créer un utilisateur de test et obtenir son token
        RegisterDTO registerDTO = new RegisterDTO(
                "Test", "User", "test@example.com", "Test123!"
        );
        AuthResponse authResponse = authService.register(registerDTO);
        userToken = "Bearer " + authResponse.token();
    }
    
    @Test
    void createAndRetrieveTask_fullFlow() {
        // GIVEN
        CreateTaskDTO createDTO = new CreateTaskDTO();
        createDTO.setTitle("Tâche d'intégration");
        createDTO.setPriority(Task.Priority.HIGH);
        
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", userToken);
        HttpEntity<CreateTaskDTO> request = new HttpEntity<>(createDTO, headers);
        
        // WHEN : Créer la tâche
        ResponseEntity<Task> createResponse = restTemplate.postForEntity(
                "/api/tasks", request, Task.class
        );
        
        // THEN : Vérifier la création
        assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        Task createdTask = createResponse.getBody();
        assertThat(createdTask).isNotNull();
        assertThat(createdTask.getId()).isNotNull();
        
        // WHEN : Récupérer la tâche créée
        HttpEntity<Void> getRequest = new HttpEntity<>(headers);
        ResponseEntity<Task> getResponse = restTemplate.exchange(
                "/api/tasks/" + createdTask.getId(),
                HttpMethod.GET,
                getRequest,
                Task.class
        );
        
        // THEN : Vérifier la récupération
        assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(getResponse.getBody().getTitle()).isEqualTo("Tâche d'intégration");
        
        // Vérifier en base de données
        assertThat(taskRepository.findById(createdTask.getId())).isPresent();
    }
}
```

---

## Récapitulatif du Module 07

[OK] **@ExtendWith(MockitoExtension.class)** : Tests unitaires sans Spring  
[OK] **@Mock + @InjectMocks** : Isoler le composant testé  
[OK] **when().thenReturn()** : Configurer les comportements mock  
[OK] **verify()** : Vérifier que les méthodes ont été appelées  
[OK] **@WebMvcTest** : Tester uniquement la couche Controller  
[OK] **MockMvc** : Simuler des requêtes HTTP sans serveur réel  
[OK] **@SpringBootTest** : Tests d'intégration avec le contexte complet  
[OK] **@Transactional** sur les tests : Rollback automatique  

---

---

# Module 08 : Documentation API avec Swagger/OpenAPI

## Objectifs du module
- Documenter l'API automatiquement avec SpringDoc
- Ajouter des descriptions et exemples
- Gérer l'authentification dans Swagger UI

**Durée estimée :** 2-3 heures

---

## 8.1 Dépendance Maven

```xml
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.3.0</version>
</dependency>
```

Accès automatique à :
- **Swagger UI** : http://localhost:8080/swagger-ui.html
- **OpenAPI JSON** : http://localhost:8080/v3/api-docs

---

## 8.2 Configuration globale

```java
// src/main/java/com/taskflow/config/OpenApiConfig.java
@Configuration
public class OpenApiConfig {
    
    @Bean
    public OpenAPI taskFlowOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("TaskFlow API")
                        .description("API REST pour la gestion de tâches et projets")
                        .version("v1.0.0")
                        .contact(new Contact()
                                .name("TaskFlow Team")
                                .email("contact@taskflow.com"))
                        .license(new License()
                                .name("MIT")
                                .url("https://opensource.org/licenses/MIT")))
                // Configurer l'authentification Bearer dans Swagger UI
                .components(new Components()
                        .addSecuritySchemes("bearerAuth",
                                new SecurityScheme()
                                        .type(SecurityScheme.Type.HTTP)
                                        .scheme("bearer")
                                        .bearerFormat("JWT")
                                        .description("Entrez votre token JWT")))
                .addSecurityItem(new SecurityRequirement().addList("bearerAuth"));
    }
}
```

---

## 8.3 Annotations sur les Controllers

```java
@RestController
@RequestMapping("/api/tasks")
@Tag(name = "Tasks", description = "Gestion des tâches")
public class TaskController {
    
    @Operation(
        summary = "Lister toutes les tâches",
        description = "Récupère la liste des tâches avec filtres optionnels",
        responses = {
            @ApiResponse(responseCode = "200", description = "Liste récupérée avec succès",
                         content = @Content(array = @ArraySchema(
                                 schema = @Schema(implementation = Task.class)))),
            @ApiResponse(responseCode = "401", description = "Non authentifié",
                         content = @Content(schema = @Schema(hidden = true)))
        }
    )
    @GetMapping
    public ResponseEntity<List<Task>> getAllTasks(
            @Parameter(description = "Filtrer par complétion") 
            @RequestParam(required = false) Boolean completed,
            @Parameter(description = "Filtrer par priorité", 
                       example = "HIGH",
                       schema = @Schema(allowableValues = {"LOW", "MEDIUM", "HIGH", "URGENT"}))
            @RequestParam(required = false) String priority) {
        return ResponseEntity.ok(taskService.getAllTasks(completed, priority));
    }
    
    @Operation(summary = "Créer une tâche")
    @ApiResponse(responseCode = "201", description = "Tâche créée")
    @ApiResponse(responseCode = "400", description = "Données invalides")
    @PostMapping
    public ResponseEntity<Task> createTask(
            @io.swagger.v3.oas.annotations.parameters.RequestBody(
                description = "Données de la nouvelle tâche",
                required = true,
                content = @Content(examples = @ExampleObject(value = """
                    {
                        "title": "Implémenter la feature X",
                        "description": "Description détaillée...",
                        "priority": "HIGH",
                        "assignedTo": "alice"
                    }
                    """)))
            @Valid @RequestBody CreateTaskDTO dto) {
        return ResponseEntity.status(201).body(taskService.createTask(dto));
    }
}
```

---

## 8.4 Annoter les modèles

```java
@Schema(description = "Données pour créer une tâche")
@Data
public class CreateTaskDTO {
    
    @Schema(description = "Titre de la tâche", 
            example = "Implémenter l'authentification JWT",
            minLength = 3, 
            maxLength = 100)
    @NotBlank
    @Size(min = 3, max = 100)
    private String title;
    
    @Schema(description = "Description détaillée (optionnel)", 
            example = "Utiliser Spring Security avec jjwt...")
    private String description;
    
    @Schema(description = "Priorité de la tâche", 
            example = "HIGH",
            defaultValue = "MEDIUM")
    private Task.Priority priority;
    
    @Schema(description = "Personne assignée (optionnel)", example = "alice")
    private String assignedTo;
}
```

---

## 8.5 Accès Swagger UI avec authentification

1. Ouvrez http://localhost:8080/swagger-ui.html
2. Faites `POST /api/auth/login` pour obtenir un token
3. Cliquez sur **Authorize** (cadenas en haut à droite)
4. Entrez `Bearer votre_token_ici`
5. Tous les endpoints utilisent maintenant votre token

---

## 8.6 Configuration application.properties

```properties
# Documentation API
springdoc.api-docs.path=/v3/api-docs
springdoc.swagger-ui.path=/swagger-ui.html
springdoc.swagger-ui.operationsSorter=alpha  # Trier par ordre alphabétique
springdoc.swagger-ui.tagsSorter=alpha
springdoc.swagger-ui.displayRequestDuration=true
```

---

## Récapitulatif du Module 08

[OK] **SpringDoc** : génère automatiquement la documentation OpenAPI  
[OK] **Swagger UI** : interface interactive pour tester l'API  
[OK] **@Operation, @ApiResponse** : documenter les endpoints  
[OK] **@Schema** : documenter les modèles et champs  
[OK] **SecurityScheme** : authentification JWT dans Swagger UI  

---

---

# Module 09 : Déploiement avec Docker

## Objectifs du module
- Conteneuriser l'application avec Docker
- Orchestrer avec docker-compose
- Préparer pour la production

**Durée estimée :** 4-5 heures

---

## 9.1 Dockerfile

```dockerfile
# Dockerfile
# ─────────────────────────────────────────────
# ÉTAPE 1 : Build (Maven)
# ─────────────────────────────────────────────
FROM eclipse-temurin:17-jdk-alpine AS builder

WORKDIR /app

# Copier les fichiers Maven en premier (cache des dépendances)
COPY pom.xml .
COPY .mvn .mvn
COPY mvnw .
RUN chmod +x mvnw

# Télécharger les dépendances (mis en cache si pom.xml n'a pas changé)
RUN ./mvnw dependency:go-offline -q

# Copier le code source et builder
COPY src ./src
RUN ./mvnw package -DskipTests -q

# ─────────────────────────────────────────────
# ÉTAPE 2 : Image finale (JRE only, plus légère)
# ─────────────────────────────────────────────
FROM eclipse-temurin:17-jre-alpine

WORKDIR /app

# Utilisateur non-root pour la sécurité
RUN addgroup -S taskflow && adduser -S taskflow -G taskflow

# Copier le JAR depuis l'étape de build
COPY --from=builder /app/target/*.jar app.jar

# Changer le propriétaire
RUN chown taskflow:taskflow app.jar
USER taskflow

# Port exposé
EXPOSE 8080

# Optimisations JVM pour les conteneurs
ENTRYPOINT ["java", \
            "-XX:+UseContainerSupport", \
            "-XX:MaxRAMPercentage=75.0", \
            "-Djava.security.egd=file:/dev/./urandom", \
            "-jar", "app.jar"]
```

---

## 9.2 docker-compose.yml complet

```yaml
# docker-compose.yml
version: '3.8'

services:
  
  # ──────────────────────────────────────
  # BASE DE DONNÉES
  # ──────────────────────────────────────
  db:
    image: postgres:16-alpine
    container_name: taskflow-db
    environment:
      POSTGRES_DB: taskflow
      POSTGRES_USER: ${DB_USER:-taskflow_user}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-taskflow_pass}
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-taskflow_user} -d taskflow"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - taskflow-network

  # ──────────────────────────────────────
  # APPLICATION SPRING BOOT
  # ──────────────────────────────────────
  app:
    build: .
    container_name: taskflow-app
    ports:
      - "8080:8080"
    environment:
      # Base de données
      SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/taskflow
      SPRING_DATASOURCE_USERNAME: ${DB_USER:-taskflow_user}
      SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD:-taskflow_pass}
      
      # JPA
      SPRING_JPA_HIBERNATE_DDL_AUTO: update
      SPRING_JPA_SHOW_SQL: "false"
      
      # JWT (OBLIGATOIRE en production : changer cette valeur !)
      APP_JWT_SECRET: ${JWT_SECRET:-ChangezMoiEnProductionAvecUneValeurDe256BitsMinimum!}
      APP_JWT_EXPIRATION: 86400000
      
      # Profil
      SPRING_PROFILES_ACTIVE: prod
    depends_on:
      db:
        condition: service_healthy  # Attendre que PostgreSQL soit prêt
    networks:
      - taskflow-network
    restart: unless-stopped

  # ──────────────────────────────────────
  # PROXY INVERSE (optionnel, recommandé)
  # ──────────────────────────────────────
  nginx:
    image: nginx:alpine
    container_name: taskflow-nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro  # Certificats SSL
    depends_on:
      - app
    networks:
      - taskflow-network

volumes:
  postgres_data:

networks:
  taskflow-network:
    driver: bridge
```

---

## 9.3 Variables d'environnement (.env)

```bash
# .env (NE PAS COMMITTER CE FICHIER !)
# Ajoutez .env à votre .gitignore

DB_USER=taskflow_user
DB_PASSWORD=MonMotDePasseSecurise123!
JWT_SECRET=MaCleJWTSuperSecreteDe256BitsMinimumPourLaProduction2024!
```

```bash
# .gitignore
.env
*.env
```

---

## 9.4 Configuration nginx.conf

```nginx
# nginx/nginx.conf
upstream taskflow {
    server app:8080;
}

server {
    listen 80;
    server_name taskflow.example.com;
    
    # Rediriger vers HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl;
    server_name taskflow.example.com;
    
    ssl_certificate /etc/nginx/ssl/taskflow.crt;
    ssl_certificate_key /etc/nginx/ssl/taskflow.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    
    location / {
        proxy_pass http://taskflow;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # Timeouts
        proxy_connect_timeout 30s;
        proxy_read_timeout 60s;
    }
}
```

---

## 9.5 Spring Actuator (monitoring)

```xml
<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
```

```properties
# application-prod.properties
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=when-authorized
management.info.env.enabled=true

info.app.name=TaskFlow
info.app.version=@project.version@
info.app.description=API de gestion de tâches
```

Endpoints disponibles :
- `GET /actuator/health` -> État de l'application et de la base de données
- `GET /actuator/info` -> Informations sur l'application
- `GET /actuator/metrics` -> Métriques (mémoire, requêtes, etc.)

---

## 9.6 Commandes Docker utiles

```bash
# Build et démarrer
docker compose up -d --build

# Voir les logs
docker compose logs -f app

# Redémarrer l'application (sans reconstruire)
docker compose restart app

# Arrêter
docker compose down

# Arrêter et supprimer les données
docker compose down -v

# Entrer dans le conteneur
docker exec -it taskflow-app sh

# Vérifier l'état de santé
curl http://localhost:8080/actuator/health
```

---

## 9.7 Configuration production (application-prod.properties)

```properties
# application-prod.properties

# Pas d'affichage SQL en production
spring.jpa.show-sql=false
spring.jpa.hibernate.ddl-auto=validate  # Valider, ne PAS modifier le schéma !

# Logging minimal
logging.level.root=WARN
logging.level.com.taskflow=INFO

# Swagger désactivé en production (optionnel)
springdoc.swagger-ui.enabled=false
springdoc.api-docs.enabled=false

# Connection pool optimisé
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000
```

---

## 9.8 Exercices pratiques

### Exercice 1 : Multi-stage build optimisé

Le Dockerfile actuel peut être optimisé. Ajoutez une couche de cache pour les dépendances Maven en copiant d'abord le pom.xml, en téléchargeant les dépendances, puis en copiant le code source.

### Exercice 2 : Health check personnalisé

Créez un `HealthIndicator` personnalisé qui vérifie qu'au moins un utilisateur admin existe dans la base de données.

### Exercice 3 : Backup automatique

Créez un script bash et un service docker-compose qui effectue un backup automatique de PostgreSQL toutes les 24 heures en utilisant `pg_dump`.

---

## Récapitulatif du Module 09

[OK] **Multi-stage Dockerfile** : image légère avec JRE uniquement  
[OK] **docker-compose** : orchestration de l'application complète  
[OK] **Variables d'environnement** : configuration sans hardcoding  
[OK] **Nginx** : proxy inverse avec SSL  
[OK] **Spring Actuator** : health checks et métriques  
[OK] **application-prod.properties** : configuration de production  
[OK] **.env et .gitignore** : ne jamais committer les secrets  

---

---

# [BRAVO] Félicitations ! Vous avez terminé le guide TaskFlow

## Ce que vous avez appris

Sur les **10 modules** de ce guide, vous avez construit une API REST professionnelle complète avec :

**Architecture solide :**
- Séparation Controller -> Service -> Repository
- Injection de dépendances avec Spring IoC
- DTOs pour découpler les couches

**Persistance :**
- JPA/Hibernate avec Spring Data JPA
- Relations (OneToMany, ManyToMany)
- Requêtes optimisées (JOIN FETCH, EntityGraph)

**Qualité :**
- Validation avec Bean Validation
- Gestion des erreurs cohérente
- Tests unitaires et d'intégration

**Sécurité :**
- Authentification JWT
- Autorisation par rôles
- Mots de passe hashés

**Production :**
- Documentation Swagger/OpenAPI
- Conteneurisation Docker
- Monitoring avec Actuator

## Prochaines étapes

1. **Améliorez TaskFlow** : Ajoutez des fonctionnalités (notifications par email, export CSV, intégration Slack...)

2. **Explorez l'écosystème Spring** :
   - **Spring Batch** : Traitement de données en masse
   - **Spring WebFlux** : API réactive (non-bloquante)
   - **Spring Cloud** : Microservices, Service Discovery, API Gateway

3. **CI/CD** : Automatisez les déploiements avec GitHub Actions ou GitLab CI

4. **Monitoring avancé** : Intégrez Prometheus + Grafana pour visualiser les métriques

Bonne continuation ! [RAPIDE]