# [COURS] Maîtriser Jakarta EE — Cours Complet Fil Rouge
## Guide du Grand Débutant -> Expert

---

## [IMPORTANT] À propos de ce cours

Ce cours est conçu pour vous emmener, **pas à pas**, de zéro connaissance en Java Enterprise jusqu'au niveau **expert** capable de concevoir, développer et déployer des applications d'entreprise complètes en production.

Chaque notion est expliquée **en détail**, accompagnée de **code commenté**, d'**exercices progressifs**, et surtout reliée à un **projet fil rouge réel** qui grandit tout au long de la formation.

---

## [SPOOL_OF_THREAD] Le Projet Fil Rouge : **EduShop**

### Qu'est-ce qu'EduShop ?

**EduShop** est une plateforme e-commerce de vente de formations en ligne. C'est votre projet central : vous allez le construire brique par brique, en ajoutant des fonctionnalités à chaque nouveau chapitre.

### Pourquoi un projet fil rouge ?
- Vous ne construisez pas des "exercices isolés" sans sens.
- Vous voyez **comment les technologies s'imbriquent** dans un vrai contexte.
- À la fin, vous avez **un projet complet** à montrer à un employeur.

### [WORLD_MAP] Évolution d'EduShop au fil du cours

```
Chapitre 7-8  -> EduShop v0.1 : Application web basique (Servlet + JSP)
                 -> Affichage de formations, ajout au panier (session)

Chapitre 9-11 -> EduShop v0.2 : Persistance des données (JPA + MySQL)
                 -> Formations en base de données, gestion des commandes

Chapitre 12   -> EduShop v0.3 : Injection de dépendances (CDI)
                 -> Services métier découplés et testables

Chapitre 13   -> EduShop v0.4 : Sécurité (JWT + Authentification)
                 -> Espace membre, rôles admin/étudiant

Chapitre 14-15-> EduShop v0.5 : API REST complète (JAX-RS)
                 -> API RESTful documentée avec OpenAPI

Chapitre 16   -> EduShop v0.6 : Transactions & EJB
                 -> Paiements transactionnels sécurisés

Chapitre 17   -> EduShop v0.7 : Messaging asynchrone (JMS)
                 -> Notifications email, traitement des commandes

Chapitre 18-19-> EduShop v0.8 : Couverture de tests complète
                 -> Tests unitaires + intégration

Chapitre 20-22-> EduShop v0.9 : Déploiement Docker + CI/CD
                 -> Conteneurisation et pipeline automatisé

Chapitre 23-30-> EduShop v1.0 : Architecture Microservices
                 -> Décomposition en services indépendants déployés en cloud
```

### [CONSTRUCTION] Architecture finale d'EduShop

```
┌─────────────────────────────────────────────────────────────┐
│                      FRONTEND (React / JSP)                  │
└──────────────────────────┬──────────────────────────────────┘
                           │ HTTPS
┌──────────────────────────[BLACK_DOWN-POINTING_TRIANGLE]──────────────────────────────────┐
│                     API GATEWAY (Nginx)                       │
└──────┬───────────────────┬──────────────────────┬───────────┘
       │                   │                      │
┌──────[BLACK_DOWN-POINTING_TRIANGLE]──────┐   ┌────────[BLACK_DOWN-POINTING_TRIANGLE]────────┐   ┌────────[BLACK_DOWN-POINTING_TRIANGLE]────────┐
│  Auth       │   │  Catalogue      │   │  Commandes      │
│  Service    │   │  Service        │   │  Service        │
│  (JWT/KC)   │   │  (JAX-RS+JPA)   │   │  (EJB+JMS)      │
└──────┬──────┘   └────────┬────────┘   └────────┬────────┘
       │                   │                      │
┌──────[BLACK_DOWN-POINTING_TRIANGLE]───────────────────[BLACK_DOWN-POINTING_TRIANGLE]──────────────────────[BLACK_DOWN-POINTING_TRIANGLE]─────────┐
│              Base de données (PostgreSQL)                    │
│              Message Broker (ActiveMQ)                       │
│              Cache (Redis)                                   │
└────────────────────────────────────────────────────────────┘
```

---

## [PACKAGE] Prérequis techniques

### Logiciels à installer AVANT de commencer

#### 1. Java Development Kit (JDK 17+)
```bash
# Vérification après installation
java -version
# Résultat attendu : openjdk 17.x.x ou supérieur

javac -version
# Résultat attendu : javac 17.x.x
```

**Où télécharger ?** -> https://adoptium.net (Eclipse Temurin, gratuit)

#### 2. IntelliJ IDEA Community (IDE gratuit)
-> https://www.jetbrains.com/idea/download/

#### 3. Maven (gestionnaire de dépendances)
```bash
mvn -version
# Résultat attendu : Apache Maven 3.x.x
```

#### 4. Docker Desktop
```bash
docker --version
docker compose version
```

#### 5. WildFly 27+ (serveur d'application JEE)
-> https://www.wildfly.org/downloads/

#### 6. MySQL Workbench (client base de données)
-> https://dev.mysql.com/downloads/workbench/

---

## [CALENDRIER] Plan d'Étude Détaillé

### Vue d'ensemble

| Module | Chapitres | Durée estimée | Niveau atteint |
|--------|-----------|---------------|----------------|
| **Module 1** — Java Fondations | 1 -> 5 | 3 semaines | Intermédiaire Java |
| **Module 2** — Architecture & Web | 6 -> 8 | 2 semaines | JEE Débutant |
| **Module 3** — Persistance & CDI | 9 -> 12 | 3 semaines | JEE Intermédiaire |
| **Module 4** — Sécurité & REST | 13 -> 15 | 3 semaines | JEE Avancé |
| **Module 5** — EJB, Messaging, Tests | 16 -> 19 | 3 semaines | JEE Confirmé |
| **Module 6** — DevOps & Docker | 20 -> 22 | 2 semaines | DevOps Opérationnel |
| **Module 7** — Expert & Microservices | 23 -> 30 | 4 semaines | Expert JEE |

**Total : 20 semaines (5 mois) — à raison de 2h/jour**

---

### Planning hebdomadaire type

```
Lundi    -> Théorie + lecture du chapitre
Mardi    -> Exemples de code + compréhension
Mercredi -> Exercices pratiques
Jeudi    -> Intégration dans EduShop (fil rouge)
Vendredi -> Révision + quiz mental + difficultés
Samedi   -> Projet + exploration libre
Dimanche -> Repos (le cerveau consolide la nuit !)
```

---

## [DOSSIER] Structure des fichiers du cours

```
jee-course/
├── 00_introduction_et_fil_rouge.md       <- Vous êtes ici
├── 01_java_fondations.md                 <- Chapitres 1-5
├── 02_architecture_servlets_jsp.md       <- Chapitres 6-8
├── 03_jpa_et_cdi.md                      <- Chapitres 9-12
├── 04_securite_et_jaxrs.md               <- Chapitres 13-15
├── 05_ejb_messaging_tests.md             <- Chapitres 16-19
├── 06_devops_et_docker.md                <- Chapitres 20-22
└── 07_microservices_et_expert.md         <- Chapitres 23-30
```

---

## [OBJECTIF] Comment utiliser ce cours efficacement

### [OK] Les bonnes pratiques

1. **Ne sautez aucune étape** — chaque chapitre dépend du précédent.
2. **Tapez tout le code vous-même** — ne faites pas de copier-coller.
3. **Provoquez des erreurs** — comprendre les erreurs = progresser vite.
4. **Tenez un journal** — notez ce que vous avez appris chaque jour.
5. **Faites les exercices** — la théorie sans pratique ne sert à rien.

### [X] Les pièges à éviter

- [X] Ne pas relire passivement sans coder.
- [X] Passer au chapitre suivant sans comprendre l'actuel.
- [X] Chercher la perfection dès le début — avancez, refactorisez après.
- [X] Ignorer les messages d'erreur — **lisez-les attentivement**.

---

## [GUIDE] Conventions utilisées dans ce cours

### Blocs de code
```java
// [OK] Bonne pratique — code recommandé
public class Example {
    // commentaire explicatif
}
```

```java
// [X] À éviter — code non recommandé
public class BadExample {
    public String name; // champ public = mauvaise encapsulation
}
```

### Icônes
| Icône | Signification |
|-------|---------------|
| [LIVRE] | Chapitre / Section théorique |
| [IDEE] | Conseil important |
| [ATTENTION] | Piège courant |
| [OUTIL] | Exercice pratique |
| [SPOOL_OF_THREAD] | Ajout au projet EduShop |
| [OK] | Bonne pratique |
| [X] | Mauvaise pratique |
| [LOGIQUE] | Point à mémoriser |

---

## [SYNC] Cycle d'apprentissage de chaque chapitre

```
  1. CONCEPT      -> Qu'est-ce que c'est ? Pourquoi ça existe ?
        v
  2. ANATOMIE     -> Comment ça fonctionne en détail ?
        v
  3. CODE DE BASE -> Premier exemple simple et commenté
        v
  4. APPROFONDISSEMENT -> Cas d'usage avancés
        v
  5. PIÈGES       -> Erreurs courantes + comment les éviter
        v
  6. EXERCICE     -> Mise en pratique guidée
        v
  7. FIL ROUGE    -> Intégration dans EduShop
```

---

## [CHEQUERED_FLAG] Prêt à commencer ?

Ouvrez le fichier **`01_java_fondations.md`** et commencez le Module 1.

> [IDEE] **Conseil de départ** : Avant de lire la première ligne de code, installez tous les logiciels listés dans les prérequis. Un environnement de travail opérationnel est la clé d'un apprentissage sans friction.

---

*Bonne chance ! Chaque expert était autrefois un débutant.* [RAPIDE]

# [LIVRE] Module 1 — Fondations Java Enterprise
## Chapitres 1 à 5 : Maîtriser Java avant de maîtriser JEE

> [OBJECTIF] **Objectif du module** : Avoir une base Java solide, orientée développement enterprise. Sans ces fondations, JEE sera incompréhensible.

---

# [LIVRE] Chapitre 1 — POO Avancée (Programmation Orientée Objet)

## 1.1 Pourquoi la POO en contexte enterprise ?

Dans une application d'entreprise, vous travaillez avec des **centaines de classes**. Sans POO correctement appliquée, votre code devient rapidement un "plat de spaghetti" ingérable.

La POO enterprise repose sur 4 piliers :
- **Encapsulation** -> protéger les données internes
- **Héritage** -> réutiliser et spécialiser le comportement
- **Polymorphisme** -> traiter des objets différents de manière uniforme
- **Abstraction** -> cacher la complexité, exposer l'essentiel

---

## 1.2 Encapsulation avancée

### Concept
L'encapsulation consiste à **cacher les détails internes** d'une classe et n'exposer que ce qui est nécessaire.

### Exemple de base (MAUVAIS)

```java
// [X] MAUVAIS : tout est public, n'importe qui peut modifier n'importe quoi
public class Formation {
    public String titre;
    public double prix;
    public int nombreInscrits;
}

// Problème : on peut faire cela n'importe où dans le code
Formation f = new Formation();
f.prix = -500.0;          // Un prix négatif ?? C'est absurde !
f.nombreInscrits = -10;   // Impossible en réalité
```

### Exemple corrigé (BON)

```java
// [OK] BON : encapsulation correcte avec validation
public class Formation {

    // Les champs sont privés : personne ne peut y accéder directement
    private String titre;
    private double prix;
    private int nombreInscrits;

    // Constructeur : point d'entrée contrôlé
    public Formation(String titre, double prix) {
        // On valide dès la création
        if (titre == null || titre.isBlank()) {
            throw new IllegalArgumentException("Le titre ne peut pas être vide");
        }
        if (prix < 0) {
            throw new IllegalArgumentException("Le prix ne peut pas être négatif");
        }
        this.titre = titre;
        this.prix = prix;
        this.nombreInscrits = 0; // valeur initiale logique
    }

    // GETTER : lecture autorisée
    public String getTitre() {
        return titre;
    }

    public double getPrix() {
        return prix;
    }

    public int getNombreInscrits() {
        return nombreInscrits;
    }

    // SETTER avec validation : modification contrôlée
    public void setPrix(double prix) {
        if (prix < 0) {
            throw new IllegalArgumentException("Le prix ne peut pas être négatif : " + prix);
        }
        this.prix = prix;
    }

    // Méthode métier : pas un simple setter, c'est une action avec logique
    public void inscrireEtudiant() {
        this.nombreInscrits++;
    }

    // toString utile pour le débogage
    @Override
    public String toString() {
        return "Formation{titre='" + titre + "', prix=" + prix +
               ", inscrits=" + nombreInscrits + "}";
    }
}
```

---

## 1.3 Héritage et Polymorphisme réel

### Concept
L'héritage permet de **créer des classes spécialisées** à partir d'une classe générale.

Le polymorphisme permet de **manipuler des objets de types différents** via une référence commune.

### Exemple dans le contexte EduShop

```java
// Classe parente abstraite
public abstract class Produit {

    private Long id;
    private String nom;
    private double prix;

    public Produit(String nom, double prix) {
        this.nom = nom;
        this.prix = prix;
    }

    // Méthode abstraite : chaque sous-classe DOIT l'implémenter
    public abstract String getDescription();

    // Méthode concrète partagée par tous
    public double calculerTVA() {
        return prix * 0.20; // 20% de TVA
    }

    // Getters
    public String getNom() { return nom; }
    public double getPrix() { return prix; }

    @Override
    public String toString() {
        return nom + " - " + prix + "€";
    }
}
```

```java
// Sous-classe : Formation en ligne
public class FormationEnLigne extends Produit {

    private int dureeHeures;
    private String niveau; // "débutant", "intermédiaire", "expert"

    public FormationEnLigne(String nom, double prix, int dureeHeures, String niveau) {
        super(nom, prix); // Appel du constructeur parent
        this.dureeHeures = dureeHeures;
        this.niveau = niveau;
    }

    @Override
    public String getDescription() {
        // Implémentation propre à la formation en ligne
        return "Formation en ligne : " + getNom() +
               " | Durée : " + dureeHeures + "h | Niveau : " + niveau;
    }

    // Méthode spécifique aux formations en ligne
    public String obtenirCertificat() {
        return "Certificat de réussite pour : " + getNom();
    }
}
```

```java
// Sous-classe : Livre numérique
public class LivreNumerique extends Produit {

    private String auteur;
    private int nombrePages;

    public LivreNumerique(String nom, double prix, String auteur, int nombrePages) {
        super(nom, prix);
        this.auteur = auteur;
        this.nombrePages = nombrePages;
    }

    @Override
    public String getDescription() {
        return "Livre numérique : " + getNom() +
               " | Auteur : " + auteur +
               " | " + nombrePages + " pages";
    }
}
```

```java
// Le POLYMORPHISME en action
public class CatalogueService {

    public void afficherCatalogue(List<Produit> produits) {
        // On traite tous les produits de la MÊME façon
        // même s'ils sont de types différents !
        for (Produit p : produits) {
            System.out.println(p.getDescription()); // appel polymorphique
            System.out.println("TVA : " + p.calculerTVA() + "€");
            System.out.println("---");
        }
    }

    public static void main(String[] args) {
        List<Produit> catalogue = new ArrayList<>();
        catalogue.add(new FormationEnLigne("Java Enterprise", 199.0, 40, "avancé"));
        catalogue.add(new LivreNumerique("Clean Code", 29.0, "Robert C. Martin", 464));
        catalogue.add(new FormationEnLigne("Docker & DevOps", 149.0, 20, "intermédiaire"));

        CatalogueService service = new CatalogueService();
        service.afficherCatalogue(catalogue); // Fonctionne pour tous les types !
    }
}
```

---

## 1.4 Classes abstraites vs Interfaces

### La règle fondamentale

| | Classe abstraite | Interface |
|--|--|--|
| **Utilisation** | "Est un" (IS-A) | "Peut faire" (CAN-DO) |
| **Héritage** | Un seul parent | Plusieurs interfaces |
| **État** | Peut avoir des champs | Que des constantes (Java < 8) |
| **Constructeur** | Oui | Non |

### Exemple concret

```java
// Interface : définit un COMPORTEMENT (capacité)
public interface Payable {
    // Tout ce qui est payable doit savoir calculer son prix final
    double calculerPrixFinal();

    // Depuis Java 8 : méthodes par défaut autorisées
    default String formaterPrix() {
        return String.format("%.2f €", calculerPrixFinal());
    }
}

// Interface : une autre capacité
public interface Telechargeable {
    String genererLienTelechargement();
    long getTailleMo();
}

// Une formation implémente les DEUX interfaces
public class FormationEnLigne extends Produit implements Payable, Telechargeable {

    private double remise; // en pourcentage

    public FormationEnLigne(String nom, double prix, double remise) {
        super(nom, prix);
        this.remise = remise;
    }

    @Override
    public double calculerPrixFinal() {
        return getPrix() * (1 - remise / 100);
    }

    @Override
    public String genererLienTelechargement() {
        return "https://edushop.com/formations/" + getNom().toLowerCase().replace(" ", "-");
    }

    @Override
    public long getTailleMo() {
        return 2500L; // 2.5 Go de contenu vidéo
    }

    @Override
    public String getDescription() {
        return "Formation : " + getNom() + " (remise : " + remise + "%)";
    }
}
```

---

## 1.5 Records (Java 16+)

Les **Records** sont des classes immuables conçues pour porter des données. Parfaites pour les DTOs (Data Transfer Objects) utilisés partout en JEE.

```java
// AVANT les Records : beaucoup de code répétitif
public class FormationDTO {
    private final String titre;
    private final double prix;
    private final String niveau;

    public FormationDTO(String titre, double prix, String niveau) {
        this.titre = titre;
        this.prix = prix;
        this.niveau = niveau;
    }

    public String getTitre() { return titre; }
    public double getPrix() { return prix; }
    public String getNiveau() { return niveau; }

    @Override
    public boolean equals(Object o) { /* ... 15 lignes ... */ }
    @Override
    public int hashCode() { /* ... */ }
    @Override
    public String toString() { /* ... */ }
}
```

```java
// AVEC les Records : une seule ligne !
// Java génère automatiquement : constructeur, getters, equals, hashCode, toString
public record FormationDTO(String titre, double prix, String niveau) {}

// Utilisation identique
FormationDTO dto = new FormationDTO("Java Enterprise", 199.0, "avancé");
System.out.println(dto.titre());   // getter automatique
System.out.println(dto.prix());
System.out.println(dto);           // toString automatique
```

> [IDEE] **Vous verrez les Records partout en JEE moderne** : réponses API REST, DTOs de transfert, résultats de requêtes.

---

## 1.6 Les principes SOLID

Les principes SOLID sont **la boussole du développeur enterprise**. Sans eux, le code devient ingérable à l'échelle.

### S — Single Responsibility Principle (Responsabilité unique)
> Une classe = une seule raison de changer.

```java
// [X] MAUVAIS : la classe fait trop de choses
public class Formation {
    public void sauvegarderEnBase() { /* ... */ }     // persistance
    public void envoyerEmail() { /* ... */ }           // notification
    public void genererPDF() { /* ... */ }             // rapport
    public double calculerPrix() { return prix; }      // métier
}

// [OK] BON : chaque classe a UNE responsabilité
public class Formation { /* données et logique métier uniquement */ }
public class FormationRepository { /* persistance uniquement */ }
public class NotificationService { /* emails uniquement */ }
public class RapportService { /* génération PDF uniquement */ }
```

### O — Open/Closed Principle (Ouvert/Fermé)
> Ouvert à l'extension, fermé à la modification.

```java
// [X] MAUVAIS : ajouter un type de remise = modifier la classe
public class PrixCalculator {
    public double calculer(Formation f, String typeRemise) {
        if (typeRemise.equals("etudiant")) return f.getPrix() * 0.8;
        if (typeRemise.equals("senior")) return f.getPrix() * 0.9;
        // [X] Pour ajouter "entreprise", je dois modifier ce code
        return f.getPrix();
    }
}

// [OK] BON : on ÉTEND sans MODIFIER
public interface StrategieRemise {
    double appliquer(double prixBase);
}

public class RemiseEtudiant implements StrategieRemise {
    @Override
    public double appliquer(double prixBase) { return prixBase * 0.8; }
}

public class RemiseEntreprise implements StrategieRemise {
    @Override
    public double appliquer(double prixBase) { return prixBase * 0.7; }
}

public class PrixCalculator {
    public double calculer(Formation f, StrategieRemise strategie) {
        return strategie.appliquer(f.getPrix());
        // [OK] Pour ajouter un type, je crée juste une nouvelle classe !
    }
}
```

### D — Dependency Inversion Principle (Inversion de dépendance)
> Dépendre des abstractions, pas des implémentations. C'est le fondement de CDI en JEE !

```java
// [X] MAUVAIS : couplage fort à une implémentation concrète
public class FormationService {
    private MySQLFormationRepository repo = new MySQLFormationRepository(); // [DANGER] couplage fort
    // Si je veux passer à PostgreSQL, je dois modifier cette classe
}

// [OK] BON : dépendance à une interface (abstraction)
public class FormationService {
    private final FormationRepository repo; // interface, pas d'implémentation

    // L'implémentation concrète est INJECTÉE (CDI le fera automatiquement)
    public FormationService(FormationRepository repo) {
        this.repo = repo;
    }
}
```

---

## [OUTIL] Exercice Chapitre 1

Créez la hiérarchie de classes pour EduShop :

1. Classe abstraite `UtilisateurBase` avec : `id`, `email`, `motDePasse` (encapsulés)
2. Classe `Etudiant extends UtilisateurBase` avec `formations` (liste)
3. Classe `Formateur extends UtilisateurBase` avec `specialite` et `biographie`
4. Interface `Authentifiable` avec méthode `verifierMotDePasse(String mdp)`
5. Les deux classes doivent implémenter `Authentifiable`

---

# [LIVRE] Chapitre 2 — Collections & Generics

## 2.1 Pourquoi les Collections ?

Les applications enterprise manipulent **des listes de données** en permanence : liste de formations, panier d'achat, résultats de recherche, etc. Les Collections Java offrent les structures de données adaptées.

## 2.2 Vue d'ensemble des Collections

```
java.util.Collection
    │
    ├── List (ordonnée, doublons OK)
    │     ├── ArrayList  -> accès rapide par index
    │     └── LinkedList -> insertion/suppression rapides
    │
    ├── Set (pas de doublons)
    │     ├── HashSet    -> non ordonné, ultra-rapide
    │     ├── LinkedHashSet -> ordre d'insertion conservé
    │     └── TreeSet    -> trié automatiquement
    │
    └── Queue (file d'attente)
          └── LinkedList, PriorityQueue

java.util.Map (clé -> valeur)
    ├── HashMap    -> non ordonné, le plus utilisé
    ├── LinkedHashMap -> ordre d'insertion
    └── TreeMap    -> trié par clé
```

## 2.3 List — La collection la plus utilisée

```java
import java.util.*;

public class ExemplesListEduShop {

    public static void main(String[] args) {

        // ── ArrayList : votre choix par défaut ──
        List<Formation> catalogue = new ArrayList<>();

        // Ajout
        catalogue.add(new Formation("Java JEE", 199.0));
        catalogue.add(new Formation("Spring Boot", 149.0));
        catalogue.add(new Formation("Docker", 99.0));

        // Accès par index — O(1) très rapide
        Formation premiere = catalogue.get(0);
        System.out.println("Première : " + premiere.getTitre());

        // Taille
        System.out.println("Nombre de formations : " + catalogue.size());

        // Parcours moderne (for-each)
        for (Formation f : catalogue) {
            System.out.println(f);
        }

        // Vérification d'existence
        boolean existe = catalogue.contains(premiere);
        System.out.println("Existe ? " + existe);

        // Suppression
        catalogue.remove(0);              // par index
        catalogue.remove(premiere);       // par objet (utilise equals())

        // ── Transformer en liste non modifiable ──
        List<String> niveaux = List.of("débutant", "intermédiaire", "expert");
        // niveaux.add("guru"); // [X] UnsupportedOperationException !
    }
}
```

## 2.4 Map — Indispensable en enterprise

```java
public class ExemplesMapEduShop {

    public static void main(String[] args) {

        // Map<clé, valeur> : ici l'ID de l'étudiant -> ses formations
        Map<Long, List<Formation>> formationsParEtudiant = new HashMap<>();

        // Ajout
        Long etudiantId = 1L;
        formationsParEtudiant.put(etudiantId, new ArrayList<>());
        formationsParEtudiant.get(etudiantId).add(new Formation("Java JEE", 199.0));

        // Lecture sécurisée avec valeur par défaut
        List<Formation> formations = formationsParEtudiant
            .getOrDefault(999L, Collections.emptyList());
        System.out.println("Formations étudiant inconnu : " + formations); // []

        // Parcours des entrées
        for (Map.Entry<Long, List<Formation>> entry : formationsParEtudiant.entrySet()) {
            Long id = entry.getKey();
            List<Formation> fs = entry.getValue();
            System.out.println("Étudiant " + id + " : " + fs.size() + " formation(s)");
        }

        // computeIfAbsent : pratique pour initialiser à la volée
        formationsParEtudiant
            .computeIfAbsent(2L, k -> new ArrayList<>())
            .add(new Formation("Docker", 99.0));
    }
}
```

## 2.5 Generics — Pourquoi c'est indispensable

Les Generics permettent d'écrire du code **réutilisable et type-safe** (le compilateur détecte les erreurs).

```java
// Sans generics (Java < 5) : dangereux !
List liste = new ArrayList();
liste.add("une formation");
liste.add(42); // [X] Pas d'erreur à la compilation !
String s = (String) liste.get(1); // [DANGER] ClassCastException au runtime !

// Avec generics : erreur détectée à la COMPILATION
List<String> liste = new ArrayList<>();
liste.add("une formation");
// liste.add(42); // [OK] Erreur de compilation immédiate !
String s = liste.get(0); // Pas besoin de cast !
```

### Créer vos propres classes génériques

```java
// Un résultat paginé générique — utilisé partout en JEE pour les APIs
public class PageResultat<T> {

    private final List<T> elements;
    private final int pageActuelle;
    private final int totalPages;
    private final long totalElements;

    public PageResultat(List<T> elements, int pageActuelle,
                        int totalPages, long totalElements) {
        this.elements = Collections.unmodifiableList(elements);
        this.pageActuelle = pageActuelle;
        this.totalPages = totalPages;
        this.totalElements = totalElements;
    }

    public List<T> getElements() { return elements; }
    public int getPageActuelle() { return pageActuelle; }
    public int getTotalPages() { return totalPages; }
    public long getTotalElements() { return totalElements; }

    public boolean aUneSuivante() { return pageActuelle < totalPages - 1; }
    public boolean aUnePrecedente() { return pageActuelle > 0; }

    @Override
    public String toString() {
        return "Page " + (pageActuelle + 1) + "/" + totalPages +
               " (" + elements.size() + "/" + totalElements + " éléments)";
    }
}

// Utilisation typée
PageResultat<Formation> pageFormations = new PageResultat<>(
    formations, 0, 5, 47L
);
PageResultat<Etudiant> pageEtudiants = new PageResultat<>(
    etudiants, 0, 3, 28L
);
```

## 2.6 Streams API — Traitement de données fonctionnel

Les Streams sont **la révolution Java 8**. En JEE, vous les utilisez constamment pour transformer et filtrer des listes de données.

```java
import java.util.stream.*;

public class ExemplesStreamsEduShop {

    public static void main(String[] args) {

        List<Formation> catalogue = List.of(
            new Formation("Java JEE", 199.0, "avancé"),
            new Formation("HTML/CSS", 49.0, "débutant"),
            new Formation("Docker", 99.0, "intermédiaire"),
            new Formation("Spring Boot", 149.0, "intermédiaire"),
            new Formation("Python", 79.0, "débutant")
        );

        // ── FILTER : garder uniquement les formations > 100€ ──
        List<Formation> formationsCheres = catalogue.stream()
            .filter(f -> f.getPrix() > 100)
            .collect(Collectors.toList());
        // Résultat : Java JEE, Spring Boot

        // ── MAP : extraire les titres ──
        List<String> titres = catalogue.stream()
            .map(Formation::getTitre)      // référence de méthode
            .collect(Collectors.toList());

        // ── SORTED : trier par prix croissant ──
        List<Formation> parPrixCroissant = catalogue.stream()
            .sorted(Comparator.comparingDouble(Formation::getPrix))
            .collect(Collectors.toList());

        // ── FILTER + MAP + COLLECT : pipeline complet ──
        List<String> titresIntermediaires = catalogue.stream()
            .filter(f -> "intermédiaire".equals(f.getNiveau()))
            .sorted(Comparator.comparingDouble(Formation::getPrix))
            .map(f -> f.getTitre() + " (" + f.getPrix() + "€)")
            .collect(Collectors.toList());

        // ── REDUCE : calcul du total du panier ──
        double total = catalogue.stream()
            .mapToDouble(Formation::getPrix)
            .sum();
        System.out.println("Total catalogue : " + total + "€");

        // ── GROUP BY : regrouper par niveau ──
        Map<String, List<Formation>> parNiveau = catalogue.stream()
            .collect(Collectors.groupingBy(Formation::getNiveau));
        parNiveau.forEach((niveau, fs) -> {
            System.out.println(niveau + " : " + fs.size() + " formation(s)");
        });

        // ── FIND FIRST : chercher une formation ──
        Optional<Formation> javaFormation = catalogue.stream()
            .filter(f -> f.getTitre().contains("Java"))
            .findFirst();

        // Utilisation de l'Optional (jamais de NullPointerException !)
        javaFormation.ifPresent(f -> System.out.println("Trouvé : " + f));
        Formation defaut = javaFormation.orElse(new Formation("Intro", 0.0, "débutant"));
    }
}
```

## 2.7 Optional — Éradiquer les NullPointerException

Le `NullPointerException` est l'erreur numéro 1 en Java. `Optional` est la solution moderne.

```java
// [X] Code dangereux (style ancien)
public Formation trouverParId(Long id) {
    return formationMap.get(id); // peut retourner null !
}

// Utilisation dangereuse
Formation f = service.trouverParId(99L);
System.out.println(f.getTitre()); // [DANGER] NullPointerException si non trouvé !

// [OK] Code sécurisé avec Optional
public Optional<Formation> trouverParId(Long id) {
    return Optional.ofNullable(formationMap.get(id)); // jamais null !
}

// Utilisation sécurisée
Optional<Formation> optFormation = service.trouverParId(99L);

// 1. Vérifier et utiliser
if (optFormation.isPresent()) {
    System.out.println(optFormation.get().getTitre());
}

// 2. Encore mieux : ifPresent (lambda)
optFormation.ifPresent(f -> System.out.println(f.getTitre()));

// 3. Valeur par défaut
Formation f = optFormation.orElse(Formation.vide());

// 4. Exception personnalisée si absent
Formation f2 = optFormation
    .orElseThrow(() -> new FormationNotFoundException("Formation introuvable"));

// 5. Transformer si présent
Optional<String> titre = optFormation.map(Formation::getTitre);
```

---

## [OUTIL] Exercice Chapitre 2

Créez un `PanierService` pour EduShop :

1. Utiliser une `Map<Long, List<Formation>>` pour stocker les paniers par utilisateur
2. Méthode `ajouterFormation(Long userId, Formation f)` — utiliser `computeIfAbsent`
3. Méthode `getTotal(Long userId)` utilisant les Streams -> retourner le total
4. Méthode `getFormationsParNiveau(Long userId)` -> retourner une `Map<String, List<Formation>>`
5. Méthode `trouverFormationLaPlusChere(Long userId)` -> retourner un `Optional<Formation>`

---

# [LIVRE] Chapitre 3 — Gestion des Exceptions

## 3.1 Exceptions en contexte enterprise

Dans une application enterprise, les exceptions sont des **événements attendus** qu'on doit gérer proprement. Elles font partie du contrat de votre API.

## 3.2 Checked vs Unchecked

```
Throwable
    │
    ├── Error (grave, ne pas attraper : OutOfMemoryError, StackOverflowError)
    │
    └── Exception
          │
          ├── RuntimeException (Unchecked — pas obligé de déclarer)
          │     ├── NullPointerException
          │     ├── IllegalArgumentException
          │     ├── IndexOutOfBoundsException
          │     └── Vos exceptions métier (recommandé en enterprise)
          │
          └── IOException, SQLException... (Checked — DOIT être déclarée/catchée)
```

## 3.3 Créer des exceptions métier (indispensable en JEE)

```java
// ── Exception de base pour EduShop ──
public class EduShopException extends RuntimeException {
    // On étend RuntimeException (unchecked) : plus pratique en enterprise

    private final String codeErreur; // ex: "FORMATION_NOT_FOUND"

    public EduShopException(String codeErreur, String message) {
        super(message);
        this.codeErreur = codeErreur;
    }

    public EduShopException(String codeErreur, String message, Throwable cause) {
        super(message, cause);
        this.codeErreur = codeErreur;
    }

    public String getCodeErreur() {
        return codeErreur;
    }
}

// ── Exceptions spécialisées ──
public class FormationNotFoundException extends EduShopException {
    public FormationNotFoundException(Long id) {
        super("FORMATION_NOT_FOUND", "Formation introuvable avec l'ID : " + id);
    }
}

public class PaiementEchecException extends EduShopException {
    public PaiementEchecException(String raison) {
        super("PAIEMENT_ECHEC", "Le paiement a échoué : " + raison);
    }
}

public class UtilisateurDejaCritException extends EduShopException {
    public UtilisateurDejaCritException(String email) {
        super("USER_ALREADY_EXISTS", "Un compte existe déjà avec l'email : " + email);
    }
}
```

## 3.4 Bonnes pratiques de gestion d'exceptions

```java
public class FormationService {

    private final FormationRepository repository;

    // [OK] BON : re-lancer une exception métier, pas une technique
    public Formation trouverParId(Long id) {
        // Pas de try-catch ici : on laisse propager si pas trouvé
        return repository.findById(id)
            .orElseThrow(() -> new FormationNotFoundException(id));
    }

    // [OK] BON : wrapper les exceptions techniques en exceptions métier
    public Formation sauvegarder(Formation formation) {
        try {
            return repository.save(formation);
        } catch (DatabaseException e) {
            // On transforme l'exception technique en exception métier
            throw new EduShopException("SAVE_ERROR",
                "Impossible de sauvegarder la formation : " + formation.getTitre(), e);
        }
    }

    // [X] MAUVAIS : avaler l'exception (le pire antipattern !)
    public Formation mauvaiseSauvegarde(Formation formation) {
        try {
            return repository.save(formation);
        } catch (Exception e) {
            e.printStackTrace(); // [X] L'erreur est perdue !
            return null;         // [X] Retourner null = future NPE
        }
    }
}
```

---

# [LIVRE] Chapitre 4 — Concurrence

## 4.1 Pourquoi la concurrence en JEE ?

Un serveur JEE gère **des milliers de requêtes simultanément**. Chaque requête est traitée dans un thread séparé. Comprendre la concurrence évite des bugs catastrophiques (données corrompues, deadlocks).

## 4.2 Les bases des Threads

```java
// En JEE, vous ne créez JAMAIS de threads manuellement (c'est le serveur qui gère)
// Mais vous devez comprendre ce qui se passe dessous

public class ExempleThread {

    public static void main(String[] args) throws InterruptedException {

        // Runnable : tâche sans retour
        Runnable tache = () -> {
            System.out.println("Exécuté par : " + Thread.currentThread().getName());
        };

        Thread t = new Thread(tache, "MonThread");
        t.start();
        t.join(); // Attendre la fin du thread
    }
}
```

## 4.3 ExecutorService — La façon moderne

```java
import java.util.concurrent.*;

public class ExempleExecutorService {

    public static void main(String[] args) throws Exception {

        // Pool de 4 threads
        ExecutorService executor = Executors.newFixedThreadPool(4);

        // Soumettre des tâches
        Future<String> futur = executor.submit(() -> {
            Thread.sleep(1000); // Simule un traitement long
            return "Résultat du traitement";
        });

        // Faire autre chose pendant ce temps...
        System.out.println("Je travaille pendant que la tâche s'exécute...");

        // Récupérer le résultat (bloque si pas encore terminé)
        String resultat = futur.get(5, TimeUnit.SECONDS); // timeout de 5s
        System.out.println("Résultat : " + resultat);

        // TOUJOURS fermer l'executor !
        executor.shutdown();
    }
}
```

## 4.4 CompletableFuture — La programmation asynchrone moderne

```java
// En JEE, utilisé pour les appels non-bloquants (ex: notification email)
public class NotificationAsyncService {

    public CompletableFuture<Void> envoyerEmailBienvenue(String email) {
        return CompletableFuture
            .runAsync(() -> {
                // Simule l'envoi d'email (peut prendre du temps)
                System.out.println("Envoi email à " + email + "...");
                try { Thread.sleep(2000); } catch (InterruptedException e) {}
                System.out.println("Email envoyé à " + email);
            })
            .exceptionally(ex -> {
                System.err.println("Échec envoi email : " + ex.getMessage());
                return null;
            });
    }

    // Chaîner des opérations asynchrones
    public CompletableFuture<String> inscriptionComplete(Etudiant etudiant) {
        return CompletableFuture
            .supplyAsync(() -> creerCompte(etudiant))          // étape 1
            .thenApply(compte -> genererToken(compte))         // étape 2
            .thenCompose(token -> envoyerEmailAvecToken(token)) // étape 3 (async)
            .thenApply(result -> "Inscription réussie pour " + etudiant.getEmail());
    }
}
```

## 4.5 Variables partagées et synchronisation

```java
// [ATTENTION] PROBLÈME : variable partagée entre threads
public class CompteurNonThread {
    private int count = 0; // [X] Accès concurrent non protégé !

    public void incrementer() {
        count++; // n'est PAS atomique ! (lire + incrémenter + écrire)
    }
}

// [OK] SOLUTION 1 : AtomicInteger
public class CompteurAtomique {
    private final AtomicInteger count = new AtomicInteger(0);

    public void incrementer() {
        count.incrementAndGet(); // [OK] Atomique, thread-safe
    }

    public int getCount() {
        return count.get();
    }
}

// [OK] SOLUTION 2 : synchronized (plus général)
public class CompteurSynchronise {
    private int count = 0;

    public synchronized void incrementer() {
        count++; // [OK] Un seul thread à la fois
    }
}
```

---

# [LIVRE] Chapitre 5 — I/O & Sérialisation

## 5.1 Lecture/Écriture de fichiers

```java
import java.nio.file.*;
import java.io.*;

public class ExempleFichiers {

    public static void main(String[] args) throws IOException {

        Path fichier = Path.of("formations.txt");

        // ── ÉCRITURE ──
        List<String> lignes = List.of(
            "Java JEE;199.0;avancé",
            "Docker;99.0;intermédiaire",
            "HTML/CSS;49.0;débutant"
        );
        Files.write(fichier, lignes, StandardCharsets.UTF_8);

        // ── LECTURE ──
        List<String> contenu = Files.readAllLines(fichier, StandardCharsets.UTF_8);
        contenu.forEach(System.out::println);

        // ── LECTURE LIGNE PAR LIGNE (gros fichiers) ──
        try (BufferedReader reader = Files.newBufferedReader(fichier)) {
            String ligne;
            while ((ligne = reader.readLine()) != null) {
                String[] parts = ligne.split(";");
                System.out.printf("Formation: %s, Prix: %s€%n", parts[0], parts[1]);
            }
        } // try-with-resources : fermeture automatique !
    }
}
```

## 5.2 JSON avec Jackson (indispensable en JEE REST)

Jackson est la bibliothèque standard pour sérialiser/désérialiser du JSON en Java.

```xml
<!-- Dans pom.xml -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.15.2</version>
</dependency>
```

```java
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.*;

// La classe à sérialiser
@JsonIgnoreProperties(ignoreUnknown = true) // Ignore les champs JSON inconnus
public class FormationDTO {

    private String titre;
    private double prix;
    private String niveau;

    @JsonProperty("date_creation") // Nom différent en JSON
    private LocalDate dateCreation;

    @JsonIgnore // Ne pas inclure dans le JSON
    private String motDePasseAdmin;

    // Constructeur sans argument obligatoire pour Jackson !
    public FormationDTO() {}

    // Getters et setters...
    public String getTitre() { return titre; }
    public void setTitre(String titre) { this.titre = titre; }
    // etc.
}

public class ExempleJackson {

    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule()); // pour LocalDate

        // ── Objet -> JSON (Sérialisation) ──
        FormationDTO dto = new FormationDTO();
        dto.setTitre("Java JEE");
        dto.setPrix(199.0);

        String json = mapper.writeValueAsString(dto);
        // {"titre":"Java JEE","prix":199.0,"niveau":null}

        String jsonFormate = mapper.writerWithDefaultPrettyPrinter()
            .writeValueAsString(dto);
        System.out.println(jsonFormate);

        // ── JSON -> Objet (Désérialisation) ──
        String jsonEntree = """
            {
                "titre": "Docker",
                "prix": 99.0,
                "niveau": "intermédiaire"
            }
            """;
        FormationDTO dtoLu = mapper.readValue(jsonEntree, FormationDTO.class);
        System.out.println(dtoLu.getTitre()); // Docker

        // ── JSON -> Liste d'objets ──
        String jsonListe = "[{\"titre\":\"Java\"}, {\"titre\":\"Python\"}]";
        List<FormationDTO> liste = mapper.readValue(
            jsonListe,
            mapper.getTypeFactory().constructCollectionType(List.class, FormationDTO.class)
        );
    }
}
```

---

## [SPOOL_OF_THREAD] Intégration Fil Rouge — Module 1

À ce stade, créez le **squelette du projet EduShop** :

```
edushop/
├── src/main/java/com/edushop/
│   ├── model/
│   │   ├── Formation.java          (encapsulation, getters/setters)
│   │   ├── Etudiant.java           (héritage de UtilisateurBase)
│   │   └── Formateur.java
│   ├── dto/
│   │   ├── FormationDTO.java       (record Java)
│   │   └── InscriptionDTO.java
│   ├── exception/
│   │   ├── EduShopException.java
│   │   ├── FormationNotFoundException.java
│   │   └── PaiementEchecException.java
│   ├── service/
│   │   ├── CatalogueService.java   (streams, optionals)
│   │   └── PanierService.java      (map, collections)
│   └── util/
│       └── JsonUtil.java           (wrapper Jackson)
└── src/test/java/com/edushop/
    └── service/
        └── CatalogueServiceTest.java
```

### pom.xml de départ

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

    <groupId>com.edushop</groupId>
    <artifactId>edushop-web</artifactId>
    <version>0.1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <!-- Version Jakarta EE 10 -->
        <jakarta.version>10.0.0</jakarta.version>
    </properties>

    <dependencies>
        <!-- Jakarta EE 10 (fourni par le serveur WildFly) -->
        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-api</artifactId>
            <version>${jakarta.version}</version>
            <scope>provided</scope>
        </dependency>

        <!-- Jackson (JSON) -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.15.2</version>
        </dependency>

        <!-- Tests -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>5.4.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>edushop</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.3.2</version>
            </plugin>
        </plugins>
    </build>
</project>
```

---

## [OK] Checklist Module 1

Avant de passer au Module 2, vérifiez que vous maîtrisez :

- [ ] Créer une hiérarchie de classes avec héritage et interfaces
- [ ] Appliquer l'encapsulation correctement (private + getters/setters validés)
- [ ] Utiliser `List`, `Map`, `Set` dans des contextes réels
- [ ] Écrire des pipelines de Streams (filter, map, collect, reduce)
- [ ] Utiliser `Optional` pour éviter les NPE
- [ ] Créer des exceptions métier personnalisées
- [ ] Lire/écrire du JSON avec Jackson
- [ ] Comprendre `CompletableFuture` pour l'asynchrone

---

*Prochain module -> Architecture d'entreprise, Servlets et JSP* ->  `02_architecture_servlets_jsp.md`

# [LIVRE] Module 2 — Architecture Enterprise, Servlets & JSP
## Chapitres 6, 7 et 8 : Comprendre JEE et le Web

> [OBJECTIF] **Objectif** : Comprendre l'architecture des applications d'entreprise et construire votre première application web dynamique avec Servlets et JSP.

---

# [LIVRE] Chapitre 6 — Architecture des Applications d'Entreprise

## 6.1 Qu'est-ce qu'une application d'entreprise ?

Une application d'entreprise (ou "enterprise application") est différente d'un simple script ou d'une application de bureau. Elle doit :

- Servir **des milliers d'utilisateurs simultanément**
- Garantir la **cohérence des données** (transactions)
- Être **sécurisée** (authentification, autorisations)
- Être **maintenable** sur des années
- Pouvoir évoluer (**scalabilité**)
- Fonctionner **24h/24, 7j/7** (haute disponibilité)

C'est exactement pour ces défis que Jakarta EE (anciennement Java EE) a été créé.

---

## 6.2 Architecture 3-Tiers — Le fondement de JEE

L'architecture 3-tiers est la base de toute application enterprise. Elle sépare les responsabilités en **3 couches distinctes**.

```
┌─────────────────────────────────────────────────────────────┐
│                  TIER 1 : PRÉSENTATION                       │
│   (Ce que l'utilisateur voit et avec quoi il interagit)      │
│                                                               │
│   Navigateur web / Application mobile / Client lourd          │
│   -> JSP, HTML, CSS, JavaScript, React, Angular...            │
└────────────────────────┬────────────────────────────────────┘
                         │ HTTP Request / Response
┌────────────────────────[BLACK_DOWN-POINTING_TRIANGLE]────────────────────────────────────┐
│                  TIER 2 : LOGIQUE MÉTIER                     │
│   (Le cœur de l'application — les règles de gestion)        │
│                                                               │
│   Serveur d'application JEE (WildFly, GlassFish...)          │
│   -> Servlets, EJB, CDI, JAX-RS, Services...                  │
└────────────────────────┬────────────────────────────────────┘
                         │ JDBC / JPA
┌────────────────────────[BLACK_DOWN-POINTING_TRIANGLE]────────────────────────────────────┐
│                  TIER 3 : DONNÉES                            │
│   (Stockage et accès aux données persistantes)               │
│                                                               │
│   -> Base de données relationnelle (MySQL, PostgreSQL...)      │
│   -> NoSQL (MongoDB, Redis...)                                 │
│   -> Fichiers, systèmes de fichiers distribués                 │
└─────────────────────────────────────────────────────────────┘
```

### Pourquoi cette séparation ?

| Sans 3-tiers | Avec 3-tiers |
|---|---|
| Modifier l'interface casse la logique | Chaque couche change indépendamment |
| Impossible de tester la logique seule | Logique testable sans interface |
| Passer de MySQL à PostgreSQL = tout réécrire | Seule la couche données change |
| Code spaghetti ingérable | Code organisé et maintenable |

---

## 6.3 Architecture en couches dans le code

Dans le code JEE, le 3-tiers se traduit en couches plus détaillées :

```
┌─────────────────────────────────────────────────────────────┐
│  COUCHE PRÉSENTATION                                         │
│  -> Servlets, JSP, REST Controllers (JAX-RS)                  │
│  -> Responsabilité : recevoir la requête, retourner la réponse│
│  -> NE contient PAS de logique métier                        │
└────────────────────────┬────────────────────────────────────┘
                         │ Appelle
┌────────────────────────[BLACK_DOWN-POINTING_TRIANGLE]────────────────────────────────────┐
│  COUCHE SERVICE (Métier)                                     │
│  -> Services, EJB                                             │
│  -> Responsabilité : règles de gestion, orchestration        │
│  -> NE sait pas d'où vient la requête (HTTP? Queue? Timer?)  │
└────────────────────────┬────────────────────────────────────┘
                         │ Appelle
┌────────────────────────[BLACK_DOWN-POINTING_TRIANGLE]────────────────────────────────────┐
│  COUCHE REPOSITORY (Accès aux données)                       │
│  -> JPA Repositories, DAOs                                    │
│  -> Responsabilité : CRUD base de données uniquement         │
│  -> NE contient PAS de logique métier                        │
└────────────────────────┬────────────────────────────────────┘
                         │ SQL / JPQL
┌────────────────────────[BLACK_DOWN-POINTING_TRIANGLE]────────────────────────────────────┐
│  BASE DE DONNÉES                                             │
└─────────────────────────────────────────────────────────────┘
```

---

## 6.4 Le pattern MVC (Model-View-Controller)

Le MVC est le pattern architectural le plus utilisé pour les applications web. Il organise le code en 3 rôles :

```
REQUÊTE HTTP
    │
    [BLACK_DOWN-POINTING_TRIANGLE]
┌───────────────┐     délègue      ┌───────────────┐
│  CONTROLLER   │ ─────────────[BLACK_RIGHT-POINTING_POINTER] │    SERVICE     │
│  (Servlet)    │                  │  (Logique      │
│               │ [BLACK_LEFT-POINTING_POINTER]───────────── │   métier)      │
│ Orchestre     │   retourne       └───────────────┘
│ le flux       │   données                │
└───────┬───────┘                          │
        │ passe le modèle                  [BLACK_DOWN-POINTING_TRIANGLE]
        [BLACK_DOWN-POINTING_TRIANGLE]                          ┌───────────────┐
┌───────────────┐                  │  REPOSITORY   │
│     VIEW      │                  │  (Base de     │
│   (JSP/HTML)  │                  │   données)    │
│               │                  └───────────────┘
│ Affiche les   │
│ données       │
└───────────────┘
        │
        [BLACK_DOWN-POINTING_TRIANGLE]
  RÉPONSE HTTP
```

**Règle d'or du MVC :**
- **Model** = les données (objets Java, DTOs)
- **View** = le rendu HTML (JSP, templates)
- **Controller** = le chef d'orchestre (Servlet, REST controller)
- Le Controller ne fait **jamais** d'accès direct à la base de données
- La View ne fait **jamais** de logique métier

---

## 6.5 Monolithe vs Microservices

### Le Monolithe

```
┌─────────────────────────────────┐
│         EduShop Monolithe        │
│                                  │
│  ┌──────────┐  ┌──────────┐     │
│  │Catalogue │  │Utilisat. │     │
│  │ Module   │  │  Module  │     │
│  └──────────┘  └──────────┘     │
│  ┌──────────┐  ┌──────────┐     │
│  │Commandes │  │Paiements │     │
│  │ Module   │  │  Module  │     │
│  └──────────┘  └──────────┘     │
│                                  │
│  Base de données unique          │
└─────────────────────────────────┘
```

**Avantages :** Simple à développer et déployer au début, tout est dans le même processus (appels directs).
**Inconvénients :** Scaling difficile, déploiement de l'ensemble pour une petite modification, une panne = tout tombe.

### Les Microservices

```
┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐
│Catalogue │  │Utilisat. │  │Commandes │  │Paiements │
│ Service  │  │  Service │  │ Service  │  │ Service  │
│          │  │          │  │          │  │          │
│  BD      │  │  BD      │  │  BD      │  │  BD      │
└──────────┘  └──────────┘  └──────────┘  └──────────┘
      │              │             │             │
      └──────────────┴─────────────┴─────────────┘
                         │
                   API Gateway
                         │
                    Client
```

> [IDEE] **Pour EduShop** : On commence en **monolithe** (plus simple à apprendre), puis on décompose en microservices au Module 7. C'est exactement ce que font les équipes professionnelles.

---

## 6.6 REST vs SOAP

### SOAP (ancien, encore en entreprise legacy)
- Protocole rigide basé sur XML
- Nécessite un WSDL (contrat)
- Plus verbeux mais plus strict
- Encore utilisé dans les banques, assurances

### REST (moderne, standard actuel)
- Style architectural sur HTTP
- Utilise JSON (léger, lisible)
- Simple et intuitif
- Standard pour les APIs modernes

```
SOAP :
POST /WebService HTTP/1.1
Content-Type: text/xml

<soap:Envelope>
  <soap:Body>
    <getFormation>
      <id>42</id>
    </getFormation>
  </soap:Body>
</soap:Envelope>

REST :
GET /api/formations/42 HTTP/1.1
Accept: application/json

-> {"id": 42, "titre": "Java JEE", "prix": 199.0}
```

---

# [LIVRE] Chapitre 7 — Servlets

## 7.1 Qu'est-ce qu'une Servlet ?

Une **Servlet** est une classe Java qui reçoit des requêtes HTTP et produit des réponses HTTP. C'est la brique fondamentale de toute application web JEE.

### Cycle de vie d'une Servlet

```
Première requête :
  1. Le serveur charge la classe Servlet
  2. Instancie une SEULE fois (constructeur)
  3. Appelle init() -> initialisation
  4. Appelle service() à chaque requête -> qui appelle doGet/doPost/...
  
Requêtes suivantes :
  4. Appelle service() directement (instance déjà créée)
  
Arrêt du serveur :
  5. Appelle destroy() -> libération des ressources
  6. Garbage collection de la Servlet

[ATTENTION] UNE SEULE instance de Servlet sert TOUTES les requêtes !
   -> Pas de variables d'instance non thread-safe !
```

## 7.2 Votre première Servlet

### Structure du projet Maven

```
edushop/
├── pom.xml
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/edushop/servlet/
│   │   │       └── AccueilServlet.java
│   │   └── webapp/
│   │       ├── WEB-INF/
│   │       │   └── web.xml
│   │       └── index.jsp
│   └── test/
```

### La Servlet la plus simple

```java
package com.edushop.servlet;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @WebServlet : mappe cette servlet sur l'URL /accueil
 * Plus besoin de configurer web.xml !
 */
@WebServlet("/accueil")
public class AccueilServlet extends HttpServlet {

    // [ATTENTION] PAS de variables d'instance mutables ici ! (pas thread-safe)
    // Seules les variables finales (constantes) sont OK :
    private static final long serialVersionUID = 1L;

    /**
     * init() : appelée UNE SEULE FOIS au démarrage
     * Utiliser pour : initialiser des ressources partagées
     */
    @Override
    public void init() throws ServletException {
        System.out.println("AccueilServlet initialisée !");
        // Ici on pourrait : charger la config, initialiser un pool, etc.
    }

    /**
     * doGet() : répond aux requêtes HTTP GET
     * Utilisé pour : afficher des données, récupérer des ressources
     */
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        // 1. Définir le type de contenu de la réponse
        response.setContentType("text/html;charset=UTF-8");

        // 2. Obtenir le writer pour écrire la réponse
        PrintWriter out = response.getWriter();

        // 3. Écrire la réponse HTML
        out.println("<!DOCTYPE html>");
        out.println("<html lang='fr'>");
        out.println("<head><title>EduShop - Accueil</title></head>");
        out.println("<body>");
        out.println("<h1>Bienvenue sur EduShop !</h1>");
        out.println("<p>Nombre de formations disponibles : 42</p>");
        out.println("</body></html>");
    }

    /**
     * destroy() : appelée UNE SEULE FOIS à l'arrêt
     * Utiliser pour : libérer des ressources (connexions, fichiers...)
     */
    @Override
    public void destroy() {
        System.out.println("AccueilServlet détruite !");
    }
}
```

## 7.3 L'objet HttpServletRequest — Lire la requête

```java
@WebServlet("/recherche")
public class RechercheServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        // ── PARAMÈTRES DE L'URL ──
        // URL : /recherche?mot=java&niveau=avancé
        String motCle = request.getParameter("mot");         // "java"
        String niveau = request.getParameter("niveau");       // "avancé"
        String page   = request.getParameter("page");         // null si absent

        // Valeur par défaut si le paramètre est absent
        int numPage = (page != null) ? Integer.parseInt(page) : 0;

        // ── PARAMÈTRES MULTIPLES ──
        // URL : /recherche?tags=java&tags=jee&tags=enterprise
        String[] tags = request.getParameterValues("tags"); // ["java", "jee", "enterprise"]

        // ── INFORMATIONS SUR LA REQUÊTE ──
        String methode  = request.getMethod();         // "GET"
        String uri      = request.getRequestURI();     // "/recherche"
        String userAgent = request.getHeader("User-Agent"); // navigateur

        // ── CHEMIN ──
        String contextPath = request.getContextPath(); // "/edushop"

        // ── ATTRIBUTS (pour passer des données à une JSP) ──
        // Différent d'un paramètre : créé par le code, pas par l'URL
        request.setAttribute("resultats", List.of("Java JEE", "Java Spring"));
        request.setAttribute("motCle", motCle);

        // Déléguer l'affichage à une JSP
        request.getRequestDispatcher("/WEB-INF/views/recherche.jsp")
               .forward(request, response);
    }
}
```

## 7.4 L'objet HttpServletResponse — Écrire la réponse

```java
protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

    // ── TYPE DE CONTENU ──
    response.setContentType("application/json;charset=UTF-8");

    // ── CODE DE STATUT HTTP ──
    response.setStatus(HttpServletResponse.SC_OK);          // 200
    response.setStatus(HttpServletResponse.SC_NOT_FOUND);   // 404
    response.setStatus(HttpServletResponse.SC_BAD_REQUEST); // 400

    // ── REDIRECTION ──
    response.sendRedirect("/edushop/accueil");   // 302 Found
    response.sendRedirect("https://edushop.com"); // redirection externe

    // ── EN-TÊTES ──
    response.setHeader("Cache-Control", "no-cache");
    response.setHeader("X-Custom-Header", "EduShop-v1");

    // ── ÉCRIRE LE CONTENU ──
    PrintWriter out = response.getWriter();
    out.print("{\"message\": \"Bonjour!\"}");
}
```

## 7.5 Sessions et Cookies

### Sessions HTTP

```java
@WebServlet("/panier")
public class PanierServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        // ── OBTENIR LA SESSION (créer si elle n'existe pas) ──
        HttpSession session = request.getSession(); // true par défaut = crée si besoin
        HttpSession sessionExistante = request.getSession(false); // null si pas de session

        // ── STOCKER DES DONNÉES EN SESSION ──
        // La session est liée à UN utilisateur (identifié par un cookie JSESSIONID)
        List<Long> panier = (List<Long>) session.getAttribute("panier");
        if (panier == null) {
            panier = new ArrayList<>();
            session.setAttribute("panier", panier);
        }

        // Ajouter une formation au panier
        String formationIdStr = request.getParameter("formationId");
        if (formationIdStr != null) {
            panier.add(Long.parseLong(formationIdStr));
            session.setAttribute("panier", panier); // mise à jour
        }

        // ── LIRE DES DONNÉES DE SESSION ──
        Long userId = (Long) session.getAttribute("userId");
        if (userId == null) {
            // L'utilisateur n'est pas connecté
            response.sendRedirect(request.getContextPath() + "/connexion");
            return; // IMPORTANT : arrêter le traitement après une redirection !
        }

        // ── INFORMATIONS SUR LA SESSION ──
        String sessionId = session.getId();              // identifiant unique
        long creationTime = session.getCreationTime();   // timestamp
        long lastAccess = session.getLastAccessedTime();

        // ── INVALIDER LA SESSION (déconnexion) ──
        // session.invalidate(); // supprime toutes les données de session

        // ── TIMEOUT ──
        session.setMaxInactiveInterval(30 * 60); // 30 minutes en secondes

        request.setAttribute("panier", panier);
        request.getRequestDispatcher("/WEB-INF/views/panier.jsp")
               .forward(request, response);
    }
}
```

### Cookies

```java
// CRÉER un cookie
Cookie cookie = new Cookie("langue", "fr");
cookie.setMaxAge(365 * 24 * 60 * 60); // 1 an en secondes
cookie.setPath("/");                    // disponible sur tout le site
cookie.setHttpOnly(true);               // non accessible en JavaScript (sécurité !)
cookie.setSecure(true);                 // HTTPS uniquement (production)
response.addCookie(cookie);

// LIRE les cookies
Cookie[] cookies = request.getCookies();
if (cookies != null) {
    for (Cookie c : cookies) {
        if ("langue".equals(c.getName())) {
            String langue = c.getValue(); // "fr"
        }
    }
}

// SUPPRIMER un cookie (mettre maxAge à 0)
Cookie cookieASupprimer = new Cookie("langue", "");
cookieASupprimer.setMaxAge(0);
response.addCookie(cookieASupprimer);
```

## 7.6 Servlet de Catalogue complet (Exemple réel)

```java
package com.edushop.servlet;

import com.edushop.model.Formation;
import com.edushop.service.CatalogueService;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.*;
import java.io.IOException;
import java.util.List;

@WebServlet("/formations")
public class CatalogueServlet extends HttpServlet {

    // [OK] Service sans état : on peut le garder comme variable d'instance
    private CatalogueService catalogueService;

    @Override
    public void init() {
        this.catalogueService = new CatalogueService(); // sera remplacé par @Inject en CDI
    }

    /**
     * GET /formations -> liste toutes les formations
     * GET /formations?niveau=avancé -> filtre par niveau
     * GET /formations?id=42 -> affiche une formation
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws jakarta.servlet.ServletException, IOException {

        String idParam = req.getParameter("id");

        if (idParam != null) {
            // Afficher une formation spécifique
            afficherFormationDetail(req, resp, Long.parseLong(idParam));
        } else {
            // Lister les formations (avec filtre optionnel)
            afficherListe(req, resp);
        }
    }

    private void afficherListe(HttpServletRequest req, HttpServletResponse resp)
            throws jakarta.servlet.ServletException, IOException {

        String niveau = req.getParameter("niveau");
        List<Formation> formations;

        if (niveau != null && !niveau.isBlank()) {
            formations = catalogueService.rechercherParNiveau(niveau);
        } else {
            formations = catalogueService.toutesLesFormations();
        }

        // Passer les données à la vue JSP
        req.setAttribute("formations", formations);
        req.setAttribute("niveauFiltre", niveau);
        req.setAttribute("nombreTotal", formations.size());

        req.getRequestDispatcher("/WEB-INF/views/catalogue.jsp")
           .forward(req, resp);
    }

    private void afficherFormationDetail(HttpServletRequest req,
                                          HttpServletResponse resp, Long id)
            throws jakarta.servlet.ServletException, IOException {

        Formation formation = catalogueService.trouverParId(id)
            .orElse(null);

        if (formation == null) {
            resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
            req.setAttribute("erreur", "Formation introuvable (ID : " + id + ")");
            req.getRequestDispatcher("/WEB-INF/views/erreur.jsp").forward(req, resp);
            return;
        }

        req.setAttribute("formation", formation);
        req.getRequestDispatcher("/WEB-INF/views/formation-detail.jsp")
           .forward(req, resp);
    }

    /**
     * POST /formations -> créer une nouvelle formation (admin)
     */
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws jakarta.servlet.ServletException, IOException {

        // Vérifier les droits admin (anticipation du chapitre sécurité)
        HttpSession session = req.getSession(false);
        if (session == null || session.getAttribute("role") == null
                || !"ADMIN".equals(session.getAttribute("role"))) {
            resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Accès refusé");
            return;
        }

        // Lire les paramètres du formulaire
        String titre = req.getParameter("titre");
        String prixStr = req.getParameter("prix");
        String niveau = req.getParameter("niveau");

        // Validation basique
        if (titre == null || titre.isBlank() || prixStr == null) {
            req.setAttribute("erreur", "Tous les champs sont obligatoires");
            req.getRequestDispatcher("/WEB-INF/views/formation-form.jsp")
               .forward(req, resp);
            return;
        }

        try {
            double prix = Double.parseDouble(prixStr);
            Formation nouvelle = new Formation(titre, prix, niveau);
            catalogueService.sauvegarder(nouvelle);

            // Redirect After Post pattern (évite la double soumission)
            resp.sendRedirect(req.getContextPath() + "/formations");

        } catch (NumberFormatException e) {
            req.setAttribute("erreur", "Prix invalide : " + prixStr);
            req.getRequestDispatcher("/WEB-INF/views/formation-form.jsp")
               .forward(req, resp);
        }
    }
}
```

---

# [LIVRE] Chapitre 8 — JSP (JavaServer Pages)

## 8.1 Qu'est-ce que JSP ?

JSP est une technologie qui permet d'écrire du **HTML avec du Java embarqué**. En pratique : vous écrivez un template HTML, et les parties dynamiques sont générées par Java.

> [ATTENTION] **Règle moderne** : En JEE professionnel, on **minimise** le Java dans les JSP. Les JSP doivent être des templates d'affichage, pas des contrôleurs. C'est la Servlet qui fait le travail, la JSP qui affiche.

## 8.2 Anatomie d'une JSP

```jsp
<%-- Ceci est un commentaire JSP (n'apparaît pas dans le HTML généré) --%>

<%-- 1. DIRECTIVE PAGE : configuration de la JSP --%>
<%@ page language="java"
         contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"
         import="com.edushop.model.Formation, java.util.List" %>

<%-- 2. DIRECTIVE TAGLIB : importer des bibliothèques de tags --%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>EduShop</title>
</head>
<body>

    <%-- 3. EXPRESSION LANGUAGE (EL) : afficher des variables --%>
    <p>Bonjour, ${sessionScope.prenom} !</p>
    <p>Nombre de formations : ${nombreTotal}</p>

    <%-- 4. JSTL : logique sans Java brut --%>
    <c:if test="${not empty formations}">
        <ul>
            <c:forEach var="f" items="${formations}">
                <li>${f.titre} — <fmt:formatNumber value="${f.prix}" type="currency"/></li>
            </c:forEach>
        </ul>
    </c:if>

    <%-- À ÉVITER : Java brut dans JSP (scriptlets) --%>
    <%-- <% List<Formation> list = (List) request.getAttribute("f"); %> --%>

</body>
</html>
```

## 8.3 Expression Language (EL) — Accéder aux données

```jsp
<%-- Les SCOPES : où chercher la variable ? --%>
${prenom}               <%-- cherche dans pageScope, requestScope, sessionScope, applicationScope --%>
${requestScope.prenom}  <%-- cherche uniquement dans request --%>
${sessionScope.userId}  <%-- cherche uniquement dans session --%>
${param.nom}            <%-- paramètre de l'URL : /page?nom=Jean --%>
${header['User-Agent']} <%-- en-tête HTTP --%>

<%-- Accès aux propriétés des objets --%>
${formation.titre}          <%-- appelle formation.getTitre() --%>
${formation.prix}           <%-- appelle formation.getPrix() --%>
${formation.formateur.nom}  <%-- chaîné : getTitre().getNom() --%>

<%-- Opérations arithmétiques --%>
${formation.prix * 1.20}    <%-- prix TTC --%>
${nombreTotal + 1}

<%-- Conditions --%>
${empty formations}         <%-- true si null ou vide --%>
${not empty formations}     <%-- true si non vide --%>
${formation.prix > 100}     <%-- comparaison --%>
${formation.niveau == 'avancé'} <%-- égalité --%>

<%-- Ternaire --%>
${formation.prix > 0 ? 'Payant' : 'Gratuit'}
```

## 8.4 JSTL — Logique de présentation

### c:forEach — La boucle

```jsp
<%-- Boucle simple --%>
<c:forEach var="formation" items="${formations}">
    <div class="formation-card">
        <h3>${formation.titre}</h3>
        <p>${formation.niveau}</p>
    </div>
</c:forEach>

<%-- Avec index et séparateur --%>
<c:forEach var="f" items="${formations}" varStatus="status">
    <div class="${status.even ? 'pair' : 'impair'}">
        ${status.index + 1}. ${f.titre}
        <c:if test="${status.last}"> <- Dernier !</c:if>
    </div>
</c:forEach>

<%-- Boucle numérique --%>
<c:forEach begin="1" end="10" var="i" step="2">
    ${i}  <%-- 1, 3, 5, 7, 9 --%>
</c:forEach>
```

### c:if et c:choose — Les conditions

```jsp
<%-- if simple --%>
<c:if test="${sessionScope.userId != null}">
    <a href="/deconnexion">Se déconnecter</a>
</c:if>
<c:if test="${sessionScope.userId == null}">
    <a href="/connexion">Se connecter</a>
</c:if>

<%-- choose / when / otherwise (if/else if/else) --%>
<c:choose>
    <c:when test="${formation.niveau == 'débutant'}">
        <span class="badge green">Débutant</span>
    </c:when>
    <c:when test="${formation.niveau == 'intermédiaire'}">
        <span class="badge orange">Intermédiaire</span>
    </c:when>
    <c:otherwise>
        <span class="badge red">Avancé</span>
    </c:otherwise>
</c:choose>
```

### c:url et c:redirect

```jsp
<%-- Construire une URL avec le context path --%>
<a href="<c:url value='/formations'/>">Catalogue</a>
<a href="<c:url value='/formations'>
    <c:param name="niveau" value="débutant"/>
</c:url>">Formations débutant</a>

<%-- Redirection --%>
<c:redirect url="/connexion"/>
```

## 8.5 La vue Catalogue complète (EduShop)

```jsp
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>EduShop — Catalogue de Formations</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 0; background: #f5f5f5; }
        .header { background: #2c3e50; color: white; padding: 1rem 2rem; }
        .header h1 { margin: 0; }
        .nav a { color: white; margin-right: 1rem; text-decoration: none; }
        .container { max-width: 1200px; margin: 2rem auto; padding: 0 1rem; }
        .filtres { background: white; padding: 1rem; border-radius: 8px; margin-bottom: 2rem; }
        .filtres form { display: flex; gap: 1rem; align-items: center; }
        .catalogue { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.5rem; }
        .card { background: white; border-radius: 8px; padding: 1.5rem; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        .card h3 { color: #2c3e50; margin-top: 0; }
        .badge { padding: 4px 12px; border-radius: 20px; font-size: 0.8rem; font-weight: bold; }
        .badge-debutant { background: #d4efdf; color: #1e8449; }
        .badge-intermediaire { background: #fde8d8; color: #c35200; }
        .badge-avance { background: #f9d5d3; color: #c0392b; }
        .prix { font-size: 1.5rem; font-weight: bold; color: #e74c3c; }
        .btn { display: inline-block; padding: 8px 20px; border-radius: 4px;
               text-decoration: none; background: #3498db; color: white; border: none; cursor: pointer; }
        .btn:hover { background: #2980b9; }
        .vide { text-align: center; padding: 3rem; color: #7f8c8d; }
        .erreur { background: #fde8d8; color: #c35200; padding: 1rem; border-radius: 4px; }
        .info-bar { display: flex; justify-content: space-between; margin-bottom: 1rem; }
    </style>
</head>
<body>

<%-- ── HEADER ── --%>
<header class="header">
    <h1>[COURS] EduShop</h1>
    <nav class="nav">
        <a href="<c:url value='/'/>">Accueil</a>
        <a href="<c:url value='/formations'/>">Catalogue</a>
        <c:choose>
            <c:when test="${sessionScope.userId != null}">
                <a href="<c:url value='/panier'/>">[SHOPPING_TROLLEY] Panier (${fn:length(sessionScope.panier)})</a>
                <a href="<c:url value='/deconnexion'/>">Déconnexion (${sessionScope.prenom})</a>
            </c:when>
            <c:otherwise>
                <a href="<c:url value='/connexion'/>">Connexion</a>
                <a href="<c:url value='/inscription'/>">S'inscrire</a>
            </c:otherwise>
        </c:choose>
    </nav>
</header>

<%-- ── CONTENU PRINCIPAL ── --%>
<main class="container">

    <%-- Message d'erreur éventuel --%>
    <c:if test="${not empty erreur}">
        <p class="erreur">[ATTENTION] ${erreur}</p>
    </c:if>

    <h2>Catalogue des Formations</h2>

    <%-- ── FILTRES ── --%>
    <div class="filtres">
        <form action="<c:url value='/formations'/>" method="get">
            <label for="niveau">Filtrer par niveau :</label>
            <select id="niveau" name="niveau">
                <option value="">Tous les niveaux</option>
                <option value="débutant"      ${niveauFiltre == 'débutant' ? 'selected' : ''}>Débutant</option>
                <option value="intermédiaire" ${niveauFiltre == 'intermédiaire' ? 'selected' : ''}>Intermédiaire</option>
                <option value="avancé"        ${niveauFiltre == 'avancé' ? 'selected' : ''}>Avancé</option>
            </select>
            <button type="submit" class="btn">Filtrer</button>
            <c:if test="${not empty niveauFiltre}">
                <a href="<c:url value='/formations'/>" class="btn" style="background:#7f8c8d;">Effacer</a>
            </c:if>
        </form>
    </div>

    <%-- ── BARRE D'INFO ── --%>
    <div class="info-bar">
        <span>
            <c:choose>
                <c:when test="${not empty niveauFiltre}">
                    <strong>${nombreTotal}</strong> formation(s) de niveau "${niveauFiltre}"
                </c:when>
                <c:otherwise>
                    <strong>${nombreTotal}</strong> formation(s) disponibles
                </c:otherwise>
            </c:choose>
        </span>
        <c:if test="${sessionScope.role == 'ADMIN'}">
            <a href="<c:url value='/formations/nouveau'/>" class="btn">+ Ajouter une formation</a>
        </c:if>
    </div>

    <%-- ── GRILLE DE FORMATIONS ── --%>
    <c:choose>
        <c:when test="${empty formations}">
            <div class="vide">
                <p>[PENSIVE_FACE] Aucune formation trouvée.</p>
                <a href="<c:url value='/formations'/>" class="btn">Voir tout le catalogue</a>
            </div>
        </c:when>
        <c:otherwise>
            <div class="catalogue">
                <c:forEach var="f" items="${formations}" varStatus="s">
                    <div class="card">
                        <%-- Badge de niveau --%>
                        <c:choose>
                            <c:when test="${f.niveau == 'débutant'}">
                                <span class="badge badge-debutant">Débutant</span>
                            </c:when>
                            <c:when test="${f.niveau == 'intermédiaire'}">
                                <span class="badge badge-intermediaire">Intermédiaire</span>
                            </c:when>
                            <c:otherwise>
                                <span class="badge badge-avance">Avancé</span>
                            </c:otherwise>
                        </c:choose>

                        <h3>${f.titre}</h3>
                        <p>${f.description}</p>
                        <p>[TEMPS] Durée : ${f.dureeHeures}h</p>
                        <p>[PERSONNE][ECOLE] Formateur : ${f.formateurNom}</p>

                        <p class="prix">
                            <fmt:formatNumber value="${f.prix}" type="currency" currencySymbol="€" maxFractionDigits="2"/>
                        </p>

                        <a href="<c:url value='/formations'>
                                     <c:param name='id' value='${f.id}'/>
                                 </c:url>" class="btn">Voir la formation</a>
                    </div>
                </c:forEach>
            </div>
        </c:otherwise>
    </c:choose>

</main>

</body>
</html>
```

## 8.6 Pattern Redirect After Post (PRG)

Ce pattern est **essentiel** pour éviter la double soumission de formulaires.

```
Sans PRG (MAUVAIS) :
  1. Utilisateur soumet formulaire -> POST /formations
  2. Serveur sauvegarde -> répond avec la page
  3. Utilisateur rafraîchit (F5) -> POST envoyé à NOUVEAU
  -> Double création en base de données ! [DANGER]

Avec PRG (BON) :
  1. Utilisateur soumet formulaire -> POST /formations
  2. Serveur sauvegarde -> REDIRECT vers GET /formations
  3. Navigateur fait GET /formations -> affiche la liste
  4. Utilisateur rafraîchit (F5) -> GET envoyé (sans effet de bord)
```

```java
// Dans la Servlet (POST)
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {

    // ... traitement et sauvegarde ...

    // [OK] REDIRECT (pas forward !) après le POST
    resp.sendRedirect(req.getContextPath() + "/formations?succes=true");
}

// Dans la JSP, afficher le message de succès
```

```jsp
<c:if test="${param.succes == 'true'}">
    <div class="succes">[OK] Formation ajoutée avec succès !</div>
</c:if>
```

---

## [SPOOL_OF_THREAD] EduShop v0.1 — Application CRUD complète

### Structure finale du projet EduShop v0.1

```
src/main/
├── java/com/edushop/
│   ├── model/
│   │   └── Formation.java
│   ├── service/
│   │   └── CatalogueService.java     (données en mémoire pour l'instant)
│   └── servlet/
│       ├── AccueilServlet.java        (@WebServlet("/"))
│       ├── CatalogueServlet.java      (@WebServlet("/formations"))
│       ├── PanierServlet.java         (@WebServlet("/panier"))
│       └── ConnexionServlet.java      (@WebServlet("/connexion"))
└── webapp/
    ├── WEB-INF/
    │   ├── web.xml
    │   └── views/
    │       ├── accueil.jsp
    │       ├── catalogue.jsp           (le code ci-dessus)
    │       ├── formation-detail.jsp
    │       ├── panier.jsp
    │       ├── connexion.jsp
    │       └── erreur.jsp
    └── resources/
        ├── css/
        └── images/
```

### web.xml (configuration minimale)

```xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
                             https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">

    <display-name>EduShop</display-name>

    <!-- Paramètre d'application global -->
    <context-param>
        <param-name>appVersion</param-name>
        <param-value>0.1.0</param-value>
    </context-param>

    <!-- Page d'erreur globale -->
    <error-page>
        <error-code>404</error-code>
        <location>/WEB-INF/views/erreur-404.jsp</location>
    </error-page>
    <error-page>
        <error-code>500</error-code>
        <location>/WEB-INF/views/erreur-500.jsp</location>
    </error-page>

    <!-- Page de démarrage -->
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>

    <!-- Encodage UTF-8 pour tous les formulaires -->
    <filter>
        <filter-name>EncodageFilter</filter-name>
        <filter-class>com.edushop.filter.EncodageFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>EncodageFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

</web-app>
```

### Filtre d'encodage (bonus)

```java
package com.edushop.filter;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter("/*")  // S'applique à toutes les URL
public class EncodageFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {

        // Définir l'encodage UTF-8 pour toutes les requêtes/réponses
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");

        // Continuer la chaîne de filtres (obligatoire !)
        chain.doFilter(request, response);
    }
}
```

---

## [OUTIL] Exercice Module 2

### Exercice 1 : Servlet de connexion
Créez `ConnexionServlet` :
- `doGet` : affiche le formulaire de connexion (JSP)
- `doPost` : vérifie email/mdp (hardcodé pour l'instant), stocke `userId` et `role` en session, redirige vers le catalogue

### Exercice 2 : Panier en session
Créez `PanierServlet` :
- `doGet` : affiche le panier (formations en session), le total
- `doPost` (param `action=ajouter&formationId=X`) : ajoute au panier, redirige
- `doPost` (param `action=supprimer&formationId=X`) : supprime du panier, redirige

### Exercice 3 : Filtre d'authentification
Créez `AuthFilter` qui :
- Protège toutes les URLs `/panier`, `/commandes`, `/profil`
- Si pas de session userId, redirige vers `/connexion?redirect=/panier`
- Sinon laisse passer la requête

---

## [OK] Checklist Module 2

- [ ] Comprendre le 3-tiers et le MVC
- [ ] Créer une Servlet fonctionnelle avec doGet et doPost
- [ ] Lire les paramètres de requête et les attributs de session
- [ ] Passer des données de la Servlet à la JSP via `setAttribute`
- [ ] Utiliser JSTL (c:forEach, c:if, c:choose) dans une JSP
- [ ] Utiliser l'Expression Language (EL) pour afficher les données
- [ ] Implémenter le pattern PRG (Redirect After Post)
- [ ] Créer un filtre Servlet
- [ ] Déployer sur WildFly et tester dans le navigateur

---

*Prochain module -> JPA (Persistance) et CDI (Injection de dépendances)* -> `03_jpa_et_cdi.md`

# [LIVRE] Module 3 — JPA & CDI
## Chapitres 9 à 12 : Persistance des données et Injection de dépendances

> [OBJECTIF] **Objectif** : Connecter EduShop à une vraie base de données avec JPA, et découpler les composants avec CDI.

---

# [LIVRE] Chapitre 9 — Introduction à JPA

## 9.1 Qu'est-ce que JPA ?

**JPA** (Jakarta Persistence API) est la spécification Java pour la **persistance objet-relationnel (ORM)**. Elle permet de travailler avec des objets Java normaux au lieu d'écrire du SQL brut.

### Le problème que JPA résout

```
┌─────────────────────┐              ┌─────────────────────┐
│    MONDE OBJET      │              │    MONDE RELATIONNEL │
│    (Java)           │              │    (Base de données) │
│                     │              │                     │
│  Formation {        │              │  TABLE formation     │
│    Long id;         │[BLACK_LEFT-POINTING_POINTER]────────────[BLACK_RIGHT-POINTING_POINTER]│    id BIGINT PK     │
│    String titre;    │    JPA MAP   │    titre VARCHAR     │
│    double prix;     │              │    prix DECIMAL      │
│    Formateur f;     │              │    formateur_id FK   │
│  }                  │              │                     │
└─────────────────────┘              └─────────────────────┘
```

### Sans JPA (JDBC brut) — douloureux

```java
// [X] Sans JPA : beaucoup de code répétitif et fragile
Connection conn = DriverManager.getConnection(url, user, pwd);
PreparedStatement stmt = conn.prepareStatement(
    "SELECT id, titre, prix FROM formation WHERE id = ?"
);
stmt.setLong(1, formationId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
    Formation f = new Formation();
    f.setId(rs.getLong("id"));
    f.setTitre(rs.getString("titre"));
    f.setPrix(rs.getDouble("prix"));
    // Que se passe-t-il si on ajoute un champ ? Modifier partout !
}
rs.close(); stmt.close(); conn.close(); // N'oubliez pas !
```

### Avec JPA — élégant

```java
// [OK] Avec JPA : concis, maintenable, sans SQL
EntityManager em = /* injecté */;
Formation f = em.find(Formation.class, formationId); // Tout est automatique
```

## 9.2 Configuration de JPA — persistence.xml

```xml
<!-- src/main/resources/META-INF/persistence.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             version="3.1">

    <persistence-unit name="EduShopPU" transaction-type="JTA">

        <!-- Fournisseur ORM (Hibernate est le plus utilisé) -->
        <!-- En JEE, le serveur (WildFly) l'inclut automatiquement -->

        <!-- Nos entités JPA -->
        <class>com.edushop.entity.Formation</class>
        <class>com.edushop.entity.Etudiant</class>
        <class>com.edushop.entity.Commande</class>
        <class>com.edushop.entity.Categorie</class>

        <properties>
            <!-- Datasource JNDI configuré dans WildFly -->
            <property name="jakarta.persistence.jtaDataSource"
                      value="java:/EduShopDS"/>

            <!-- Dialecte SQL (Hibernate choisit le bon SQL pour MySQL) -->
            <property name="hibernate.dialect"
                      value="org.hibernate.dialect.MySQL8Dialect"/>

            <!-- DDL : create-drop (dev), validate (prod), update (attention!) -->
            <property name="hibernate.hbm2ddl.auto" value="update"/>

            <!-- Voir le SQL généré (dev uniquement) -->
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>

            <!-- Stats (optionnel, utile pour optimisation) -->
            <property name="hibernate.generate_statistics" value="false"/>
        </properties>
    </persistence-unit>
</persistence>
```

## 9.3 Votre première entité JPA

```java
package com.edushop.entity;

import jakarta.persistence.*;
import java.time.LocalDateTime;

/**
 * @Entity : déclare que cette classe est une entité JPA
 * -> JPA la mappera à une table de base de données
 *
 * @Table : spécifie le nom de la table (optionnel, par défaut = nom de la classe)
 */
@Entity
@Table(name = "formation",
       uniqueConstraints = @UniqueConstraint(columnNames = {"titre", "formateur_id"}))
public class Formation {

    /**
     * @Id : clé primaire
     * @GeneratedValue : JPA gère l'auto-incrémentation
     * IDENTITY -> AUTO_INCREMENT MySQL
     * SEQUENCE -> séquence Oracle/PostgreSQL
     * AUTO    -> JPA choisit
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /**
     * @Column : personnaliser le mapping de colonne
     * nullable = false -> contrainte NOT NULL en base
     * length = 200      -> VARCHAR(200)
     */
    @Column(name = "titre", nullable = false, length = 200)
    private String titre;

    @Column(nullable = false)
    private String description;

    /**
     * @Column avec precision et scale pour les décimaux financiers
     */
    @Column(name = "prix", nullable = false, precision = 10, scale = 2)
    private double prix;

    /**
     * @Enumerated : mapper une enum Java vers la base de données
     * STRING -> stocke "DEBUTANT" (lisible en base)
     * ORDINAL -> stocke 0,1,2 (fragile si on réordonne l'enum !)
     */
    @Enumerated(EnumType.STRING)
    @Column(name = "niveau", length = 20)
    private Niveau niveau;

    @Column(name = "duree_heures")
    private int dureeHeures;

    /**
     * @Column avec columnDefinition pour des types spéciaux
     */
    @Column(name = "contenu_programme", columnDefinition = "TEXT")
    private String contenuProgramme;

    /**
     * Mapping des dates Java vers SQL
     */
    @Column(name = "date_creation", nullable = false, updatable = false)
    private LocalDateTime dateCreation;

    @Column(name = "date_modification")
    private LocalDateTime dateModification;

    /**
     * @PrePersist : appelé juste AVANT la sauvegarde initiale
     */
    @PrePersist
    protected void onCreate() {
        this.dateCreation = LocalDateTime.now();
        this.dateModification = LocalDateTime.now();
    }

    /**
     * @PreUpdate : appelé juste AVANT chaque mise à jour
     */
    @PreUpdate
    protected void onUpdate() {
        this.dateModification = LocalDateTime.now();
    }

    /**
     * @Transient : cette propriété n'est PAS sauvegardée en base
     */
    @Transient
    private transient String informationsTemporaires;

    // ── Constructeurs ──
    protected Formation() {} // Obligatoire pour JPA (ne pas mettre private)

    public Formation(String titre, String description, double prix, Niveau niveau) {
        this.titre = titre;
        this.description = description;
        this.prix = prix;
        this.niveau = niveau;
    }

    // ── Getters & Setters ──
    public Long getId() { return id; }
    public String getTitre() { return titre; }
    public void setTitre(String titre) { this.titre = titre; }
    public double getPrix() { return prix; }
    public void setPrix(double prix) { this.prix = prix; }
    public Niveau getNiveau() { return niveau; }
    public void setNiveau(Niveau niveau) { this.niveau = niveau; }
    public int getDureeHeures() { return dureeHeures; }
    public void setDureeHeures(int dureeHeures) { this.dureeHeures = dureeHeures; }
    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }
    public LocalDateTime getDateCreation() { return dateCreation; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Formation f)) return false;
        return id != null && id.equals(f.id);
    }

    @Override
    public int hashCode() {
        return getClass().hashCode();
    }

    @Override
    public String toString() {
        return "Formation{id=" + id + ", titre='" + titre + "', prix=" + prix + "}";
    }
}
```

```java
// L'enum pour le niveau
public enum Niveau {
    DEBUTANT("Débutant"),
    INTERMEDIAIRE("Intermédiaire"),
    AVANCE("Avancé"),
    EXPERT("Expert");

    private final String libelle;

    Niveau(String libelle) { this.libelle = libelle; }

    public String getLibelle() { return libelle; }
}
```

## 9.4 EntityManager — Les opérations CRUD

```java
@Stateless // EJB sans état (Chapitre 16) — gère les transactions automatiquement
public class FormationDAO {

    @PersistenceContext(unitName = "EduShopPU")
    private EntityManager em; // Injecté par le conteneur JEE

    // ── CREATE ──
    public Formation creer(Formation formation) {
        em.persist(formation);      // INSERT en base
        // L'ID est généré et mis à jour dans l'objet formation
        return formation;           // maintenant formation.getId() != null
    }

    // ── READ ──
    public Formation trouverParId(Long id) {
        // find() : retourne null si non trouvé (pas d'exception)
        return em.find(Formation.class, id);
    }

    // ── UPDATE ──
    public Formation modifier(Formation formation) {
        // merge() : met à jour si l'entité est detachée
        // Si l'entité est managed (dans le contexte), l'update est automatique !
        return em.merge(formation);
    }

    // ── DELETE ──
    public void supprimer(Long id) {
        Formation formation = em.find(Formation.class, id);
        if (formation != null) {
            em.remove(formation); // DELETE en base
        }
    }

    // ── LIST ALL ──
    public List<Formation> toutesLesFormations() {
        return em.createQuery("SELECT f FROM Formation f", Formation.class)
                 .getResultList();
    }
}
```

---

# [LIVRE] Chapitre 10 — Relations entre Entités

## 10.1 Vue d'ensemble des relations JPA

```
Formation ──────── Formateur      @ManyToOne / @OneToMany
Formation ──────── Categorie      @ManyToOne
Commande  ──────── Etudiant       @ManyToOne
Commande  ──────── Formation(s)   @ManyToMany
Etudiant  ──────── Profil         @OneToOne
```

## 10.2 @ManyToOne — Relation "N vers 1"

```java
// UNE Formation appartient à UN Formateur
// Côté "N" (Many) : Formation
@Entity
@Table(name = "formation")
public class Formation {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String titre;

    /**
     * @ManyToOne : plusieurs formations -> un seul formateur
     * @JoinColumn : nom de la colonne clé étrangère en base
     * fetch = LAZY : le formateur n'est chargé QUE si on y accède
     */
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "formateur_id", nullable = false)
    private Formateur formateur;

    // Méthode utilitaire pour définir la relation des deux côtés
    public void setFormateur(Formateur formateur) {
        this.formateur = formateur;
        if (formateur != null) {
            formateur.getFormations().add(this); // cohérence bidirectionnelle
        }
    }

    // getters/setters...
}
```

## 10.3 @OneToMany — Relation "1 vers N"

```java
// UN Formateur a PLUSIEURS Formations
// Côté "1" (One) : Formateur
@Entity
@Table(name = "formateur")
public class Formateur {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String nom;
    private String prenom;
    private String specialite;

    /**
     * @OneToMany : un formateur -> plusieurs formations
     * mappedBy : indique que la relation est GÉRÉE par Formation.formateur
     *             -> la clé étrangère est dans la table formation, pas formateur
     * cascade = ALL : les opérations (persist, remove...) se propagent aux formations
     * orphanRemoval : si on retire une formation de la liste, elle est supprimée en base
     */
    @OneToMany(mappedBy = "formateur",
               cascade = CascadeType.ALL,
               orphanRemoval = true,
               fetch = FetchType.LAZY)
    private List<Formation> formations = new ArrayList<>();

    // Méthodes utilitaires pour gérer la relation des deux côtés
    public void ajouterFormation(Formation formation) {
        formations.add(formation);
        formation.setFormateurInternal(this); // setter interne sans effet de bord
    }

    public void retirerFormation(Formation formation) {
        formations.remove(formation);
        formation.setFormateurInternal(null);
    }

    public List<Formation> getFormations() { return formations; }
    // autres getters/setters...
}
```

## 10.4 @ManyToMany — Relation "N vers N"

```java
// Un Etudiant peut être inscrit à PLUSIEURS Formations
// Une Formation peut avoir PLUSIEURS Etudiants inscrits
@Entity
@Table(name = "etudiant")
public class Etudiant {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String nom;
    private String prenom;
    private String email;

    /**
     * @ManyToMany : étudiants <-> formations
     * @JoinTable : configure la table de jointure créée automatiquement
     *   joinColumns : la colonne qui référence CETTE entité (Etudiant)
     *   inverseJoinColumns : la colonne qui référence l'AUTRE entité (Formation)
     *
     * -> Crée la table : inscription(etudiant_id, formation_id)
     */
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(
        name = "inscription",
        joinColumns = @JoinColumn(name = "etudiant_id"),
        inverseJoinColumns = @JoinColumn(name = "formation_id")
    )
    private Set<Formation> formationsInscrites = new HashSet<>();

    public void inscrire(Formation formation) {
        formationsInscrites.add(formation);
    }

    public void desinscrire(Formation formation) {
        formationsInscrites.remove(formation);
    }

    // getters/setters...
}
```

## 10.5 @OneToOne — Relation "1 vers 1"

```java
@Entity
public class Etudiant {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // ...

    /**
     * @OneToOne : chaque étudiant a UN profil
     * cascade = ALL : créer/modifier/supprimer le profil avec l'étudiant
     * fetch = LAZY : ne charger le profil que si nécessaire
     */
    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
    @JoinColumn(name = "profil_id", referencedColumnName = "id")
    private ProfilEtudiant profil;

    public ProfilEtudiant getProfil() { return profil; }
    public void setProfil(ProfilEtudiant profil) {
        this.profil = profil;
        if (profil != null) profil.setEtudiant(this);
    }
}

@Entity
public class ProfilEtudiant {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String biographie;
    private String photoUrl;
    private String siteWeb;

    // Relation inverse (côté "mappedBy")
    @OneToOne(mappedBy = "profil")
    private Etudiant etudiant;

    // getters/setters...
}
```

## 10.6 FetchType : LAZY vs EAGER

```java
// LAZY (chargement paresseux) — RECOMMANDÉ par défaut
// -> La donnée n'est PAS chargée depuis la base tant qu'on n'y accède pas
@ManyToOne(fetch = FetchType.LAZY)
private Formateur formateur;

// Utilisation :
Formation f = em.find(Formation.class, 1L);
// Ici, le formateur n'est PAS encore en mémoire (pas de requête SQL)
String nom = f.getFormateur().getNom(); // ICI la requête SQL est faite
// Problème : si la session JPA est fermée -> LazyInitializationException !

// EAGER (chargement immédiat) — À éviter sauf cas particuliers
// -> La donnée est TOUJOURS chargée, même si on n'en a pas besoin
@ManyToOne(fetch = FetchType.EAGER)
private Formateur formateur;
// -> Peut charger beaucoup de données inutilement (N+1 problem)
```

**Règle d'or : Utilisez LAZY partout, et chargez avec JOIN FETCH quand vous en avez besoin.**

---

# [LIVRE] Chapitre 11 — JPQL, Criteria API & Transactions

## 11.1 JPQL (Jakarta Persistence Query Language)

JPQL est un langage de requête orienté **objet** (contrairement au SQL qui est orienté table).

```java
@Stateless
public class FormationRepository {

    @PersistenceContext(unitName = "EduShopPU")
    private EntityManager em;

    // ── JPQL de base ──
    // Attention : Formation est le NOM DE LA CLASSE, pas le nom de la table !
    public List<Formation> findAll() {
        return em.createQuery(
            "SELECT f FROM Formation f ORDER BY f.titre", Formation.class
        ).getResultList();
    }

    // ── Avec paramètres nommés ──
    public List<Formation> findByNiveau(Niveau niveau) {
        return em.createQuery(
            "SELECT f FROM Formation f WHERE f.niveau = :niveau ORDER BY f.titre",
            Formation.class
        )
        .setParameter("niveau", niveau)
        .getResultList();
    }

    // ── Avec JOIN FETCH (résoudre le LazyInitializationException) ──
    // Charge Formation ET Formateur en UNE SEULE requête SQL
    public List<Formation> findAllAvecFormateur() {
        return em.createQuery(
            "SELECT DISTINCT f FROM Formation f " +
            "LEFT JOIN FETCH f.formateur " +
            "ORDER BY f.titre",
            Formation.class
        ).getResultList();
    }

    // ── Requête scalaire (pas d'entité complète) ──
    public long compterParNiveau(Niveau niveau) {
        return em.createQuery(
            "SELECT COUNT(f) FROM Formation f WHERE f.niveau = :niveau",
            Long.class
        )
        .setParameter("niveau", niveau)
        .getSingleResult();
    }

    // ── Agrégats ──
    public Double prixMoyenParNiveau(Niveau niveau) {
        return em.createQuery(
            "SELECT AVG(f.prix) FROM Formation f WHERE f.niveau = :niveau",
            Double.class
        )
        .setParameter("niveau", niveau)
        .getSingleResult();
    }

    // ── Recherche textuelle ──
    public List<Formation> rechercher(String motCle) {
        String pattern = "%" + motCle.toLowerCase() + "%";
        return em.createQuery(
            "SELECT f FROM Formation f " +
            "WHERE LOWER(f.titre) LIKE :pattern " +
            "   OR LOWER(f.description) LIKE :pattern " +
            "ORDER BY f.titre",
            Formation.class
        )
        .setParameter("pattern", pattern)
        .getResultList();
    }

    // ── Pagination ──
    public List<Formation> findPagine(int page, int taille) {
        return em.createQuery(
            "SELECT f FROM Formation f ORDER BY f.dateCreation DESC",
            Formation.class
        )
        .setFirstResult(page * taille)   // offset = numéro de page × taille
        .setMaxResults(taille)            // LIMIT
        .getResultList();
    }

    // ── UPDATE en masse ──
    public int appliquerRemise(Niveau niveau, double pourcentage) {
        return em.createQuery(
            "UPDATE Formation f SET f.prix = f.prix * :facteur " +
            "WHERE f.niveau = :niveau"
        )
        .setParameter("facteur", 1.0 - pourcentage / 100)
        .setParameter("niveau", niveau)
        .executeUpdate();
    }
}
```

## 11.2 Named Queries — Requêtes nommées

```java
// Définir les requêtes au niveau de la classe (compilées au démarrage)
@Entity
@Table(name = "formation")
@NamedQueries({
    @NamedQuery(
        name = "Formation.findAll",
        query = "SELECT f FROM Formation f ORDER BY f.titre"
    ),
    @NamedQuery(
        name = "Formation.findByNiveau",
        query = "SELECT f FROM Formation f WHERE f.niveau = :niveau"
    ),
    @NamedQuery(
        name = "Formation.countAll",
        query = "SELECT COUNT(f) FROM Formation f"
    )
})
public class Formation {
    // ...
}

// Utilisation
public List<Formation> findByNiveau(Niveau niveau) {
    return em.createNamedQuery("Formation.findByNiveau", Formation.class)
             .setParameter("niveau", niveau)
             .getResultList();
}
```

## 11.3 Transactions JPA

```java
// En JEE avec @Stateless EJB : les transactions sont AUTOMATIQUES
// Le conteneur gère BEGIN, COMMIT, ROLLBACK
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED) // par défaut
public class FormationService {

    @PersistenceContext(unitName = "EduShopPU")
    private EntityManager em;

    @EJB
    private NotificationService notificationService;

    // Cette méthode s'exécute dans UNE TRANSACTION ATOMIQUE
    public void inscrireEtudiantAFormation(Long etudiantId, Long formationId) {
        // Tout ceci est dans la MÊME transaction

        Etudiant etudiant = em.find(Etudiant.class, etudiantId);
        Formation formation = em.find(Formation.class, formationId);

        if (etudiant == null || formation == null) {
            throw new EduShopException("NOT_FOUND", "Entité introuvable");
        }

        // 1. Inscrire l'étudiant
        etudiant.inscrire(formation);
        em.merge(etudiant);

        // 2. Incrémenter le compteur
        formation.setNombreInscrits(formation.getNombreInscrits() + 1);
        em.merge(formation);

        // Si une exception arrive ici -> tout est ANNULÉ (rollback automatique)
        // notificationService.envoyerEmail(...); // <- si ça plante, retour en arrière !
    }

    // Méthode en lecture seule : optimisée, pas de lock
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<Formation> listerFormations() {
        return em.createQuery("SELECT f FROM Formation f", Formation.class)
                 .getResultList();
    }
}
```

---

# [LIVRE] Chapitre 12 — CDI (Contexts and Dependency Injection)

## 12.1 Pourquoi CDI ?

**Le problème sans CDI :**

```java
// [X] Sans CDI : couplage fort, difficile à tester
public class CatalogueServlet extends HttpServlet {

    // Création directe -> couplage fort !
    private CatalogueService service = new CatalogueService(
        new FormationRepository(           // qui dépend de
            new EntityManagerFactory(...)  // qui dépend de la config
        )
    );
    // -> Impossible d'échanger l'implémentation pour les tests
    // -> Les dépendances transitives se propagent partout
}
```

**Avec CDI :**

```java
// [OK] Avec CDI : couplage lâche, facile à tester
public class CatalogueServlet extends HttpServlet {

    @Inject
    private CatalogueService service; // Le conteneur crée et injecte !

    // CatalogueServlet ne sait PAS comment CatalogueService est créé
    // -> On peut changer l'implémentation sans toucher CatalogueServlet
    // -> Pour les tests, on injecte un mock
}
```

## 12.2 beans.xml — Activer CDI

```xml
<!-- src/main/webapp/WEB-INF/beans.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
                           https://jakarta.ee/xml/ns/jakartaee/beans_4_0.xsd"
       version="4.0"
       bean-discovery-mode="annotated">
       <!-- annotated : seules les classes annotées sont des beans CDI -->
</beans>
```

## 12.3 Les Scopes CDI

Le **scope** détermine le cycle de vie du bean — combien de temps il vit.

```
@RequestScoped    -> Un par requête HTTP (créé au début, détruit à la fin)
@SessionScoped    -> Un par session utilisateur (vit aussi longtemps que la session)
@ApplicationScoped -> Un pour toute l'application (singleton)
@ConversationScoped -> Durée de vie gérée manuellement
@Dependent         -> Suit le scope de son injecteur (par défaut)
```

```java
// ── @ApplicationScoped : singleton partagé ──
@ApplicationScoped
public class CatalogueService {
    // Un seul objet pour toute l'application
    // Attention : doit être THREAD-SAFE !

    @Inject
    private FormationRepository repository;

    public List<Formation> toutesLesFormations() {
        return repository.findAll();
    }

    public Optional<Formation> trouverParId(Long id) {
        return repository.findById(id);
    }
}

// ── @RequestScoped : nouveau à chaque requête ──
@RequestScoped
public class PanierRequestBean {
    // Nouveau à chaque requête HTTP
    // Peut stocker l'état de la requête courante sans souci de thread-safety

    private List<Long> idsFormationsVues = new ArrayList<>();

    public void marquerVue(Long id) {
        idsFormationsVues.add(id);
    }
}

// ── @SessionScoped : lié à la session utilisateur ──
@SessionScoped
public class SessionUtilisateur implements Serializable {
    // Doit être Serializable pour la réplication de session !

    private Long userId;
    private String email;
    private String role;
    private List<Long> panierIds = new ArrayList<>();

    public boolean estConnecte() {
        return userId != null;
    }

    // getters/setters...
}
```

## 12.4 @Inject — Injection de dépendances

```java
// ── Injection par champ (la plus courante en JEE) ──
@ApplicationScoped
public class CommandeService {

    @Inject
    private FormationRepository formationRepository;

    @Inject
    private EtudiantRepository etudiantRepository;

    @Inject
    private NotificationService notificationService;

    // Le conteneur CDI résout et injecte toutes les dépendances automatiquement
}

// ── Injection par constructeur (recommandée pour les tests) ──
@ApplicationScoped
public class CommandeService {

    private final FormationRepository formationRepository;
    private final NotificationService notificationService;

    @Inject
    public CommandeService(FormationRepository formationRepository,
                           NotificationService notificationService) {
        this.formationRepository = formationRepository;
        this.notificationService = notificationService;
    }
    // -> Facile à tester : passer des mocks directement
}
```

## 12.5 Qualifiers — Choisir l'implémentation à injecter

```java
// Définir un qualifier
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD})
public @interface Production {}

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD})
public @interface Test {}

// Deux implémentations du même service
@ApplicationScoped
@Production
public class EmailServiceSMTP implements EmailService {
    @Override
    public void envoyer(String to, String sujet, String corps) {
        // Envoie un vrai email via SMTP
    }
}

@ApplicationScoped
@Test
public class EmailServiceFake implements EmailService {
    private List<String> emailsEnvoyes = new ArrayList<>();

    @Override
    public void envoyer(String to, String sujet, String corps) {
        emailsEnvoyes.add(to + ": " + sujet);
        System.out.println("[FAKE EMAIL] -> " + to + " | " + sujet);
    }

    public List<String> getEmailsEnvoyes() { return emailsEnvoyes; }
}

// Injection avec le qualifier
@ApplicationScoped
public class InscriptionService {

    @Inject @Production  // Choisit l'implémentation SMTP
    private EmailService emailService;
}
```

## 12.6 Cycle de vie CDI — @PostConstruct et @PreDestroy

```java
@ApplicationScoped
public class CacheFormations {

    private Map<Long, Formation> cache = new ConcurrentHashMap<>();

    @Inject
    private FormationRepository repository;

    /**
     * @PostConstruct : exécuté APRÈS la création et l'injection des dépendances
     * -> Parfait pour l'initialisation
     */
    @PostConstruct
    public void initialiser() {
        System.out.println("CacheFormations : initialisation du cache...");
        // Charger toutes les formations en cache au démarrage
        repository.findAll().forEach(f -> cache.put(f.getId(), f));
        System.out.println("CacheFormations : " + cache.size() + " formations en cache");
    }

    /**
     * @PreDestroy : exécuté AVANT la destruction du bean
     * -> Parfait pour libérer des ressources
     */
    @PreDestroy
    public void nettoyer() {
        System.out.println("CacheFormations : nettoyage du cache...");
        cache.clear();
    }

    public Optional<Formation> trouver(Long id) {
        return Optional.ofNullable(cache.get(id));
    }

    public void invalider(Long id) {
        cache.remove(id);
    }
}
```

## 12.7 Events CDI — Communication découplée

```java
// ── Définir un événement ──
public record FormationCreeEvent(Formation formation, LocalDateTime moment) {}
public record InscriptionEvent(Etudiant etudiant, Formation formation) {}

// ── Producteur : qui lance l'événement ──
@ApplicationScoped
public class FormationService {

    @Inject
    private FormationRepository repository;

    @Inject
    private Event<FormationCreeEvent> formationCreeeEvent; // Injecter l'Event CDI

    public Formation creer(Formation formation) {
        Formation sauvegardee = repository.save(formation);

        // Lancer l'événement (tous les observateurs seront notifiés)
        formationCreeeEvent.fire(new FormationCreeEvent(sauvegardee, LocalDateTime.now()));

        return sauvegardee;
    }
}

// ── Consommateur : qui réagit à l'événement ──
@ApplicationScoped
public class IndexationService {

    // @Observes : méthode appelée quand FormationCreeEvent est lancé
    public void onFormationCreee(@Observes FormationCreeEvent event) {
        System.out.println("Indexation de la formation : " + event.formation().getTitre());
        // Mettre à jour l'index de recherche
    }
}

@ApplicationScoped
public class StatistiquesService {

    public void onFormationCreee(@Observes FormationCreeEvent event) {
        System.out.println("Mise à jour des stats après création : "
            + event.formation().getTitre());
    }
}

// ── Événement asynchrone ──
@ApplicationScoped
public class NotificationService {

    // @ObservesAsync : exécuté dans un thread séparé, ne bloque pas l'appelant
    public void onInscription(@ObservesAsync InscriptionEvent event) {
        // Peut prendre du temps -> ne bloque pas la requête HTTP principale
        envoyerEmailConfirmation(event.etudiant(), event.formation());
    }
}
```

---

## [SPOOL_OF_THREAD] EduShop v0.3 — Architecture CDI complète

### Structure du projet avec CDI et JPA

```
src/main/java/com/edushop/
├── entity/
│   ├── Formation.java          (@Entity)
│   ├── Formateur.java          (@Entity)
│   ├── Etudiant.java           (@Entity)
│   ├── Commande.java           (@Entity)
│   └── enums/
│       └── Niveau.java
├── repository/
│   ├── FormationRepository.java    (@ApplicationScoped)
│   ├── EtudiantRepository.java     (@ApplicationScoped)
│   └── CommandeRepository.java     (@ApplicationScoped)
├── service/
│   ├── CatalogueService.java       (@ApplicationScoped)
│   ├── InscriptionService.java     (@ApplicationScoped)
│   └── PaiementService.java        (@ApplicationScoped)
├── event/
│   ├── FormationCreeEvent.java     (record)
│   └── InscriptionEvent.java       (record)
├── session/
│   └── SessionUtilisateur.java     (@SessionScoped)
├── servlet/
│   ├── CatalogueServlet.java       (@WebServlet + @Inject)
│   └── PanierServlet.java
└── qualifier/
    ├── Production.java
    └── Test.java
```

### Exemple de Servlet avec CDI

```java
@WebServlet("/formations")
public class CatalogueServlet extends HttpServlet {

    @Inject
    private CatalogueService catalogueService;

    @Inject
    private SessionUtilisateur sessionUtilisateur; // @SessionScoped auto

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        String niveauParam = req.getParameter("niveau");

        List<Formation> formations;
        if (niveauParam != null) {
            Niveau niveau = Niveau.valueOf(niveauParam.toUpperCase());
            formations = catalogueService.rechercherParNiveau(niveau);
        } else {
            formations = catalogueService.toutesLesFormations();
        }

        req.setAttribute("formations", formations);
        req.setAttribute("utilisateur", sessionUtilisateur);
        req.getRequestDispatcher("/WEB-INF/views/catalogue.jsp").forward(req, resp);
    }
}
```

---

## [OUTIL] Exercice Module 3

### Exercice 1 : Entités JPA
Créez l'entité `Commande` avec :
- `@ManyToOne` vers `Etudiant`
- `@ManyToMany` vers `Formation` (table `ligne_commande`)
- `@Enumerated` pour le statut (`EN_ATTENTE`, `CONFIRMEE`, `ANNULEE`)
- `@PrePersist` et `@PreUpdate` pour les dates

### Exercice 2 : Repository
Créez `CommandeRepository` avec :
- `findByEtudiant(Long etudiantId)` -> les commandes d'un étudiant
- `findByStatut(StatutCommande statut)` -> filtrer par statut
- `calculerTotalEtudiant(Long etudiantId)` -> total dépensé par un étudiant (JPQL)
- `findRecentes(int jours)` -> commandes des N derniers jours

### Exercice 3 : Service CDI
Créez `CommandeService` (`@ApplicationScoped`) qui :
- Injecte `CommandeRepository`, `FormationRepository`, `SessionUtilisateur`
- Méthode `passerCommande(List<Long> formationIds)` -> crée et sauvegarde la commande
- Lance un `CommandePasseeEvent` avec les formations achetées
- `NotificationService` observe cet événement et "envoie" un email (affichage console)

---

## [OK] Checklist Module 3

- [ ] Créer une entité JPA avec toutes les annotations principales
- [ ] Mapper les 4 types de relations (@OneToOne, @OneToMany, @ManyToOne, @ManyToMany)
- [ ] Écrire des requêtes JPQL paramétrées avec pagination
- [ ] Comprendre LAZY vs EAGER et éviter les LazyInitializationException
- [ ] Configurer CDI avec beans.xml
- [ ] Utiliser @Inject avec les différents scopes
- [ ] Créer et observer des événements CDI
- [ ] Utiliser @PostConstruct pour l'initialisation

---

*Prochain module -> Sécurité JEE et API REST avec JAX-RS* -> `04_securite_et_jaxrs.md`

# [LIVRE] Module 4 — Sécurité & API REST (JAX-RS)
## Chapitres 13, 14 et 15 : Sécuriser EduShop et exposer une API REST

> [OBJECTIF] **Objectif** : Ajouter une authentification JWT robuste à EduShop et construire une API REST documentée et versionnée avec JAX-RS.

---

# [LIVRE] Chapitre 13 — Sécurité en JEE

## 13.1 Les enjeux de la sécurité en enterprise

La sécurité d'une application d'entreprise couvre 4 domaines :

```
┌─────────────────────────────────────────────────────────────┐
│  1. AUTHENTIFICATION                                         │
│     "Qui êtes-vous ?" -> Vérifier l'identité                 │
│     -> Login/Mot de passe, Token JWT, OAuth2                  │
├─────────────────────────────────────────────────────────────┤
│  2. AUTORISATION                                             │
│     "Que pouvez-vous faire ?" -> Vérifier les permissions     │
│     -> Rôles (ADMIN, ETUDIANT, FORMATEUR), droits fins        │
├─────────────────────────────────────────────────────────────┤
│  3. CONFIDENTIALITÉ                                          │
│     "Les données ne sont lisibles que par les autorisés"     │
│     -> HTTPS/TLS, chiffrement des données sensibles           │
├─────────────────────────────────────────────────────────────┤
│  4. INTÉGRITÉ                                                │
│     "Les données n'ont pas été altérées"                     │
│     -> Signatures numériques, checksums                       │
└─────────────────────────────────────────────────────────────┘
```

## 13.2 Hachage des mots de passe

> [ATTENTION] **RÈGLE ABSOLUE** : On ne stocke JAMAIS un mot de passe en clair en base de données. On stocke uniquement son **hash** (empreinte irréversible).

```xml
<!-- Dépendance BCrypt -->
<dependency>
    <groupId>org.mindrot</groupId>
    <artifactId>jbcrypt</artifactId>
    <version>0.4</version>
</dependency>
```

```java
import org.mindrot.jbcrypt.BCrypt;

@ApplicationScoped
public class MotDePasseService {

    // Coût BCrypt : plus c'est élevé, plus c'est lent (et sécurisé)
    // 12 = bon équilibre sécurité/performance (≈ 300ms)
    private static final int COUT = 12;

    /**
     * Hacher un mot de passe avant de le stocker
     */
    public String hacher(String motDePasse) {
        // BCrypt génère automatiquement un salt aléatoire
        return BCrypt.hashpw(motDePasse, BCrypt.gensalt(COUT));
        // Exemple de hash : $2a$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
    }

    /**
     * Vérifier un mot de passe contre son hash
     */
    public boolean verifier(String motDePasseBrut, String hashStocke) {
        if (motDePasseBrut == null || hashStocke == null) return false;
        return BCrypt.checkpw(motDePasseBrut, hashStocke);
    }
}

// Dans votre service d'inscription :
@ApplicationScoped
public class AuthService {

    @Inject
    private MotDePasseService mdpService;

    @Inject
    private EtudiantRepository etudiantRepository;

    public Etudiant inscrire(String email, String motDePasseBrut) {
        // 1. Vérifier que l'email n'existe pas déjà
        if (etudiantRepository.existsByEmail(email)) {
            throw new UtilisateurDejaCritException(email);
        }

        // 2. Hacher le mot de passe AVANT de créer l'entité
        String hash = mdpService.hacher(motDePasseBrut);

        // 3. Créer et sauvegarder l'étudiant avec le hash
        Etudiant etudiant = new Etudiant(email, hash);
        return etudiantRepository.save(etudiant);
    }

    public Etudiant connecter(String email, String motDePasseBrut) {
        Etudiant etudiant = etudiantRepository.findByEmail(email)
            .orElseThrow(() -> new AuthException("Email ou mot de passe incorrect"));

        if (!mdpService.verifier(motDePasseBrut, etudiant.getMotDePasseHash())) {
            throw new AuthException("Email ou mot de passe incorrect");
            // [ATTENTION] Même message que si l'email n'existe pas (sécurité !)
        }

        return etudiant;
    }
}
```

## 13.3 JWT (JSON Web Token)

### Qu'est-ce qu'un JWT ?

Un JWT est un **token signé** qui contient des informations sur l'utilisateur. Il permet l'authentification **sans état** (stateless) — idéal pour les APIs REST.

```
Structure d'un JWT :

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9         <- Header (base64)
.
eyJzdWIiOiIxMjMiLCJlbWFpbCI6InVzZXJAZ...     <- Payload (base64)
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c  <- Signature HMAC

Header (décodé)  : {"alg": "HS256", "typ": "JWT"}
Payload (décodé) : {"sub": "123", "email": "user@edushop.com",
                    "role": "ETUDIANT", "iat": 1710000000, "exp": 1710086400}
```

```xml
<!-- Dépendance JWT (JJWT) -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.3</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.3</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.3</version>
    <scope>runtime</scope>
</dependency>
```

```java
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;

@ApplicationScoped
public class JwtService {

    // Clé secrète (en production : depuis les variables d'environnement !)
    private static final String SECRET = System.getenv()
        .getOrDefault("JWT_SECRET", "EduShopSuperSecretKeyPourJWTMinimum256bits!");

    private final SecretKey cleSecrete = Keys.hmacShaKeyFor(SECRET.getBytes());

    private static final long EXPIRATION_HEURES = 24;

    /**
     * Créer un token JWT pour un utilisateur authentifié
     */
    public String generer(Etudiant etudiant) {
        Instant maintenant = Instant.now();

        return Jwts.builder()
            // Subject : identifiant unique de l'utilisateur
            .subject(String.valueOf(etudiant.getId()))
            // Claims personnalisés
            .claim("email", etudiant.getEmail())
            .claim("role", etudiant.getRole().name())
            .claim("prenom", etudiant.getPrenom())
            // Métadonnées
            .issuedAt(Date.from(maintenant))
            .expiration(Date.from(maintenant.plus(EXPIRATION_HEURES, ChronoUnit.HOURS)))
            .issuer("edushop.com")
            // Signature
            .signWith(cleSecrete)
            .compact();
    }

    /**
     * Valider et parser un token JWT
     * Retourne les claims (données) du token
     * Lance une exception si invalide ou expiré
     */
    public Claims validerEtParser(String token) {
        return Jwts.parser()
            .verifyWith(cleSecrete)
            .build()
            .parseSignedClaims(token)
            .getPayload();
        // Lance JwtException si invalide ou ExpiredJwtException si expiré
    }

    /**
     * Extraire l'ID utilisateur d'un token
     */
    public Long extraireUserId(String token) {
        Claims claims = validerEtParser(token);
        return Long.parseLong(claims.getSubject());
    }

    /**
     * Extraire le rôle d'un token
     */
    public String extraireRole(String token) {
        Claims claims = validerEtParser(token);
        return claims.get("role", String.class);
    }
}
```

## 13.4 Filtre d'authentification JWT (JAX-RS)

```java
import jakarta.ws.rs.container.*;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Provider;

/**
 * Ce filtre intercepte TOUTES les requêtes vers l'API REST
 * et vérifie le token JWT dans l'en-tête Authorization
 */
@Provider
@Authenticated  // Notre annotation personnalisée (voir ci-dessous)
@Priority(Priorities.AUTHENTICATION)
public class JwtAuthFilter implements ContainerRequestFilter {

    @Inject
    private JwtService jwtService;

    @Inject
    private EtudiantRepository etudiantRepository;

    @Override
    public void filter(ContainerRequestContext requestContext) {

        // 1. Extraire le token du header "Authorization: Bearer <token>"
        String authHeader = requestContext.getHeaderString("Authorization");

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            refuser(requestContext, "Token manquant");
            return;
        }

        String token = authHeader.substring(7); // Retirer "Bearer "

        try {
            // 2. Valider le token
            Claims claims = jwtService.validerEtParser(token);
            Long userId = Long.parseLong(claims.getSubject());
            String role = claims.get("role", String.class);

            // 3. Injecter les infos dans le contexte de sécurité
            SecurityContext originalContext = requestContext.getSecurityContext();
            requestContext.setSecurityContext(new SecurityContext() {

                @Override
                public Principal getUserPrincipal() {
                    return () -> claims.get("email", String.class);
                }

                @Override
                public boolean isUserInRole(String roleCheck) {
                    return role.equals(roleCheck);
                }

                @Override
                public boolean isSecure() {
                    return originalContext.isSecure();
                }

                @Override
                public String getAuthenticationScheme() {
                    return "Bearer";
                }
            });

            // 4. Ajouter l'ID utilisateur comme propriété pour les endpoints
            requestContext.setProperty("userId", userId);
            requestContext.setProperty("userRole", role);

        } catch (ExpiredJwtException e) {
            refuser(requestContext, "Token expiré");
        } catch (JwtException e) {
            refuser(requestContext, "Token invalide");
        }
    }

    private void refuser(ContainerRequestContext ctx, String message) {
        ctx.abortWith(
            Response.status(Response.Status.UNAUTHORIZED)
                .entity(new ErrorResponse("AUTH_ERROR", message))
                .build()
        );
    }
}
```

```java
// Annotation personnalisée pour marquer les endpoints protégés
@NameBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Authenticated {}

// Annotation pour les endpoints admin uniquement
@NameBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface AdminOnly {}
```

---

# [LIVRE] Chapitre 14 — API REST avec JAX-RS

## 14.1 Qu'est-ce que JAX-RS ?

**JAX-RS** (Jakarta RESTful Web Services) est la spécification JEE pour créer des APIs REST. Elle transforme des méthodes Java en endpoints HTTP via des annotations.

## 14.2 Configuration JAX-RS

```java
// Point d'entrée de l'application JAX-RS
@ApplicationPath("/api") // L'API sera disponible à /edushop/api/...
public class EduShopApplication extends Application {
    // Vide : JAX-RS scan automatiquement les classes annotées @Path
}
```

## 14.3 Votre premier Resource JAX-RS

```java
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.*;

/**
 * @Path : l'URL de base de ce resource
 * @Produces : le type de contenu produit par défaut
 * @Consumes : le type de contenu accepté par défaut
 */
@Path("/formations")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class FormationResource {

    @Inject
    private CatalogueService catalogueService;

    /**
     * GET /api/formations
     * Lister toutes les formations avec filtres optionnels
     */
    @GET
    public Response listerFormations(
            @QueryParam("niveau") String niveauParam,
            @QueryParam("page") @DefaultValue("0") int page,
            @QueryParam("taille") @DefaultValue("10") int taille,
            @QueryParam("recherche") String motCle) {

        List<FormationDTO> formations;

        if (motCle != null && !motCle.isBlank()) {
            formations = catalogueService.rechercher(motCle, page, taille);
        } else if (niveauParam != null) {
            Niveau niveau = parseNiveau(niveauParam);
            formations = catalogueService.rechercherParNiveau(niveau, page, taille);
        } else {
            formations = catalogueService.lister(page, taille);
        }

        long total = catalogueService.compter();

        // Construire la réponse avec métadonnées de pagination
        Map<String, Object> reponse = new LinkedHashMap<>();
        reponse.put("data", formations);
        reponse.put("page", page);
        reponse.put("taille", taille);
        reponse.put("total", total);
        reponse.put("totalPages", (int) Math.ceil((double) total / taille));

        return Response.ok(reponse).build();
    }

    /**
     * GET /api/formations/{id}
     * Récupérer une formation par son ID
     */
    @GET
    @Path("/{id}")
    public Response trouverFormation(@PathParam("id") Long id) {
        return catalogueService.trouverParId(id)
            .map(FormationDTO::from) // conversion en DTO
            .map(dto -> Response.ok(dto).build())
            .orElseGet(() -> Response.status(Status.NOT_FOUND)
                .entity(new ErrorResponse("NOT_FOUND", "Formation introuvable : " + id))
                .build());
    }

    /**
     * POST /api/formations
     * Créer une nouvelle formation (admin uniquement)
     */
    @POST
    @Authenticated  // Filtre JWT actif
    @AdminOnly
    public Response creerFormation(
            @Valid FormationCreerDTO dto,  // @Valid déclenche la validation Bean Validation
            @Context SecurityContext ctx) {

        try {
            Formation creee = catalogueService.creer(dto);
            FormationDTO reponse = FormationDTO.from(creee);

            // 201 Created avec l'URL de la ressource créée dans le header Location
            URI location = UriBuilder.fromResource(FormationResource.class)
                .path("/{id}")
                .build(creee.getId());

            return Response.created(location).entity(reponse).build();

        } catch (FormationDejaCreeException e) {
            return Response.status(Status.CONFLICT)
                .entity(new ErrorResponse("CONFLICT", e.getMessage()))
                .build();
        }
    }

    /**
     * PUT /api/formations/{id}
     * Remplacer une formation complète
     */
    @PUT
    @Path("/{id}")
    @Authenticated
    @AdminOnly
    public Response modifierFormation(@PathParam("id") Long id,
                                       @Valid FormationModifierDTO dto) {
        return catalogueService.trouverParId(id)
            .map(f -> {
                Formation modifiee = catalogueService.modifier(id, dto);
                return Response.ok(FormationDTO.from(modifiee)).build();
            })
            .orElseGet(() -> Response.status(Status.NOT_FOUND)
                .entity(new ErrorResponse("NOT_FOUND", "Formation introuvable"))
                .build());
    }

    /**
     * PATCH /api/formations/{id}/prix
     * Modifier partiellement (seulement le prix)
     */
    @PATCH
    @Path("/{id}/prix")
    @Authenticated
    @AdminOnly
    public Response modifierPrix(@PathParam("id") Long id,
                                  Map<String, Double> body) {
        Double nouveauPrix = body.get("prix");
        if (nouveauPrix == null || nouveauPrix < 0) {
            return Response.status(Status.BAD_REQUEST)
                .entity(new ErrorResponse("INVALID_PRICE", "Prix invalide"))
                .build();
        }

        catalogueService.modifierPrix(id, nouveauPrix);
        return Response.noContent().build(); // 204 No Content
    }

    /**
     * DELETE /api/formations/{id}
     */
    @DELETE
    @Path("/{id}")
    @Authenticated
    @AdminOnly
    public Response supprimerFormation(@PathParam("id") Long id) {
        if (!catalogueService.existeParId(id)) {
            return Response.status(Status.NOT_FOUND)
                .entity(new ErrorResponse("NOT_FOUND", "Formation introuvable"))
                .build();
        }
        catalogueService.supprimer(id);
        return Response.noContent().build(); // 204 No Content
    }

    // Méthode utilitaire
    private Niveau parseNiveau(String niveau) {
        try {
            return Niveau.valueOf(niveau.toUpperCase());
        } catch (IllegalArgumentException e) {
            throw new WebApplicationException(
                Response.status(Status.BAD_REQUEST)
                    .entity(new ErrorResponse("INVALID_NIVEAU",
                        "Niveau invalide. Valeurs acceptées : " +
                        Arrays.toString(Niveau.values())))
                    .build()
            );
        }
    }
}
```

## 14.4 DTOs — Séparer les données d'API des entités

```java
// DTO de sortie (ce que l'API retourne)
public record FormationDTO(
    Long id,
    String titre,
    String description,
    double prix,
    String niveau,
    int dureeHeures,
    String formateurNom,
    int nombreInscrits,
    LocalDateTime dateCreation
) {
    // Factory method pour créer depuis une entité
    public static FormationDTO from(Formation f) {
        return new FormationDTO(
            f.getId(),
            f.getTitre(),
            f.getDescription(),
            f.getPrix(),
            f.getNiveau().getLibelle(),
            f.getDureeHeures(),
            f.getFormateur() != null ? f.getFormateur().getNomComplet() : null,
            f.getNombreInscrits(),
            f.getDateCreation()
        );
    }
}

// DTO de création (ce que l'API reçoit)
public class FormationCreerDTO {

    @NotBlank(message = "Le titre est obligatoire")
    @Size(min = 3, max = 200, message = "Le titre doit faire entre 3 et 200 caractères")
    private String titre;

    @NotBlank(message = "La description est obligatoire")
    private String description;

    @NotNull(message = "Le prix est obligatoire")
    @DecimalMin(value = "0.0", message = "Le prix ne peut pas être négatif")
    private Double prix;

    @NotNull(message = "Le niveau est obligatoire")
    private Niveau niveau;

    @Min(value = 1, message = "La durée doit être au moins 1 heure")
    @Max(value = 500, message = "La durée ne peut pas dépasser 500 heures")
    private int dureeHeures;

    @NotNull(message = "L'ID du formateur est obligatoire")
    private Long formateurId;

    // Getters/setters...
}
```

## 14.5 Gestion globale des erreurs — ExceptionMapper

```java
// Mapper pour nos exceptions métier
@Provider
public class EduShopExceptionMapper implements ExceptionMapper<EduShopException> {

    @Override
    public Response toResponse(EduShopException exception) {
        int status;

        switch (exception.getCodeErreur()) {
            case "NOT_FOUND" -> status = 404;
            case "CONFLICT" -> status = 409;
            case "AUTH_ERROR" -> status = 401;
            case "FORBIDDEN" -> status = 403;
            case "VALIDATION_ERROR" -> status = 422;
            default -> status = 500;
        }

        ErrorResponse erreur = new ErrorResponse(
            exception.getCodeErreur(),
            exception.getMessage()
        );

        return Response.status(status).entity(erreur).build();
    }
}

// Mapper pour les erreurs de validation Bean Validation
@Provider
public class ValidationExceptionMapper
        implements ExceptionMapper<ConstraintViolationException> {

    @Override
    public Response toResponse(ConstraintViolationException e) {
        List<String> erreurs = e.getConstraintViolations()
            .stream()
            .map(cv -> cv.getPropertyPath() + " : " + cv.getMessage())
            .sorted()
            .collect(Collectors.toList());

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("code", "VALIDATION_ERROR");
        body.put("message", "Données invalides");
        body.put("details", erreurs);

        return Response.status(422).entity(body).build();
    }
}

// La réponse d'erreur standardisée
public record ErrorResponse(
    String code,
    String message,
    LocalDateTime timestamp
) {
    public ErrorResponse(String code, String message) {
        this(code, message, LocalDateTime.now());
    }
}
```

## 14.6 Resource d'authentification

```java
@Path("/auth")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class AuthResource {

    @Inject private AuthService authService;
    @Inject private JwtService jwtService;

    @POST
    @Path("/connexion")
    public Response connexion(ConnexionDTO dto) {
        try {
            Etudiant etudiant = authService.connecter(dto.email(), dto.motDePasse());
            String token = jwtService.generer(etudiant);

            return Response.ok(new TokenDTO(
                token,
                "Bearer",
                24 * 3600, // expiration en secondes
                etudiant.getPrenom(),
                etudiant.getRole().name()
            )).build();

        } catch (AuthException e) {
            return Response.status(Response.Status.UNAUTHORIZED)
                .entity(new ErrorResponse("AUTH_FAILED", e.getMessage()))
                .build();
        }
    }

    @POST
    @Path("/inscription")
    public Response inscription(@Valid InscriptionDTO dto) {
        try {
            Etudiant etudiant = authService.inscrire(dto);
            String token = jwtService.generer(etudiant);
            URI location = URI.create("/api/etudiants/" + etudiant.getId());
            return Response.created(location)
                .entity(new TokenDTO(token, "Bearer", 24 * 3600,
                    etudiant.getPrenom(), etudiant.getRole().name()))
                .build();
        } catch (UtilisateurDejaCritException e) {
            return Response.status(Status.CONFLICT)
                .entity(new ErrorResponse("EMAIL_EXISTS", e.getMessage()))
                .build();
        }
    }

    @POST
    @Path("/deconnexion")
    @Authenticated
    public Response deconnexion() {
        // Avec JWT, la déconnexion côté serveur = blacklister le token
        // Pour simplifier, on retourne juste 200
        return Response.ok(Map.of("message", "Déconnecté avec succès")).build();
    }
}

// DTO simples
public record ConnexionDTO(
    @NotBlank String email,
    @NotBlank String motDePasse
) {}

public record TokenDTO(
    String accessToken,
    String type,
    int expiresIn,
    String prenom,
    String role
) {}
```

---

# [LIVRE] Chapitre 15 — REST Avancé

## 15.1 HATEOAS — Navigabilité de l'API

HATEOAS (Hypermedia As The Engine Of Application State) consiste à inclure des **liens** dans les réponses pour guider le client.

```java
// Réponse avec liens HATEOAS
public class FormationHateoasDTO {

    private Long id;
    private String titre;
    private double prix;
    private Map<String, String> liens = new LinkedHashMap<>();

    public static FormationHateoasDTO from(Formation f, UriInfo uriInfo) {
        FormationHateoasDTO dto = new FormationHateoasDTO();
        dto.id = f.getId();
        dto.titre = f.getTitre();
        dto.prix = f.getPrix();

        // Liens de navigation
        String base = uriInfo.getBaseUri().toString();
        dto.liens.put("self", base + "formations/" + f.getId());
        dto.liens.put("inscrire", base + "formations/" + f.getId() + "/inscription");
        dto.liens.put("formateur", base + "formateurs/" + f.getFormateur().getId());
        dto.liens.put("avis", base + "formations/" + f.getId() + "/avis");

        return dto;
    }

    // getters...
}
```

```json
// Exemple de réponse JSON avec HATEOAS
{
    "id": 42,
    "titre": "Java JEE Enterprise",
    "prix": 199.0,
    "_links": {
        "self": {"href": "/api/formations/42"},
        "inscrire": {"href": "/api/formations/42/inscription"},
        "formateur": {"href": "/api/formateurs/7"},
        "avis": {"href": "/api/formations/42/avis"}
    }
}
```

## 15.2 Pagination avancée dans les réponses

```java
@GET
public Response listerAvecPagination(
        @QueryParam("page") @DefaultValue("0") int page,
        @QueryParam("taille") @DefaultValue("10") int taille,
        @QueryParam("tri") @DefaultValue("titre") String tri,
        @QueryParam("ordre") @DefaultValue("asc") String ordre,
        @Context UriInfo uriInfo) {

    // Validation des paramètres
    if (page < 0) throw new WebApplicationException(Response.status(400)
        .entity(new ErrorResponse("INVALID_PAGE", "Page doit être >= 0")).build());
    if (taille < 1 || taille > 100) throw new WebApplicationException(Response.status(400)
        .entity(new ErrorResponse("INVALID_SIZE", "Taille entre 1 et 100")).build());

    List<FormationDTO> formations = catalogueService.lister(page, taille, tri, ordre);
    long total = catalogueService.compter();
    int totalPages = (int) Math.ceil((double) total / taille);

    // Construire les liens de pagination
    String baseUrl = uriInfo.getAbsolutePath().toString();
    Map<String, String> liens = new LinkedHashMap<>();
    liens.put("self", baseUrl + "?page=" + page + "&taille=" + taille);
    if (page > 0)
        liens.put("premiere", baseUrl + "?page=0&taille=" + taille);
    if (page > 0)
        liens.put("precedente", baseUrl + "?page=" + (page-1) + "&taille=" + taille);
    if (page < totalPages - 1)
        liens.put("suivante", baseUrl + "?page=" + (page+1) + "&taille=" + taille);
    if (page < totalPages - 1)
        liens.put("derniere", baseUrl + "?page=" + (totalPages-1) + "&taille=" + taille);

    Map<String, Object> reponse = new LinkedHashMap<>();
    reponse.put("data", formations);
    reponse.put("_pagination", Map.of(
        "page", page,
        "taille", taille,
        "total", total,
        "totalPages", totalPages,
        "premier", page == 0,
        "dernier", page >= totalPages - 1
    ));
    reponse.put("_links", liens);

    // Header Link selon RFC 5988
    String linkHeader = liens.entrySet().stream()
        .map(e -> "<" + e.getValue() + ">; rel=\"" + e.getKey() + "\"")
        .collect(Collectors.joining(", "));

    return Response.ok(reponse)
        .header("Link", linkHeader)
        .header("X-Total-Count", total)
        .build();
}
```

## 15.3 Versioning de l'API

```java
// Stratégie 1 : Versioning par URL (la plus simple et explicite)
@Path("/v1/formations")
public class FormationResourceV1 { /* ... */ }

@Path("/v2/formations")
public class FormationResourceV2 {
    // V2 : nouvelles fonctionnalités, format de réponse enrichi
}

// Stratégie 2 : Versioning par en-tête (plus élégant mais moins visible)
@Path("/formations")
public class FormationResource {

    @GET
    public Response lister(@HeaderParam("Accept-Version") String version) {
        if ("v2".equals(version)) {
            return Response.ok(listerV2()).build();
        }
        return Response.ok(listerV1()).build(); // par défaut
    }
}

// Stratégie 3 : Versioning par media type (RESTful pur)
// Accept: application/vnd.edushop.v2+json
```

## 15.4 Documentation avec OpenAPI / Swagger

```xml
<!-- Dépendance MicroProfile OpenAPI -->
<dependency>
    <groupId>org.eclipse.microprofile.openapi</groupId>
    <artifactId>microprofile-openapi-api</artifactId>
    <version>3.1</version>
    <scope>provided</scope>
</dependency>
```

```java
import org.eclipse.microprofile.openapi.annotations.*;
import org.eclipse.microprofile.openapi.annotations.media.*;
import org.eclipse.microprofile.openapi.annotations.responses.*;

@Path("/formations")
@Tag(name = "Formations", description = "Gestion du catalogue de formations")
@Produces(MediaType.APPLICATION_JSON)
public class FormationResource {

    @GET
    @Operation(
        summary = "Lister les formations",
        description = "Retourne la liste paginée de toutes les formations disponibles"
    )
    @APIResponses({
        @APIResponse(
            responseCode = "200",
            description = "Liste des formations",
            content = @Content(schema = @Schema(implementation = FormationListDTO.class))
        ),
        @APIResponse(responseCode = "400", description = "Paramètres invalides")
    })
    @Parameter(name = "page", description = "Numéro de page (commence à 0)", example = "0")
    @Parameter(name = "taille", description = "Nombre d'éléments par page", example = "10")
    public Response listerFormations(
            @QueryParam("page") @DefaultValue("0") int page,
            @QueryParam("taille") @DefaultValue("10") int taille) {
        // ...
    }

    @POST
    @Operation(summary = "Créer une formation", description = "Accessible aux admins uniquement")
    @SecurityRequirement(name = "bearerAuth")
    @APIResponse(responseCode = "201", description = "Formation créée avec succès")
    @APIResponse(responseCode = "401", description = "Token JWT manquant ou invalide")
    @APIResponse(responseCode = "403", description = "Droits insuffisants")
    @APIResponse(responseCode = "422", description = "Données de la formation invalides")
    public Response creerFormation(@Valid FormationCreerDTO dto) {
        // ...
    }
}
```

```java
// Configuration OpenAPI globale
@OpenAPIDefinition(
    info = @Info(
        title = "EduShop API",
        version = "1.0.0",
        description = "API REST de la plateforme de formations EduShop",
        contact = @Contact(name = "Support EduShop", email = "api@edushop.com"),
        license = @License(name = "MIT")
    ),
    servers = {
        @Server(url = "http://localhost:8080/edushop/api", description = "Développement"),
        @Server(url = "https://api.edushop.com", description = "Production")
    },
    security = @SecurityRequirement(name = "bearerAuth")
)
@SecurityScheme(
    securitySchemeName = "bearerAuth",
    type = SecuritySchemeType.HTTP,
    scheme = "bearer",
    bearerFormat = "JWT"
)
public class EduShopApplication extends Application {}
```

L'API sera disponible à : `http://localhost:8080/edushop/openapi` (JSON)  
Et l'interface Swagger UI à : `http://localhost:8080/edushop/swagger-ui`

---

## [SPOOL_OF_THREAD] EduShop v0.5 — API REST Complète

### Endpoints de l'API EduShop

```
AUTH
  POST   /api/auth/connexion            -> login, retourne JWT
  POST   /api/auth/inscription          -> créer compte
  POST   /api/auth/deconnexion          -> déconnexion ([VERROUILLE])

FORMATIONS
  GET    /api/formations                -> liste paginée
  GET    /api/formations/{id}           -> détail
  POST   /api/formations               -> créer ([VERROUILLE] ADMIN)
  PUT    /api/formations/{id}           -> modifier ([VERROUILLE] ADMIN)
  DELETE /api/formations/{id}           -> supprimer ([VERROUILLE] ADMIN)

INSCRIPTIONS
  POST   /api/formations/{id}/inscription -> s'inscrire ([VERROUILLE])
  DELETE /api/formations/{id}/inscription -> se désinscrire ([VERROUILLE])

PROFIL ÉTUDIANT
  GET    /api/profil                    -> mon profil ([VERROUILLE])
  PUT    /api/profil                    -> modifier mon profil ([VERROUILLE])
  GET    /api/profil/formations         -> mes formations ([VERROUILLE])

[VERROUILLE] = Requiert JWT valide
```

### Test avec curl

```bash
# 1. Inscription
curl -X POST http://localhost:8080/edushop/api/auth/inscription \
  -H "Content-Type: application/json" \
  -d '{"prenom":"Alice","nom":"Dupont","email":"alice@test.com","motDePasse":"Secret123!"}'

# 2. Connexion -> récupérer le token
TOKEN=$(curl -s -X POST http://localhost:8080/edushop/api/auth/connexion \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@test.com","motDePasse":"Secret123!"}' \
  | jq -r '.accessToken')

# 3. Lister les formations
curl http://localhost:8080/edushop/api/formations?page=0&taille=5

# 4. Créer une formation (avec le token)
curl -X POST http://localhost:8080/edushop/api/formations \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "titre": "Microservices avec JEE",
    "description": "Architectures cloud-native",
    "prix": 249.0,
    "niveau": "AVANCE",
    "dureeHeures": 45,
    "formateurId": 1
  }'
```

---

## [OUTIL] Exercice Module 4

### Exercice 1 : Resource "Avis"
Créez `AvisResource` avec :
- `GET /api/formations/{id}/avis` -> liste paginée des avis d'une formation
- `POST /api/formations/{id}/avis` -> poster un avis ([VERROUILLE], note de 1 à 5)
- `DELETE /api/avis/{avisId}` -> supprimer son propre avis ([VERROUILLE])
- `GET /api/formations/{id}/note-moyenne` -> retourne la note moyenne

### Exercice 2 : Sécurité par rôle
Créez un filtre `@AdminOnly` qui :
- Vérifie que le token JWT contient `role = "ADMIN"`
- Retourne `403 Forbidden` si le rôle est insuffisant
- Appliquez-le sur les endpoints de création/modification/suppression

### Exercice 3 : Validation avancée
Ajoutez à `FormationCreerDTO` :
- Un validateur personnalisé `@PrixCoherentAvecNiveau` qui vérifie que les formations débutant coûtent moins de 100€
- Testez avec une requête invalide et vérifiez le message d'erreur

---

## [OK] Checklist Module 4

- [ ] Comprendre l'authentification vs l'autorisation
- [ ] Hacher les mots de passe avec BCrypt
- [ ] Générer et valider des tokens JWT
- [ ] Créer un resource JAX-RS avec @GET, @POST, @PUT, @DELETE
- [ ] Utiliser @PathParam, @QueryParam, @HeaderParam, @Context
- [ ] Retourner des réponses HTTP avec les bons codes de statut
- [ ] Implémenter un ExceptionMapper global
- [ ] Séparer entités et DTOs
- [ ] Valider les entrées avec Bean Validation (@Valid)
- [ ] Documenter l'API avec OpenAPI

---

*Prochain module -> EJB, Messaging JMS et Tests* -> `05_ejb_messaging_tests.md`

# [LIVRE] Module 5 — EJB, Messaging & Tests
## Chapitres 16, 17, 18 et 19 : Transactions robustes, communication asynchrone et qualité du code

> [OBJECTIF] **Objectif** : Rendre EduShop robuste avec des transactions EJB, asynchrone avec JMS, et fiable avec des tests automatisés.

---

# [LIVRE] Chapitre 16 — EJB (Enterprise Java Beans)

## 16.1 Qu'est-ce qu'un EJB ?

Les **EJBs** (Enterprise Java Beans) sont des composants gérés par le serveur d'application qui fournissent des services enterprise **gratuitement** : transactions, pool de threads, sécurité, timer, etc.

```
Sans EJB :                          Avec EJB :
[X] Gérer les transactions soi-même   [OK] @Stateless -> transactions automatiques
[X] Gérer la concurrence              [OK] Pool de threads géré par le serveur
[X] Coder le scheduling               [OK] @Schedule -> timer déclaratif
[X] Appels distants complexes         [OK] @Remote -> appels distribués auto
```

## 16.2 @Stateless — L'EJB sans état

Le **Stateless Session Bean** est le plus utilisé. Il ne conserve pas d'état entre deux appels — chaque appel est indépendant.

```java
import jakarta.ejb.*;
import jakarta.persistence.*;

/**
 * @Stateless : EJB sans état
 * Le conteneur maintient un POOL d'instances (configurable)
 * -> Chaque requête prend une instance du pool, l'utilise, la remet dans le pool
 * -> Parfait pour les services métier
 */
@Stateless
public class PaiementService {

    @PersistenceContext(unitName = "EduShopPU")
    private EntityManager em;

    @EJB
    private NotificationService notificationService;

    /**
     * Par défaut, toutes les méthodes EJB sont REQUIRED (transaction obligatoire)
     * -> Si une transaction existe déjà, on l'utilise
     * -> Sinon, on en crée une nouvelle
     */
    public Commande payer(Long etudiantId, List<Long> formationIds, InfosPaiement infos) {

        // ── Tout ceci est dans UNE SEULE TRANSACTION ATOMIQUE ──

        // 1. Récupérer les entités
        Etudiant etudiant = em.find(Etudiant.class, etudiantId);
        if (etudiant == null) throw new EduShopException("NOT_FOUND", "Étudiant introuvable");

        List<Formation> formations = formationIds.stream()
            .map(id -> {
                Formation f = em.find(Formation.class, id);
                if (f == null) throw new EduShopException("NOT_FOUND", "Formation introuvable : " + id);
                return f;
            })
            .collect(Collectors.toList());

        // 2. Calculer le total
        double total = formations.stream().mapToDouble(Formation::getPrix).sum();

        // 3. Traiter le paiement (appel externe simulé)
        String referencePaiement = traiterPaiement(infos, total);

        // 4. Créer la commande
        Commande commande = new Commande(etudiant, formations, total, referencePaiement);
        em.persist(commande);

        // 5. Inscrire l'étudiant aux formations
        for (Formation f : formations) {
            etudiant.inscrire(f);
            f.setNombreInscrits(f.getNombreInscrits() + 1);
            em.merge(f);
        }
        em.merge(etudiant);

        // Si QUOI QUE CE SOIT échoue -> rollback automatique de TOUT !
        // Si succès -> commit automatique en fin de méthode

        return commande;
    }

    private String traiterPaiement(InfosPaiement infos, double montant) {
        // Simulation appel passerelle de paiement
        if (infos.getNumeroCarteTest().endsWith("0000")) {
            // Simuler un échec de paiement
            throw new PaiementEchecException("Carte refusée par la banque");
        }
        return "PAY-" + System.currentTimeMillis();
    }
}
```

## 16.3 Types de transactions EJB

```java
@Stateless
public class ExemplesTransactions {

    @PersistenceContext
    private EntityManager em;

    /**
     * REQUIRED (défaut) : Join si existe, crée sinon
     * 99% des cas d'usage
     */
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void methodeRequise() { /* ... */ }

    /**
     * REQUIRES_NEW : TOUJOURS créer une nouvelle transaction
     * Utiliser pour : logs d'audit (écrits même si la transaction principale échoue)
     */
    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public void loggerEvenement(String message) {
        // Cette écriture sera COMMITÉE même si la transaction appelante est rollbackée
        LogEvenement log = new LogEvenement(message, LocalDateTime.now());
        em.persist(log);
    }

    /**
     * SUPPORTS : utilise la transaction si elle existe, sinon sans transaction
     * Pour les lectures
     */
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<Formation> lireFormations() {
        return em.createQuery("SELECT f FROM Formation f", Formation.class)
                 .getResultList();
    }

    /**
     * NOT_SUPPORTED : suspend la transaction active
     * Pour les opérations qui ne doivent PAS participer à une transaction
     */
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public void envoyerEmailExternal(String destinataire, String message) {
        // Les emails ne peuvent pas être rollbackés !
        // -> On les envoie HORS transaction pour ne pas les envoyer si la transaction échoue
    }

    /**
     * NEVER : Lance une exception si une transaction existe
     */
    @TransactionAttribute(TransactionAttributeType.NEVER)
    public void methodeSansTransaction() { /* ... */ }

    /**
     * MANDATORY : Lance une exception si AUCUNE transaction n'existe
     */
    @TransactionAttribute(TransactionAttributeType.MANDATORY)
    public void doitEtreAppeleeDansTransaction() { /* ... */ }
}
```

## 16.4 @Stateful — EJB avec état

Le **Stateful Session Bean** conserve l'état entre les appels d'UN client particulier.

```java
/**
 * @Stateful : conserve l'état entre les appels du MÊME client
 * -> Une instance par client (pas de pool partagé)
 * -> Consomme plus de mémoire
 * -> Utiliser pour : wizard multi-étapes, panier complexe
 */
@Stateful
@StatefulTimeout(value = 30, unit = TimeUnit.MINUTES)
public class PanierEJB {

    @PersistenceContext(type = PersistenceContextType.EXTENDED)
    private EntityManager em; // Contexte étendu : entités restent "managed" entre les appels

    private List<Formation> formationsAuPanier = new ArrayList<>();
    private String codeDePanier;

    @PostConstruct
    public void init() {
        this.codeDePanier = "PANIER-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
    }

    // Méthode appelée à chaque ajout au panier
    public void ajouter(Long formationId) {
        Formation f = em.find(Formation.class, formationId);
        if (f == null) throw new FormationNotFoundException(formationId);
        if (!formationsAuPanier.contains(f)) {
            formationsAuPanier.add(f);
        }
    }

    public void retirer(Long formationId) {
        formationsAuPanier.removeIf(f -> f.getId().equals(formationId));
    }

    public List<Formation> getContenu() {
        return Collections.unmodifiableList(formationsAuPanier);
    }

    public double getTotal() {
        return formationsAuPanier.stream().mapToDouble(Formation::getPrix).sum();
    }

    /**
     * @Remove : marque la fin du cycle de vie du bean Stateful
     * Après cet appel, le bean est SUPPRIMÉ du serveur
     */
    @Remove
    public Commande validerCommande() {
        if (formationsAuPanier.isEmpty()) {
            throw new EduShopException("PANIER_VIDE", "Le panier est vide");
        }
        // ... créer la commande
        formationsAuPanier.clear(); // nettoyage (le bean sera de toute façon supprimé)
        return null; // retourner la commande créée
    }
}
```

## 16.5 @Singleton — EJB partagé

```java
/**
 * @Singleton : UNE SEULE instance pour toute l'application
 * Thread-safe par nature (verrou par défaut sur toutes les méthodes)
 * -> Parfait pour : caches, configuration, compteurs globaux
 */
@Singleton
@Startup // Créé au démarrage du serveur (pas à la première utilisation)
@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)
public class ConfigurationService {

    private Map<String, String> configuration = new ConcurrentHashMap<>();

    @PersistenceContext
    private EntityManager em;

    @PostConstruct
    public void chargerConfiguration() {
        System.out.println("=== Chargement de la configuration EduShop ===");
        // Charger depuis la base de données
        em.createQuery("SELECT c FROM Configuration c", Configuration.class)
          .getResultList()
          .forEach(c -> configuration.put(c.getCle(), c.getValeur()));
        System.out.println("Configuration chargée : " + configuration.size() + " paramètres");
    }

    @Lock(LockType.READ)  // Plusieurs lecteurs simultanés autorisés
    public String get(String cle) {
        return configuration.get(cle);
    }

    @Lock(LockType.READ)
    public String get(String cle, String defaut) {
        return configuration.getOrDefault(cle, defaut);
    }

    @Lock(LockType.WRITE) // Un seul écrivain à la fois, bloque les lecteurs
    public void set(String cle, String valeur) {
        configuration.put(cle, valeur);
        // Persister aussi en base
    }
}
```

## 16.6 Timer Service — Tâches planifiées

```java
@Singleton
@Startup
public class PlanificateurEduShop {

    @Inject private FormationService formationService;
    @Inject private NotificationService notificationService;

    /**
     * @Schedule : tâche récurrente déclarative
     * Expression cron-like
     */
    @Schedule(hour = "2", minute = "0", second = "0", persistent = false)
    public void rapportQuotidien() {
        System.out.println("[" + LocalDateTime.now() + "] Génération du rapport quotidien...");
        // Générer et envoyer le rapport
    }

    @Schedule(dayOfWeek = "Mon", hour = "8", minute = "0", persistent = false)
    public void rapportHebdomadaire() {
        System.out.println("[" + LocalDateTime.now() + "] Rapport hebdomadaire...");
    }

    // Toutes les 5 minutes : vérifier les formations non publiées
    @Schedule(minute = "*/5", hour = "*", persistent = false)
    public void verifierFormationsEnAttente() {
        List<Formation> enAttente = formationService.trouverEnAttente();
        if (!enAttente.isEmpty()) {
            System.out.println("Formations en attente de validation : " + enAttente.size());
        }
    }
}
```

---

# [LIVRE] Chapitre 17 — Messaging avec JMS

## 17.1 Pourquoi le messaging asynchrone ?

```
Problème avec les appels SYNCHRONES :
  Client -> Serveur A -> (attendre) -> Serveur B -> (attendre) -> retourne
  -> Si B est lent, A attend -> requête lente -> utilisateur frustré
  -> Si B tombe, A tombe aussi -> couplage fort

Solution avec le messaging ASYNCHRONE :
  Client -> Serveur A -> place message dans la queue -> répond immédiatement
  Serveur B -> consomme le message en arrière-plan -> traite à son rythme
  -> A ne dépend plus de B -> couplage faible
  -> Si B est lent, les messages s'accumulent dans la queue
  -> Si B tombe, les messages attendent -> pas de perte
```

## 17.2 Concepts JMS fondamentaux

```
PRODUCER   ->   MESSAGE BROKER   ->   CONSUMER
(publie)       (ActiveMQ, etc.)      (consomme)

Deux modèles de destination :

QUEUE (Point-à-point) :
  Producer -> [Queue] -> Consumer A
  -> UN seul consommateur reçoit le message
  -> Utilisé pour : tâches de traitement (envoi email, génération PDF)

TOPIC (Publish-Subscribe) :
  Producer -> [Topic] -> Consumer A
                     -> Consumer B
                     -> Consumer C
  -> TOUS les abonnés reçoivent le message
  -> Utilisé pour : notifications d'événements, broadcast
```

## 17.3 Configuration JMS dans WildFly

```xml
<!-- Ajouter dans la configuration WildFly (standalone.xml) -->
<!-- Ou utiliser la console d'administration WildFly -->

<!-- Queue pour les emails -->
<jms-queue name="EmailQueue" entries="/queue/EmailQueue"/>

<!-- Queue pour les notifications -->
<jms-queue name="NotificationQueue" entries="/queue/NotificationQueue"/>

<!-- Topic pour les événements système -->
<jms-topic name="EvenementsTopic" entries="/topic/EvenementsTopic"/>
```

## 17.4 Producer JMS — Envoyer des messages

```java
import jakarta.jms.*;

@Stateless
public class EmailQueueService {

    /**
     * @JMSConnectionFactory : injection de la factory de connexion JMS
     * La valeur correspond à la ressource configurée dans le serveur
     */
    @Inject
    @JMSConnectionFactory("java:/ConnectionFactory")
    private JMSContext jmsContext;

    /**
     * Injection de la Destination (Queue ou Topic)
     */
    @Resource(lookup = "java:/queue/EmailQueue")
    private Queue emailQueue;

    @Resource(lookup = "java:/topic/EvenementsTopic")
    private Topic evenementsTopic;

    /**
     * Envoyer une demande d'email en queue
     */
    public void demanderEnvoiEmail(EmailMessage email) {
        // Convertir en JSON pour le transport
        String jsonEmail = JsonUtil.toJson(email);

        // Créer et envoyer le message
        Message message = jmsContext.createTextMessage(jsonEmail);
        message.setStringProperty("type", "EMAIL");
        message.setStringProperty("destinataire", email.getTo());

        jmsContext.createProducer()
            .setDeliveryMode(DeliveryMode.PERSISTENT)  // Message survit au redémarrage
            .setPriority(Message.DEFAULT_PRIORITY)
            .send(emailQueue, message);

        System.out.println("Email mis en queue pour : " + email.getTo());
    }

    /**
     * Envoyer un événement à tous les abonnés du topic
     */
    public void publierEvenement(EvenementEduShop evenement) {
        String jsonEvenement = JsonUtil.toJson(evenement);
        Message message = jmsContext.createTextMessage(jsonEvenement);
        message.setStringProperty("type", evenement.getType());
        jmsContext.createProducer().send(evenementsTopic, message);
    }
}
```

## 17.5 MDB — Message Driven Bean (Consommateur)

```java
import jakarta.ejb.*;
import jakarta.jms.*;

/**
 * MDB : EJB qui consomme les messages d'une queue/topic
 * Déclenché automatiquement par le serveur quand un message arrive
 */
@MessageDriven(
    name = "EmailConsumer",
    activationConfig = {
        // Mapper ce MDB à la queue JMS
        @ActivationConfigProperty(
            propertyName = "destinationType",
            propertyValue = "jakarta.jms.Queue"
        ),
        @ActivationConfigProperty(
            propertyName = "destination",
            propertyValue = "java:/queue/EmailQueue"
        ),
        // Nombre de sessions concurrentes (pool de MDBs)
        @ActivationConfigProperty(
            propertyName = "maxSession",
            propertyValue = "5"
        ),
        // Accusé de réception automatique si pas d'exception
        @ActivationConfigProperty(
            propertyName = "acknowledgeMode",
            propertyValue = "Auto-acknowledge"
        )
    }
)
public class EmailConsumerMDB implements MessageListener {

    @Inject
    private SmtpEmailService smtpService;

    /**
     * Appelé automatiquement pour chaque message de la queue
     * S'exécute dans un thread séparé géré par le serveur
     */
    @Override
    public void onMessage(Message message) {
        try {
            if (!(message instanceof TextMessage textMessage)) {
                System.err.println("Type de message inattendu : " + message.getClass());
                return;
            }

            String json = textMessage.getText();
            EmailMessage email = JsonUtil.fromJson(json, EmailMessage.class);

            System.out.println("[EmailConsumer] Traitement email pour : " + email.getTo());

            // Envoyer l'email via SMTP
            smtpService.envoyer(email);

            System.out.println("[EmailConsumer] Email envoyé avec succès à : " + email.getTo());

            // Si pas d'exception -> le message est automatiquement acquitté (supprimé de la queue)

        } catch (JMSException e) {
            System.err.println("[EmailConsumer] Erreur lecture message : " + e.getMessage());
            // Sans acquittement -> le message sera retraité (retry automatique)
            throw new RuntimeException("Erreur traitement message email", e);
        } catch (Exception e) {
            System.err.println("[EmailConsumer] Erreur envoi email : " + e.getMessage());
            // Selon la config : Dead Letter Queue après N tentatives
        }
    }
}
```

## 17.6 Intégration JMS dans EduShop

```java
// Dans PaiementService, après un paiement réussi :
@Stateless
public class PaiementService {

    @EJB
    private EmailQueueService emailQueueService;

    public Commande payer(Long etudiantId, List<Long> formationIds, InfosPaiement infos) {
        // ... paiement ...

        // [OK] Appel asynchrone : l'email est mis en queue, on ne bloque pas
        EmailMessage email = EmailMessage.builder()
            .to(etudiant.getEmail())
            .sujet("Confirmation de commande #" + commande.getId())
            .corps(genererCorpsEmail(commande))
            .build();
        emailQueueService.demanderEnvoiEmail(email);

        // Publier l'événement pour les autres services
        emailQueueService.publierEvenement(
            new EvenementEduShop("COMMANDE_VALIDEE", commande.getId())
        );

        return commande; // <- Retour immédiat sans attendre l'email !
    }
}
```

---

# [LIVRE] Chapitre 18 — Tests Unitaires

## 18.1 Pourquoi tester ?

```
Sans tests :
  -> "Ça marche sur ma machine"
  -> Une modification casse quelque chose d'autre -> on ne le sait pas
  -> Peur de modifier du code existant
  -> Bugs en production = coût élevé

Avec tests :
  -> Filet de sécurité : si ça casse, le test l'indique immédiatement
  -> Confiance pour modifier le code
  -> Documentation vivante du comportement attendu
  -> Bugs détectés tôt = coût faible
```

## 18.2 JUnit 5 — Les bases

```java
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;

import static org.junit.jupiter.api.Assertions.*;

class FormationTest {

    private Formation formation;

    @BeforeEach
    void setup() {
        // Exécuté avant CHAQUE test
        formation = new Formation("Java JEE", "Description", 199.0, Niveau.AVANCE);
    }

    @AfterEach
    void teardown() {
        // Exécuté après CHAQUE test
        formation = null;
    }

    @BeforeAll
    static void setupAll() {
        // Exécuté UNE FOIS avant tous les tests de la classe
        System.out.println("=== Début des tests Formation ===");
    }

    @Test
    @DisplayName("Un prix négatif doit lever une exception")
    void prixNegatifLeveException() {
        assertThrows(
            IllegalArgumentException.class,
            () -> new Formation("Test", "Desc", -10.0, Niveau.DEBUTANT),
            "Une formation avec un prix négatif devrait être invalide"
        );
    }

    @Test
    @DisplayName("La TVA est calculée à 20%")
    void calculerTVARetourne20Pourcent() {
        double tva = formation.calculerTVA();
        assertEquals(39.8, tva, 0.001, "La TVA doit être 20% du prix");
    }

    @Test
    @DisplayName("Le titre ne peut pas être vide")
    void titrePasVide() {
        assertThrows(
            IllegalArgumentException.class,
            () -> formation.setTitre(""),
            "Le titre vide doit être rejeté"
        );
        assertThrows(
            IllegalArgumentException.class,
            () -> formation.setTitre(null),
            "Le titre null doit être rejeté"
        );
    }

    @Test
    @DisplayName("inscrireEtudiant incrémente le compteur")
    void inscrireEtudiantIncrementerCompteur() {
        int initial = formation.getNombreInscrits();
        formation.inscrireEtudiant();
        assertEquals(initial + 1, formation.getNombreInscrits());
    }

    @Test
    @Disabled("Désactivé temporairement - TODO : corriger le calcul de remise")
    void calculRemise() {
        // Ce test est ignoré
    }

    // Test avec plusieurs valeurs d'entrée
    @ParameterizedTest
    @ValueSource(doubles = {0.0, 10.0, 99.99, 999.0})
    @DisplayName("Les prix positifs sont valides")
    void prixPositifsValides(double prix) {
        assertDoesNotThrow(
            () -> formation.setPrix(prix),
            "Le prix " + prix + " devrait être accepté"
        );
    }

    @ParameterizedTest
    @CsvSource({
        "DEBUTANT, 49.0, true",    // formation débutant à 49€ -> remise autorisée
        "AVANCE, 199.0, false",     // formation avancée -> pas de remise auto
        "INTERMEDIAIRE, 0.0, true"  // formation gratuite -> toujours autorisée
    })
    void remiseAutorisee(String niveau, double prix, boolean attendu) {
        Formation f = new Formation("Test", "Desc", prix, Niveau.valueOf(niveau));
        assertEquals(attendu, f.isRemiseAutorisee());
    }
}
```

## 18.3 Mockito — Tester avec des mocks

```java
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;

@ExtendWith(MockitoExtension.class)
class CatalogueServiceTest {

    /**
     * @Mock : crée un faux objet qui ne fait rien par défaut
     * Permet de tester CatalogueService SANS vraie base de données
     */
    @Mock
    private FormationRepository repository;

    @Mock
    private CacheFormations cache;

    /**
     * @InjectMocks : crée l'objet réel avec les mocks injectés
     */
    @InjectMocks
    private CatalogueService service;

    /**
     * Capturer les arguments passés aux mocks
     */
    @Captor
    private ArgumentCaptor<Formation> formationCaptor;

    @Test
    @DisplayName("trouverParId retourne la formation si elle existe")
    void trouverParId_existant() {
        // ── GIVEN : configuration du mock ──
        Formation formation = new Formation("Java JEE", "Desc", 199.0, Niveau.AVANCE);
        formation.setId(1L);

        // "quand repository.findById(1L) est appelé -> retourner cette formation"
        when(repository.findById(1L)).thenReturn(Optional.of(formation));

        // ── WHEN : appel de la méthode testée ──
        Optional<Formation> resultat = service.trouverParId(1L);

        // ── THEN : vérifications ──
        assertTrue(resultat.isPresent());
        assertEquals("Java JEE", resultat.get().getTitre());
        assertEquals(199.0, resultat.get().getPrix());

        // Vérifier que le repository a bien été appelé UNE fois avec l'ID 1
        verify(repository, times(1)).findById(1L);
        // Vérifier qu'aucune autre méthode du repository n'a été appelée
        verifyNoMoreInteractions(repository);
    }

    @Test
    @DisplayName("trouverParId retourne empty si la formation n'existe pas")
    void trouverParId_inexistant() {
        when(repository.findById(anyLong())).thenReturn(Optional.empty());

        Optional<Formation> resultat = service.trouverParId(99L);

        assertFalse(resultat.isPresent());
    }

    @Test
    @DisplayName("creer sauvegarde la formation et notifie")
    void creer_sauvegardeLaFormation() {
        FormationCreerDTO dto = new FormationCreerDTO("Spring Boot", "Desc", 149.0, Niveau.INTERMEDIAIRE, 30, 1L);
        Formation sauvegardee = new Formation("Spring Boot", "Desc", 149.0, Niveau.INTERMEDIAIRE);
        sauvegardee.setId(42L);

        when(repository.save(any(Formation.class))).thenReturn(sauvegardee);

        Formation resultat = service.creer(dto);

        // Capturer l'argument passé à save
        verify(repository).save(formationCaptor.capture());
        Formation formationSauvegardee = formationCaptor.getValue();

        assertEquals("Spring Boot", formationSauvegardee.getTitre());
        assertEquals(149.0, formationSauvegardee.getPrix());
        assertNotNull(resultat);
        assertEquals(42L, resultat.getId());
    }

    @Test
    @DisplayName("supprimer lance une exception si la formation n'existe pas")
    void supprimer_formationInexistante_leveException() {
        when(repository.findById(99L)).thenReturn(Optional.empty());

        assertThrows(
            FormationNotFoundException.class,
            () -> service.supprimer(99L)
        );

        // Vérifier que delete N'A PAS été appelé
        verify(repository, never()).delete(any());
    }

    @Test
    @DisplayName("repository.save lance une RuntimeException -> service la wrap")
    void creer_erreurRepository_leveEduShopException() {
        FormationCreerDTO dto = new FormationCreerDTO("Test", "D", 99.0, Niveau.DEBUTANT, 10, 1L);

        // Mock qui lance une exception
        when(repository.save(any())).thenThrow(new RuntimeException("Connexion BD perdue"));

        EduShopException ex = assertThrows(
            EduShopException.class,
            () -> service.creer(dto)
        );

        assertEquals("SAVE_ERROR", ex.getCodeErreur());
        assertTrue(ex.getMessage().contains("Impossible de sauvegarder"));
    }
}
```

## 18.4 Test du Resource JAX-RS

```java
@ExtendWith(MockitoExtension.class)
class FormationResourceTest {

    @Mock
    private CatalogueService catalogueService;

    @InjectMocks
    private FormationResource resource;

    @Test
    void listerFormations_retourneListeFormatee() {
        List<FormationDTO> formations = List.of(
            new FormationDTO(1L, "Java JEE", "Desc", 199.0, "Avancé", 40, "Alice", 150, null),
            new FormationDTO(2L, "Docker", "Desc", 99.0, "Intermédiaire", 20, "Bob", 89, null)
        );

        when(catalogueService.lister(0, 10, "titre", "asc")).thenReturn(formations);
        when(catalogueService.compter()).thenReturn(2L);

        Response response = resource.listerFormations(null, 0, 10, null, null);

        assertEquals(200, response.getStatus());
        assertNotNull(response.getEntity());
    }

    @Test
    void trouverFormation_inexistante_retourne404() {
        when(catalogueService.trouverParId(99L)).thenReturn(Optional.empty());

        Response response = resource.trouverFormation(99L);

        assertEquals(404, response.getStatus());
    }
}
```

---

# [LIVRE] Chapitre 19 — Tests d'Intégration

## 19.1 Testcontainers — Tester avec une vraie base de données

Testcontainers lance des **vrais conteneurs Docker** pendant les tests.

```xml
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.19.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>mysql</artifactId>
    <version>1.19.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>1.19.0</version>
    <scope>test</scope>
</dependency>
```

```java
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.*;

/**
 * @Testcontainers : active l'intégration Testcontainers
 */
@Testcontainers
class FormationRepositoryIntegrationTest {

    /**
     * @Container : démarre ce conteneur pour les tests
     * static : partagé entre tous les tests de la classe (plus rapide)
     */
    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
        .withDatabaseName("edushop_test")
        .withUsername("test")
        .withPassword("test")
        .withInitScript("sql/schema-test.sql"); // Script de création des tables

    private EntityManagerFactory emf;
    private EntityManager em;
    private FormationRepository repository;

    @BeforeEach
    void setup() {
        Map<String, String> properties = new HashMap<>();
        properties.put("jakarta.persistence.jdbc.url", mysql.getJdbcUrl());
        properties.put("jakarta.persistence.jdbc.user", mysql.getUsername());
        properties.put("jakarta.persistence.jdbc.password", mysql.getPassword());
        properties.put("hibernate.hbm2ddl.auto", "create-drop");
        properties.put("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect");

        emf = Persistence.createEntityManagerFactory("EduShopPU", properties);
        em = emf.createEntityManager();
        repository = new FormationRepository();
        // Injection manuelle pour le test
        injectEntityManager(repository, em);
    }

    @AfterEach
    void teardown() {
        em.close();
        emf.close();
    }

    @Test
    void sauvegarderEtRetouver() {
        // GIVEN
        Formation formation = new Formation("Java JEE", "Desc", 199.0, Niveau.AVANCE);

        // WHEN
        em.getTransaction().begin();
        repository.save(formation);
        em.getTransaction().commit();

        em.clear(); // Vider le cache pour forcer la lecture depuis la BD

        // THEN
        Optional<Formation> retrouvee = repository.findById(formation.getId());
        assertTrue(retrouvee.isPresent());
        assertEquals("Java JEE", retrouvee.get().getTitre());
        assertEquals(199.0, retrouvee.get().getPrix());
    }

    @Test
    void rechercherParNiveau_filtreCorrectement() {
        // GIVEN : insérer des données de test
        em.getTransaction().begin();
        em.persist(new Formation("Java JEE", "D", 199.0, Niveau.AVANCE));
        em.persist(new Formation("Docker", "D", 99.0, Niveau.INTERMEDIAIRE));
        em.persist(new Formation("HTML/CSS", "D", 49.0, Niveau.DEBUTANT));
        em.persist(new Formation("Spring Boot", "D", 149.0, Niveau.INTERMEDIAIRE));
        em.getTransaction().commit();
        em.clear();

        // WHEN
        List<Formation> intermediaires = repository.findByNiveau(Niveau.INTERMEDIAIRE);

        // THEN
        assertEquals(2, intermediaires.size());
        assertTrue(intermediaires.stream()
            .allMatch(f -> f.getNiveau() == Niveau.INTERMEDIAIRE));
    }
}
```

## 19.2 Stratégie de tests pour EduShop

```
PYRAMIDE DE TESTS :

         ╔══════════════╗
         ║  Tests E2E   ║  <- Peu nombreux, lents (Selenium, REST assured)
        ╔╬══════════════╬╗
        ║ Tests d'intégr ║ <- Nombre modéré (Testcontainers, Arquillian)
       ╔╬╬══════════════╬╬╗
       ║   Tests Unitaires ║ <- Nombreux, rapides (JUnit + Mockito)
       ╚╩══════════════╩╩╝

Objectif EduShop :
  -> 70% Tests unitaires (services, logique métier)
  -> 20% Tests d'intégration (repositories, API REST)
  -> 10% Tests E2E (scénarios utilisateur complets)
```

### Exemple de test E2E avec REST Assured

```java
import io.restassured.RestAssured;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;

@Testcontainers
class EduShopApiE2ETest {

    // ... (setup Testcontainers + serveur embarqué) ...

    @Test
    void scenarioInscriptionEtAchat() {
        // 1. Inscription
        String email = "test-" + System.currentTimeMillis() + "@test.com";

        String token = given()
            .contentType("application/json")
            .body("""
                {
                    "prenom": "Test",
                    "nom": "User",
                    "email": "%s",
                    "motDePasse": "Password123!"
                }
                """.formatted(email))
        .when()
            .post("/api/auth/inscription")
        .then()
            .statusCode(201)
            .body("accessToken", notNullValue())
            .extract().path("accessToken");

        // 2. Lister les formations
        int nbFormations = given()
        .when()
            .get("/api/formations")
        .then()
            .statusCode(200)
            .body("data", hasSize(greaterThan(0)))
            .extract().path("total");

        System.out.println("Formations disponibles : " + nbFormations);

        // 3. S'inscrire à une formation
        given()
            .header("Authorization", "Bearer " + token)
            .contentType("application/json")
        .when()
            .post("/api/formations/1/inscription")
        .then()
            .statusCode(201);

        // 4. Vérifier les formations du profil
        given()
            .header("Authorization", "Bearer " + token)
        .when()
            .get("/api/profil/formations")
        .then()
            .statusCode(200)
            .body("$", hasSize(1))
            .body("[0].id", equalTo(1));
    }
}
```

---

## [SPOOL_OF_THREAD] EduShop v0.8 — Tests complets

### Structure des tests

```
src/test/java/com/edushop/
├── unit/
│   ├── model/
│   │   ├── FormationTest.java
│   │   └── CommandeTest.java
│   ├── service/
│   │   ├── CatalogueServiceTest.java
│   │   ├── PaiementServiceTest.java
│   │   └── AuthServiceTest.java
│   └── security/
│       └── JwtServiceTest.java
├── integration/
│   ├── repository/
│   │   ├── FormationRepositoryIT.java
│   │   └── EtudiantRepositoryIT.java
│   └── api/
│       ├── FormationResourceIT.java
│       └── AuthResourceIT.java
└── e2e/
    └── EduShopScenarioE2ETest.java
```

---

## [OUTIL] Exercice Module 5

### Exercice 1 : Tests unitaires du service Panier
Écrivez des tests JUnit + Mockito pour `PanierService` :
- Test : ajouter une formation au panier -> elle apparaît dans le contenu
- Test : ajouter deux fois la même -> elle n'apparaît qu'une fois
- Test : supprimer une formation -> elle disparaît
- Test : total correct -> somme des prix

### Exercice 2 : MDB de notification
Créez un MDB `InscriptionConsumerMDB` qui :
- Consomme les messages de `java:/queue/InscriptionQueue`
- Parse le JSON et extrait l'email de l'étudiant et le titre de la formation
- "Envoie" un email de confirmation (affichage console)
- Gère les erreurs (JSON invalide -> log + acquittement pour éviter la boucle infinie)

### Exercice 3 : Timer EJB
Ajoutez dans `PlanificateurEduShop` :
- Timer toutes les nuits à 3h : marquer les formations sans inscriptions depuis 6 mois comme "à archiver"
- Timer chaque lundi matin à 7h : envoyer un résumé hebdomadaire à chaque étudiant (via JMS)

---

## [OK] Checklist Module 5

- [ ] Créer un @Stateless EJB avec transactions automatiques
- [ ] Comprendre les 6 types de TransactionAttribute
- [ ] Utiliser @Schedule pour les tâches planifiées
- [ ] Configurer une Queue JMS dans WildFly
- [ ] Envoyer un message JMS depuis un EJB
- [ ] Créer un MDB qui consomme les messages
- [ ] Écrire des tests JUnit 5 avec assertions multiples
- [ ] Utiliser Mockito pour mocker les dépendances (@Mock, @InjectMocks)
- [ ] Vérifier les interactions avec verify()
- [ ] Écrire un test d'intégration avec Testcontainers

---

*Prochain module -> Déploiement Docker, Maven et DevOps* -> `06_devops_et_docker.md`

# [LIVRE] Module 6 — DevOps, Docker & CI/CD
## Chapitres 20 à 22 : Conteneuriser et automatiser EduShop

> [OBJECTIF] **Objectif** : Conteneuriser EduShop avec Docker, orchestrer les services avec Docker Compose, et automatiser les déploiements avec GitHub Actions.

---

# [LIVRE] Chapitre 20 — Serveurs d'Application JEE

## 20.1 Comparaison des serveurs

| Serveur | Type | Spéc. JEE | Cas d'usage |
|---------|------|-----------|-------------|
| **WildFly** | Complet | Jakarta EE 10 | Production enterprise |
| **GlassFish** | Complet | Jakarta EE 10 | Référence officielle |
| **Open Liberty** | Modulaire | Jakarta EE 10 | Microservices, cloud |
| **Tomcat** | Partiel (Servlet+JSP) | — | Applications web simples |
| **Payara** | Complet | Jakarta EE 10 | Cloud, microservices |

## 20.2 Structure de WildFly

```
wildfly-30.0.0.Final/
├── bin/
│   ├── standalone.sh          <- Démarrer en mode standalone
│   ├── domain.sh              <- Démarrer en mode cluster
│   └── jboss-cli.sh           <- CLI d'administration
├── standalone/
│   ├── configuration/
│   │   └── standalone.xml     <- Fichier de configuration principal
│   ├── deployments/           <- Déposer les WAR ici pour déployer
│   └── log/
│       └── server.log         <- Logs applicatifs
└── modules/                   <- Drivers JDBC, modules partagés
```

## 20.3 Déploiement sur WildFly

### Via Maven (méthode recommandée en développement)

```xml
<!-- Dans pom.xml -->
<plugin>
    <groupId>org.wildfly.plugins</groupId>
    <artifactId>wildfly-maven-plugin</artifactId>
    <version>4.2.0.Final</version>
    <configuration>
        <hostname>localhost</hostname>
        <port>9990</port>
        <username>admin</username>
        <password>admin123</password>
    </configuration>
</plugin>
```

```bash
# Démarrer WildFly
$WILDFLY_HOME/bin/standalone.sh

# 1. Construire et déployer
mvn clean package wildfly:deploy

# 2. Redéployer (si déjà présent)
mvn clean package wildfly:redeploy

# 3. Désinstaller
mvn wildfly:undeploy

# 4. Déploiement manuel (copier le WAR directement)
cp target/edushop.war $WILDFLY_HOME/standalone/deployments/
# WildFly détecte automatiquement le fichier et déploie !
# Un fichier edushop.war.deployed apparaît quand c'est bon
```

### Configurer la datasource via CLI

```bash
# Se connecter à la CLI WildFly
$WILDFLY_HOME/bin/jboss-cli.sh --connect

# Ajouter le module driver MySQL
module add --name=com.mysql \
           --resources=/path/to/mysql-connector-j-8.0.33.jar \
           --dependencies=javax.api,sun.jdk

# Enregistrer le driver
/subsystem=datasources/jdbc-driver=mysql:add(\
    driver-name=mysql,\
    driver-module-name=com.mysql,\
    driver-xa-datasource-class-name=com.mysql.cj.jdbc.MysqlXADataSource)

# Créer la datasource
data-source add \
    --name=EduShopDS \
    --jndi-name=java:/EduShopDS \
    --connection-url=jdbc:mysql://localhost:3306/edushop?useSSL=false \
    --driver-name=mysql \
    --user-name=edushop \
    --password=edushop_secret \
    --min-pool-size=5 \
    --max-pool-size=20

# Tester la connexion
/subsystem=datasources/data-source=EduShopDS:test-connection-in-pool

# Quitter
exit
```

### Configurer JMS (ActiveMQ) dans WildFly

```bash
# Via CLI WildFly
/subsystem=messaging-activemq/server=default/jms-queue=EmailQueue:add(\
    entries=["/queue/EmailQueue"])

/subsystem=messaging-activemq/server=default/jms-queue=NotificationQueue:add(\
    entries=["/queue/NotificationQueue"])

/subsystem=messaging-activemq/server=default/jms-topic=EvenementsTopic:add(\
    entries=["/topic/EvenementsTopic"])
```

---

# [LIVRE] Chapitre 21 — Maven : Maîtriser la construction

## 21.1 Le cycle de vie Maven

```
Phase                 Ce qui se passe
─────────────────────────────────────────────────────────────
validate            -> Vérifier que le pom.xml est correct
compile             -> javac : compiler src/main/java
test-compile        -> Compiler src/test/java
test                -> Exécuter les tests (*Test.java) avec Surefire
package             -> Créer le WAR/JAR dans target/
integration-test    -> Tests d'intégration (*IT.java) avec Failsafe
verify              -> Vérifier les résultats des tests
install             -> Copier dans ~/.m2/repository (local)
deploy              -> Publier sur Nexus/Artifactory (remote)
```

```bash
# Commandes Maven fréquentes

# Compiler sans exécuter les tests
mvn compile

# Exécuter uniquement les tests unitaires
mvn test

# Créer le WAR (+ tests unitaires)
mvn package

# Créer le WAR en ignorant les tests (CI rapide)
mvn package -DskipTests

# Tests unitaires + intégration
mvn verify

# Nettoyer (supprimer target/) avant de reconstruire
mvn clean package

# Passer en mode silencieux (moins de logs)
mvn clean package -q

# Voir les dépendances effectives
mvn dependency:tree

# Mettre à jour les snapshots
mvn clean package -U
```

## 21.2 pom.xml enterprise multi-modules

```xml
<!-- Parent POM : edushop/pom.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
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.edushop</groupId>
    <artifactId>edushop-parent</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    <name>EduShop - Parent POM</name>

    <!-- Modules enfants du projet -->
    <modules>
        <module>edushop-core</module>
        <module>edushop-api</module>
        <module>edushop-web</module>
    </modules>

    <properties>
        <java.version>17</java.version>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

        <!-- Versions centralisées ici : les modules les héritent -->
        <jakarta.version>10.0.0</jakarta.version>
        <jackson.version>2.15.2</jackson.version>
        <jjwt.version>0.12.3</jjwt.version>
        <junit.version>5.10.0</junit.version>
        <mockito.version>5.4.0</mockito.version>
        <testcontainers.version>1.19.0</testcontainers.version>
    </properties>

    <!-- Versions déclarées mais NON ajoutées aux modules -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>jakarta.platform</groupId>
                <artifactId>jakarta.jakartaee-api</artifactId>
                <version>${jakarta.version}</version>
                <scope>provided</scope>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
                <version>${jackson.version}</version>
            </dependency>
            <dependency>
                <groupId>org.junit.jupiter</groupId>
                <artifactId>junit-jupiter</artifactId>
                <version>${junit.version}</version>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>org.mockito</groupId>
                <artifactId>mockito-core</artifactId>
                <version>${mockito.version}</version>
                <scope>test</scope>
            </dependency>
            <!-- BOM Testcontainers : gère toutes ses versions en un seul bloc -->
            <dependency>
                <groupId>org.testcontainers</groupId>
                <artifactId>testcontainers-bom</artifactId>
                <version>${testcontainers.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.11.0</version>
                    <configuration>
                        <release>${java.version}</release>
                        <encoding>UTF-8</encoding>
                    </configuration>
                </plugin>
                <!-- Tests unitaires -->
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <version>3.1.2</version>
                    <configuration>
                        <excludes><exclude>**/*IT.java</exclude></excludes>
                    </configuration>
                </plugin>
                <!-- Tests d'intégration -->
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-failsafe-plugin</artifactId>
                    <version>3.1.2</version>
                    <executions>
                        <execution>
                            <goals>
                                <goal>integration-test</goal>
                                <goal>verify</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
                <!-- Couverture de code -->
                <plugin>
                    <groupId>org.jacoco</groupId>
                    <artifactId>jacoco-maven-plugin</artifactId>
                    <version>0.8.10</version>
                    <executions>
                        <execution>
                            <goals><goal>prepare-agent</goal></goals>
                        </execution>
                        <execution>
                            <id>report</id>
                            <phase>test</phase>
                            <goals><goal>report</goal></goals>
                        </execution>
                        <!-- Échec si couverture < 70% -->
                        <execution>
                            <id>check</id>
                            <goals><goal>check</goal></goals>
                            <configuration>
                                <rules>
                                    <rule>
                                        <limits>
                                            <limit>
                                                <counter>LINE</counter>
                                                <value>COVEREDRATIO</value>
                                                <minimum>0.70</minimum>
                                            </limit>
                                        </limits>
                                    </rule>
                                </rules>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

    <!-- Profils Maven : configurations par environnement -->
    <profiles>
        <profile>
            <id>dev</id>
            <activation><activeByDefault>true</activeByDefault></activation>
            <properties>
                <db.url>jdbc:mysql://localhost:3306/edushop_dev</db.url>
                <hibernate.ddl>update</hibernate.ddl>
                <log.level>DEBUG</log.level>
            </properties>
        </profile>

        <profile>
            <id>ci</id>
            <properties>
                <db.url>jdbc:mysql://mysql:3306/edushop_test</db.url>
                <hibernate.ddl>create-drop</hibernate.ddl>
                <log.level>WARN</log.level>
            </properties>
        </profile>

        <profile>
            <id>prod</id>
            <properties>
                <db.url>${env.DB_URL}</db.url>  <!-- Depuis l'env système -->
                <hibernate.ddl>validate</hibernate.ddl>
                <log.level>WARN</log.level>
            </properties>
        </profile>
    </profiles>
</project>
```

---

# [LIVRE] Chapitre 22 — Docker & CI/CD

## 22.1 Pourquoi Docker ?

```
Problème classique : "Ça marche sur ma machine !"

Sans Docker :
  Développeur   -> Java 17, MySQL 8.0, WildFly 30
  Serveur test  -> Java 11, MySQL 5.7, WildFly 27 -> Comportement différent !
  Serveur prod  -> Java 17, MySQL 8.0, WildFly 28 -> Bugs mystérieux !

Avec Docker :
  Tout le monde -> Même image Docker -> Comportement identique partout
  -> "Build Once, Run Anywhere"
```

## 22.2 Dockerfile EduShop (multi-stage)

```dockerfile
# ═══════════════════════════════════════════════════════════
# ÉTAPE 1 : Construction du WAR avec Maven
# ═══════════════════════════════════════════════════════════
FROM eclipse-temurin:17-jdk-alpine AS builder

LABEL maintainer="team@edushop.com"
LABEL description="Build stage - compile EduShop WAR"

WORKDIR /build

# ─── Astuce cache Docker : copier pom.xml AVANT les sources ───
# Si les sources changent mais pas les dépendances,
# Docker réutilise le cache de cette couche (téléchargement Maven évité)
COPY pom.xml .
COPY edushop-core/pom.xml edushop-core/
COPY edushop-api/pom.xml edushop-api/
RUN mvn dependency:go-offline -B --no-transfer-progress

# Maintenant copier les sources (invalidera le cache seulement ici)
COPY edushop-core/src edushop-core/src
COPY edushop-api/src edushop-api/src

# Construire sans les tests (les tests tournent dans la CI séparément)
RUN mvn clean package -DskipTests -B --no-transfer-progress

# ═══════════════════════════════════════════════════════════
# ÉTAPE 2 : Image de production WildFly (légère, sans JDK)
# ═══════════════════════════════════════════════════════════
FROM quay.io/wildfly/wildfly:30.0.0.Final-jdk17

LABEL maintainer="team@edushop.com"
LABEL description="EduShop production image"
LABEL version="1.0.0"

# Passer à l'utilisateur non-root de WildFly (sécurité)
USER jboss

# Copier la configuration WildFly personnalisée
COPY --chown=jboss:root \
    docker/wildfly/standalone.xml \
    /opt/jboss/wildfly/standalone/configuration/standalone.xml

# Copier le driver MySQL dans les modules WildFly
COPY --chown=jboss:root \
    docker/wildfly/modules/com/mysql/ \
    /opt/jboss/wildfly/modules/com/mysql/

# Copier le WAR depuis l'étape de build
COPY --from=builder --chown=jboss:root \
    /build/edushop-api/target/edushop.war \
    /opt/jboss/wildfly/standalone/deployments/edushop.war

# Exposer les ports
EXPOSE 8080   # Application HTTP
EXPOSE 9990   # Console admin WildFly

# Variables d'environnement avec valeurs par défaut (dev)
# SURCHARGER en production avec docker run -e ou dans docker-compose.yml
ENV DB_HOST=mysql \
    DB_PORT=3306 \
    DB_NAME=edushop \
    DB_USER=edushop \
    DB_PASSWORD=changeme \
    JWT_SECRET=changeme-in-production-min-32-chars \
    ACTIVEMQ_HOST=activemq \
    ACTIVEMQ_PORT=61616 \
    JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC"

# Health check : vérifier que l'app répond
HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \
    CMD curl -f http://localhost:8080/edushop/api/health || exit 1

# Démarrer WildFly en mode standalone (bind sur toutes les interfaces)
CMD ["/opt/jboss/wildfly/bin/standalone.sh", \
     "-b", "0.0.0.0", \
     "-bmanagement", "0.0.0.0"]
```

## 22.3 Docker Compose — Environnement local complet

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

services:

  # ─────────────────────────────────────────
  # Base de données MySQL
  # ─────────────────────────────────────────
  mysql:
    image: mysql:8.0
    container_name: edushop-mysql
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: edushop
      MYSQL_USER: edushop
      MYSQL_PASSWORD: edushop_secret
    ports:
      - "3306:3306"  # Accessible depuis l'hôte (DBeaver, Workbench...)
    volumes:
      - mysql_data:/var/lib/mysql           # Persistance des données
      - ./sql/init.sql:/docker-entrypoint-initdb.d/01-init.sql:ro
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "edushop", "-pedushop_secret"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 30s
    networks:
      - edushop-net

  # ─────────────────────────────────────────
  # Message Broker ActiveMQ
  # ─────────────────────────────────────────
  activemq:
    image: apache/activemq-classic:5.18.3
    container_name: edushop-activemq
    restart: unless-stopped
    environment:
      ACTIVEMQ_USERNAME: admin
      ACTIVEMQ_PASSWORD: admin123
    ports:
      - "61616:61616"   # Protocole JMS (OpenWire)
      - "8161:8161"     # Console web -> http://localhost:8161
    volumes:
      - activemq_data:/opt/activemq/data
    networks:
      - edushop-net

  # ─────────────────────────────────────────
  # Application EduShop (WildFly)
  # ─────────────────────────────────────────
  app:
    build:
      context: .
      dockerfile: Dockerfile
      cache_from:
        - edushop/app:latest   # Réutiliser les couches de la dernière image
    image: edushop/app:${APP_VERSION:-latest}
    container_name: edushop-app
    restart: unless-stopped
    depends_on:
      mysql:
        condition: service_healthy    # Attendre que MySQL réponde !
      activemq:
        condition: service_started
    environment:
      DB_HOST: mysql
      DB_PORT: 3306
      DB_NAME: edushop
      DB_USER: edushop
      DB_PASSWORD: edushop_secret
      ACTIVEMQ_HOST: activemq
      JWT_SECRET: "${JWT_SECRET}"     # Depuis le fichier .env
      JAVA_OPTS: "-Xms256m -Xmx768m"
    ports:
      - "8080:8080"
      - "9990:9990"
    volumes:
      - app_logs:/opt/jboss/wildfly/standalone/log
    networks:
      - edushop-net

  # ─────────────────────────────────────────
  # Nginx — Reverse Proxy & HTTPS
  # ─────────────────────────────────────────
  nginx:
    image: nginx:1.25-alpine
    container_name: edushop-nginx
    restart: unless-stopped
    depends_on:
      - app
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./docker/nginx/ssl:/etc/nginx/ssl:ro
      - nginx_logs:/var/log/nginx
    networks:
      - edushop-net

volumes:
  mysql_data:
  activemq_data:
  app_logs:
  nginx_logs:

networks:
  edushop-net:
    driver: bridge
```

```bash
# ── Fichier .env (à NE PAS committer en git !) ──
JWT_SECRET=super-secret-key-minimum-256-bits-for-production
APP_VERSION=1.0.0
```

```
# .gitignore
.env
*.env
docker/nginx/ssl/
```

## 22.4 Configuration Nginx

```nginx
# docker/nginx/nginx.conf
events { worker_connections 1024; }

http {
    # Logs
    access_log /var/log/nginx/access.log;
    error_log  /var/log/nginx/error.log warn;

    # Compression gzip
    gzip on;
    gzip_types application/json text/html text/css application/javascript;

    # Upstream : le service "app" de Docker Compose
    upstream edushop_app {
        server app:8080;  # "app" = nom du service dans docker-compose.yml
    }

    # Rediriger HTTP -> HTTPS
    server {
        listen 80;
        server_name edushop.com www.edushop.com;
        return 301 https://$host$request_uri;
    }

    # Serveur HTTPS principal
    server {
        listen 443 ssl http2;
        server_name edushop.com www.edushop.com;

        ssl_certificate     /etc/nginx/ssl/edushop.crt;
        ssl_certificate_key /etc/nginx/ssl/edushop.key;
        ssl_protocols       TLSv1.2 TLSv1.3;

        # En-têtes de sécurité
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
        add_header X-Frame-Options DENY;
        add_header X-Content-Type-Options nosniff;
        add_header X-XSS-Protection "1; mode=block";

        # Proxy vers WildFly
        location / {
            proxy_pass         http://edushop_app;
            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;
            proxy_read_timeout 120s;
            proxy_connect_timeout 30s;
        }

        # Cache des ressources statiques
        location ~* \.(css|js|png|jpg|gif|ico|woff2)$ {
            proxy_pass http://edushop_app;
            expires 30d;
            add_header Cache-Control "public, immutable";
        }

        # Bloquer l'accès à la console admin WildFly depuis l'extérieur
        location /management {
            deny all;
            return 403;
        }
    }
}
```

## 22.5 Commandes Docker essentielles

```bash
# ── IMAGES ──
docker build -t edushop/app:1.0.0 .              # Construire une image
docker images                                      # Lister les images locales
docker rmi edushop/app:1.0.0                       # Supprimer une image
docker image prune                                 # Nettoyer les images inutilisées

# ── CONTENEURS ──
docker run -d -p 8080:8080 --name edushop edushop/app:1.0.0  # Lancer
docker ps                                          # Conteneurs en cours
docker ps -a                                       # Tous les conteneurs
docker stop edushop                                # Arrêter
docker start edushop                               # Redémarrer
docker rm edushop                                  # Supprimer
docker logs -f edushop                             # Logs en temps réel
docker exec -it edushop /bin/sh                    # Shell dans le conteneur

# ── DOCKER COMPOSE ──
docker compose up -d                               # Démarrer en arrière-plan
docker compose up -d --build                       # Forcer la reconstruction
docker compose down                                # Arrêter et supprimer
docker compose down -v                             # + supprimer les volumes !
docker compose logs -f app                         # Logs du service "app"
docker compose restart app                         # Redémarrer un service
docker compose exec app /bin/sh                    # Shell dans un service
docker compose ps                                  # État des services

# ── NETTOYAGE ──
docker system prune -af                            # Tout nettoyer (images, conteneurs, réseaux)
docker volume prune                                # Nettoyer les volumes inutilisés
```

## 22.6 CI/CD avec GitHub Actions

```yaml
# .github/workflows/ci-cd.yml

name: EduShop CI/CD Pipeline

# Déclencheurs du pipeline
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

# Variables d'environnement globales
env:
  REGISTRY: ghcr.io                              # GitHub Container Registry
  IMAGE_NAME: ${{ github.repository }}/edushop  # edushop/edushop-app

jobs:

  # ══════════════════════════════════════════════════
  # JOB 1 : Tests unitaires
  # ══════════════════════════════════════════════════
  tests-unitaires:
    name: "[TEST] Tests Unitaires"
    runs-on: ubuntu-latest

    steps:
      - name: "[ENTREE] Récupérer le code"
        uses: actions/checkout@v4

      - name: "[HOT_BEVERAGE] Configurer Java 17"
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: maven  # Cache Maven entre les runs (plus rapide !)

      - name: "[OUTIL] Compiler et tester"
        run: mvn clean test -B --no-transfer-progress

      - name: "[GRAPHIQUE] Publier le rapport de couverture"
        uses: codecov/codecov-action@v3
        with:
          file: target/site/jacoco/jacoco.xml
          fail_ci_if_error: false

      - name: "[SORTIE] Archiver les résultats de tests"
        uses: actions/upload-artifact@v3
        if: always()  # Même si les tests échouent
        with:
          name: test-results
          path: target/surefire-reports/

  # ══════════════════════════════════════════════════
  # JOB 2 : Tests d'intégration (avec Docker)
  # ══════════════════════════════════════════════════
  tests-integration:
    name: "[LIEN] Tests d'Intégration"
    runs-on: ubuntu-latest
    needs: tests-unitaires  # Seulement si les tests unitaires passent

    services:
      # Service MySQL pour les tests d'intégration
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: edushop_test
          MYSQL_USER: test
          MYSQL_PASSWORD: test
        ports:
          - 3306:3306
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-retries=5

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: maven

      - name: "[LIEN] Tests d'intégration (Testcontainers)"
        run: mvn verify -Pci -B --no-transfer-progress
        env:
          MYSQL_HOST: localhost
          MYSQL_PORT: 3306

  # ══════════════════════════════════════════════════
  # JOB 3 : Analyse de sécurité
  # ══════════════════════════════════════════════════
  securite:
    name: "[VERROUILLE] Analyse de Sécurité"
    runs-on: ubuntu-latest
    needs: tests-unitaires

    steps:
      - uses: actions/checkout@v4

      - name: "[RECHERCHE] Scan des dépendances (OWASP)"
        run: mvn dependency-check:check -B
        continue-on-error: true  # Ne pas bloquer sur les avertissements

      - name: "[SECURISE] Scan de l'image Docker (Trivy)"
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'edushop/app:latest'
          format: 'sarif'
          output: 'trivy-results.sarif'
        continue-on-error: true

  # ══════════════════════════════════════════════════
  # JOB 4 : Construire et publier l'image Docker
  # ══════════════════════════════════════════════════
  build-image:
    name: "[DOCKER] Build & Push Docker Image"
    runs-on: ubuntu-latest
    needs: [tests-integration, securite]
    # Seulement sur main (pas sur les PRs)
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'

    permissions:
      contents: read
      packages: write  # Nécessaire pour pousser sur ghcr.io

    steps:
      - uses: actions/checkout@v4

      - name: "[CLE] Authentification sur le registry"
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: "[LABEL] Extraire les métadonnées Docker"
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=sha-
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=raw,value=latest,enable={{is_default_branch}}

      - name: "[RAPIDE] Configurer BuildKit (build plus rapide)"
        uses: docker/setup-buildx-action@v3

      - name: "[OUTIL] Construire et pousser l'image"
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha  # Cache GitHub Actions
          cache-to: type=gha,mode=max

      - name: "[LISTE] Résumé du déploiement"
        run: |
          echo "## [DOCKER] Image Docker publiée" >> $GITHUB_STEP_SUMMARY
          echo "- **Registry**: ${{ env.REGISTRY }}" >> $GITHUB_STEP_SUMMARY
          echo "- **Image**: ${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY
          echo "- **Tags**: ${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY

  # ══════════════════════════════════════════════════
  # JOB 5 : Déploiement en production
  # ══════════════════════════════════════════════════
  deploiement-prod:
    name: "[RAPIDE] Déploiement Production"
    runs-on: ubuntu-latest
    needs: build-image
    environment:
      name: production
      url: https://edushop.com
    # Déclenche une validation manuelle dans GitHub

    steps:
      - name: "[RAPIDE] Déployer via SSH"
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.PROD_HOST }}
          username: ${{ secrets.PROD_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            cd /opt/edushop
            echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
            docker compose pull app
            docker compose up -d --no-deps app
            docker compose exec -T app curl -f http://localhost:8080/edushop/api/health
            echo "[OK] Déploiement EduShop v${{ github.sha }} réussi !"

      - name: "[ANNONCE] Notification Slack"
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: "EduShop déployé en production - commit ${{ github.sha }}"
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
        if: always()
```

## 22.7 Endpoint de Health Check

```java
// Endpoint requis par le Docker Healthcheck et les load balancers
@Path("/health")
@Produces(MediaType.APPLICATION_JSON)
public class HealthResource {

    @PersistenceContext
    private EntityManager em;

    @GET
    public Response health() {
        Map<String, Object> statut = new LinkedHashMap<>();

        // Vérifier la connexion à la base de données
        boolean dbOk = verifierBaseDeDonnees();
        statut.put("database", dbOk ? "UP" : "DOWN");
        statut.put("application", "UP");
        statut.put("version", "1.0.0");
        statut.put("timestamp", LocalDateTime.now().toString());

        int httpCode = dbOk ? 200 : 503;
        return Response.status(httpCode).entity(statut).build();
    }

    private boolean verifierBaseDeDonnees() {
        try {
            em.createNativeQuery("SELECT 1").getSingleResult();
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}
```

---

## [SPOOL_OF_THREAD] EduShop v0.9 — Déploiement complet

```bash
# ── Workflow de déploiement complet ──

# 1. Cloner le projet
git clone https://github.com/votre-compte/edushop.git
cd edushop

# 2. Configurer les variables
cp .env.example .env
nano .env  # Renseigner JWT_SECRET, mots de passe...

# 3. Démarrer l'environnement
docker compose up -d

# 4. Attendre que tout soit prêt (90s environ pour WildFly)
docker compose logs -f app

# 5. Vérifier la santé
curl http://localhost/edushop/api/health

# 6. Accéder à l'application
echo "Application : http://localhost"
echo "API : http://localhost/edushop/api"
echo "Console WildFly : http://localhost:9990"
echo "Console ActiveMQ : http://localhost:8161"
```

---

## [OUTIL] Exercice Module 6

### Exercice 1 : Dockerfile optimisé
Créez un Dockerfile multi-stage qui :
- Utilise `eclipse-temurin:17-jdk-alpine` pour la compilation
- Utilise `eclipse-temurin:17-jre-alpine` (pas JDK, plus léger) pour l'exécution
- Configure correctement le `.dockerignore` pour exclure `target/`, `.git/`, `*.md`
- Vérifiez que la taille finale de l'image est raisonnable : `docker images`

### Exercice 2 : Pipeline GitHub Actions
Créez un pipeline CI qui :
- Déclenche sur push vers `develop` et `main`
- Job 1 : compile + tests unitaires (JUnit)
- Job 2 (si Job 1 OK) : tests d'intégration avec service MySQL
- Job 3 (si Job 2 OK, branche main uniquement) : build Docker + push sur GHCR
- Publie un résumé dans le Step Summary de GitHub

### Exercice 3 : Docker Compose de développement
Créez un `docker-compose.dev.yml` qui :
- Lance uniquement MySQL et ActiveMQ (l'app tourne localement via Maven)
- Expose les ports nécessaires
- Monte un script d'init SQL avec des données de test
- Fournit une commande dans le `Makefile` pour démarrer le tout

---

## [OK] Checklist Module 6

- [ ] Configurer une datasource MySQL dans WildFly
- [ ] Déployer un WAR via Maven WildFly Plugin
- [ ] Écrire un Dockerfile multi-stage optimisé
- [ ] Créer un docker-compose.yml avec dépendances et healthcheck
- [ ] Configurer Nginx comme reverse proxy
- [ ] Écrire un pipeline CI/CD complet GitHub Actions
- [ ] Implémenter un endpoint /health pour les health checks
- [ ] Utiliser les profils Maven (dev, ci, prod)

---

*Prochain module -> Microservices, MicroProfile et Architecture Expert* -> `07_microservices_et_expert.md`

# [LIVRE] Module 7 — Microservices, MicroProfile & Architecture Expert
## Chapitres 23 à 30 : Du monolithe aux microservices cloud-native

> [OBJECTIF] **Objectif** : Décomposer EduShop en microservices, appliquer les patterns d'architecture cloud-native, et maîtriser les concepts avancés du niveau expert.

---

# [LIVRE] Chapitre 23 — MicroProfile

## 23.1 Qu'est-ce que MicroProfile ?

**MicroProfile** est un ensemble de spécifications Jakarta EE pensées pour les **microservices**. Là où Jakarta EE complet est lourd (EJB, EAR...), MicroProfile fournit les fonctionnalités essentielles de manière légère.

```
Jakarta EE Complet :          MicroProfile :
  EJB                            CDI
  JPA                            JAX-RS
  JMS                            JSON-B
  JSF                            Config <- Nouveauté
  ...beaucoup d'autres           Fault Tolerance <- Nouveauté
  -> Serveur lourd ~200Mo         Health <- Nouveauté
                                 Metrics <- Nouveauté
                                 JWT <- Nouveauté
                                 OpenAPI
                                 -> Serveur léger ~30Mo
```

## 23.2 MicroProfile Config — Configuration externalisée

La configuration ne doit **jamais** être dans le code. MicroProfile Config offre plusieurs sources de configuration avec priorité.

```
Priorité (la plus haute gagne) :
  1. Variables système : -Ddb.host=localhost (priorité 400)
  2. Variables d'environnement : DB_HOST=localhost (priorité 300)
  3. Fichiers microprofile-config.properties (priorité 100)
  4. Valeurs par défaut dans le code (priorité plus basse)
```

```properties
# src/main/resources/META-INF/microprofile-config.properties
# Configuration de développement (surchargée en prod par variables d'env)

# Base de données
db.host=localhost
db.port=3306
db.name=edushop

# JWT
jwt.expiration.heures=24
jwt.issuer=edushop.com

# Catalogue
catalogue.page.taille.defaut=10
catalogue.page.taille.max=100

# Email
email.smtp.host=smtp.mailtrap.io
email.smtp.port=587
email.from=noreply@edushop.com
```

```java
import org.eclipse.microprofile.config.inject.ConfigProperty;

@ApplicationScoped
public class AppConfig {

    /**
     * @ConfigProperty : injection de valeur de configuration
     * name : le nom de la propriété
     * defaultValue : valeur si la propriété n'est pas définie
     */
    @Inject
    @ConfigProperty(name = "db.host", defaultValue = "localhost")
    private String dbHost;

    @Inject
    @ConfigProperty(name = "db.port", defaultValue = "3306")
    private int dbPort;

    @Inject
    @ConfigProperty(name = "jwt.expiration.heures", defaultValue = "24")
    private long jwtExpirationHeures;

    @Inject
    @ConfigProperty(name = "catalogue.page.taille.max", defaultValue = "100")
    private int pageTailleMax;

    // La config peut aussi être typée automatiquement
    @Inject
    @ConfigProperty(name = "email.smtp.host")
    private Optional<String> smtpHost; // Optional si la propriété est facultative

    // Getters...
    public String getDbHost() { return dbHost; }
    public int getDbPort() { return dbPort; }
    public long getJwtExpirationHeures() { return jwtExpirationHeures; }
    public int getPageTailleMax() { return pageTailleMax; }
}
```

```java
// Utilisation dans un service
@ApplicationScoped
public class CatalogueService {

    @Inject
    private AppConfig config;

    public PageResultat<FormationDTO> lister(int page, int taille) {
        // La taille est limitée par la config, pas hardcodée
        int tailleEffective = Math.min(taille, config.getPageTailleMax());
        // ...
    }
}
```

## 23.3 MicroProfile Fault Tolerance — Résilience

Les microservices communiquent sur le réseau. Le réseau est **instable**. La tolérance aux pannes est donc essentielle.

```xml
<dependency>
    <groupId>org.eclipse.microprofile.fault-tolerance</groupId>
    <artifactId>microprofile-fault-tolerance-api</artifactId>
    <version>4.0</version>
    <scope>provided</scope>
</dependency>
```

```java
import org.eclipse.microprofile.faulttolerance.*;

@ApplicationScoped
public class CatalogueExterneService {

    /**
     * @Retry : réessayer si l'appel échoue
     * maxRetries : nombre max de tentatives
     * delay : attendre avant de réessayer (éviter de spammer)
     * retryOn : quelles exceptions déclenchent un retry
     */
    @Retry(maxRetries = 3, delay = 500, retryOn = {IOException.class, TimeoutException.class})
    public List<FormationDTO> recupererFormationsExternes() {
        // Appel HTTP externe qui peut échouer
        return httpClient.get("/api/formations");
    }

    /**
     * @Timeout : abandonner si trop long
     * value : durée max (en millisecondes par défaut)
     */
    @Timeout(value = 5, unit = ChronoUnit.SECONDS)
    @Retry(maxRetries = 2)
    public FormationDTO rechercherFormation(String titre) {
        return httpClient.get("/api/formations?search=" + titre);
    }

    /**
     * @CircuitBreaker : éviter d'appeler un service en panne
     *
     * Fonctionnement :
     * FERMÉ -> Appels normaux
     *   v Si requestVolumeThreshold appels et failureRatio% échouent
     * OUVERT -> Rejette immédiatement tous les appels (retourne le fallback)
     *   v Après delay
     * DEMI-OUVERT -> Laisse passer quelques appels test
     *   v Si succès -> FERMÉ | Si échec -> OUVERT
     */
    @CircuitBreaker(
        requestVolumeThreshold = 10,  // Évaluer sur les 10 derniers appels
        failureRatio = 0.5,           // Ouvrir si 50% échouent
        delay = 30,                   // Rester ouvert 30s avant de réessayer
        delayUnit = ChronoUnit.SECONDS,
        successThreshold = 5          // 5 succès consécutifs pour refermer
    )
    @Fallback(fallbackMethod = "formationsParDefaut")
    public List<FormationDTO> getFormationsRecommandees(Long etudiantId) {
        return serviceRecommandation.get("/reco/" + etudiantId);
    }

    /**
     * Méthode de repli : appelée quand le circuit est ouvert
     * Doit avoir la même signature que la méthode principale
     */
    private List<FormationDTO> formationsParDefaut(Long etudiantId) {
        // Retourner des formations populaires depuis le cache local
        return cacheFormations.getFormationsPopulaires();
    }

    /**
     * @Bulkhead : limiter les appels concurrents
     * value : nombre max d'appels simultanés
     * waitingTaskQueue : file d'attente si bulkhead plein
     */
    @Bulkhead(value = 5, waitingTaskQueue = 10)
    public byte[] telechargerSupport(Long formationId) {
        // Limiter à 5 téléchargements simultanés (éviter de saturer)
        return stockageService.download("formations/" + formationId + ".zip");
    }
}
```

## 23.4 MicroProfile Health — Sondes de vie

```java
import org.eclipse.microprofile.health.*;

/**
 * Liveness : l'application est-elle vivante ? (doit être redémarrée ?)
 * -> Kubernetes redémarre si liveness = DOWN
 */
@Liveness
@ApplicationScoped
public class LivenessCheck implements HealthCheck {

    @Override
    public HealthCheckResponse call() {
        return HealthCheckResponse.builder()
            .name("edushop-liveness")
            .up()
            .withData("version", "1.0.0")
            .withData("time", LocalDateTime.now().toString())
            .build();
    }
}

/**
 * Readiness : l'application est-elle prête à recevoir des requêtes ?
 * -> Kubernetes retire du load balancer si readiness = DOWN
 */
@Readiness
@ApplicationScoped
public class ReadinessCheck implements HealthCheck {

    @Inject
    private EntityManager em;

    @Inject
    @ConfigProperty(name = "db.host")
    private String dbHost;

    @Override
    public HealthCheckResponse call() {
        HealthCheckResponseBuilder builder = HealthCheckResponse.builder()
            .name("edushop-readiness");

        try {
            // Vérifier connexion base de données
            em.createNativeQuery("SELECT 1").getSingleResult();
            builder.up()
                .withData("database", "connected")
                .withData("db.host", dbHost);
        } catch (Exception e) {
            builder.down()
                .withData("database", "disconnected")
                .withData("error", e.getMessage());
        }

        return builder.build();
    }
}
```

```bash
# Endpoints Health générés automatiquement :
curl http://localhost:8080/health        # Toutes les sondes
curl http://localhost:8080/health/live   # Liveness uniquement
curl http://localhost:8080/health/ready  # Readiness uniquement
```

## 23.5 MicroProfile Metrics

```java
import org.eclipse.microprofile.metrics.annotation.*;

@ApplicationScoped
public class PaiementService {

    /**
     * @Counted : compter le nombre de fois que la méthode est appelée
     */
    @Counted(name = "paiements.total",
             description = "Nombre total de paiements traités")
    public Commande payer(PaiementRequest request) {
        // ...
    }

    /**
     * @Timed : mesurer le temps d'exécution
     */
    @Timed(name = "paiements.duree",
           description = "Durée de traitement des paiements")
    public Commande payerAvecMesure(PaiementRequest request) {
        // ...
    }

    /**
     * @Gauge : valeur instantanée (ex: taille du pool)
     */
    @Gauge(name = "panier.taille.moyenne",
           description = "Taille moyenne des paniers en cours",
           unit = "formations")
    public double getTailleMoyennePaniers() {
        return panierService.getMoyenne();
    }
}
```

```bash
# Métriques disponibles aux formats Prometheus et JSON
curl http://localhost:8080/metrics
curl -H "Accept: application/json" http://localhost:8080/metrics
```

---

# [LIVRE] Chapitre 24 — Architecture Microservices

## 24.1 Décomposer EduShop en microservices

```
EduShop Monolithe (v0.9)          EduShop Microservices (v1.0)
────────────────────────          ──────────────────────────────────────

┌─────────────────────┐           ┌──────────────────┐ <- Port 8081
│    EduShop.war      │           │  auth-service    │   JWT, comptes
│                     │    ->->->    │  (Open Liberty)  │
│  - Authentification │           └──────────────────┘
│  - Catalogue        │           ┌──────────────────┐ <- Port 8082
│  - Commandes        │           │ catalogue-service│   Formations, avis
│  - Notifications    │           │  (Open Liberty)  │
│  - Paiements        │           └──────────────────┘
│                     │           ┌──────────────────┐ <- Port 8083
│  Une seule BD       │           │ commande-service │   Paniers, commandes
│                     │           │  (Open Liberty)  │
└─────────────────────┘           └──────────────────┘
                                  ┌──────────────────┐ <- Port 8084
                                  │   notif-service  │   Emails, SMS
                                  │   (Open Liberty) │
                                  └──────────────────┘

                                  ┌──────────────────┐ <- Port 80/443
                                  │   API Gateway    │   Nginx / Kong
                                  │                  │   Routing, auth
                                  └──────────────────┘
```

## 24.2 Service Discovery et Communication inter-services

### Communication synchrone (REST)

```java
// catalogue-service : appeler auth-service pour valider un token
@ApplicationScoped
public class AuthServiceClient {

    @Inject
    @ConfigProperty(name = "auth.service.url", defaultValue = "http://auth-service:8080")
    private String authServiceUrl;

    // MicroProfile Rest Client (déclaratif)
    @Inject
    @RestClient
    private AuthServiceApi authApi;

    public boolean validerToken(String token) {
        try {
            TokenValidationResponse resp = authApi.valider(token);
            return resp.isValide();
        } catch (WebApplicationException e) {
            return false;
        }
    }
}

// Interface du client REST (générée automatiquement par MicroProfile)
@Path("/api/auth")
@RegisterRestClient(configKey = "auth-service")
@Produces(MediaType.APPLICATION_JSON)
public interface AuthServiceApi {

    @POST
    @Path("/validate")
    @Consumes(MediaType.APPLICATION_JSON)
    TokenValidationResponse valider(String token);
}
```

```properties
# microprofile-config.properties du catalogue-service
auth-service/mp-rest/url=http://auth-service:8080
auth-service/mp-rest/connectTimeout=5000
auth-service/mp-rest/readTimeout=10000
```

### Communication asynchrone (JMS/Kafka)

```java
// commande-service publie un événement quand une commande est passée
@Stateless
public class CommandeService {

    @Inject
    private EventPublisher eventPublisher;

    public Commande creer(CommandeCreerDTO dto) {
        Commande commande = /* ... créer la commande ... */;

        // Publier l'événement -> notif-service et catalogue-service réagissent
        eventPublisher.publier("commandes", new CommandePasseeEvent(
            commande.getId(),
            dto.getEtudiantEmail(),
            commande.getFormations(),
            commande.getTotal()
        ));

        return commande;
    }
}

// notif-service consomme l'événement et envoie l'email
@MessageDriven(activationConfig = {
    @ActivationConfigProperty(propertyName = "destination", propertyValue = "commandes")
})
public class CommandeConsumerMDB implements MessageListener {

    @Inject
    private EmailService emailService;

    @Override
    public void onMessage(Message message) {
        CommandePasseeEvent event = parseEvent(message);
        emailService.envoyerConfirmation(event.getEmail(), event.getFormations());
    }
}
```

## 24.3 API Gateway avec Nginx

```nginx
# Nginx comme API Gateway simple
upstream auth_service    { server auth-service:8080; }
upstream catalogue_service { server catalogue-service:8080; }
upstream commande_service  { server commande-service:8080; }

server {
    listen 80;

    # Routing basé sur le préfixe d'URL
    location /api/auth/      { proxy_pass http://auth_service/api/auth/; }
    location /api/formations/{ proxy_pass http://catalogue_service/api/formations/; }
    location /api/commandes/ { proxy_pass http://commande_service/api/commandes/; }

    # Rate limiting global
    limit_req_zone $binary_remote_addr zone=api:10m rate=100r/m;
    location /api/ {
        limit_req zone=api burst=20 nodelay;
        proxy_pass http://...;
    }
}
```

## 24.4 Docker Compose pour tous les microservices

```yaml
# docker-compose.microservices.yml
version: '3.9'

services:

  # ── Infrastructure ──
  mysql-auth:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: edushop_auth
      MYSQL_USER: auth
      MYSQL_PASSWORD: auth_secret
      MYSQL_ROOT_PASSWORD: root

  mysql-catalogue:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: edushop_catalogue
      MYSQL_USER: catalogue
      MYSQL_PASSWORD: catalogue_secret
      MYSQL_ROOT_PASSWORD: root

  activemq:
    image: apache/activemq-classic:5.18.3

  # ── Microservices ──
  auth-service:
    build: ./services/auth-service
    environment:
      DB_HOST: mysql-auth
      JWT_SECRET: "${JWT_SECRET}"
    depends_on:
      - mysql-auth
    ports:
      - "8081:8080"

  catalogue-service:
    build: ./services/catalogue-service
    environment:
      DB_HOST: mysql-catalogue
      AUTH_SERVICE_URL: http://auth-service:8080
    depends_on:
      - mysql-catalogue
      - auth-service
    ports:
      - "8082:8080"

  commande-service:
    build: ./services/commande-service
    environment:
      CATALOGUE_SERVICE_URL: http://catalogue-service:8080
      AUTH_SERVICE_URL: http://auth-service:8080
      ACTIVEMQ_HOST: activemq
    ports:
      - "8083:8080"

  notif-service:
    build: ./services/notif-service
    environment:
      ACTIVEMQ_HOST: activemq
      SMTP_HOST: "${SMTP_HOST}"
    ports:
      - "8084:8080"

  # ── API Gateway ──
  gateway:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./gateway/nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - auth-service
      - catalogue-service
      - commande-service
```

---

# [LIVRE] Chapitres 25-26 — Performance & Clean Architecture

## 25.1 Cache JCache (JSR 107)

```java
import javax.cache.annotation.*;

@ApplicationScoped
public class CatalogueService {

    /**
     * @CacheResult : le résultat est mis en cache
     * Si le cache contient déjà la réponse -> pas d'appel à la méthode
     */
    @CacheResult(cacheName = "formations")
    public FormationDTO trouverParId(Long id) {
        // Appelée uniquement si pas dans le cache
        return repository.findById(id)
            .map(FormationDTO::from)
            .orElseThrow(() -> new FormationNotFoundException(id));
    }

    /**
     * @CacheRemove : invalider l'entrée du cache quand la formation est modifiée
     */
    @CacheRemove(cacheName = "formations")
    public FormationDTO modifier(Long id, FormationModifierDTO dto) {
        // Après modification -> le cache est invalidé -> prochaine lecture = BDD
        Formation f = repository.findById(id).orElseThrow(...);
        // ... modifier ...
        return FormationDTO.from(repository.save(f));
    }

    /**
     * @CacheRemoveAll : vider tout le cache
     */
    @CacheRemoveAll(cacheName = "formations")
    public void invaliderToutLeCache() {
        System.out.println("Cache formations invalidé");
    }
}
```

## 25.2 Pool de connexions et optimisation JPA

```java
// ── Problème N+1 : LA cause n°1 de lenteur JPA ──

// [X] PROBLÈME N+1 : 1 requête pour les formations + N requêtes pour les formateurs
List<Formation> formations = em.createQuery(
    "SELECT f FROM Formation f", Formation.class
).getResultList();

for (Formation f : formations) {
    // Chaque appel à getFormateur() déclenche UNE requête SQL !
    System.out.println(f.getFormateur().getNom()); // N requêtes supplémentaires !
}
// Total : 1 + N requêtes SQL -> très lent pour 1000 formations !

// [OK] SOLUTION : JOIN FETCH -> une seule requête
List<Formation> formations = em.createQuery(
    "SELECT DISTINCT f FROM Formation f " +
    "LEFT JOIN FETCH f.formateur " +       // Charger en une fois
    "LEFT JOIN FETCH f.categories",        // Charger en une fois
    Formation.class
).getResultList();
// Total : 1 requête SQL -> rapide même pour 10000 formations !

// ── EntityGraph : alternative moderne au JOIN FETCH ──
@Entity
@NamedEntityGraph(
    name = "Formation.avecFormateur",
    attributeNodes = @NamedAttributeNode("formateur")
)
public class Formation { /* ... */ }

// Utilisation
EntityGraph<?> graph = em.getEntityGraph("Formation.avecFormateur");
List<Formation> formations = em.createQuery(
    "SELECT f FROM Formation f", Formation.class
)
.setHint("jakarta.persistence.fetchgraph", graph)
.getResultList();
```

## 25.3 Clean Architecture (Architecture Hexagonale)

```
┌─────────────────────────────────────────────┐
│               INFRASTRUCTURE                 │
│  (Adapters : REST, JPA, JMS, Email...)       │
│  ┌───────────────────────────────────────┐   │
│  │           APPLICATION                 │   │
│  │  (Services, Use Cases, Orchestration) │   │
│  │  ┌─────────────────────────────────┐  │   │
│  │  │           DOMAINE               │  │   │
│  │  │  (Entités, Règles métier,        │  │   │
│  │  │   Value Objects, Repository API) │  │   │
│  │  └─────────────────────────────────┘  │   │
│  └───────────────────────────────────────┘   │
└─────────────────────────────────────────────┘

Règle : les flèches pointent vers l'intérieur.
-> Le domaine ne dépend de rien.
-> L'application dépend du domaine.
-> L'infrastructure dépend de l'application.
```

```java
// ── DOMAINE : entités pures, aucune dépendance framework ──
package com.edushop.domain.model;

public class Formation {
    // Value Object : type riche plutôt que String/double bruts
    private FormationId id;
    private Titre titre;
    private Prix prix;
    private Niveau niveau;

    // Règles métier dans le domaine
    public void appliquerRemise(Remise remise) {
        if (remise.getPourcentage() > 80) {
            throw new DomainException("Remise maximale autorisée : 80%");
        }
        this.prix = this.prix.appliquer(remise);
    }
}

// Value Object immutable
public final class Prix {
    private final BigDecimal montant;
    private final String devise;

    public Prix(BigDecimal montant, String devise) {
        if (montant.compareTo(BigDecimal.ZERO) < 0) {
            throw new DomainException("Le prix ne peut pas être négatif");
        }
        this.montant = montant;
        this.devise = devise;
    }

    public Prix appliquer(Remise remise) {
        BigDecimal facteur = BigDecimal.ONE
            .subtract(remise.getPourcentage().divide(BigDecimal.valueOf(100)));
        return new Prix(this.montant.multiply(facteur), this.devise);
    }
}

// Port (interface) du repository : défini dans le domaine
package com.edushop.domain.port;

public interface FormationRepository {
    Optional<Formation> findById(FormationId id);
    Formation save(Formation formation);
    List<Formation> findByNiveau(Niveau niveau);
}

// ── APPLICATION : use cases ──
package com.edushop.application.usecase;

@ApplicationScoped
public class InscrireEtudiantUseCase {

    // Dépendances via ports (interfaces du domaine)
    private final FormationRepository formationRepo;
    private final EtudiantRepository etudiantRepo;
    private final EventPublisher eventPublisher;

    @Inject
    public InscrireEtudiantUseCase(
            FormationRepository formationRepo,
            EtudiantRepository etudiantRepo,
            EventPublisher eventPublisher) {
        this.formationRepo = formationRepo;
        this.etudiantRepo = etudiantRepo;
        this.eventPublisher = eventPublisher;
    }

    public InscriptionResult execute(InscrireEtudiantCommand command) {
        // Logique d'orchestration pure, sans détails d'infrastructure
        Formation formation = formationRepo.findById(command.formationId())
            .orElseThrow(() -> new FormationNotFoundException(command.formationId()));

        Etudiant etudiant = etudiantRepo.findById(command.etudiantId())
            .orElseThrow(() -> new EtudiantNotFoundException(command.etudiantId()));

        etudiant.inscrire(formation);
        etudiantRepo.save(etudiant);
        eventPublisher.publish(new EtudiantInscritEvent(etudiant, formation));

        return new InscriptionResult(etudiant.getId(), formation.getId(), LocalDateTime.now());
    }
}

// ── INFRASTRUCTURE : adaptateurs ──
package com.edushop.infrastructure.persistence;

@ApplicationScoped
public class JpaFormationRepository implements FormationRepository {
    // Adapter JPA -> Port du domaine
    @PersistenceContext
    private EntityManager em;

    @Override
    public Optional<Formation> findById(FormationId id) {
        FormationEntity entity = em.find(FormationEntity.class, id.getValue());
        return Optional.ofNullable(entity).map(FormationMapper::toDomain);
    }

    @Override
    public Formation save(Formation formation) {
        FormationEntity entity = FormationMapper.toEntity(formation);
        return FormationMapper.toDomain(em.merge(entity));
    }
}

package com.edushop.infrastructure.rest;

@Path("/api/formations")
public class FormationRestAdapter {
    // Adapter REST -> Use Case

    @Inject
    private InscrireEtudiantUseCase inscrireUseCase;

    @POST
    @Path("/{formationId}/inscription")
    @Authenticated
    public Response inscrire(@PathParam("formationId") Long formationId,
                              @Context SecurityContext ctx) {
        Long etudiantId = (Long) ctx.getUserPrincipal(); // extrait du JWT

        InscrireEtudiantCommand command = new InscrireEtudiantCommand(
            new EtudiantId(etudiantId),
            new FormationId(formationId)
        );

        InscriptionResult resultat = inscrireUseCase.execute(command);

        return Response.status(201)
            .entity(Map.of("message", "Inscription réussie", "timestamp", resultat.timestamp()))
            .build();
    }
}
```

---

# [LIVRE] Chapitres 27-29 — Patterns Avancés, OAuth2 & CI/CD Expert

## 27.1 Patterns de conception avancés

```java
// ── REPOSITORY PATTERN ──
// Définir un repository générique réutilisable
public interface Repository<T, ID> {
    Optional<T> findById(ID id);
    List<T> findAll();
    T save(T entity);
    void delete(ID id);
    boolean existsById(ID id);
    long count();
}

// ── SPECIFICATION PATTERN ── (pour les critères de recherche dynamiques)
@FunctionalInterface
public interface Specification<T> {
    Predicate toPredicate(CriteriaBuilder cb, Root<T> root, CriteriaQuery<?> query);

    default Specification<T> and(Specification<T> other) {
        return (cb, root, query) ->
            cb.and(this.toPredicate(cb, root, query), other.toPredicate(cb, root, query));
    }

    default Specification<T> or(Specification<T> other) {
        return (cb, root, query) ->
            cb.or(this.toPredicate(cb, root, query), other.toPredicate(cb, root, query));
    }
}

// Spécifications réutilisables
public class FormationSpecifications {
    public static Specification<Formation> avecNiveau(Niveau niveau) {
        return (cb, root, query) -> cb.equal(root.get("niveau"), niveau);
    }

    public static Specification<Formation> prixMaximum(double max) {
        return (cb, root, query) -> cb.lessThanOrEqualTo(root.get("prix"), max);
    }

    public static Specification<Formation> titreContient(String motCle) {
        return (cb, root, query) ->
            cb.like(cb.lower(root.get("titre")), "%" + motCle.toLowerCase() + "%");
    }
}

// Utilisation composable
Specification<Formation> recherche = FormationSpecifications
    .avecNiveau(Niveau.INTERMEDIAIRE)
    .and(FormationSpecifications.prixMaximum(150.0))
    .and(FormationSpecifications.titreContient("java"));

List<Formation> resultats = repository.findAll(recherche);
```

## 28.1 OAuth2 avec Keycloak

```
FLUX OAuth2 Authorization Code :

1. Utilisateur clique "Se connecter avec Google/Keycloak"
2. Navigateur -> Keycloak (page de login)
3. Utilisateur s'authentifie sur Keycloak
4. Keycloak -> redirige vers EduShop avec un "code"
5. EduShop -> échange le code contre un token JWT (via appel serveur-serveur)
6. EduShop utilise le JWT pour les appels API
```

```yaml
# docker-compose : ajouter Keycloak
keycloak:
  image: quay.io/keycloak/keycloak:23.0
  command: start-dev
  environment:
    KC_DB: mysql
    KC_DB_URL: jdbc:mysql://mysql-keycloak:3306/keycloak
    KC_DB_USERNAME: keycloak
    KC_DB_PASSWORD: keycloak_secret
    KEYCLOAK_ADMIN: admin
    KEYCLOAK_ADMIN_PASSWORD: admin123
  ports:
    - "8180:8080"
  depends_on:
    - mysql-keycloak
```

```java
// Valider le token JWT émis par Keycloak
@ApplicationScoped
public class KeycloakJwtValidator {

    @Inject
    @ConfigProperty(name = "keycloak.realm.url")
    private String realmUrl;
    // ex: http://keycloak:8080/realms/edushop

    private PublicKey publicKey;

    @PostConstruct
    public void init() {
        // Récupérer la clé publique Keycloak via JWKS endpoint
        // http://keycloak:8080/realms/edushop/protocol/openid-connect/certs
        this.publicKey = fetchPublicKeyFromKeycloak();
    }

    public Claims valider(String token) {
        return Jwts.parser()
            .verifyWith((PublicKey) publicKey)  // Vérifier avec la clé PUBLIQUE Keycloak
            .build()
            .parseSignedClaims(token)
            .getPayload();
    }
}
```

## 29.1 Pipeline CI/CD Avancé

```yaml
# .github/workflows/pipeline-complet.yml
name: EduShop - Pipeline Complet

on:
  push:
    branches: [main, 'release/**']
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 2 * * 1'  # Tests complets chaque lundi à 2h

jobs:

  lint:
    name: "[LISTE] Qualité du code"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v3
        with: { java-version: '17', distribution: 'temurin', cache: maven }
      - run: mvn checkstyle:check pmd:check -B --no-transfer-progress

  tests:
    name: "[TEST] Tests (Java ${{ matrix.java }})"
    runs-on: ubuntu-latest
    needs: lint
    strategy:
      matrix:
        java: [17, 21]  # Tester sur plusieurs versions Java
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v3
        with: { java-version: '${{ matrix.java }}', distribution: 'temurin', cache: maven }
      - run: mvn verify -B --no-transfer-progress
      - uses: codecov/codecov-action@v3

  build-and-push:
    name: "[DOCKER] Build & Push (${{ matrix.service }})"
    needs: tests
    if: github.ref == 'refs/heads/main'
    strategy:
      matrix:
        service: [auth-service, catalogue-service, commande-service, notif-service]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          context: ./services/${{ matrix.service }}
          push: true
          tags: ghcr.io/${{ github.repository }}/${{ matrix.service }}:${{ github.sha }}

  deploy-staging:
    name: "[SIGNAL] Déploiement Staging"
    needs: build-and-push
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - name: Déployer sur staging
        run: |
          # Mise à jour des images sur le serveur staging
          ssh staging-server "
            cd /opt/edushop &&
            export IMAGE_TAG=${{ github.sha }} &&
            docker compose pull &&
            docker compose up -d --remove-orphans
          "
      - name: "Tests de smoke"
        run: |
          sleep 30
          curl -f https://staging.edushop.com/api/health
          curl -f https://staging.edushop.com/api/formations

  deploy-production:
    name: "[RAPIDE] Déploiement Production"
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://edushop.com
    steps:
      - name: "[SYNC] Rolling deployment"
        run: |
          # Déploiement sans downtime (rolling update)
          for service in auth catalogue commande notif; do
            echo "Déploiement $service-service..."
            ssh prod-server "
              cd /opt/edushop &&
              docker service update \
                --image ghcr.io/edushop/${service}-service:${{ github.sha }} \
                --update-parallelism 1 \
                --update-delay 10s \
                edushop_${service}-service
            "
            sleep 15
            curl -f https://edushop.com/api/health || exit 1
          done
```

---

# [LIVRE] Chapitre 30 — Projet Final Expert : EduShop v1.0

## 30.1 Architecture finale

```
┌──────────────────────────────────────────────────────────────────┐
│                           CLIENTS                                 │
│          Navigateur / App Mobile / Client API                     │
└────────────────────────────┬─────────────────────────────────────┘
                             │ HTTPS
┌────────────────────────────[BLACK_DOWN-POINTING_TRIANGLE]─────────────────────────────────────┐
│                        API GATEWAY (Nginx)                        │
│   Rate limiting | SSL termination | Routing | Auth forwarding     │
└──────────┬───────────────────────────────────────┬───────────────┘
           │                                       │
┌──────────[BLACK_DOWN-POINTING_TRIANGLE]───────────┐               ┌───────────[BLACK_DOWN-POINTING_TRIANGLE]──────────────┐
│    Auth Service       │               │    Catalogue Service      │
│    (Open Liberty)     │               │    (Open Liberty)         │
│  - Keycloak/JWT       │               │  - Formations, Avis       │
│  - Comptes utilisateur│               │  - Recherche, Cache       │
│  - Rôles, Permissions │               │  - JCache (Redis)         │
│  BD: edushop_auth     │               │  BD: edushop_catalogue    │
└──────────────────────┘               └──────────────────────────┘
           │                                       │
┌──────────[BLACK_DOWN-POINTING_TRIANGLE]───────────┐  ActiveMQ  ┌─────────────[BLACK_DOWN-POINTING_TRIANGLE]────────────┐
│   Commande Service    │[BLACK_LEFT-POINTING_POINTER]──────────[BLACK_RIGHT-POINTING_POINTER]│    Notif Service          │
│   (Open Liberty)      │            │    (Open Liberty)         │
│  - Paniers            │            │  - Email (SMTP)           │
│  - Commandes          │            │  - Push notifications     │
│  - Paiements (Stripe) │            │  - Templates HTML         │
│  BD: edushop_commandes│            │  (sans BD propre)         │
└──────────────────────┘            └──────────────────────────┘
           │
┌──────────[BLACK_DOWN-POINTING_TRIANGLE]───────────────────────────────────────────────────────┐
│                    INFRASTRUCTURE PARTAGÉE                        │
│   MySQL (multi-BD) | ActiveMQ | Redis | Keycloak | Prometheus     │
└──────────────────────────────────────────────────────────────────┘
```

## 30.2 Récapitulatif des technologies utilisées

| Couche | Technologie | Rôle |
|--------|-------------|------|
| **Runtime** | Java 17, Jakarta EE 10 | Base |
| **Serveur** | Open Liberty / WildFly | Exécution JEE |
| **API** | JAX-RS + OpenAPI | Endpoints REST documentés |
| **Persistance** | JPA + Hibernate + MySQL | ORM et base de données |
| **IoC** | CDI | Injection de dépendances |
| **Transactions** | EJB @Stateless | Transactions ACID |
| **Messaging** | JMS + ActiveMQ | Communication async |
| **Sécurité** | JWT + Keycloak + OAuth2 | Auth enterprise |
| **Résilience** | MicroProfile Fault Tolerance | Circuit Breaker, Retry |
| **Config** | MicroProfile Config | Configuration externalisée |
| **Observabilité** | MicroProfile Health + Metrics | Monitoring |
| **Tests** | JUnit 5 + Mockito + Testcontainers | Qualité |
| **Build** | Maven multi-modules | Construction |
| **Conteneurs** | Docker + Docker Compose | Déploiement local |
| **CI/CD** | GitHub Actions | Automatisation |
| **Proxy** | Nginx | Gateway, HTTPS, cache |

## 30.3 Scénario de démonstration complet

```bash
#!/bin/bash
# script de démonstration EduShop v1.0

echo "=== DÉMONSTRATION EDUSHOP v1.0 ==="
BASE="http://localhost/api"

# 1. Health check global
echo -e "\n--- 1. Health Check ---"
curl -s "$BASE/health" | jq .

# 2. Inscription d'un nouvel étudiant
echo -e "\n--- 2. Inscription ---"
INSCRIPTION=$(curl -s -X POST "$BASE/auth/inscription" \
  -H "Content-Type: application/json" \
  -d '{
    "prenom": "Marie",
    "nom": "Curie",
    "email": "marie.curie@science.fr",
    "motDePasse": "Radium1898!"
  }')
echo $INSCRIPTION | jq .
TOKEN=$(echo $INSCRIPTION | jq -r '.accessToken')

# 3. Parcourir le catalogue
echo -e "\n--- 3. Catalogue (page 1, 3 résultats) ---"
curl -s "$BASE/formations?page=0&taille=3" | jq '.data[].titre'

# 4. Rechercher une formation
echo -e "\n--- 4. Recherche 'java' ---"
curl -s "$BASE/formations?recherche=java" | jq '.data | length' | xargs echo "Formations trouvées:"

# 5. Voir le détail d'une formation
echo -e "\n--- 5. Détail formation #1 ---"
curl -s "$BASE/formations/1" | jq '{titre, prix, niveau, nombreInscrits}'

# 6. S'inscrire à une formation
echo -e "\n--- 6. Inscription à la formation #1 ---"
curl -s -X POST "$BASE/formations/1/inscription" \
  -H "Authorization: Bearer $TOKEN" | jq .

# 7. Vérifier mon profil
echo -e "\n--- 7. Mon profil ---"
curl -s "$BASE/profil" \
  -H "Authorization: Bearer $TOKEN" | jq '{prenom, email, role}'

# 8. Mes formations
echo -e "\n--- 8. Mes formations ---"
curl -s "$BASE/profil/formations" \
  -H "Authorization: Bearer $TOKEN" | jq '.[].titre'

echo -e "\n=== FIN DE LA DÉMONSTRATION ==="
```

## 30.4 Checklist de déploiement en production

```
SÉCURITÉ
  [OK] Mots de passe hashés (BCrypt)
  [OK] Tokens JWT avec expiration courte (24h)
  [OK] HTTPS partout (TLS 1.2+)
  [OK] En-têtes de sécurité HTTP (HSTS, CSP, X-Frame-Options)
  [OK] Variables sensibles dans des secrets (jamais dans le code)
  [OK] Accès console admin WildFly bloqué depuis l'extérieur
  [OK] Images Docker avec utilisateur non-root
  [OK] Scan de vulnérabilités des dépendances (OWASP, Trivy)

FIABILITÉ
  [OK] Health checks configurés (liveness + readiness)
  [OK] Circuit Breakers sur les appels inter-services
  [OK] Retry avec back-off exponentiel
  [OK] Timeout sur tous les appels réseau
  [OK] Pool de connexions BD configuré (min 5, max 20)
  [OK] Messages JMS persistants (survivent aux redémarrages)
  [OK] Dead Letter Queue configurée

PERFORMANCE
  [OK] Index BD sur les colonnes filtrées (titre, niveau, prix)
  [OK] Pas de N+1 (JOIN FETCH ou EntityGraph)
  [OK] Cache JCache sur les lectures fréquentes
  [OK] Compression gzip activée (Nginx)
  [OK] Cache navigateur sur les ressources statiques

OBSERVABILITÉ
  [OK] Logs structurés (JSON) vers Elasticsearch/Loki
  [OK] Métriques Prometheus exposées (/metrics)
  [OK] Dashboard Grafana configuré
  [OK] Alertes sur : erreurs 5xx > 1%, latence p99 > 2s

DÉPLOIEMENT
  [OK] Pipeline CI/CD automatisé (GitHub Actions)
  [OK] Tests automatiques bloquent le déploiement si KO
  [OK] Rolling deployment (pas de downtime)
  [OK] Rollback possible en 1 commande
  [OK] Backups base de données automatiques (quotidien + avant déploiement)
  [OK] Runbook de procédures d'urgence documenté
```

---

## [TROPHEE] Récapitulatif du Parcours Complet

### Vous avez maîtrisé :

**Module 1 — Java Enterprise**
POO avancée • SOLID • Collections • Streams • Optional • Exceptions • Concurrence • JSON

**Module 2 — JEE Web**
Architecture 3-tiers • MVC • Servlets • JSP • JSTL • EL • Filtres • Session • Cookies

**Module 3 — Persistance & CDI**
JPA • ORM • Entités • Relations • JPQL • Transactions • CDI • Scopes • Events

**Module 4 — Sécurité & REST**
BCrypt • JWT • JAX-RS • DTOs • Bean Validation • ExceptionMapper • OpenAPI • HATEOAS

**Module 5 — Enterprise**
EJB • @Stateless • @Stateful • @Singleton • @Schedule • JMS • MDB • JUnit 5 • Mockito • Testcontainers

**Module 6 — DevOps**
WildFly • Maven multi-modules • Profils • Docker multi-stage • Docker Compose • Nginx • GitHub Actions CI/CD

**Module 7 — Expert**
MicroProfile Config/FaultTolerance/Health/Metrics • Microservices • Clean Architecture • DDD • Keycloak OAuth2

---

## [OBJECTIF] Et maintenant ?

Avec ces compétences, vous pouvez prétendre aux postes :
- **Développeur Java EE/JEE** (Junior -> Senior)
- **Développeur Backend Java** (APIs REST)
- **Architecte Solutions Java** (avec expérience)
- **DevOps Java** (avec spécialisation)

**Prochaines étapes recommandées :**
1. **Contribuer à un projet open source** -> montrer vos compétences
2. **Apprendre Spring Boot** -> très utilisé, compétences JEE transférables
3. **Apprendre Kubernetes** -> orchestration de conteneurs au-delà de Docker
4. **Passer des certifications** : Oracle Java Certified, Jakarta EE Professional
5. **Construire un vrai projet** -> montrez EduShop dans vos entretiens !

---

*Félicitations — vous avez terminé le cours complet JEE d'EduShop ! [COURS]*
