# [LIVRE] Chapitre 1 & 2 — Introduction à Spring Boot & Installation

---

## [IMPORTANT] Chapitre 1 — Qu'est-ce que Spring Boot ?

### 1.1 L'écosystème Spring — Vue d'ensemble

Avant de parler de Spring Boot, il faut comprendre **Spring Framework** qui existe depuis 2003. C'est un cadre de travail (framework) Java qui résout des problèmes récurrents du développement d'applications d'entreprise.

**Le problème historique :**
En Java classique (Java EE), créer une simple application web nécessitait des dizaines de fichiers XML de configuration, des serveurs d'application lourds (JBoss, WebLogic), et une connaissance encyclopédique de l'infrastructure. C'était long, complexe, et source d'erreurs.

**La solution Spring :**
Spring a simplifié Java EE en introduisant deux concepts révolutionnaires :

#### [CLE] Concept 1 : L'Inversion de Contrôle (IoC)

En Java classique, **vous** créez les objets dont vous avez besoin :
```java
// Sans Spring — vous gérez tout vous-même
public class BookService {
    private BookRepository repo;
    
    public BookService() {
        this.repo = new BookRepository(); // Vous créez l'objet
    }
}
```

Avec Spring, **le framework** crée et gère les objets pour vous :
```java
// Avec Spring — Spring gère la création
public class BookService {
    private BookRepository repo;
    
    public BookService(BookRepository repo) {
        this.repo = repo; // Spring vous "injecte" l'objet
    }
}
```

Ce principe s'appelle **Inversion of Control** : le contrôle de la création des objets est inversé — c'est Spring qui contrôle, pas vous.

#### [CLE] Concept 2 : L'Injection de Dépendances (DI)

La DI est la mise en œuvre concrète de l'IoC. Spring maintient un "conteneur" (le **ApplicationContext**) qui :
1. Scanne votre code à la recherche d'objets à gérer (appelés **Beans**)
2. Les crée dans le bon ordre
3. Les "injecte" là où ils sont nécessaires

```
Votre code demande un BookRepository
        v
Spring ApplicationContext vérifie s'il a déjà un BookRepository
        v
Si non -> Spring le crée
Si oui -> Spring réutilise celui existant
        v
Spring l'injecte dans votre BookService
```

---

### 1.2 Pourquoi Spring Boot ?

Spring Framework était puissant mais toujours complexe à configurer. Spring Boot (lancé en 2014) apporte **3 révolutions** :

#### [RAPIDE] Révolution 1 : Auto-configuration

Spring Boot examine votre `pom.xml` et configure automatiquement votre application. Si vous ajoutez la dépendance `spring-boot-starter-data-jpa`, Spring Boot configure automatiquement Hibernate, les transactions, le pool de connexions...

Vous n'écrivez plus des centaines de lignes de configuration XML.

#### [RAPIDE] Révolution 2 : Les Starters

Les starters sont des dépendances "tout-en-un". Au lieu d'ajouter 15 dépendances séparées avec les bonnes versions compatibles, vous ajoutez un seul starter :

```xml
<!-- Un seul starter = toutes les dépendances web configurées et compatibles -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
```

Ce starter inclut automatiquement : Spring MVC, Jackson (JSON), Tomcat embarqué, validation...

#### [RAPIDE] Révolution 3 : Serveur embarqué

En Java EE classique, vous déployiez votre application sur un serveur externe (Tomcat, JBoss). Avec Spring Boot, Tomcat est **embarqué dans votre application**. Votre app devient un simple fichier `.jar` exécutable :

```bash
java -jar libraryhub.jar   # Votre application démarre avec son propre serveur !
```

---

### 1.3 Spring Boot vs Autres Frameworks

| Critère | Spring Boot | Django (Python) | Node/Express |
|---|---|---|---|
| Typage | Fort (Java) | Dynamique | Dynamique |
| Performance | Excellente | Bonne | Très bonne |
| Écosystème entreprise | ***** | *** | *** |
| Courbe d'apprentissage | Modérée | Facile | Facile |
| Sécurité intégrée | ***** | **** | ** |

---

## [IMPORTANT] Chapitre 2 — Installation et Premier Projet

### 2.1 Installation de l'environnement

#### Étape 1 : Installer JDK 17

Spring Boot 3.x requiert Java 17 minimum. Java 17 est une version LTS (Long Term Support), stable et recommandée.

**Sur Windows :**
1. Allez sur [adoptium.net](https://adoptium.net)
2. Téléchargez "Eclipse Temurin 17 (LTS)"
3. Lancez l'installateur `.msi`
4. Vérifiez l'installation :
```bash
java -version
# Doit afficher : openjdk version "17.x.x"
```

**Sur macOS avec Homebrew :**
```bash
brew install openjdk@17
echo 'export JAVA_HOME=/opt/homebrew/opt/openjdk@17' >> ~/.zshrc
source ~/.zshrc
java -version
```

**Sur Linux (Ubuntu/Debian) :**
```bash
sudo apt update
sudo apt install openjdk-17-jdk
java -version
```

#### Étape 2 : Installer IntelliJ IDEA

1. Téléchargez **IntelliJ IDEA Community** (gratuite) sur [jetbrains.com/idea](https://jetbrains.com/idea)
2. Installez normalement
3. Au premier lancement, sélectionnez le thème qui vous plaît

> [IDEE] **Pourquoi IntelliJ ?** L'autocomplétion, le refactoring, les raccourcis Spring... IntelliJ est de loin le meilleur IDE pour Spring Boot.

#### Étape 3 : Installer Postman

Postman permet de tester vos API REST sans navigateur.
1. Téléchargez sur [postman.com](https://postman.com)
2. Créez un compte gratuit

---

### 2.2 Créer votre Premier Projet Spring Boot

#### Option A : Spring Initializr (Recommandé)

Spring Initializr ([start.spring.io](https://start.spring.io)) est un générateur de projet officiel.

**Configuration pour LibraryHub :**

| Champ | Valeur |
|---|---|
| Project | Maven |
| Language | Java |
| Spring Boot | 3.2.x (dernière stable) |
| Group | `com.libraryhub` |
| Artifact | `libraryhub` |
| Packaging | Jar |
| Java | 17 |

**Dépendances à ajouter (cliquez sur "Add Dependencies") :**
- `Spring Web` — Pour créer des API REST
- `Spring Data JPA` — Pour l'accès base de données
- `H2 Database` — Base de données en mémoire (pour débuter)
- `Spring Boot DevTools` — Rechargement automatique
- `Lombok` — Réduit le code répétitif
- `Validation` — Validation des données

Cliquez **GENERATE**, décompressez le fichier téléchargé.

#### Option B : Directement depuis IntelliJ

1. `File` -> `New` -> `Project`
2. Sélectionnez `Spring Initializr`
3. Remplissez les mêmes infos que ci-dessus
4. Cliquez `Next`, sélectionnez les mêmes dépendances
5. `Finish`

---

### 2.3 Structure du Projet Généré — Explication Détaillée

```
libraryhub/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/libraryhub/
│   │   │       └── LibraryhubApplication.java   <- Point d'entrée
│   │   └── resources/
│   │       ├── application.properties           <- Configuration
│   │       ├── static/                          <- Fichiers statiques (CSS, JS)
│   │       └── templates/                       <- Templates HTML (Thymeleaf)
│   └── test/
│       └── java/
│           └── com/libraryhub/
│               └── LibraryhubApplicationTests.java
├── pom.xml                                      <- Dépendances Maven
├── .mvn/
└── mvnw                                         <- Maven wrapper
```

#### Le fichier `pom.xml` — Décrypté

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    
    <!-- Informations sur votre projet -->
    <groupId>com.libraryhub</groupId>
    <artifactId>libraryhub</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>libraryhub</name>
    
    <!-- Spring Boot gère les versions de toutes les dépendances -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.1</version>
    </parent>
    
    <properties>
        <java.version>17</java.version>
    </properties>
    
    <dependencies>
        <!-- API REST -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <!-- Pas besoin de version : Spring Boot parent la gère -->
        </dependency>
        
        <!-- Base de données JPA -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        
        <!-- H2 : base de données en mémoire pour les tests -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>  <!-- Uniquement au runtime, pas en compilation -->
        </dependency>
        
        <!-- Lombok : génère getters, setters, constructeurs automatiquement -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <!-- DevTools : redémarrage auto quand vous modifiez du code -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        
        <!-- Tests -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
```

#### Le fichier `LibraryhubApplication.java` — Décrypté

```java
package com.libraryhub;

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

// Cette annotation fait 3 choses en une :
// 1. @Configuration : Cette classe est une source de configuration Spring
// 2. @EnableAutoConfiguration : Active l'auto-configuration Spring Boot
// 3. @ComponentScan : Scanne le package courant pour trouver les Beans
@SpringBootApplication
public class LibraryhubApplication {
    
    public static void main(String[] args) {
        // Lance le conteneur Spring et démarre le serveur Tomcat embarqué
        SpringApplication.run(LibraryhubApplication.class, args);
    }
}
```

**Ce qui se passe quand vous lancez l'application :**
1. La méthode `main` est appelée
2. `SpringApplication.run()` démarre le conteneur IoC (ApplicationContext)
3. Spring scanne tous les packages sous `com.libraryhub`
4. Spring trouve toutes les classes annotées (@Controller, @Service, @Repository...)
5. Spring crée des instances (Beans) de ces classes
6. Spring injecte les dépendances entre ces Beans
7. Tomcat démarre sur le port 8080
8. Votre application est prête !

---

### 2.4 Premier Lancement

1. Dans IntelliJ, ouvrez `LibraryhubApplication.java`
2. Cliquez sur le bouton [BLACK_RIGHT-POINTING_TRIANGLE] vert à gauche du `main`
3. Regardez la console — vous devriez voir :

```
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.1)

INFO : Starting LibraryhubApplication
INFO : Tomcat started on port(s): 8080 (http)
INFO : Started LibraryhubApplication in 2.341 seconds
```

[BRAVO] **Votre serveur tourne sur http://localhost:8080 !**

---

### 2.5 Configuration initiale — application.properties

```properties
# === SERVEUR ===
server.port=8080                          # Port d'écoute (par défaut 8080)
server.servlet.context-path=/api          # Toutes les URLs commencent par /api

# === BASE DE DONNÉES H2 (en mémoire pour les tests) ===
spring.datasource.url=jdbc:h2:mem:librarydb    # URL de connexion H2
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# === CONSOLE H2 (interface graphique pour voir la BDD) ===
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console             # Accessible sur /h2-console

# === JPA / HIBERNATE ===
spring.jpa.hibernate.ddl-auto=create-drop  # Crée les tables au démarrage, les supprime à l'arrêt
spring.jpa.show-sql=true                   # Affiche les requêtes SQL dans la console
spring.jpa.properties.hibernate.format_sql=true  # Formate le SQL pour lisibilité

# === LOGS ===
logging.level.com.libraryhub=DEBUG         # Logs détaillés pour votre code
logging.level.org.springframework.web=INFO
```

> [IDEE] **Accédez à la console H2 :** Lancez l'app, allez sur `http://localhost:8080/api/h2-console`. JDBC URL : `jdbc:h2:mem:librarydb`, user : `sa`, pas de mot de passe.

---

### [OK] Exercices du Chapitre 1 & 2

1. **Installez** l'environnement complet (JDK, IntelliJ, Postman)
2. **Créez** le projet LibraryHub via Spring Initializr
3. **Lancez** l'application et vérifiez qu'elle démarre sans erreur
4. **Accédez** à la console H2 dans votre navigateur
5. **Modifiez** le port en `9090` dans `application.properties` et relancez

---

*-> Prochain fichier : `02_premiers_pas_spring_boot.md`*


# [LIVRE] Chapitre 3 & 4 — Premiers Pas : Annotations et Premier Endpoint

---

## [IMPORTANT] Chapitre 3 — Les Annotations Spring Boot

### 3.1 Qu'est-ce qu'une Annotation ?

Une annotation Java est une **métadonnée** placée sur du code (classe, méthode, champ) qui donne des instructions supplémentaires à la JVM ou à un framework.

En Java standard, vous avez déjà vu :
```java
@Override        // Dit au compilateur : "cette méthode remplace une méthode parente"
@Deprecated      // Dit que la méthode est obsolète
@SuppressWarnings("unchecked")  // Supprime un avertissement du compilateur
```

Spring Boot utilise massivement les annotations pour **éviter la configuration XML**.

---

### 3.2 Les Annotations de Stéréotypes — Déclarer les Beans

Ces annotations disent à Spring "**crée un Bean de ce type**" :

#### `@Component` — Le Bean générique
```java
@Component
public class UtilityHelper {
    public String formatTitle(String title) {
        return title.trim().toLowerCase();
    }
}
```
`@Component` est l'annotation de base. Elle dit à Spring : "Crée une instance de cette classe et gère-la."

#### `@Service` — La couche logique métier
```java
@Service
public class BookService {
    // Contient la logique métier : calculs, règles, transformations
    public boolean isAvailable(Book book) {
        return book.getCopiesAvailable() > 0;
    }
}
```
`@Service` est identique à `@Component` techniquement, mais sémantiquement différent : il signale que cette classe contient de la **logique métier**.

#### `@Repository` — La couche accès données
```java
@Repository
public class BookRepository {
    // Contient les opérations de base de données
}
```
`@Repository` ajoute en plus une gestion automatique des exceptions de base de données (les transforme en exceptions Spring standard).

#### `@Controller` — La couche présentation web
```java
@Controller
public class BookController {
    // Gère les requêtes HTTP et retourne des vues HTML
}
```

#### `@RestController` — Controller pour API REST
```java
@RestController
public class BookRestController {
    // Gère les requêtes HTTP et retourne du JSON/XML
}
```
`@RestController` = `@Controller` + `@ResponseBody` (convertit automatiquement les retours Java en JSON).

---

### 3.3 L'Injection de Dépendances — Les 3 Façons

Une fois que Spring a créé vos Beans, il faut les **connecter entre eux**. C'est l'injection de dépendances.

#### Méthode 1 : Injection par constructeur ([OK] RECOMMANDÉE)

```java
@Service
public class BookService {
    
    private final BookRepository bookRepository;
    // "final" : la dépendance ne peut pas être réassignée après injection
    
    // Spring voit qu'il y a un seul constructeur -> injection automatique
    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
}
```

**Avantages :**
- Rend les dépendances explicites et obligatoires
- Facilite les tests (vous pouvez passer des mocks)
- Permet l'immuabilité (`final`)

#### Méthode 2 : Injection par champ avec `@Autowired` ([ATTENTION] Déconseillée)

```java
@Service
public class BookService {
    
    @Autowired  // Spring injecte directement dans le champ
    private BookRepository bookRepository;
}
```

**Problèmes :**
- Difficile à tester (le champ est privé, pas d'accès direct)
- Cache les dépendances (pas visible dans le constructeur)
- Impossible de mettre `final`

#### Méthode 3 : Injection par setter (Usage rare)

```java
@Service
public class BookService {
    
    private BookRepository bookRepository;
    
    @Autowired
    public void setBookRepository(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
}
```
Utile pour les dépendances **optionnelles**.

#### Avec Lombok — La façon moderne

Lombok génère le constructeur automatiquement :

```java
@Service
@RequiredArgsConstructor  // Lombok génère un constructeur avec tous les champs "final"
public class BookService {
    
    private final BookRepository bookRepository;
    private final EmailService emailService;
    // Lombok crée : public BookService(BookRepository br, EmailService es) { ... }
}
```

---

### 3.4 Les Annotations de Configuration

#### `@Configuration` — Classe de configuration
```java
@Configuration
public class AppConfig {
    
    // Déclare un Bean manuellement
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
```

La méthode annotée `@Bean` dit à Spring : "Appelle cette méthode et stocke son résultat comme un Bean géré."

#### `@Value` — Injecter une propriété de configuration
```java
@Service
public class EmailService {
    
    @Value("${app.email.from}")     // Lit la valeur depuis application.properties
    private String fromEmail;
    
    @Value("${app.max-books:5}")    // Valeur par défaut : 5 si non défini
    private int maxBooksPerMember;
}
```

Dans `application.properties` :
```properties
app.email.from=noreply@libraryhub.com
app.max-books=3
```

---

## [IMPORTANT] Chapitre 4 — Créer votre Premier Endpoint REST

### 4.1 Rappel : HTTP et REST

**HTTP** (HyperText Transfer Protocol) est le protocole de communication du Web. Chaque requête HTTP comporte :
- Une **méthode** (verbe) : GET, POST, PUT, DELETE, PATCH
- Une **URL** (chemin) : `/api/books`
- Des **headers** : métadonnées (Content-Type, Authorization...)
- Un **body** : données envoyées (pour POST, PUT)

**REST** (REpresentational State Transfer) est un style d'architecture pour les API. Les conventions REST pour LibraryHub :

| Action | Méthode HTTP | URL | Description |
|---|---|---|---|
| Lister tous les livres | GET | `/api/books` | Récupérer la liste |
| Voir un livre | GET | `/api/books/{id}` | Récupérer par ID |
| Créer un livre | POST | `/api/books` | Créer nouveau |
| Modifier un livre | PUT | `/api/books/{id}` | Remplacer entièrement |
| Modifier partiellement | PATCH | `/api/books/{id}` | Modification partielle |
| Supprimer un livre | DELETE | `/api/books/{id}` | Supprimer |

---

### 4.2 Les Annotations de Mapping HTTP

#### `@RequestMapping` — Mapping général

```java
@RestController
@RequestMapping("/api/books")  // Préfixe URL pour toute la classe
public class BookController {
    // Toutes les méthodes auront /api/books comme base
}
```

#### Les annotations spécialisées

```java
@GetMapping("/")          // Équivalent de @RequestMapping(method=RequestMethod.GET)
@PostMapping("/")         // Pour POST
@PutMapping("/{id}")      // Pour PUT
@DeleteMapping("/{id}")   // Pour DELETE
@PatchMapping("/{id}")    // Pour PATCH
```

---

### 4.3 Premier Controller — LibraryHub

Créez le fichier `src/main/java/com/libraryhub/controller/BookController.java` :

```java
package com.libraryhub.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

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

// Pour l'instant, pas de base de données — on utilise une liste en mémoire
@RestController
@RequestMapping("/api/books")
public class BookController {
    
    // Simulation d'une base de données en mémoire (temporaire)
    private List<String> books = new ArrayList<>(
        List.of("Clean Code", "The Pragmatic Programmer", "Design Patterns")
    );
    
    // ==================== GET ALL ====================
    // Appel : GET http://localhost:8080/api/books
    @GetMapping
    public ResponseEntity<List<String>> getAllBooks() {
        return ResponseEntity.ok(books);
        // ResponseEntity.ok() = HTTP 200 OK + le body
    }
    
    // ==================== GET BY ID ====================
    // Appel : GET http://localhost:8080/api/books/0
    @GetMapping("/{index}")
    public ResponseEntity<String> getBookByIndex(
        @PathVariable int index     // Extrait "0" de l'URL /api/books/0
    ) {
        if (index < 0 || index >= books.size()) {
            return ResponseEntity.notFound().build();  // HTTP 404
        }
        return ResponseEntity.ok(books.get(index));    // HTTP 200 + livre
    }
    
    // ==================== CREATE ====================
    // Appel : POST http://localhost:8080/api/books
    // Body  : "Effective Java"  (texte brut)
    @PostMapping
    public ResponseEntity<String> createBook(
        @RequestBody String bookTitle   // Lit le body de la requête
    ) {
        books.add(bookTitle);
        return ResponseEntity
            .status(HttpStatus.CREATED)    // HTTP 201 Created
            .body("Livre ajouté : " + bookTitle);
    }
    
    // ==================== UPDATE ====================
    // Appel : PUT http://localhost:8080/api/books/0
    // Body  : "Clean Architecture"
    @PutMapping("/{index}")
    public ResponseEntity<String> updateBook(
        @PathVariable int index,
        @RequestBody String newTitle
    ) {
        if (index < 0 || index >= books.size()) {
            return ResponseEntity.notFound().build();
        }
        books.set(index, newTitle);
        return ResponseEntity.ok("Livre mis à jour : " + newTitle);
    }
    
    // ==================== DELETE ====================
    // Appel : DELETE http://localhost:8080/api/books/0
    @DeleteMapping("/{index}")
    public ResponseEntity<Void> deleteBook(@PathVariable int index) {
        if (index < 0 || index >= books.size()) {
            return ResponseEntity.notFound().build();
        }
        books.remove(index);
        return ResponseEntity.noContent().build();  // HTTP 204 No Content
    }
}
```

---

### 4.4 `ResponseEntity` — Le contrôle total de la réponse HTTP

`ResponseEntity<T>` vous permet de contrôler précisément la réponse HTTP :
- Le **code de statut** (200, 201, 404, 500...)
- Les **headers**
- Le **body**

```java
// Exemples courants

// HTTP 200 OK avec body
return ResponseEntity.ok(data);

// HTTP 201 Created
return ResponseEntity.status(HttpStatus.CREATED).body(data);

// HTTP 204 No Content (pas de body)
return ResponseEntity.noContent().build();

// HTTP 404 Not Found
return ResponseEntity.notFound().build();

// HTTP 400 Bad Request avec message d'erreur
return ResponseEntity.badRequest().body("Données invalides");

// Avec header personnalisé
return ResponseEntity.ok()
    .header("X-Custom-Header", "value")
    .body(data);
```

---

### 4.5 Recevoir des Données — Les Annotations de Paramètres

#### `@PathVariable` — Variables dans l'URL

```java
// URL : /api/books/42/chapters/5
@GetMapping("/{bookId}/chapters/{chapterId}")
public String getChapter(
    @PathVariable Long bookId,       // Extrait "42"
    @PathVariable Long chapterId     // Extrait "5"
) {
    return "Livre " + bookId + ", Chapitre " + chapterId;
}
```

#### `@RequestParam` — Paramètres de requête (query params)

```java
// URL : /api/books?title=Java&author=Bloch&page=0&size=10
@GetMapping
public List<Book> searchBooks(
    @RequestParam(required = false) String title,
    @RequestParam(required = false) String author,
    @RequestParam(defaultValue = "0") int page,
    @RequestParam(defaultValue = "10") int size
) {
    // ...
}
```

#### `@RequestBody` — Corps de la requête JSON

```java
// Requête POST avec body JSON :
// {
//   "title": "Clean Code",
//   "author": "Robert Martin",
//   "isbn": "978-0132350884"
// }
@PostMapping
public Book createBook(@RequestBody Book book) {
    // Spring convertit automatiquement le JSON en objet Book
    return bookService.save(book);
}
```

#### `@RequestHeader` — Headers HTTP

```java
@GetMapping("/secure")
public String secureEndpoint(
    @RequestHeader("Authorization") String token
) {
    return "Token reçu : " + token;
}
```

---

### 4.6 Tester avec Postman

1. **Ouvrez Postman**
2. **Créez une nouvelle collection** : LibraryHub

**Test GET ALL BOOKS :**
- Method : `GET`
- URL : `http://localhost:8080/api/books`
- Cliquez `Send`
- Vous devriez voir : `["Clean Code", "The Pragmatic Programmer", "Design Patterns"]`

**Test POST CREATE BOOK :**
- Method : `POST`
- URL : `http://localhost:8080/api/books`
- Tab `Body` -> `raw` -> `Text`
- Contenu : `Effective Java`
- Cliquez `Send`
- Réponse : `Livre ajouté : Effective Java` avec code 201

**Test GET BY INDEX :**
- Method : `GET`
- URL : `http://localhost:8080/api/books/0`
- Réponse : `Clean Code` avec code 200

---

### 4.7 Lombok — Réduire le Code Répétitif

Lombok est une bibliothèque qui génère du code Java à la compilation via des annotations. Pour LibraryHub, créez votre première vraie classe :

```java
package com.libraryhub.entity;

import lombok.*;

@Data               // Génère getters, setters, equals, hashCode, toString
@Builder            // Génère un builder pattern : Book.builder().title("...").build()
@NoArgsConstructor  // Génère un constructeur sans arguments
@AllArgsConstructor // Génère un constructeur avec tous les arguments
public class Book {
    private Long id;
    private String title;
    private String author;
    private String isbn;
    private int copies;
}
```

Sans Lombok, vous devriez écrire manuellement ~60 lignes de getters/setters/constructeurs. Avec Lombok : 5 annotations suffisent.

> [ATTENTION] **IntelliJ** : Activez le plugin Lombok (`File -> Settings -> Plugins -> cherchez Lombok`). Activez aussi le processing des annotations (`Settings -> Build -> Compiler -> Annotation Processors -> Enable`).

---

### [OK] Exercices du Chapitre 3 & 4

1. **Créez** un `HelloController` avec un endpoint `GET /api/hello` qui retourne "Bonjour LibraryHub !"
2. **Ajoutez** un endpoint `GET /api/books/search?title=clean` qui filtre les livres par titre
3. **Testez** tous les endpoints avec Postman et vérifiez les codes HTTP
4. **Créez** une classe `Book` avec Lombok et modifiez le Controller pour utiliser des `Book` au lieu de `String`

---

*-> Prochain fichier : `03_api_rest_et_couches.md`*


# [LIVRE] Chapitre 5 & 6 — Architecture en Couches et Pattern DTO

---

## [IMPORTANT] Chapitre 5 — L'Architecture en Couches

### 5.1 Pourquoi une Architecture en Couches ?

Au chapitre 4, notre Controller faisait tout : logique métier, gestion des données... C'est le **pattern Anti-Christ** (ou "God Object"). Imaginez si LibraryHub grossissait :

```java
// [X] MAUVAISE PRATIQUE — Le Controller fait tout
@RestController
public class BookController {
    
    @PostMapping("/books")
    public Book addBook(@RequestBody Book book) {
        // Validation
        if (book.getTitle() == null || book.getTitle().isEmpty()) {
            throw new RuntimeException("Titre obligatoire");
        }
        
        // Logique métier
        if (books.stream().anyMatch(b -> b.getIsbn().equals(book.getIsbn()))) {
            throw new RuntimeException("ISBN déjà existant");
        }
        
        // Accès BDD
        Connection conn = DriverManager.getConnection("jdbc:h2:...");
        // ... 30 lignes de JDBC...
        
        // Envoi email
        // ... 20 lignes de JavaMail...
        
        return book;
    }
}
```

**Problèmes :**
- Le code est impossible à tester unitairement
- Un changement de base de données implique de modifier le Controller
- Impossible à réutiliser
- Difficile à lire et maintenir

### 5.2 L'Architecture 3-Tiers de Spring Boot

Spring Boot encourage naturellement une **séparation en 3 couches** :

```
┌─────────────────────────────────────────────────────┐
│  CLIENT (Postman, navigateur, app mobile)            │
└─────────────────────────┬───────────────────────────┘
                          │ HTTP Request/Response
                          [BLACK_DOWN-POINTING_TRIANGLE]
┌─────────────────────────────────────────────────────┐
│  COUCHE PRÉSENTATION (@RestController)               │
│  • Reçoit les requêtes HTTP                          │
│  • Valide le format des données entrantes            │
│  • Convertit DTO -> Entity et Entity -> DTO            │
│  • Retourne la réponse HTTP appropriée               │
└─────────────────────────┬───────────────────────────┘
                          │ Appels de méthodes Java
                          [BLACK_DOWN-POINTING_TRIANGLE]
┌─────────────────────────────────────────────────────┐
│  COUCHE SERVICE (@Service)                           │
│  • Contient toute la logique métier                  │
│  • Applique les règles de gestion                    │
│  • Orchestre plusieurs repositories                  │
│  • Gère les transactions                             │
└─────────────────────────┬───────────────────────────┘
                          │ Appels de méthodes Java
                          [BLACK_DOWN-POINTING_TRIANGLE]
┌─────────────────────────────────────────────────────┐
│  COUCHE DONNÉES (@Repository / JPA Repository)       │
│  • Communique avec la base de données                │
│  • CRUD et requêtes personnalisées                   │
│  • Aucune logique métier ici                         │
└─────────────────────────┬───────────────────────────┘
                          │ SQL / JPQL
                          [BLACK_DOWN-POINTING_TRIANGLE]
┌─────────────────────────────────────────────────────┐
│  BASE DE DONNÉES (PostgreSQL, MySQL, H2...)          │
└─────────────────────────────────────────────────────┘
```

**Règle d'or :** Chaque couche ne connaît que la couche **immédiatement inférieure**.
- Controller parle à Service (jamais directement au Repository)
- Service parle à Repository (jamais au Controller)
- Repository parle à la BDD (jamais au Service)

---

### 5.3 Application à LibraryHub — La Couche Repository

Créez `src/main/java/com/libraryhub/repository/BookRepository.java` :

```java
package com.libraryhub.repository;

import com.libraryhub.entity.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

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

// JpaRepository<Book, Long> = Repository pour l'entité Book, dont la clé primaire est Long
// Spring Data JPA génère automatiquement toutes les implémentations CRUD !
@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
    
    // Spring Data JPA génère la requête SQL automatiquement à partir du nom de la méthode !
    // "findBy" + "Title" + "ContainingIgnoreCase"
    // -> SELECT * FROM book WHERE LOWER(title) LIKE LOWER('%titre%')
    List<Book> findByTitleContainingIgnoreCase(String title);
    
    // SELECT * FROM book WHERE author = ?
    List<Book> findByAuthor(String author);
    
    // SELECT * FROM book WHERE isbn = ? (retourne Optional car peut ne pas exister)
    Optional<Book> findByIsbn(String isbn);
    
    // SELECT * FROM book WHERE copies_available > 0
    List<Book> findByCopiesAvailableGreaterThan(int copies);
    
    // SELECT COUNT(*) FROM book WHERE author = ?
    long countByAuthor(String author);
    
    // SELECT * FROM book WHERE author = ? ORDER BY title ASC
    List<Book> findByAuthorOrderByTitleAsc(String author);
    
    // Vérifie existence : SELECT COUNT(*) > 0 WHERE isbn = ?
    boolean existsByIsbn(String isbn);
}
```

> [HOT] **Magie Spring Data JPA :** Vous n'écrivez **aucune implémentation** ! Spring génère tout le code SQL au démarrage en analysant le nom des méthodes.

---

### 5.4 La Couche Service

Créez `src/main/java/com/libraryhub/service/BookService.java` :

```java
package com.libraryhub.service;

import com.libraryhub.entity.Book;
import com.libraryhub.exception.BookNotFoundException;
import com.libraryhub.exception.DuplicateIsbnException;
import com.libraryhub.repository.BookRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
@Slf4j  // Lombok génère un logger : private static final Logger log = ...
public class BookService {
    
    private final BookRepository bookRepository;
    
    // ==================== RÉCUPÉRER TOUS LES LIVRES ====================
    
    @Transactional(readOnly = true)  // Optimisation : pas besoin de gérer les transactions d'écriture
    public List<Book> findAllBooks() {
        log.info("Récupération de tous les livres");
        return bookRepository.findAll();
    }
    
    // ==================== RÉCUPÉRER UN LIVRE PAR ID ====================
    
    @Transactional(readOnly = true)
    public Book findBookById(Long id) {
        log.info("Recherche du livre avec id={}", id);
        // orElseThrow : si le livre n'existe pas -> lève une exception personnalisée
        return bookRepository.findById(id)
            .orElseThrow(() -> new BookNotFoundException("Livre non trouvé avec id : " + id));
    }
    
    // ==================== CRÉER UN LIVRE ====================
    
    @Transactional  // Nécessaire pour les opérations d'écriture
    public Book createBook(Book book) {
        log.info("Création du livre : {}", book.getTitle());
        
        // RÈGLE MÉTIER 1 : L'ISBN doit être unique
        if (bookRepository.existsByIsbn(book.getIsbn())) {
            throw new DuplicateIsbnException("Un livre avec l'ISBN " + book.getIsbn() + " existe déjà");
        }
        
        // RÈGLE MÉTIER 2 : Le nombre de copies ne peut pas être négatif
        if (book.getCopiesAvailable() < 0) {
            throw new IllegalArgumentException("Le nombre de copies ne peut pas être négatif");
        }
        
        Book savedBook = bookRepository.save(book);
        log.info("Livre créé avec succès, id={}", savedBook.getId());
        return savedBook;
    }
    
    // ==================== METTRE À JOUR UN LIVRE ====================
    
    @Transactional
    public Book updateBook(Long id, Book bookDetails) {
        // On récupère d'abord le livre existant (ou lève une exception s'il n'existe pas)
        Book existingBook = findBookById(id);
        
        // On met à jour les champs (on ne touche pas à l'ID !)
        existingBook.setTitle(bookDetails.getTitle());
        existingBook.setAuthor(bookDetails.getAuthor());
        existingBook.setIsbn(bookDetails.getIsbn());
        existingBook.setCopiesAvailable(bookDetails.getCopiesAvailable());
        
        return bookRepository.save(existingBook);
    }
    
    // ==================== SUPPRIMER UN LIVRE ====================
    
    @Transactional
    public void deleteBook(Long id) {
        Book book = findBookById(id);  // Lève une exception si n'existe pas
        bookRepository.delete(book);
        log.info("Livre supprimé : id={}", id);
    }
    
    // ==================== RECHERCHE ====================
    
    @Transactional(readOnly = true)
    public List<Book> searchByTitle(String title) {
        return bookRepository.findByTitleContainingIgnoreCase(title);
    }
}
```

---

### 5.5 Le Controller Réécrit Correctement

Créez `src/main/java/com/libraryhub/controller/BookController.java` :

```java
package com.libraryhub.controller;

import com.libraryhub.entity.Book;
import com.libraryhub.service.BookService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/books")
@RequiredArgsConstructor
public class BookController {
    
    // Le Controller ne connaît que le Service — jamais directement le Repository !
    private final BookService bookService;
    
    @GetMapping
    public ResponseEntity<List<Book>> getAllBooks() {
        return ResponseEntity.ok(bookService.findAllBooks());
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<Book> getBookById(@PathVariable Long id) {
        return ResponseEntity.ok(bookService.findBookById(id));
        // Si le livre n'existe pas, BookService lève BookNotFoundException
        // qui sera interceptée par le GlobalExceptionHandler (chapitre 9)
    }
    
    @PostMapping
    public ResponseEntity<Book> createBook(@RequestBody Book book) {
        Book created = bookService.createBook(book);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<Book> updateBook(
            @PathVariable Long id,
            @RequestBody Book book) {
        return ResponseEntity.ok(bookService.updateBook(id, book));
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteBook(@PathVariable Long id) {
        bookService.deleteBook(id);
        return ResponseEntity.noContent().build();
    }
    
    @GetMapping("/search")
    public ResponseEntity<List<Book>> searchBooks(
            @RequestParam String title) {
        return ResponseEntity.ok(bookService.searchByTitle(title));
    }
}
```

**Comparez ce Controller avec celui du chapitre 4 :**
- Le Controller est court et lisible (~ 30 lignes de logique)
- Toute la logique est dans le Service
- Le Controller délègue, le Service décide

---

## [IMPORTANT] Chapitre 6 — Le Pattern DTO (Data Transfer Object)

### 6.1 Le Problème de l'Exposition Directe des Entités

Dans notre code actuel, on expose directement les entités JPA dans les réponses HTTP. C'est **dangereux** :

```java
// Entité JPA complète — contient tout !
@Entity
public class Member {
    private Long id;
    private String name;
    private String email;
    private String password;        // <- DANGER : ne jamais exposer !
    private String creditCardNumber; // <- DANGER : données sensibles
    private List<Loan> loans;       // <- Peut causer des boucles infinies JSON
    private LocalDateTime createdAt;
}
```

Si vous retournez cette entité directement, le mot de passe sera dans le JSON. [!]

### 6.2 La Solution : Les DTOs

Un **DTO** (Data Transfer Object) est un objet simple qui contient exactement les données qu'on veut transférer — ni plus, ni moins.

```
CLIENT ──────────────────────────────── SERVEUR
     ────── MemberCreateDTO ──────────->         (Données envoyées par le client)
     <-───── MemberResponseDTO ──────────        (Données retournées au client)
                                        │
                                    Conversion
                                     (Mapper)
                                        │
                                    Member (Entité JPA en base de données)
```

---

### 6.3 Création des DTOs pour LibraryHub

#### DTOs pour les Livres

```java
// src/main/java/com/libraryhub/dto/book/BookCreateDTO.java
// Données reçues quand on crée un livre
package com.libraryhub.dto.book;

import jakarta.validation.constraints.*;
import lombok.Data;

@Data
public class BookCreateDTO {
    
    @NotBlank(message = "Le titre est obligatoire")
    @Size(max = 255, message = "Le titre ne peut pas dépasser 255 caractères")
    private String title;
    
    @NotBlank(message = "L'auteur est obligatoire")
    private String author;
    
    @NotBlank(message = "L'ISBN est obligatoire")
    @Pattern(regexp = "^(?:\\d{10}|\\d{13})$", message = "L'ISBN doit avoir 10 ou 13 chiffres")
    private String isbn;
    
    @Min(value = 0, message = "Le nombre de copies ne peut pas être négatif")
    private int copiesAvailable;
    
    private int publicationYear;
    private String description;
}
```

```java
// src/main/java/com/libraryhub/dto/book/BookResponseDTO.java
// Données retournées au client (pas de champs sensibles)
package com.libraryhub.dto.book;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class BookResponseDTO {
    private Long id;
    private String title;
    private String author;
    private String isbn;
    private int copiesAvailable;
    private int publicationYear;
    private String description;
    private boolean available;    // Champ calculé : copiesAvailable > 0
    // Pas de champs internes (createdAt, updatedAt, etc.)
}
```

#### DTOs pour les Membres

```java
// src/main/java/com/libraryhub/dto/member/MemberCreateDTO.java
package com.libraryhub.dto.member;

import jakarta.validation.constraints.*;
import lombok.Data;

@Data
public class MemberCreateDTO {
    
    @NotBlank
    @Size(min = 2, max = 50)
    private String firstName;
    
    @NotBlank
    @Size(min = 2, max = 50)
    private String lastName;
    
    @NotBlank
    @Email(message = "Email invalide")
    private String email;
    
    @NotBlank
    @Size(min = 8, message = "Le mot de passe doit avoir au moins 8 caractères")
    @Pattern(
        regexp = "^(?=.*[A-Z])(?=.*[0-9]).+$",
        message = "Le mot de passe doit contenir au moins une majuscule et un chiffre"
    )
    private String password;
}
```

```java
// src/main/java/com/libraryhub/dto/member/MemberResponseDTO.java
package com.libraryhub.dto.member;

import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;

@Data
@Builder
public class MemberResponseDTO {
    private Long id;
    private String firstName;
    private String lastName;
    private String email;
    private LocalDateTime memberSince;
    private int activeLoans;
    // Pas de mot de passe ! Jamais !
}
```

---

### 6.4 Le Mapper — Conversion Entity <-> DTO

#### Option A : Manuellement (recommandé pour débuter)

```java
// src/main/java/com/libraryhub/mapper/BookMapper.java
package com.libraryhub.mapper;

import com.libraryhub.dto.book.BookCreateDTO;
import com.libraryhub.dto.book.BookResponseDTO;
import com.libraryhub.entity.Book;
import org.springframework.stereotype.Component;

@Component
public class BookMapper {
    
    // DTO -> Entity (pour créer/sauvegarder)
    public Book toEntity(BookCreateDTO dto) {
        Book book = new Book();
        book.setTitle(dto.getTitle());
        book.setAuthor(dto.getAuthor());
        book.setIsbn(dto.getIsbn());
        book.setCopiesAvailable(dto.getCopiesAvailable());
        book.setPublicationYear(dto.getPublicationYear());
        book.setDescription(dto.getDescription());
        return book;
    }
    
    // Entity -> DTO (pour retourner au client)
    public BookResponseDTO toResponseDTO(Book book) {
        return BookResponseDTO.builder()
            .id(book.getId())
            .title(book.getTitle())
            .author(book.getAuthor())
            .isbn(book.getIsbn())
            .copiesAvailable(book.getCopiesAvailable())
            .publicationYear(book.getPublicationYear())
            .description(book.getDescription())
            .available(book.getCopiesAvailable() > 0)  // Champ calculé !
            .build();
    }
    
    // Convertir une liste d'entités en liste de DTOs
    public List<BookResponseDTO> toResponseDTOList(List<Book> books) {
        return books.stream()
            .map(this::toResponseDTO)
            .collect(Collectors.toList());
    }
}
```

#### Option B : MapStruct (automatique — à introduire après les bases)

```java
// MapStruct génère l'implémentation à la compilation
@Mapper(componentModel = "spring")
public interface BookMapper {
    Book toEntity(BookCreateDTO dto);
    BookResponseDTO toResponseDTO(Book book);
    
    @Mapping(source = "copiesAvailable", target = "available", 
             qualifiedByName = "mapAvailability")
    BookResponseDTO toResponseDTOWithAvailability(Book book);
    
    @Named("mapAvailability")
    default boolean mapAvailability(int copies) {
        return copies > 0;
    }
}
```

---

### 6.5 Controller Final avec DTOs

```java
@RestController
@RequestMapping("/api/books")
@RequiredArgsConstructor
public class BookController {
    
    private final BookService bookService;
    private final BookMapper bookMapper;
    
    @GetMapping
    public ResponseEntity<List<BookResponseDTO>> getAllBooks() {
        List<Book> books = bookService.findAllBooks();
        return ResponseEntity.ok(bookMapper.toResponseDTOList(books));
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<BookResponseDTO> getBookById(@PathVariable Long id) {
        Book book = bookService.findBookById(id);
        return ResponseEntity.ok(bookMapper.toResponseDTO(book));
    }
    
    @PostMapping
    public ResponseEntity<BookResponseDTO> createBook(
            @Valid @RequestBody BookCreateDTO createDTO) {
        // @Valid déclenche la validation des annotations sur BookCreateDTO
        Book book = bookMapper.toEntity(createDTO);
        Book saved = bookService.createBook(book);
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(bookMapper.toResponseDTO(saved));
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<BookResponseDTO> updateBook(
            @PathVariable Long id,
            @Valid @RequestBody BookCreateDTO updateDTO) {
        Book bookDetails = bookMapper.toEntity(updateDTO);
        Book updated = bookService.updateBook(id, bookDetails);
        return ResponseEntity.ok(bookMapper.toResponseDTO(updated));
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteBook(@PathVariable Long id) {
        bookService.deleteBook(id);
        return ResponseEntity.noContent().build();
    }
}
```

---

### 6.6 Résumé du Flux de Données

```
POST /api/books
{
  "title": "Clean Code",
  "author": "Robert Martin",
  "isbn": "9780132350884",
  "copiesAvailable": 3
}
         │
         [BLACK_DOWN-POINTING_TRIANGLE]
BookController.createBook(@Valid BookCreateDTO dto)
    │ bookMapper.toEntity(dto)
    [BLACK_DOWN-POINTING_TRIANGLE]
BookService.createBook(Book book)
    │ bookRepository.save(book)
    [BLACK_DOWN-POINTING_TRIANGLE]
Database : INSERT INTO book (title, author, isbn, copies_available)
           VALUES ('Clean Code', 'Robert Martin', '9780132350884', 3)
    │ Retourne Book avec id=1
    [BLACK_DOWN-POINTING_TRIANGLE]
BookService retourne Book
    │ bookMapper.toResponseDTO(book)
    [BLACK_DOWN-POINTING_TRIANGLE]
BookResponseDTO { id:1, title:"Clean Code", ..., available:true }
    │ JSON serialization
    [BLACK_DOWN-POINTING_TRIANGLE]
HTTP 201 Created
{
  "id": 1,
  "title": "Clean Code",
  "author": "Robert Martin",
  "isbn": "9780132350884",
  "copiesAvailable": 3,
  "available": true
}
```

---

### [OK] Exercices du Chapitre 5 & 6

1. **Créez** le `MemberController`, `MemberService`, `MemberRepository` suivant la même architecture
2. **Ajoutez** un DTO `BookUpdateDTO` différent de `BookCreateDTO` (pas d'ISBN modifiable après création)
3. **Testez** qu'un appel `POST /api/books` avec un ISBN déjà existant retourne une erreur
4. **Vérifiez** avec Postman que le mot de passe n'apparaît jamais dans les réponses

---

*-> Prochain fichier : `04_base_de_donnees_jpa.md`*


# [LIVRE] Chapitre 7 & 8 — JPA, Hibernate et Relations entre Entités

---

## [IMPORTANT] Chapitre 7 — JPA et Hibernate en Profondeur

### 7.1 JPA vs Hibernate — Quelle différence ?

**JPA (Java Persistence API)** est une **spécification** (un contrat, une interface). Elle définit les règles pour faire du mapping objet-relationnel (ORM) en Java. JPA n'est pas du code exécutable — c'est une liste de règles.

**Hibernate** est une **implémentation** de JPA. C'est Hibernate qui fait réellement le travail : il traduit vos objets Java en SQL et vice-versa. Spring Boot utilise Hibernate par défaut.

```
Votre Code Java
      v
JPA (définit les annotations : @Entity, @Column, @OneToMany...)
      v
Hibernate (traduit les objets Java en SQL)
      v
JDBC (communique avec la base de données)
      v
Base de données (PostgreSQL, MySQL, H2...)
```

**Analogie :** JPA est comme une prise électrique standardisée (définit la forme). Hibernate est comme l'appareil électrique (fait le travail).

---

### 7.2 Les Entités JPA — Mapper les Classes aux Tables

Une **entité JPA** est une classe Java qui correspond à une table en base de données. Chaque instance de la classe = une ligne dans la table.

```java
// src/main/java/com/libraryhub/entity/Book.java
package com.libraryhub.entity;

import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

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

@Entity                          // Dit à JPA : "crée une table pour cette classe"
@Table(name = "books",           // Nom de la table (optionnel, sinon = nom de la classe)
    uniqueConstraints = {
        @UniqueConstraint(columnNames = "isbn")  // Contrainte UNIQUE sur isbn
    }
)
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Book {
    
    @Id                          // Clé primaire
    @GeneratedValue(strategy = GenerationType.IDENTITY)  // Auto-incrémenté par la BDD
    private Long id;
    
    @Column(
        name = "title",
        nullable = false,         // NOT NULL en SQL
        length = 255              // VARCHAR(255)
    )
    private String title;
    
    @Column(nullable = false)
    private String author;
    
    @Column(unique = true, nullable = false, length = 13)
    private String isbn;
    
    @Column(name = "copies_available", nullable = false)
    private int copiesAvailable;
    
    @Column(name = "publication_year")
    private Integer publicationYear;
    
    @Column(columnDefinition = "TEXT")  // Colonne TEXT (pas VARCHAR) pour les longs textes
    private String description;
    
    @Column(name = "cover_image_url")
    private String coverImageUrl;
    
    // Audit automatique : rempli automatiquement par Hibernate
    @CreationTimestamp
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;
    
    @UpdateTimestamp
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
    
    // ==================== RELATION ====================
    // Un livre peut avoir plusieurs emprunts (OneToMany)
    @OneToMany(mappedBy = "book", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @Builder.Default
    private List<Loan> loans = new ArrayList<>();
    
    // Méthode métier pratique
    public boolean isAvailable() {
        return copiesAvailable > 0;
    }
}
```

**SQL généré par Hibernate :**
```sql
CREATE TABLE books (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    author VARCHAR(255) NOT NULL,
    isbn VARCHAR(13) UNIQUE NOT NULL,
    copies_available INT NOT NULL,
    publication_year INT,
    description TEXT,
    cover_image_url VARCHAR(255),
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    CONSTRAINT uk_books_isbn UNIQUE (isbn)
);
```

---

### 7.3 Les Stratégies de Génération d'ID

```java
// IDENTITY : La BDD gère l'auto-incrémentation (recommandé pour PostgreSQL, MySQL)
@GeneratedValue(strategy = GenerationType.IDENTITY)

// SEQUENCE : Utilise une séquence BDD (plus performant pour les insertions en masse)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "book_seq")
@SequenceGenerator(name = "book_seq", sequenceName = "book_sequence", allocationSize = 50)

// UUID : Identifiant universel unique (utile pour les APIs distribuées)
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;  // Type String pour UUID
```

---

### 7.4 Les Types de Données et Colonnes Spéciales

```java
@Entity
public class Member {
    
    // Enum stocké comme String (recommandé)
    @Enumerated(EnumType.STRING)     // Stocke "ADMIN" ou "MEMBER" (pas 0 ou 1)
    @Column(nullable = false)
    private Role role;
    
    // Enum stocké comme Int (déconseillé — fragile si l'ordre de l'enum change)
    @Enumerated(EnumType.ORDINAL)
    private Status status;
    
    // Collections simples
    @ElementCollection
    @CollectionTable(name = "member_phone_numbers")
    @Column(name = "phone_number")
    private List<String> phoneNumbers;
    
    // Type personnalisé
    @Convert(converter = AddressConverter.class)
    private Address address;
}

public enum Role {
    ADMIN, MEMBER, LIBRARIAN
}
```

---

## [IMPORTANT] Chapitre 8 — Les Relations entre Entités

### 8.1 Les 4 Types de Relations

**LibraryHub a ces entités :**
- `Book` (Livre)
- `Member` (Membre)
- `Loan` (Emprunt)
- `Author` (Auteur — optionnel avancé)

**Relations entre entités :**
```
Book ──────── Loan ──────── Member
  1              N                N
              (Many)
Un livre peut avoir plusieurs emprunts
Un membre peut avoir plusieurs emprunts
```

---

### 8.2 `@ManyToOne` et `@OneToMany` — Relation 1-N

La relation la plus courante. **Un membre a plusieurs emprunts, un emprunt appartient à un membre.**

```java
// src/main/java/com/libraryhub/entity/Loan.java
@Entity
@Table(name = "loans")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Loan {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    // CÔTÉ PROPRIÉTAIRE DE LA RELATION (contient la clé étrangère)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(
        name = "book_id",      // Nom de la colonne FK dans la table loans
        nullable = false
    )
    private Book book;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id", nullable = false)
    private Member member;
    
    @Column(name = "loan_date", nullable = false)
    private LocalDate loanDate;
    
    @Column(name = "due_date", nullable = false)
    private LocalDate dueDate;
    
    @Column(name = "return_date")
    private LocalDate returnDate;  // Null si pas encore retourné
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    @Builder.Default
    private LoanStatus status = LoanStatus.ACTIVE;
    
    // Méthode métier
    public boolean isOverdue() {
        return status == LoanStatus.ACTIVE && LocalDate.now().isAfter(dueDate);
    }
}
```

```java
// src/main/java/com/libraryhub/entity/Member.java
@Entity
@Table(name = "members")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "first_name", nullable = false, length = 50)
    private String firstName;
    
    @Column(name = "last_name", nullable = false, length = 50)
    private String lastName;
    
    @Column(unique = true, nullable = false)
    private String email;
    
    @Column(nullable = false)
    private String password;  // Stocké hashé (bcrypt)
    
    @Enumerated(EnumType.STRING)
    @Builder.Default
    private Role role = Role.MEMBER;
    
    @Column(name = "member_since")
    @CreationTimestamp
    private LocalDateTime memberSince;
    
    // CÔTÉ INVERSE DE LA RELATION (mappedBy = nom du champ dans Loan)
    @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @Builder.Default
    private List<Loan> loans = new ArrayList<>();
    
    // Méthode métier
    public long countActiveLoans() {
        return loans.stream()
            .filter(l -> l.getStatus() == LoanStatus.ACTIVE)
            .count();
    }
}
```

**SQL généré :**
```sql
CREATE TABLE loans (
    id BIGINT PRIMARY KEY,
    book_id BIGINT NOT NULL,        -- Clé étrangère vers books
    member_id BIGINT NOT NULL,      -- Clé étrangère vers members
    loan_date DATE NOT NULL,
    due_date DATE NOT NULL,
    return_date DATE,
    status VARCHAR(20) NOT NULL,
    FOREIGN KEY (book_id) REFERENCES books(id),
    FOREIGN KEY (member_id) REFERENCES members(id)
);
```

---

### 8.3 FetchType — LAZY vs EAGER

```java
// EAGER : Charge TOUJOURS la relation, même si vous n'en avez pas besoin
// [ATTENTION] DANGEREUX : Peut charger des milliers de lignes inutilement
@OneToMany(fetch = FetchType.EAGER)
private List<Loan> loans;

// LAZY : Charge la relation UNIQUEMENT quand vous y accédez
// [OK] RECOMMANDÉ : Beaucoup plus performant
@OneToMany(fetch = FetchType.LAZY)
private List<Loan> loans;
```

**Exemple du problème EAGER :**
```java
// Si EAGER, cette ligne charge TOUS les membres ET TOUS leurs emprunts
// -> Peut être des milliers de requêtes SQL !
List<Member> members = memberRepository.findAll();

// Avec LAZY, les emprunts ne sont chargés QUE quand on y accède :
Member member = memberRepository.findById(1L).get();
// Pas encore de requête pour les emprunts...
int loanCount = member.getLoans().size();  // <- C'est ICI que la requête SQL est faite
```

---

### 8.4 CascadeType — Propager les Opérations

```java
@OneToMany(
    mappedBy = "member",
    cascade = CascadeType.ALL,  // Propage TOUTES les opérations
    orphanRemoval = true        // Supprime automatiquement les emprunts orphelins
)
private List<Loan> loans;
```

**Les options de Cascade :**

| Type | Effet |
|---|---|
| `PERSIST` | Si je sauvegarde un Member, ses Loans sont aussi sauvegardés |
| `MERGE` | Si je merge un Member, ses Loans sont aussi mergés |
| `REMOVE` | Si je supprime un Member, ses Loans sont aussi supprimés |
| `REFRESH` | Recharge les Loans quand le Member est rafraîchi |
| `ALL` | Combine tous les types ci-dessus |

> [ATTENTION] **Attention avec `CASCADE REMOVE` :** Si vous supprimez un `Book`, tous ses `Loans` seront supprimés en cascade. C'est peut-être voulu... ou peut-être pas !

---

### 8.5 `@ManyToMany` — Relation N-M

**Exemple :** Un livre peut appartenir à plusieurs catégories, une catégorie peut avoir plusieurs livres.

```java
// src/main/java/com/libraryhub/entity/Category.java
@Entity
@Table(name = "categories")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Category {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true, nullable = false)
    private String name;
    
    private String description;
    
    // CÔTÉ INVERSE
    @ManyToMany(mappedBy = "categories")
    private List<Book> books = new ArrayList<>();
}
```

```java
// Dans Book.java — CÔTÉ PROPRIÉTAIRE
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
    name = "book_categories",           // Nom de la table d'association
    joinColumns = @JoinColumn(name = "book_id"),
    inverseJoinColumns = @JoinColumn(name = "category_id")
)
@Builder.Default
private List<Category> categories = new ArrayList<>();
```

**SQL généré :**
```sql
CREATE TABLE book_categories (
    book_id BIGINT NOT NULL,
    category_id BIGINT NOT NULL,
    PRIMARY KEY (book_id, category_id),
    FOREIGN KEY (book_id) REFERENCES books(id),
    FOREIGN KEY (category_id) REFERENCES categories(id)
);
```

---

### 8.6 Requêtes Avancées avec Spring Data JPA

#### Méthodes nommées (Derived Queries)

```java
public interface LoanRepository extends JpaRepository<Loan, Long> {
    
    // Emprunts actifs d'un membre
    List<Loan> findByMemberIdAndStatus(Long memberId, LoanStatus status);
    
    // Emprunts en retard (statut ACTIVE et date dépassée)
    List<Loan> findByStatusAndDueDateBefore(LoanStatus status, LocalDate date);
    
    // Nombre d'emprunts actifs pour un membre
    long countByMemberIdAndStatus(Long memberId, LoanStatus status);
    
    // Vérifier si un livre est actuellement emprunté
    boolean existsByBookIdAndStatus(Long bookId, LoanStatus status);
    
    // Emprunts d'un livre triés par date
    List<Loan> findByBookIdOrderByLoanDateDesc(Long bookId);
}
```

#### `@Query` — JPQL personnalisé

```java
public interface BookRepository extends JpaRepository<Book, Long> {
    
    // JPQL : requête sur les objets Java (pas directement le SQL)
    @Query("SELECT b FROM Book b WHERE b.copiesAvailable > 0 ORDER BY b.title")
    List<Book> findAvailableBooks();
    
    // Requête avec paramètre nommé
    @Query("SELECT b FROM Book b WHERE LOWER(b.title) LIKE LOWER(CONCAT('%', :search, '%')) " +
           "OR LOWER(b.author) LIKE LOWER(CONCAT('%', :search, '%'))")
    List<Book> searchByTitleOrAuthor(@Param("search") String search);
    
    // Statistiques
    @Query("SELECT COUNT(b) FROM Book b WHERE b.copiesAvailable = 0")
    long countUnavailableBooks();
    
    // Projection : ne récupérer que certains champs (plus performant)
    @Query("SELECT new com.libraryhub.dto.book.BookSummaryDTO(b.id, b.title, b.author) FROM Book b")
    List<BookSummaryDTO> findAllSummaries();
    
    // SQL natif (quand JPQL ne suffit pas)
    @Query(value = "SELECT * FROM books WHERE EXTRACT(YEAR FROM created_at) = :year",
           nativeQuery = true)
    List<Book> findBooksAddedInYear(@Param("year") int year);
    
    // Modification : nécessite @Modifying
    @Modifying
    @Transactional
    @Query("UPDATE Book b SET b.copiesAvailable = b.copiesAvailable - 1 WHERE b.id = :id")
    void decrementCopies(@Param("id") Long id);
}
```

---

### 8.7 Passer à PostgreSQL

Passez de H2 à PostgreSQL pour la production :

**Ajoutez dans `pom.xml` :**
```xml
<!-- Remplacez H2 par PostgreSQL -->
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>
```

**Créez `application-prod.properties` :**
```properties
# PostgreSQL
spring.datasource.url=jdbc:postgresql://localhost:5432/librarydb
spring.datasource.username=libraryuser
spring.datasource.password=securepassword
spring.datasource.driver-class-name=org.postgresql.Driver

# JPA
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=validate  # En prod : validate (ne modifie pas le schéma)
spring.jpa.show-sql=false               # Désactivé en production

# Flyway (migrations de schéma — optionnel mais recommandé)
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
```

**Avec Docker Compose (développement local) :**
```yaml
# docker-compose.yml
version: '3.8'
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: librarydb
      POSTGRES_USER: libraryuser
      POSTGRES_PASSWORD: securepassword
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:
```

---

### 8.8 Service d'Emprunt — Logique Métier Complète

```java
// src/main/java/com/libraryhub/service/LoanService.java
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class LoanService {
    
    private final LoanRepository loanRepository;
    private final BookRepository bookRepository;
    private final MemberRepository memberRepository;
    
    private static final int MAX_LOANS_PER_MEMBER = 3;
    private static final int DEFAULT_LOAN_DURATION_DAYS = 14;
    
    public Loan borrowBook(Long bookId, Long memberId) {
        
        // RÈGLE 1 : Le livre doit exister
        Book book = bookRepository.findById(bookId)
            .orElseThrow(() -> new BookNotFoundException("Livre introuvable : " + bookId));
        
        // RÈGLE 2 : Le membre doit exister
        Member member = memberRepository.findById(memberId)
            .orElseThrow(() -> new MemberNotFoundException("Membre introuvable : " + memberId));
        
        // RÈGLE 3 : Le livre doit être disponible
        if (book.getCopiesAvailable() <= 0) {
            throw new BookNotAvailableException("Le livre '" + book.getTitle() + "' n'est plus disponible");
        }
        
        // RÈGLE 4 : Le membre ne peut pas dépasser MAX_LOANS_PER_MEMBER emprunts actifs
        long activeLoans = loanRepository.countByMemberIdAndStatus(memberId, LoanStatus.ACTIVE);
        if (activeLoans >= MAX_LOANS_PER_MEMBER) {
            throw new MaxLoansReachedException(
                "Vous avez atteint le maximum de " + MAX_LOANS_PER_MEMBER + " emprunts simultanés"
            );
        }
        
        // RÈGLE 5 : Le membre ne peut pas emprunter le même livre deux fois
        if (loanRepository.existsByBookIdAndMemberIdAndStatus(bookId, memberId, LoanStatus.ACTIVE)) {
            throw new DuplicateLoanException("Vous avez déjà emprunté ce livre");
        }
        
        // CRÉATION DE L'EMPRUNT
        Loan loan = Loan.builder()
            .book(book)
            .member(member)
            .loanDate(LocalDate.now())
            .dueDate(LocalDate.now().plusDays(DEFAULT_LOAN_DURATION_DAYS))
            .status(LoanStatus.ACTIVE)
            .build();
        
        // DÉCRÉMENTATION DES COPIES
        book.setCopiesAvailable(book.getCopiesAvailable() - 1);
        bookRepository.save(book);
        
        Loan savedLoan = loanRepository.save(loan);
        log.info("Emprunt créé : membre={}, livre={}, retour prévu={}",
            memberId, bookId, savedLoan.getDueDate());
        
        return savedLoan;
    }
    
    public Loan returnBook(Long loanId) {
        Loan loan = loanRepository.findById(loanId)
            .orElseThrow(() -> new LoanNotFoundException("Emprunt introuvable : " + loanId));
        
        if (loan.getStatus() != LoanStatus.ACTIVE) {
            throw new InvalidLoanStateException("Cet emprunt est déjà clôturé");
        }
        
        // Mise à jour de l'emprunt
        loan.setReturnDate(LocalDate.now());
        loan.setStatus(LoanStatus.RETURNED);
        
        // Incrémentation des copies disponibles
        Book book = loan.getBook();
        book.setCopiesAvailable(book.getCopiesAvailable() + 1);
        bookRepository.save(book);
        
        return loanRepository.save(loan);
    }
}
```

---

### [OK] Exercices du Chapitre 7 & 8

1. **Créez** toutes les entités : `Book`, `Member`, `Loan`, `Category`
2. **Vérifiez** que les tables sont créées en regardant la console H2
3. **Ajoutez** des données de test dans un `CommandLineRunner` (bean exécuté au démarrage)
4. **Testez** l'endpoint `POST /api/loans` pour créer un emprunt
5. **Vérifiez** qu'emprunter un livre indisponible retourne une erreur 400

---

*-> Prochain fichier : `05_validation_et_exceptions.md`*


# [LIVRE] Chapitre 9 — Validation et Gestion Globale des Exceptions

---

## [IMPORTANT] 9.1 Pourquoi Valider les Données ?

Sans validation, votre application accepte n'importe quoi :

```json
// Un client malveillant ou buggé peut envoyer :
{
  "title": "",                          // Titre vide
  "author": null,                       // Null
  "isbn": "pas-un-isbn",               // Format invalide
  "copiesAvailable": -999,             // Valeur absurde
  "publicationYear": 9999999           // Année impossible
}
```

Sans validation, Hibernate essaiera d'insérer ces données en base — et plantera avec une erreur SQL cryptique. Avec validation, vous renvoyez une erreur HTTP 400 claire **avant** même de toucher la base de données.

---

## [IMPORTANT] 9.2 Bean Validation — Les Annotations

Spring Boot intègre **Jakarta Bean Validation** (anciennement Hibernate Validator). Vous décorez vos DTOs avec des annotations de contrainte :

```java
// src/main/java/com/libraryhub/dto/book/BookCreateDTO.java
package com.libraryhub.dto.book;

import jakarta.validation.constraints.*;
import lombok.Data;

@Data
public class BookCreateDTO {
    
    // ─── Chaînes de caractères ───────────────────────────────────
    @NotNull(message = "Le titre ne peut pas être null")
    @NotBlank(message = "Le titre ne peut pas être vide")
    @Size(min = 1, max = 255, message = "Le titre doit avoir entre 1 et 255 caractères")
    private String title;
    
    @NotBlank(message = "L'auteur est obligatoire")
    @Size(max = 100)
    private String author;
    
    // ─── Patterns / Regex ────────────────────────────────────────
    @NotBlank(message = "L'ISBN est obligatoire")
    @Pattern(
        regexp = "^(?:\\d{10}|\\d{13})$",
        message = "L'ISBN doit être composé de 10 ou 13 chiffres"
    )
    private String isbn;
    
    // ─── Nombres ─────────────────────────────────────────────────
    @Min(value = 0, message = "Le nombre de copies ne peut pas être négatif")
    @Max(value = 1000, message = "Le nombre de copies semble trop élevé (max 1000)")
    private int copiesAvailable;
    
    @Min(value = 1450, message = "L'année de publication ne peut pas être avant 1450")
    @Max(value = 2100, message = "Année de publication invalide")
    private int publicationYear;
    
    // ─── Email ───────────────────────────────────────────────────
    @Email(message = "Format d'email invalide")
    private String publisherEmail;
    
    // ─── Pas null, mais peut être vide ───────────────────────────
    @NotNull
    private String description;  // "" est autorisé, null non
    
    // ─── Dates ───────────────────────────────────────────────────
    @NotNull
    @FutureOrPresent(message = "La date de publication ne peut pas être dans le passé")
    private LocalDate releaseDate;
    
    @NotNull
    @Past(message = "La date de naissance doit être dans le passé")
    private LocalDate authorBirthDate;
}
```

### Tableau des Annotations de Validation

| Annotation | S'applique à | Description |
|---|---|---|
| `@NotNull` | Tout type | L'objet ne doit pas être null |
| `@NotEmpty` | String, Collection | Non null ET non vide (`""`, `[]`) |
| `@NotBlank` | String | Non null, non vide, non espaces seuls |
| `@Size(min, max)` | String, Collection | Longueur dans l'intervalle |
| `@Min(value)` | Nombre | Valeur >= value |
| `@Max(value)` | Nombre | Valeur <= value |
| `@Positive` | Nombre | Strictement positif (> 0) |
| `@PositiveOrZero` | Nombre | Positif ou zéro (>= 0) |
| `@Email` | String | Format email valide |
| `@Pattern(regexp)` | String | Correspond à la regex |
| `@Past` | Date | Date dans le passé |
| `@Future` | Date | Date dans le futur |
| `@PastOrPresent` | Date | Passé ou présent |
| `@FutureOrPresent` | Date | Futur ou présent |
| `@AssertTrue` | Boolean | Doit être true |
| `@AssertFalse` | Boolean | Doit être false |

---

## [IMPORTANT] 9.3 Activer la Validation

Pour activer la validation dans un Controller :

```java
@RestController
@RequestMapping("/api/books")
public class BookController {
    
    @PostMapping
    public ResponseEntity<BookResponseDTO> createBook(
            @Valid @RequestBody BookCreateDTO dto  // <- @Valid ACTIVE LA VALIDATION
    ) {
        // Si le DTO est invalide, Spring lève MethodArgumentNotValidException
        // AVANT même d'entrer dans cette méthode
        Book book = bookMapper.toEntity(dto);
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(bookMapper.toResponseDTO(bookService.createBook(book)));
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<BookResponseDTO> getBook(
            @PathVariable @Positive(message = "L'ID doit être positif") Long id
    ) {
        return ResponseEntity.ok(bookMapper.toResponseDTO(bookService.findBookById(id)));
    }
}
```

---

## [IMPORTANT] 9.4 Validation Personnalisée

### Créer une Annotation Personnalisée

```java
// src/main/java/com/libraryhub/validation/ValidIsbn.java
package com.libraryhub.validation;

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

@Documented
@Constraint(validatedBy = IsbnValidator.class)  // Classe qui fait la validation
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidIsbn {
    String message() default "ISBN invalide";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
```

```java
// src/main/java/com/libraryhub/validation/IsbnValidator.java
package com.libraryhub.validation;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class IsbnValidator implements ConstraintValidator<ValidIsbn, String> {
    
    @Override
    public boolean isValid(String isbn, ConstraintValidatorContext context) {
        if (isbn == null || isbn.isBlank()) return false;
        
        // Supprimer les tirets et espaces
        String cleaned = isbn.replaceAll("[\\s-]", "");
        
        if (cleaned.length() == 10) return isValidIsbn10(cleaned);
        if (cleaned.length() == 13) return isValidIsbn13(cleaned);
        return false;
    }
    
    private boolean isValidIsbn10(String isbn) {
        try {
            int sum = 0;
            for (int i = 0; i < 9; i++) {
                sum += (i + 1) * Character.getNumericValue(isbn.charAt(i));
            }
            char last = isbn.charAt(9);
            sum += (last == 'X') ? 10 * 10 : 10 * Character.getNumericValue(last);
            return sum % 11 == 0;
        } catch (NumberFormatException e) {
            return false;
        }
    }
    
    private boolean isValidIsbn13(String isbn) {
        try {
            int sum = 0;
            for (int i = 0; i < 12; i++) {
                int digit = Character.getNumericValue(isbn.charAt(i));
                sum += (i % 2 == 0) ? digit : digit * 3;
            }
            int checkDigit = (10 - (sum % 10)) % 10;
            return checkDigit == Character.getNumericValue(isbn.charAt(12));
        } catch (NumberFormatException e) {
            return false;
        }
    }
}
```

**Utilisation :**
```java
public class BookCreateDTO {
    @ValidIsbn  // Votre annotation personnalisée !
    private String isbn;
}
```

### Validation au niveau de la classe (cross-field)

```java
// Valider que la date de fin > date de début
@Data
@ValidDateRange   // Annotation personnalisée sur la classe
public class LoanCreateDTO {
    private LocalDate startDate;
    private LocalDate endDate;
}

// Validateur
public class DateRangeValidator implements ConstraintValidator<ValidDateRange, LoanCreateDTO> {
    @Override
    public boolean isValid(LoanCreateDTO dto, ConstraintValidatorContext ctx) {
        if (dto.getStartDate() == null || dto.getEndDate() == null) return true;
        return dto.getEndDate().isAfter(dto.getStartDate());
    }
}
```

---

## [IMPORTANT] 9.5 Exceptions Personnalisées

### Créer une hiérarchie d'exceptions

```java
// Exception de base LibraryHub
// src/main/java/com/libraryhub/exception/LibraryHubException.java
public abstract class LibraryHubException extends RuntimeException {
    private final HttpStatus httpStatus;
    private final String errorCode;
    
    public LibraryHubException(String message, HttpStatus httpStatus, String errorCode) {
        super(message);
        this.httpStatus = httpStatus;
        this.errorCode = errorCode;
    }
    
    public HttpStatus getHttpStatus() { return httpStatus; }
    public String getErrorCode() { return errorCode; }
}
```

```java
// Exceptions spécifiques
public class BookNotFoundException extends LibraryHubException {
    public BookNotFoundException(String message) {
        super(message, HttpStatus.NOT_FOUND, "BOOK_NOT_FOUND");
    }
}

public class BookNotAvailableException extends LibraryHubException {
    public BookNotAvailableException(String message) {
        super(message, HttpStatus.CONFLICT, "BOOK_NOT_AVAILABLE");
    }
}

public class DuplicateIsbnException extends LibraryHubException {
    public DuplicateIsbnException(String message) {
        super(message, HttpStatus.CONFLICT, "DUPLICATE_ISBN");
    }
}

public class MaxLoansReachedException extends LibraryHubException {
    public MaxLoansReachedException(String message) {
        super(message, HttpStatus.UNPROCESSABLE_ENTITY, "MAX_LOANS_REACHED");
    }
}

public class UnauthorizedException extends LibraryHubException {
    public UnauthorizedException(String message) {
        super(message, HttpStatus.UNAUTHORIZED, "UNAUTHORIZED");
    }
}
```

---

## [IMPORTANT] 9.6 `@ControllerAdvice` — Gestion Globale des Exceptions

Sans gestion globale, une exception non capturée retourne une réponse HTTP cryptique et incomplète. Avec `@ControllerAdvice`, vous centralisez la gestion :

### Créer le DTO d'erreur

```java
// src/main/java/com/libraryhub/dto/error/ErrorResponseDTO.java
@Data
@Builder
public class ErrorResponseDTO {
    private int status;              // Code HTTP (400, 404, 500...)
    private String errorCode;        // Code métier ("BOOK_NOT_FOUND")
    private String message;          // Message lisible
    private String path;             // URL qui a causé l'erreur
    private LocalDateTime timestamp; // Quand s'est produite l'erreur
    
    // Pour les erreurs de validation
    private Map<String, String> validationErrors;
}
```

### Le Handler Global

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

import com.libraryhub.dto.error.ErrorResponseDTO;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

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

@RestControllerAdvice   // = @ControllerAdvice + @ResponseBody
@Slf4j
public class GlobalExceptionHandler {
    
    // ─── 1. Nos exceptions métier personnalisées ──────────────────
    @ExceptionHandler(LibraryHubException.class)
    public ResponseEntity<ErrorResponseDTO> handleLibraryHubException(
            LibraryHubException ex,
            HttpServletRequest request) {
        
        log.warn("Erreur métier : {} - {}", ex.getErrorCode(), ex.getMessage());
        
        ErrorResponseDTO error = ErrorResponseDTO.builder()
            .status(ex.getHttpStatus().value())
            .errorCode(ex.getErrorCode())
            .message(ex.getMessage())
            .path(request.getRequestURI())
            .timestamp(LocalDateTime.now())
            .build();
        
        return ResponseEntity.status(ex.getHttpStatus()).body(error);
    }
    
    // ─── 2. Erreurs de validation (@Valid) ────────────────────────
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponseDTO> handleValidationErrors(
            MethodArgumentNotValidException ex,
            HttpServletRequest request) {
        
        // Récupérer toutes les erreurs de validation
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach(error -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        
        log.warn("Erreur de validation sur {} : {}", request.getRequestURI(), errors);
        
        ErrorResponseDTO error = ErrorResponseDTO.builder()
            .status(400)
            .errorCode("VALIDATION_ERROR")
            .message("Les données fournies sont invalides")
            .path(request.getRequestURI())
            .timestamp(LocalDateTime.now())
            .validationErrors(errors)
            .build();
        
        return ResponseEntity.badRequest().body(error);
    }
    
    // ─── 3. Erreur de type de paramètre ──────────────────────────
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public ResponseEntity<ErrorResponseDTO> handleTypeMismatch(
            MethodArgumentTypeMismatchException ex,
            HttpServletRequest request) {
        
        String message = String.format(
            "Le paramètre '%s' avec la valeur '%s' n'est pas du bon type. Type attendu : %s",
            ex.getName(), ex.getValue(), ex.getRequiredType().getSimpleName()
        );
        
        ErrorResponseDTO error = ErrorResponseDTO.builder()
            .status(400)
            .errorCode("TYPE_MISMATCH")
            .message(message)
            .path(request.getRequestURI())
            .timestamp(LocalDateTime.now())
            .build();
        
        return ResponseEntity.badRequest().body(error);
    }
    
    // ─── 4. Toutes les autres exceptions ─────────────────────────
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponseDTO> handleGenericException(
            Exception ex,
            HttpServletRequest request) {
        
        // IMPORTANT : Loggez l'exception COMPLÈTE pour débugger
        log.error("Erreur non prévue sur {} : ", request.getRequestURI(), ex);
        
        ErrorResponseDTO error = ErrorResponseDTO.builder()
            .status(500)
            .errorCode("INTERNAL_SERVER_ERROR")
            .message("Une erreur interne s'est produite. Veuillez réessayer plus tard.")
            .path(request.getRequestURI())
            .timestamp(LocalDateTime.now())
            .build();
        
        return ResponseEntity.internalServerError().body(error);
    }
}
```

---

## [IMPORTANT] 9.7 Exemples de Réponses d'Erreur

**Erreur de validation :**
```json
HTTP 400 Bad Request
{
  "status": 400,
  "errorCode": "VALIDATION_ERROR",
  "message": "Les données fournies sont invalides",
  "path": "/api/books",
  "timestamp": "2024-01-15T10:30:00",
  "validationErrors": {
    "title": "Le titre ne peut pas être vide",
    "isbn": "L'ISBN doit être composé de 10 ou 13 chiffres",
    "copiesAvailable": "Le nombre de copies ne peut pas être négatif"
  }
}
```

**Ressource non trouvée :**
```json
HTTP 404 Not Found
{
  "status": 404,
  "errorCode": "BOOK_NOT_FOUND",
  "message": "Livre non trouvé avec id : 999",
  "path": "/api/books/999",
  "timestamp": "2024-01-15T10:30:00",
  "validationErrors": null
}
```

**Conflit :**
```json
HTTP 409 Conflict
{
  "status": 409,
  "errorCode": "DUPLICATE_ISBN",
  "message": "Un livre avec l'ISBN 9780132350884 existe déjà",
  "path": "/api/books",
  "timestamp": "2024-01-15T10:30:00",
  "validationErrors": null
}
```

---

## [IMPORTANT] 9.8 Codes HTTP — Guide Pratique

| Code | Nom | Quand l'utiliser |
|---|---|---|
| 200 | OK | Requête réussie (GET, PUT) |
| 201 | Created | Ressource créée (POST) |
| 204 | No Content | Succès sans corps (DELETE) |
| 400 | Bad Request | Données invalides, validation échouée |
| 401 | Unauthorized | Non authentifié |
| 403 | Forbidden | Authentifié mais pas autorisé |
| 404 | Not Found | Ressource introuvable |
| 409 | Conflict | Conflit (doublon ISBN) |
| 422 | Unprocessable | Données valides mais inapplicables (max emprunts) |
| 500 | Internal Server Error | Erreur serveur inattendue |

---

### [OK] Exercices du Chapitre 9

1. **Testez** qu'un POST avec un titre vide retourne 400 avec le message d'erreur approprié
2. **Créez** une annotation `@FutureLoanDate` pour valider que la date de retour est au moins 1 jour dans le futur
3. **Ajoutez** la gestion de `HttpMessageNotReadableException` (JSON malformé) dans le GlobalExceptionHandler
4. **Vérifiez** qu'une exception non prévue retourne 500 (sans exposer les détails techniques)

---

*-> Prochain fichier : `06_securite_spring_security.md`*


# [LIVRE] Chapitre 10 & 11 — Spring Security et JWT

---

## [IMPORTANT] Chapitre 10 — Spring Security : Fondamentaux

### 10.1 Qu'est-ce que Spring Security ?

Spring Security est le framework de sécurité de l'écosystème Spring. Il gère :
- **Authentification** : "Qui es-tu ?" (login, vérification d'identité)
- **Autorisation** : "As-tu le droit ?" (permissions, rôles)
- **Protection** contre les attaques courantes (CSRF, XSS, session fixation...)

### 10.2 Ajouter Spring Security

Dans `pom.xml`, ajoutez :
```xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- Pour JWT -->
<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>
```

> [ATTENTION] **Effet immédiat :** Dès que vous ajoutez Spring Security, **TOUS vos endpoints sont protégés** et nécessitent une authentification. Spring génère un mot de passe aléatoire dans les logs. Il faut configurer la sécurité manuellement.

---

### 10.3 Comment Fonctionne Spring Security

```
Requête HTTP arrivante
       │
       [BLACK_DOWN-POINTING_TRIANGLE]
┌─────────────────────────────────┐
│  CHAÎNE DE FILTRES SÉCURITÉ     │
│  (SecurityFilterChain)          │
│                                 │
│  1. JwtAuthenticationFilter     │ <- Notre filtre personnalisé
│     - Lit le header Authorization
│     - Valide le JWT
│     - Charge l'utilisateur
│     - Place dans SecurityContext
│                                 │
│  2. ExceptionTranslationFilter  │ <- Gère 401/403
│  3. AuthorizationFilter         │ <- Vérifie les droits
└───────────────┬─────────────────┘
                │
                [BLACK_DOWN-POINTING_TRIANGLE]
      Controller / Endpoint
```

**SecurityContext :**
Spring Security stocke l'utilisateur authentifié dans un `SecurityContext` local au thread. Vous pouvez y accéder depuis n'importe quel endroit :
```java
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
```

---

### 10.4 Configuration de la Sécurité

```java
// src/main/java/com/libraryhub/config/SecurityConfig.java
package com.libraryhub.config;

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

@Configuration
@EnableWebSecurity
@EnableMethodSecurity   // Active @PreAuthorize, @PostAuthorize sur les méthodes
@RequiredArgsConstructor
public class SecurityConfig {
    
    private final JwtAuthenticationFilter jwtAuthFilter;
    private final CustomUserDetailsService userDetailsService;
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // ─── Désactiver CSRF (pas nécessaire pour les API REST stateless) ───
            .csrf(csrf -> csrf.disable())
            
            // ─── Configurer les autorisations par URL ────────────────────────
            .authorizeHttpRequests(auth -> auth
                
                // Endpoints publics (pas besoin d'être connecté)
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/books").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/books/**").permitAll()
                .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
                .requestMatchers("/h2-console/**").permitAll()
                
                // Endpoints réservés aux ADMIN
                .requestMatchers(HttpMethod.DELETE, "/api/books/**").hasRole("ADMIN")
                .requestMatchers(HttpMethod.POST, "/api/books/**").hasRole("ADMIN")
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                
                // Tout le reste nécessite d'être authentifié
                .anyRequest().authenticated()
            )
            
            // ─── Pas de session (API stateless avec JWT) ─────────────────────
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            
            // ─── Provider d'authentification ─────────────────────────────────
            .authenticationProvider(authenticationProvider())
            
            // ─── Insérer notre filtre JWT avant le filtre standard ────────────
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
    
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }
    
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        // BCrypt : algorithme de hachage sécurisé pour les mots de passe
        // Factor "strength" 12 : 2^12 itérations (bon compromis sécurité/performance)
        return new BCryptPasswordEncoder(12);
    }
}
```

---

### 10.5 UserDetailsService — Charger l'Utilisateur

Spring Security a besoin de savoir comment charger un utilisateur depuis votre base de données :

```java
// src/main/java/com/libraryhub/security/CustomUserDetailsService.java
package com.libraryhub.security;

import com.libraryhub.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
    
    private final MemberRepository memberRepository;
    
    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        // Spring Security appelle cette méthode avec l'email lors du login
        return memberRepository.findByEmail(email)
            .orElseThrow(() -> new UsernameNotFoundException(
                "Aucun membre avec l'email : " + email
            ));
        // Notre entité Member doit implémenter UserDetails (voir ci-dessous)
    }
}
```

**L'entité Member doit implémenter UserDetails :**

```java
@Entity
@Table(name = "members")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member implements UserDetails {   // <- Implements UserDetails
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String firstName;
    private String lastName;
    private String email;
    private String password;
    
    @Enumerated(EnumType.STRING)
    private Role role;
    
    // ─── Méthodes UserDetails ─────────────────────────────────────
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // Convertit le rôle en liste d'autorités Spring Security
        return List.of(new SimpleGrantedAuthority("ROLE_" + role.name()));
        // "ROLE_ADMIN" ou "ROLE_MEMBER"
    }
    
    @Override
    public String getUsername() {
        return email;  // On utilise l'email comme username
    }
    
    @Override
    public String getPassword() {
        return password;  // Mot de passe haché BCrypt
    }
    
    @Override
    public boolean isAccountNonExpired() { return true; }
    
    @Override
    public boolean isAccountNonLocked() { return true; }
    
    @Override
    public boolean isCredentialsNonExpired() { return true; }
    
    @Override
    public boolean isEnabled() { return true; }
}
```

---

## [IMPORTANT] Chapitre 11 — JSON Web Token (JWT)

### 11.1 Qu'est-ce que JWT ?

Un **JWT** (JSON Web Token) est un token sécurisé qui encode des informations et peut être vérifié sans base de données.

**Structure d'un JWT :**
```
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9    <- Header (algorithme)
.
eyJzdWIiOiJ1c2VyQGVtYWlsLmNvbSIsImlhdCI6MTcwNTM0MjgwMCwiZXhwIjoxNzA1NDI5MjAwLCJyb2xlIjoiTUVNQkVSIn0=
                                            <- Payload (données)
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c   <- Signature (vérification)
```

**Payload décodé :**
```json
{
  "sub": "user@email.com",     // Subject (qui ?)
  "iat": 1705342800,           // Issued At (quand émis ?)
  "exp": 1705429200,           // Expiration (quand expire ?)
  "role": "MEMBER",            // Données supplémentaires
  "memberId": 42
}
```

**Flux JWT :**
```
1. Client envoie email + mot de passe
           v
2. Serveur vérifie les credentials -> crée un JWT -> retourne le JWT
           v
3. Client stocke le JWT (localStorage ou mémoire)
           v
4. Client envoie le JWT dans chaque requête : Authorization: Bearer <token>
           v
5. Serveur valide la signature du JWT -> autorise ou refuse
```

---

### 11.2 Service JWT

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

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

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

@Service
@Slf4j
public class JwtService {
    
    // Clé secrète depuis application.properties (minimum 256 bits pour HS256)
    @Value("${app.jwt.secret}")
    private String secretKey;
    
    @Value("${app.jwt.expiration-ms}")
    private long jwtExpirationMs;  // Ex : 86400000 = 24h en millisecondes
    
    // ─── GÉNÉRER UN TOKEN ─────────────────────────────────────────
    
    public String generateToken(UserDetails userDetails) {
        return generateToken(new HashMap<>(), userDetails);
    }
    
    public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
        Member member = (Member) userDetails;
        
        // Claims supplémentaires personnalisés
        extraClaims.put("role", member.getRole().name());
        extraClaims.put("memberId", member.getId());
        extraClaims.put("fullName", member.getFirstName() + " " + member.getLastName());
        
        return Jwts.builder()
            .claims(extraClaims)
            .subject(userDetails.getUsername())          // email
            .issuedAt(new Date(System.currentTimeMillis()))
            .expiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
            .signWith(getSigningKey())
            .compact();
    }
    
    // ─── VALIDER UN TOKEN ─────────────────────────────────────────
    
    public boolean isTokenValid(String token, UserDetails userDetails) {
        try {
            final String username = extractUsername(token);
            return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
        } catch (JwtException e) {
            log.warn("JWT invalide : {}", e.getMessage());
            return false;
        }
    }
    
    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }
    
    // ─── EXTRAIRE DES INFORMATIONS ────────────────────────────────
    
    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }
    
    public Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }
    
    public String extractRole(String token) {
        return extractClaim(token, claims -> claims.get("role", String.class));
    }
    
    public Long extractMemberId(String token) {
        return extractClaim(token, claims -> claims.get("memberId", Long.class));
    }
    
    // Méthode générique pour extraire n'importe quelle claim
    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }
    
    private Claims extractAllClaims(String token) {
        return Jwts.parser()
            .verifyWith(getSigningKey())
            .build()
            .parseSignedClaims(token)
            .getPayload();
    }
    
    private SecretKey getSigningKey() {
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);
        return Keys.hmacShaKeyFor(keyBytes);
    }
}
```

---

### 11.3 Filtre JWT — Vérifier le Token à chaque Requête

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

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

@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    // OncePerRequestFilter garantit que ce filtre n'est exécuté qu'une fois par requête
    
    private final JwtService jwtService;
    private final CustomUserDetailsService userDetailsService;
    
    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws Exception {
        
        // 1. Lire le header Authorization
        final String authHeader = request.getHeader("Authorization");
        
        // 2. Si pas de token ou mauvais format -> passer au filtre suivant (non authentifié)
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }
        
        // 3. Extraire le JWT (sans "Bearer ")
        final String jwt = authHeader.substring(7);
        
        try {
            // 4. Extraire l'email du token
            final String email = jwtService.extractUsername(jwt);
            
            // 5. Si email extrait ET pas encore authentifié dans ce contexte
            if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                
                // 6. Charger l'utilisateur depuis la BDD
                UserDetails userDetails = userDetailsService.loadUserByUsername(email);
                
                // 7. Valider le token
                if (jwtService.isTokenValid(jwt, userDetails)) {
                    
                    // 8. Créer l'objet d'authentification Spring Security
                    UsernamePasswordAuthenticationToken authToken =
                        new UsernamePasswordAuthenticationToken(
                            userDetails,
                            null,
                            userDetails.getAuthorities()  // Les rôles de l'utilisateur
                        );
                    authToken.setDetails(
                        new WebAuthenticationDetailsSource().buildDetails(request)
                    );
                    
                    // 9. Enregistrer dans le SecurityContext
                    // À partir de maintenant, cet utilisateur est "authentifié" pour cette requête
                    SecurityContextHolder.getContext().setAuthentication(authToken);
                    
                    log.debug("Utilisateur authentifié via JWT : {}", email);
                }
            }
        } catch (Exception e) {
            log.warn("Impossible de valider le JWT : {}", e.getMessage());
            // On continue sans authentification (la requête sera rejetée plus bas si besoin)
        }
        
        // 10. Continuer la chaîne de filtres
        filterChain.doFilter(request, response);
    }
}
```

---

### 11.4 Controller d'Authentification

```java
// src/main/java/com/libraryhub/controller/AuthController.java
package com.libraryhub.controller;

import com.libraryhub.dto.auth.*;
import com.libraryhub.service.AuthService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
    
    private final AuthService authService;
    
    // ─── INSCRIPTION ──────────────────────────────────────────────
    @PostMapping("/register")
    public ResponseEntity<AuthResponseDTO> register(
            @Valid @RequestBody RegisterRequestDTO request) {
        return ResponseEntity.ok(authService.register(request));
    }
    
    // ─── CONNEXION ────────────────────────────────────────────────
    @PostMapping("/login")
    public ResponseEntity<AuthResponseDTO> login(
            @Valid @RequestBody LoginRequestDTO request) {
        return ResponseEntity.ok(authService.login(request));
    }
    
    // ─── PROFIL ACTUEL ────────────────────────────────────────────
    @GetMapping("/me")
    public ResponseEntity<MemberResponseDTO> getCurrentUser() {
        return ResponseEntity.ok(authService.getCurrentMember());
    }
}
```

```java
// DTOs pour l'authentification
@Data
public class RegisterRequestDTO {
    @NotBlank private String firstName;
    @NotBlank private String lastName;
    @NotBlank @Email private String email;
    @NotBlank @Size(min = 8) private String password;
}

@Data
public class LoginRequestDTO {
    @NotBlank @Email private String email;
    @NotBlank private String password;
}

@Data
@Builder
public class AuthResponseDTO {
    private String token;
    private String tokenType = "Bearer";
    private long expiresIn;   // Durée en secondes
    private MemberResponseDTO member;
}
```

---

### 11.5 AuthService — Logique d'Authentification

```java
// src/main/java/com/libraryhub/service/AuthService.java
@Service
@RequiredArgsConstructor
@Slf4j
public class AuthService {
    
    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;
    private final JwtService jwtService;
    private final AuthenticationManager authenticationManager;
    private final MemberMapper memberMapper;
    
    @Value("${app.jwt.expiration-ms}")
    private long jwtExpirationMs;
    
    @Transactional
    public AuthResponseDTO register(RegisterRequestDTO request) {
        
        // Vérifier que l'email n'existe pas déjà
        if (memberRepository.existsByEmail(request.getEmail())) {
            throw new DuplicateEmailException("Un compte avec cet email existe déjà");
        }
        
        // Créer le membre
        Member member = Member.builder()
            .firstName(request.getFirstName())
            .lastName(request.getLastName())
            .email(request.getEmail())
            // HASH DU MOT DE PASSE — jamais stocker en clair !
            .password(passwordEncoder.encode(request.getPassword()))
            .role(Role.MEMBER)
            .build();
        
        Member saved = memberRepository.save(member);
        log.info("Nouveau membre enregistré : {}", saved.getEmail());
        
        // Générer le JWT
        String token = jwtService.generateToken(saved);
        
        return AuthResponseDTO.builder()
            .token(token)
            .expiresIn(jwtExpirationMs / 1000)
            .member(memberMapper.toResponseDTO(saved))
            .build();
    }
    
    public AuthResponseDTO login(LoginRequestDTO request) {
        
        // Spring Security vérifie email + mot de passe
        // Lève AuthenticationException si invalide
        authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                request.getEmail(),
                request.getPassword()
            )
        );
        
        // Si on arrive ici, les credentials sont valides
        Member member = memberRepository.findByEmail(request.getEmail())
            .orElseThrow();
        
        String token = jwtService.generateToken(member);
        log.info("Connexion réussie : {}", member.getEmail());
        
        return AuthResponseDTO.builder()
            .token(token)
            .expiresIn(jwtExpirationMs / 1000)
            .member(memberMapper.toResponseDTO(member))
            .build();
    }
    
    public MemberResponseDTO getCurrentMember() {
        // Récupérer l'utilisateur authentifié depuis le SecurityContext
        String email = SecurityContextHolder.getContext()
            .getAuthentication()
            .getName();
        
        Member member = memberRepository.findByEmail(email).orElseThrow();
        return memberMapper.toResponseDTO(member);
    }
}
```

---

### 11.6 Sécurité au Niveau des Méthodes

```java
// Dans votre SecurityConfig : @EnableMethodSecurity est déjà activé

// Dans votre Service ou Controller :
@Service
public class LoanService {
    
    // Seul un ADMIN peut voir tous les emprunts
    @PreAuthorize("hasRole('ADMIN')")
    public List<Loan> findAllLoans() { ... }
    
    // Un MEMBRE peut voir ses propres emprunts, un ADMIN peut voir n'importe lesquels
    @PreAuthorize("hasRole('ADMIN') or #memberId == authentication.principal.id")
    public List<Loan> findLoansByMember(Long memberId) { ... }
    
    // Seul un ADMIN ou le propriétaire peut supprimer
    @PreAuthorize("hasRole('ADMIN') or @loanSecurityService.isOwner(#loanId)")
    public void deleteLoan(Long loanId) { ... }
}
```

---

### 11.7 Configuration JWT dans application.properties

```properties
# Générez une clé secrète Base64 de 256 bits minimum
# En ligne de commande : openssl rand -base64 32
app.jwt.secret=7Xn2bKp9mRqT4vWzLdFhJcAeGsUiYoPkNtVxMjBrQwZlEyCfH6sDuO1aI3g8n5K=
app.jwt.expiration-ms=86400000   # 24 heures
```

---

### [OK] Exercices du Chapitre 10 & 11

1. **Testez** `POST /api/auth/register` avec Postman et récupérez le JWT
2. **Testez** `POST /api/auth/login` et vérifiez que le JWT est valide
3. **Testez** que `DELETE /api/books/1` retourne 403 avec le token d'un MEMBRE
4. **Décodez** votre JWT sur [jwt.io](https://jwt.io) et regardez son contenu
5. **Ajoutez** un endpoint `POST /api/auth/refresh` pour renouveler le token

---

*-> Prochain fichier : `07_configuration_et_profils.md`*


# [LIVRE] Chapitre 12 — Configuration, Profils et Variables d'Environnement

---

## [IMPORTANT] 12.1 Le Problème de la Configuration

Une application réelle tourne dans plusieurs environnements :

```
DEV (développement)     TEST (CI/CD)          PROD (production)
H2 en mémoire          H2 ou PostgreSQL       PostgreSQL
Logs DEBUG             Logs INFO              Logs WARN
JWT 24h                JWT 1h                 JWT 15min
Email désactivé        Email sandbox           Email réel
Port 8080              Port 8080               Port 443
```

Comment gérer tout ça ? Avec les **profils Spring Boot** et les **variables d'environnement**.

---

## [IMPORTANT] 12.2 application.properties vs application.yml

Spring Boot accepte deux formats de configuration :

**Format `.properties` :**
```properties
spring.datasource.url=jdbc:h2:mem:librarydb
spring.jpa.show-sql=true
app.jwt.secret=monSecret
```

**Format `.yml` (YAML) :**
```yaml
spring:
  datasource:
    url: jdbc:h2:mem:librarydb
  jpa:
    show-sql: true

app:
  jwt:
    secret: monSecret
```

YAML est **plus lisible** pour les configurations complexes avec beaucoup d'imbrication. Choisissez l'un ou l'autre — ne mélangez pas.

---

## [IMPORTANT] 12.3 Les Profils Spring Boot

### Créer des fichiers de configuration par profil

```
src/main/resources/
├── application.properties          <- Commun à TOUS les profils
├── application-dev.properties      <- Profil DEV
├── application-test.properties     <- Profil TEST
└── application-prod.properties     <- Profil PROD
```

### `application.properties` — Configuration commune

```properties
# ─── Identité de l'application ─────────────────────────────
spring.application.name=LibraryHub

# ─── Server ─────────────────────────────────────────────────
server.port=8080
server.servlet.context-path=/api

# ─── JPA Communes ───────────────────────────────────────────
spring.jpa.open-in-view=false    # Désactiver pour éviter des problèmes de lazy loading

# ─── Jackson (JSON) ─────────────────────────────────────────
spring.jackson.serialization.write-dates-as-timestamps=false  # Dates en ISO-8601
spring.jackson.default-property-inclusion=non_null            # Ignorer les champs null

# ─── Propriétés métier personnalisées ───────────────────────
app.name=LibraryHub
app.max-loans-per-member=3
app.loan-duration-days=14
app.jwt.expiration-ms=86400000
```

### `application-dev.properties` — Développement local

```properties
# ─── Base de données H2 en mémoire ──────────────────────────
spring.datasource.url=jdbc:h2:mem:librarydb;DB_CLOSE_DELAY=-1
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# ─── Console H2 ─────────────────────────────────────────────
spring.h2.console.enabled=true

# ─── JPA / Hibernate ─────────────────────────────────────────
spring.jpa.hibernate.ddl-auto=create-drop    # Recrée les tables à chaque démarrage
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# ─── Logs très détaillés ─────────────────────────────────────
logging.level.com.libraryhub=DEBUG
logging.level.org.springframework.security=DEBUG
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql=TRACE

# ─── JWT (longue durée pour le dev) ─────────────────────────
app.jwt.secret=devSecretKeyForLocalDevelopmentOnly12345678901234567890
app.jwt.expiration-ms=604800000   # 7 jours

# ─── Email (désactivé, log seulement) ───────────────────────
app.email.enabled=false
```

### `application-test.properties` — Tests automatisés

```properties
# ─── Base de données H2 (séparée de dev) ─────────────────────
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
spring.datasource.driver-class-name=org.h2.Driver

spring.jpa.hibernate.ddl-auto=create-drop

# ─── Logs minimaux pour des tests rapides ────────────────────
logging.level.com.libraryhub=INFO
logging.level.org.springframework=WARN

# ─── JWT court pour les tests ────────────────────────────────
app.jwt.secret=testSecretKeyForTestingPurposesOnly1234567890
app.jwt.expiration-ms=3600000   # 1 heure

app.email.enabled=false
```

### `application-prod.properties` — Production

```properties
# ─── Base de données PostgreSQL ──────────────────────────────
# NE PAS METTRE LES VRAIES VALEURS ICI -> utiliser des variables d'environnement !
spring.datasource.url=${DATABASE_URL}
spring.datasource.username=${DATABASE_USERNAME}
spring.datasource.password=${DATABASE_PASSWORD}
spring.datasource.driver-class-name=org.postgresql.Driver

# Pool de connexions
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000

# ─── JPA ─────────────────────────────────────────────────────
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=validate    # Ne modifie JAMAIS le schéma en prod !
spring.jpa.show-sql=false

# ─── Logs production ─────────────────────────────────────────
logging.level.root=WARN
logging.level.com.libraryhub=INFO
logging.file.name=/var/log/libraryhub/app.log
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n

# ─── JWT depuis variable d'environnement ─────────────────────
app.jwt.secret=${JWT_SECRET}
app.jwt.expiration-ms=900000    # 15 minutes en production

# ─── Email production ────────────────────────────────────────
app.email.enabled=true
spring.mail.host=${MAIL_HOST}
spring.mail.port=587
spring.mail.username=${MAIL_USERNAME}
spring.mail.password=${MAIL_PASSWORD}
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
```

---

## [IMPORTANT] 12.4 Activer un Profil

### Méthode 1 : `application.properties`
```properties
spring.profiles.active=dev
```

### Méthode 2 : Variable d'environnement (recommandée pour prod)
```bash
# Linux/macOS
export SPRING_PROFILES_ACTIVE=prod
java -jar libraryhub.jar

# Windows
set SPRING_PROFILES_ACTIVE=prod
java -jar libraryhub.jar
```

### Méthode 3 : Argument JVM
```bash
java -jar libraryhub.jar --spring.profiles.active=prod
```

### Méthode 4 : Dans les tests
```java
@SpringBootTest
@ActiveProfiles("test")   // Active le profil "test" pour ce test
class BookServiceTest { ... }
```

---

## [IMPORTANT] 12.5 Variables d'Environnement

Les secrets (mots de passe, clés API) ne doivent **jamais** être dans le code source. Utilisez des variables d'environnement :

```properties
# Dans application-prod.properties
spring.datasource.password=${DATABASE_PASSWORD}
# Spring lit la variable d'environnement DATABASE_PASSWORD

# Avec valeur par défaut si non définie
spring.datasource.password=${DATABASE_PASSWORD:defaultPassword}
```

**Définir les variables :**
```bash
# Linux/macOS
export DATABASE_URL=jdbc:postgresql://db.prod.com:5432/librarydb
export DATABASE_USERNAME=libraryuser
export DATABASE_PASSWORD=supersecretpassword
export JWT_SECRET=veryLongSecretKeyBase64Encoded...
```

---

## [IMPORTANT] 12.6 `@ConfigurationProperties` — Propriétés Typées

Plutôt que d'utiliser `@Value` partout, regroupez vos propriétés :

```java
// src/main/java/com/libraryhub/config/AppProperties.java
package com.libraryhub.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "app")   // Correspond aux propriétés commençant par "app."
@Data
public class AppProperties {
    
    private String name;
    private int maxLoansPerMember = 3;    // Valeur par défaut
    private int loanDurationDays = 14;
    
    private Jwt jwt = new Jwt();           // Sous-propriété imbriquée
    private Email email = new Email();
    
    @Data
    public static class Jwt {
        private String secret;
        private long expirationMs = 86400000L;
    }
    
    @Data
    public static class Email {
        private boolean enabled = true;
        private String from = "noreply@libraryhub.com";
    }
}
```

**Dans `application.properties` :**
```properties
app.name=LibraryHub
app.max-loans-per-member=3
app.loan-duration-days=14
app.jwt.secret=mySecret
app.jwt.expiration-ms=86400000
app.email.enabled=true
app.email.from=noreply@libraryhub.com
```

**Utilisation dans un service :**
```java
@Service
@RequiredArgsConstructor
public class LoanService {
    
    private final AppProperties appProperties;
    
    public Loan borrowBook(Long bookId, Long memberId) {
        // Utilisation propre des propriétés typées
        int maxLoans = appProperties.getMaxLoansPerMember();
        int duration = appProperties.getLoanDurationDays();
        
        // Au lieu de @Value dans chaque classe...
    }
}
```

---

## [IMPORTANT] 12.7 Beans Conditionnels par Profil

```java
// Bean actif seulement en DEV
@Component
@Profile("dev")
public class DevDataInitializer implements CommandLineRunner {
    
    @Autowired private BookRepository bookRepo;
    @Autowired private MemberRepository memberRepo;
    @Autowired private PasswordEncoder passwordEncoder;
    
    @Override
    public void run(String... args) {
        // Données de test chargées uniquement en DEV
        Book book1 = Book.builder()
            .title("Clean Code")
            .author("Robert Martin")
            .isbn("9780132350884")
            .copiesAvailable(3)
            .publicationYear(2008)
            .build();
        
        bookRepo.save(book1);
        
        Member admin = Member.builder()
            .firstName("Admin")
            .lastName("LibraryHub")
            .email("admin@libraryhub.com")
            .password(passwordEncoder.encode("Admin@123"))
            .role(Role.ADMIN)
            .build();
        
        memberRepo.save(admin);
        
        System.out.println("[OK] Données de développement initialisées !");
        System.out.println("   Admin: admin@libraryhub.com / Admin@123");
    }
}
```

```java
// Service Email : différentes implémentations selon le profil
public interface EmailService {
    void sendLoanConfirmation(Loan loan);
}

@Service
@Profile("!prod")   // Actif quand on N'EST PAS en prod
@Slf4j
public class MockEmailService implements EmailService {
    @Override
    public void sendLoanConfirmation(Loan loan) {
        log.info("[EMAIL] [MOCK] Email de confirmation -> {} : Emprunt de '{}'",
            loan.getMember().getEmail(), loan.getBook().getTitle());
        // Ne fait rien -> log seulement
    }
}

@Service
@Profile("prod")    // Actif uniquement en prod
@RequiredArgsConstructor
public class RealEmailService implements EmailService {
    private final JavaMailSender mailSender;
    
    @Override
    public void sendLoanConfirmation(Loan loan) {
        // Envoi réel d'email
    }
}
```

---

## [IMPORTANT] 12.8 Externalisation de la Configuration avec Docker

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

services:
  app:
    image: libraryhub:latest
    ports:
      - "8080:8080"
    environment:
      SPRING_PROFILES_ACTIVE: prod
      DATABASE_URL: jdbc:postgresql://postgres:5432/librarydb
      DATABASE_USERNAME: libraryuser
      DATABASE_PASSWORD: ${DB_PASSWORD}          # Depuis .env local
      JWT_SECRET: ${JWT_SECRET}
      MAIL_HOST: smtp.gmail.com
      MAIL_USERNAME: ${MAIL_USER}
      MAIL_PASSWORD: ${MAIL_PASSWORD}
    depends_on:
      postgres:
        condition: service_healthy
  
  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: librarydb
      POSTGRES_USER: libraryuser
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U libraryuser -d librarydb"]
      interval: 10s
      retries: 5

volumes:
  postgres_data:
```

Fichier `.env` local (jamais committé dans Git !) :
```
DB_PASSWORD=supersecretpassword
JWT_SECRET=veryLongBase64EncodedSecretKey...
MAIL_USER=libraryhub@gmail.com
MAIL_PASSWORD=yourAppPassword
```

```
# .gitignore
.env
*.env.local
application-prod.properties
```

---

### [OK] Exercices du Chapitre 12

1. **Créez** les 3 fichiers de configuration (dev, test, prod)
2. **Passez** du profil `dev` au profil `test` et vérifiez que les logs changent
3. **Créez** une classe `AppProperties` pour toutes vos propriétés métier
4. **Créez** un `DevDataInitializer` qui charge 5 livres et 2 membres au démarrage

---

*-> Prochain fichier : `08_tests.md`*


# [LIVRE] Chapitre 13 — Tests : Unitaires, Intégration et MockMvc

---

## [IMPORTANT] 13.1 Pourquoi Tester ?

Les tests automatisés sont **non-négociables** en développement professionnel. Ils vous permettent de :
- **Détecter les régressions** : une modification qui casse une fonctionnalité existante
- **Refactoriser en confiance** : modifier le code sans craindre de casser quelque chose
- **Documenter** le comportement attendu du code
- **Débugger plus vite** : un test qui échoue isole immédiatement le problème

---

## [IMPORTANT] 13.2 La Pyramide des Tests

```
              /\
             /  \
            / E2E \          <- Peu de tests, lents, coûteux
           /________\           (Cypress, Selenium)
          /          \
         / INTÉGRATION \     <- Tests moyens, Spring Test
        /______________\
       /                \
      /    UNITAIRES     \  <- Beaucoup de tests, rapides, isolés
     /____________________\    (JUnit 5, Mockito)
```

- **Tests Unitaires :** Testent une classe isolément. Toutes les dépendances sont des mocks.
- **Tests d'Intégration :** Testent l'interaction entre plusieurs couches (Controller + Service + BDD)
- **Tests E2E :** Testent le flux complet de l'application (comme un utilisateur réel)

---

## [IMPORTANT] 13.3 Dépendances de Test

`spring-boot-starter-test` inclut déjà :
- **JUnit 5** : Framework de test Java
- **Mockito** : Créer des mocks
- **AssertJ** : Assertions fluentes
- **Spring Test** : Utilitaires Spring pour les tests

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

<!-- Pour les tests de sécurité -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>
```

---

## [IMPORTANT] 13.4 Tests Unitaires — Tester le Service

### Règle d'or : Une unité = une méthode testée isolément

```java
// src/test/java/com/libraryhub/service/BookServiceTest.java
package com.libraryhub.service;

import com.libraryhub.entity.Book;
import com.libraryhub.exception.BookNotFoundException;
import com.libraryhub.exception.DuplicateIsbnException;
import com.libraryhub.repository.BookRepository;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;

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

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

// @ExtendWith(MockitoExtension.class) active Mockito sans Spring
// -> Tests très rapides (pas de contexte Spring à charger)
@ExtendWith(MockitoExtension.class)
@DisplayName("Tests BookService")
class BookServiceTest {
    
    // @Mock : Crée un faux BookRepository — ne fait RIEN par défaut
    @Mock
    private BookRepository bookRepository;
    
    // @InjectMocks : Crée un vrai BookService et injecte le mock dedans
    @InjectMocks
    private BookService bookService;
    
    // Données de test réutilisables
    private Book sampleBook;
    
    @BeforeEach  // Exécuté avant CHAQUE test
    void setUp() {
        sampleBook = Book.builder()
            .id(1L)
            .title("Clean Code")
            .author("Robert Martin")
            .isbn("9780132350884")
            .copiesAvailable(3)
            .build();
    }
    
    // ─── Tests findBookById ───────────────────────────────────────
    
    @Test
    @DisplayName("findBookById doit retourner le livre quand il existe")
    void findBookById_ShouldReturnBook_WhenBookExists() {
        // GIVEN (préparer) — On dit au mock quoi retourner
        when(bookRepository.findById(1L)).thenReturn(Optional.of(sampleBook));
        
        // WHEN (exécuter) — On appelle la méthode à tester
        Book result = bookService.findBookById(1L);
        
        // THEN (vérifier) — On vérifie le résultat
        assertThat(result).isNotNull();
        assertThat(result.getId()).isEqualTo(1L);
        assertThat(result.getTitle()).isEqualTo("Clean Code");
        assertThat(result.getAuthor()).isEqualTo("Robert Martin");
        
        // Vérifier que le repository a bien été appelé
        verify(bookRepository, times(1)).findById(1L);
    }
    
    @Test
    @DisplayName("findBookById doit lever BookNotFoundException quand le livre n'existe pas")
    void findBookById_ShouldThrowBookNotFoundException_WhenBookDoesNotExist() {
        // GIVEN
        when(bookRepository.findById(999L)).thenReturn(Optional.empty());
        
        // WHEN + THEN — Vérifier qu'une exception est levée
        assertThatThrownBy(() -> bookService.findBookById(999L))
            .isInstanceOf(BookNotFoundException.class)
            .hasMessageContaining("999");
        
        verify(bookRepository).findById(999L);
    }
    
    // ─── Tests createBook ─────────────────────────────────────────
    
    @Test
    @DisplayName("createBook doit sauvegarder et retourner le livre")
    void createBook_ShouldSaveAndReturnBook_WhenIsbnIsUnique() {
        // GIVEN
        Book newBook = Book.builder()
            .title("Effective Java")
            .author("Joshua Bloch")
            .isbn("9780134685991")
            .copiesAvailable(2)
            .build();
        
        when(bookRepository.existsByIsbn("9780134685991")).thenReturn(false);
        when(bookRepository.save(any(Book.class))).thenAnswer(invocation -> {
            Book saved = invocation.getArgument(0);
            saved.setId(2L);  // Simuler l'attribution d'un ID par la BDD
            return saved;
        });
        
        // WHEN
        Book result = bookService.createBook(newBook);
        
        // THEN
        assertThat(result.getId()).isEqualTo(2L);
        assertThat(result.getTitle()).isEqualTo("Effective Java");
        
        verify(bookRepository).existsByIsbn("9780134685991");
        verify(bookRepository).save(newBook);
    }
    
    @Test
    @DisplayName("createBook doit lever DuplicateIsbnException quand l'ISBN existe déjà")
    void createBook_ShouldThrowDuplicateIsbnException_WhenIsbnAlreadyExists() {
        // GIVEN
        Book book = Book.builder()
            .isbn("9780132350884")  // ISBN déjà existant
            .copiesAvailable(1)
            .build();
        
        when(bookRepository.existsByIsbn("9780132350884")).thenReturn(true);
        
        // WHEN + THEN
        assertThatThrownBy(() -> bookService.createBook(book))
            .isInstanceOf(DuplicateIsbnException.class);
        
        // Vérifier que save() n'a PAS été appelé
        verify(bookRepository, never()).save(any());
    }
    
    @Test
    @DisplayName("createBook doit rejeter un livre avec des copies négatives")
    void createBook_ShouldThrowException_WhenCopiesAreNegative() {
        // GIVEN
        Book invalidBook = Book.builder()
            .title("Bad Book")
            .isbn("1234567890123")
            .copiesAvailable(-5)  // Invalide
            .build();
        
        when(bookRepository.existsByIsbn(any())).thenReturn(false);
        
        // WHEN + THEN
        assertThatThrownBy(() -> bookService.createBook(invalidBook))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining("négatif");
    }
    
    // ─── Tests findAllBooks ───────────────────────────────────────
    
    @Test
    @DisplayName("findAllBooks doit retourner tous les livres")
    void findAllBooks_ShouldReturnAllBooks() {
        // GIVEN
        List<Book> books = List.of(
            sampleBook,
            Book.builder().id(2L).title("Effective Java").isbn("9780134685991").copiesAvailable(1).build()
        );
        when(bookRepository.findAll()).thenReturn(books);
        
        // WHEN
        List<Book> result = bookService.findAllBooks();
        
        // THEN
        assertThat(result).hasSize(2);
        assertThat(result).extracting(Book::getTitle)
            .containsExactly("Clean Code", "Effective Java");
    }
    
    @Test
    @DisplayName("findAllBooks doit retourner une liste vide si aucun livre")
    void findAllBooks_ShouldReturnEmptyList_WhenNoBooks() {
        // GIVEN
        when(bookRepository.findAll()).thenReturn(List.of());
        
        // WHEN
        List<Book> result = bookService.findAllBooks();
        
        // THEN
        assertThat(result).isEmpty();
    }
}
```

---

## [IMPORTANT] 13.5 Tests Unitaires — Tester les Règles Métier du LoanService

```java
@ExtendWith(MockitoExtension.class)
@DisplayName("Tests LoanService")
class LoanServiceTest {
    
    @Mock private LoanRepository loanRepository;
    @Mock private BookRepository bookRepository;
    @Mock private MemberRepository memberRepository;
    
    @InjectMocks private LoanService loanService;
    
    private Book availableBook;
    private Member activeMember;
    
    @BeforeEach
    void setUp() {
        availableBook = Book.builder()
            .id(1L).title("Clean Code").isbn("1234567890123")
            .copiesAvailable(2).build();
        
        activeMember = Member.builder()
            .id(1L).email("john@test.com")
            .firstName("John").lastName("Doe")
            .role(Role.MEMBER).build();
    }
    
    @Test
    void borrowBook_ShouldCreateLoan_WhenAllConditionsMet() {
        // GIVEN
        when(bookRepository.findById(1L)).thenReturn(Optional.of(availableBook));
        when(memberRepository.findById(1L)).thenReturn(Optional.of(activeMember));
        when(loanRepository.countByMemberIdAndStatus(1L, LoanStatus.ACTIVE)).thenReturn(0L);
        when(loanRepository.existsByBookIdAndMemberIdAndStatus(1L, 1L, LoanStatus.ACTIVE)).thenReturn(false);
        when(loanRepository.save(any(Loan.class))).thenAnswer(inv -> {
            Loan l = inv.getArgument(0);
            l.setId(1L);
            return l;
        });
        
        // WHEN
        Loan result = loanService.borrowBook(1L, 1L);
        
        // THEN
        assertThat(result.getStatus()).isEqualTo(LoanStatus.ACTIVE);
        assertThat(result.getLoanDate()).isEqualTo(LocalDate.now());
        assertThat(result.getDueDate()).isEqualTo(LocalDate.now().plusDays(14));
        
        // Le stock doit avoir diminué
        assertThat(availableBook.getCopiesAvailable()).isEqualTo(1);
        verify(bookRepository).save(availableBook);
    }
    
    @Test
    void borrowBook_ShouldThrow_WhenBookNotAvailable() {
        availableBook.setCopiesAvailable(0);  // Pas de copies disponibles
        
        when(bookRepository.findById(1L)).thenReturn(Optional.of(availableBook));
        when(memberRepository.findById(1L)).thenReturn(Optional.of(activeMember));
        
        assertThatThrownBy(() -> loanService.borrowBook(1L, 1L))
            .isInstanceOf(BookNotAvailableException.class);
        
        verify(loanRepository, never()).save(any());
    }
    
    @Test
    void borrowBook_ShouldThrow_WhenMemberReachedMaxLoans() {
        when(bookRepository.findById(1L)).thenReturn(Optional.of(availableBook));
        when(memberRepository.findById(1L)).thenReturn(Optional.of(activeMember));
        when(loanRepository.countByMemberIdAndStatus(1L, LoanStatus.ACTIVE)).thenReturn(3L); // MAX atteint
        
        assertThatThrownBy(() -> loanService.borrowBook(1L, 1L))
            .isInstanceOf(MaxLoansReachedException.class);
    }
}
```

---

## [IMPORTANT] 13.6 Tests d'Intégration du Repository

```java
// src/test/java/com/libraryhub/repository/BookRepositoryTest.java

// @DataJpaTest : Lance UNIQUEMENT la couche JPA (pas tout Spring)
// Utilise H2 en mémoire par défaut
@DataJpaTest
@ActiveProfiles("test")
@DisplayName("Tests BookRepository")
class BookRepositoryTest {
    
    @Autowired
    private BookRepository bookRepository;
    
    @BeforeEach
    void setUp() {
        bookRepository.save(Book.builder()
            .title("Clean Code").author("Robert Martin")
            .isbn("9780132350884").copiesAvailable(3).build());
        
        bookRepository.save(Book.builder()
            .title("Clean Architecture").author("Robert Martin")
            .isbn("9780134494166").copiesAvailable(0).build());
        
        bookRepository.save(Book.builder()
            .title("Effective Java").author("Joshua Bloch")
            .isbn("9780134685991").copiesAvailable(1).build());
    }
    
    @Test
    void findByTitleContainingIgnoreCase_ShouldReturnMatchingBooks() {
        List<Book> result = bookRepository.findByTitleContainingIgnoreCase("clean");
        
        assertThat(result).hasSize(2);
        assertThat(result).extracting(Book::getTitle)
            .containsExactlyInAnyOrder("Clean Code", "Clean Architecture");
    }
    
    @Test
    void findByCopiesAvailableGreaterThan_ShouldReturnAvailableBooks() {
        List<Book> result = bookRepository.findByCopiesAvailableGreaterThan(0);
        
        assertThat(result).hasSize(2);
        assertThat(result).noneMatch(b -> b.getCopiesAvailable() == 0);
    }
    
    @Test
    void existsByIsbn_ShouldReturnTrue_WhenIsbnExists() {
        boolean exists = bookRepository.existsByIsbn("9780132350884");
        assertThat(exists).isTrue();
    }
    
    @Test
    void existsByIsbn_ShouldReturnFalse_WhenIsbnNotFound() {
        boolean exists = bookRepository.existsByIsbn("0000000000000");
        assertThat(exists).isFalse();
    }
    
    @Test
    void findByAuthor_ShouldReturnBooksFromSameAuthor() {
        List<Book> books = bookRepository.findByAuthor("Robert Martin");
        assertThat(books).hasSize(2);
    }
}
```

---

## [IMPORTANT] 13.7 Tests d'Intégration avec MockMvc — Tester les Controllers

```java
// src/test/java/com/libraryhub/controller/BookControllerTest.java

// @WebMvcTest : Lance uniquement la couche Web (Controller, Filter, SecurityConfig)
// Les services sont mockés
@WebMvcTest(BookController.class)
@ActiveProfiles("test")
@DisplayName("Tests BookController")
class BookControllerTest {
    
    @Autowired
    private MockMvc mockMvc;    // Simule des requêtes HTTP sans démarrer un vrai serveur
    
    @Autowired
    private ObjectMapper objectMapper;   // Pour convertir objets Java <-> JSON
    
    @MockBean   // Mock Spring géré par le contexte (différent de @Mock Mockito)
    private BookService bookService;
    
    @MockBean
    private BookMapper bookMapper;
    
    @MockBean
    private JwtService jwtService;
    
    @MockBean
    private CustomUserDetailsService userDetailsService;
    
    // Données de test
    private BookResponseDTO sampleResponse;
    
    @BeforeEach
    void setUp() {
        sampleResponse = BookResponseDTO.builder()
            .id(1L).title("Clean Code").author("Robert Martin")
            .isbn("9780132350884").copiesAvailable(3).available(true)
            .build();
    }
    
    // ─── Test GET ALL ─────────────────────────────────────────────
    
    @Test
    @WithMockUser   // Simule un utilisateur authentifié
    @DisplayName("GET /api/books doit retourner la liste des livres")
    void getAllBooks_ShouldReturn200WithBookList() throws Exception {
        // GIVEN
        when(bookService.findAllBooks()).thenReturn(List.of(new Book()));
        when(bookMapper.toResponseDTOList(any())).thenReturn(List.of(sampleResponse));
        
        // WHEN + THEN
        mockMvc.perform(get("/api/books"))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
            .andExpect(jsonPath("$").isArray())
            .andExpect(jsonPath("$.length()").value(1))
            .andExpect(jsonPath("$[0].id").value(1))
            .andExpect(jsonPath("$[0].title").value("Clean Code"))
            .andExpect(jsonPath("$[0].author").value("Robert Martin"))
            .andExpect(jsonPath("$[0].available").value(true))
            .andDo(print());   // Affiche la requête et réponse dans la console
    }
    
    // ─── Test GET BY ID ───────────────────────────────────────────
    
    @Test
    @WithMockUser
    void getBookById_ShouldReturn200_WhenBookExists() throws Exception {
        when(bookService.findBookById(1L)).thenReturn(new Book());
        when(bookMapper.toResponseDTO(any())).thenReturn(sampleResponse);
        
        mockMvc.perform(get("/api/books/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id").value(1))
            .andExpect(jsonPath("$.title").value("Clean Code"));
    }
    
    @Test
    @WithMockUser
    void getBookById_ShouldReturn404_WhenBookNotFound() throws Exception {
        when(bookService.findBookById(999L))
            .thenThrow(new BookNotFoundException("Livre non trouvé avec id : 999"));
        
        mockMvc.perform(get("/api/books/999"))
            .andExpect(status().isNotFound())
            .andExpect(jsonPath("$.errorCode").value("BOOK_NOT_FOUND"))
            .andExpect(jsonPath("$.message").value("Livre non trouvé avec id : 999"));
    }
    
    // ─── Test POST CREATE ─────────────────────────────────────────
    
    @Test
    @WithMockUser(roles = "ADMIN")   // Simule un ADMIN
    void createBook_ShouldReturn201_WhenDataIsValid() throws Exception {
        // GIVEN
        BookCreateDTO createDTO = new BookCreateDTO();
        createDTO.setTitle("Clean Code");
        createDTO.setAuthor("Robert Martin");
        createDTO.setIsbn("9780132350884");
        createDTO.setCopiesAvailable(3);
        createDTO.setPublicationYear(2008);
        
        when(bookMapper.toEntity(any())).thenReturn(new Book());
        when(bookService.createBook(any())).thenReturn(new Book());
        when(bookMapper.toResponseDTO(any())).thenReturn(sampleResponse);
        
        // WHEN + THEN
        mockMvc.perform(post("/api/books")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(createDTO)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.id").value(1))
            .andExpect(jsonPath("$.title").value("Clean Code"));
    }
    
    @Test
    @WithMockUser(roles = "ADMIN")
    void createBook_ShouldReturn400_WhenTitleIsBlank() throws Exception {
        // Titre vide -> doit échouer la validation
        BookCreateDTO invalidDTO = new BookCreateDTO();
        invalidDTO.setTitle("");   // <- Invalide
        invalidDTO.setIsbn("9780132350884");
        invalidDTO.setCopiesAvailable(1);
        invalidDTO.setPublicationYear(2008);
        
        mockMvc.perform(post("/api/books")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(invalidDTO)))
            .andExpect(status().isBadRequest())
            .andExpect(jsonPath("$.errorCode").value("VALIDATION_ERROR"))
            .andExpect(jsonPath("$.validationErrors.title").exists());
    }
    
    @Test
    void createBook_ShouldReturn403_WhenUserNotAdmin() throws Exception {
        // Pas de @WithMockUser = non authentifié -> 401
        // @WithMockUser sans rôle ADMIN -> 403
        BookCreateDTO dto = buildValidCreateDTO();
        
        mockMvc.perform(post("/api/books")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(dto)))
            .andExpect(status().isUnauthorized());   // 401
        
        // Avec un MEMBRE
        mockMvc.perform(post("/api/books")
                .with(user("member@test.com").roles("MEMBER"))
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(dto)))
            .andExpect(status().isForbidden());   // 403
    }
    
    // ─── Test DELETE ──────────────────────────────────────────────
    
    @Test
    @WithMockUser(roles = "ADMIN")
    void deleteBook_ShouldReturn204_WhenBookExists() throws Exception {
        doNothing().when(bookService).deleteBook(1L);
        
        mockMvc.perform(delete("/api/books/1"))
            .andExpect(status().isNoContent());
        
        verify(bookService).deleteBook(1L);
    }
    
    private BookCreateDTO buildValidCreateDTO() {
        BookCreateDTO dto = new BookCreateDTO();
        dto.setTitle("Test Book");
        dto.setAuthor("Test Author");
        dto.setIsbn("9780132350884");
        dto.setCopiesAvailable(1);
        dto.setPublicationYear(2020);
        return dto;
    }
}
```

---

## [IMPORTANT] 13.8 Tests d'Intégration Complets avec `@SpringBootTest`

```java
// Test qui lance le CONTEXTE COMPLET Spring (lent mais très complet)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@AutoConfigureMockMvc
@Transactional   // Rollback après chaque test (BDD propre)
class BookIntegrationTest {
    
    @Autowired private MockMvc mockMvc;
    @Autowired private ObjectMapper objectMapper;
    @Autowired private BookRepository bookRepository;
    @Autowired private MemberRepository memberRepository;
    @Autowired private PasswordEncoder passwordEncoder;
    @Autowired private JwtService jwtService;
    
    private String adminToken;
    private String memberToken;
    
    @BeforeEach
    void setUp() {
        // Créer un admin et récupérer son token
        Member admin = Member.builder()
            .email("admin@test.com")
            .password(passwordEncoder.encode("Admin@123"))
            .firstName("Admin").lastName("Test")
            .role(Role.ADMIN).build();
        admin = memberRepository.save(admin);
        adminToken = jwtService.generateToken(admin);
        
        // Créer un membre normal
        Member member = Member.builder()
            .email("member@test.com")
            .password(passwordEncoder.encode("Member@123"))
            .firstName("Member").lastName("Test")
            .role(Role.MEMBER).build();
        member = memberRepository.save(member);
        memberToken = jwtService.generateToken(member);
    }
    
    @Test
    void fullCrud_Book_Integration() throws Exception {
        // 1. Créer un livre (en tant qu'admin)
        BookCreateDTO createDTO = buildValidCreateDTO();
        
        String response = mockMvc.perform(post("/api/books")
                .header("Authorization", "Bearer " + adminToken)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(createDTO)))
            .andExpect(status().isCreated())
            .andReturn().getResponse().getContentAsString();
        
        BookResponseDTO created = objectMapper.readValue(response, BookResponseDTO.class);
        assertThat(created.getId()).isNotNull();
        
        // 2. Récupérer le livre créé
        mockMvc.perform(get("/api/books/" + created.getId()))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.title").value("Clean Code"));
        
        // 3. Vérifier qu'il est en BDD
        assertThat(bookRepository.findByIsbn("9780132350884")).isPresent();
        
        // 4. Supprimer (en tant qu'admin)
        mockMvc.perform(delete("/api/books/" + created.getId())
                .header("Authorization", "Bearer " + adminToken))
            .andExpect(status().isNoContent());
        
        // 5. Vérifier suppression
        assertThat(bookRepository.findById(created.getId())).isEmpty();
    }
}
```

---

## [IMPORTANT] 13.9 Couverture de Code

Configurez JaCoCo pour mesurer la couverture :

```xml
<!-- Dans pom.xml -->
<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
        <execution>
            <id>check</id>
            <goals>
                <goal>check</goal>
            </goals>
            <configuration>
                <rules>
                    <rule>
                        <limits>
                            <!-- Minimum 80% de couverture -->
                            <limit>
                                <counter>LINE</counter>
                                <value>COVEREDRATIO</value>
                                <minimum>0.80</minimum>
                            </limit>
                        </limits>
                    </rule>
                </rules>
            </configuration>
        </execution>
    </executions>
</plugin>
```

**Lancer les tests avec rapport :**
```bash
mvn test jacoco:report
# Rapport HTML dans : target/site/jacoco/index.html
```

---

### [OK] Exercices du Chapitre 13

1. **Écrivez** des tests unitaires pour `LoanService.returnBook()` (cas normal + cas d'erreur)
2. **Écrivez** des tests d'intégration MockMvc pour `AuthController` (register + login)
3. **Lancez** tous les tests et vérifiez qu'ils passent au vert
4. **Générez** le rapport JaCoCo et visez minimum 80% de couverture

---

*-> Prochain fichier : `09_fonctionnalites_avancees.md`*


# [LIVRE] Chapitre 14 & 15 — Fonctionnalités Avancées

---

## [IMPORTANT] Chapitre 14 — Email, Scheduling et Tâches Planifiées

### 14.1 Envoi d'Emails avec Spring Mail

**Ajoutez dans `pom.xml` :**
```xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

<!-- Pour les templates HTML d'email -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
```

**Configuration Gmail dans `application-prod.properties` :**
```properties
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=${GMAIL_USERNAME}
spring.mail.password=${GMAIL_APP_PASSWORD}   # Mot de passe d'application Gmail
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.connectiontimeout=5000
spring.mail.properties.mail.smtp.timeout=3000
```

**Service d'email :**

```java
// src/main/java/com/libraryhub/service/EmailService.java
package com.libraryhub.service;

import com.libraryhub.entity.Loan;
import com.libraryhub.entity.Member;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

@Service
@RequiredArgsConstructor
@Slf4j
public class EmailService {
    
    private final JavaMailSender mailSender;
    private final TemplateEngine templateEngine;  // Pour les templates HTML
    
    @Value("${app.email.from:noreply@libraryhub.com}")
    private String fromEmail;
    
    // ─── Email simple (texte brut) ────────────────────────────────
    
    public void sendSimpleEmail(String to, String subject, String body) {
        try {
            SimpleMailMessage message = new SimpleMailMessage();
            message.setFrom(fromEmail);
            message.setTo(to);
            message.setSubject(subject);
            message.setText(body);
            mailSender.send(message);
            log.info("Email envoyé à : {}", to);
        } catch (Exception e) {
            log.error("Échec envoi email à {} : {}", to, e.getMessage());
        }
    }
    
    // ─── Email HTML avec template Thymeleaf ──────────────────────
    
    @Async  // Envoi asynchrone : ne bloque pas le thread principal !
    public void sendLoanConfirmationEmail(Loan loan) {
        try {
            MimeMessage message = mailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
            
            helper.setFrom(fromEmail);
            helper.setTo(loan.getMember().getEmail());
            helper.setSubject("Confirmation d'emprunt — LibraryHub");
            
            // Préparer les variables du template
            Context context = new Context();
            context.setVariable("memberName", loan.getMember().getFirstName());
            context.setVariable("bookTitle", loan.getBook().getTitle());
            context.setVariable("loanDate", loan.getLoanDate());
            context.setVariable("dueDate", loan.getDueDate());
            
            // Générer le HTML depuis le template
            String htmlContent = templateEngine.process("email/loan-confirmation", context);
            helper.setText(htmlContent, true);   // true = HTML
            
            mailSender.send(message);
            log.info("Email de confirmation envoyé à : {}", loan.getMember().getEmail());
            
        } catch (MessagingException e) {
            log.error("Échec envoi email de confirmation : {}", e.getMessage());
        }
    }
    
    @Async
    public void sendOverdueReminderEmail(Loan loan) {
        try {
            MimeMessage message = mailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
            
            helper.setFrom(fromEmail);
            helper.setTo(loan.getMember().getEmail());
            helper.setSubject("[ATTENTION] Retour en retard — LibraryHub");
            
            Context context = new Context();
            context.setVariable("memberName", loan.getMember().getFirstName());
            context.setVariable("bookTitle", loan.getBook().getTitle());
            context.setVariable("dueDate", loan.getDueDate());
            context.setVariable("daysOverdue", 
                java.time.temporal.ChronoUnit.DAYS.between(loan.getDueDate(), java.time.LocalDate.now()));
            
            String htmlContent = templateEngine.process("email/overdue-reminder", context);
            helper.setText(htmlContent, true);
            
            mailSender.send(message);
            log.info("Rappel de retard envoyé à : {}", loan.getMember().getEmail());
            
        } catch (MessagingException e) {
            log.error("Échec envoi rappel : {}", e.getMessage());
        }
    }
}
```

**Template HTML Thymeleaf (email/loan-confirmation.html) :**
```html
<!-- src/main/resources/templates/email/loan-confirmation.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <style>
        body { font-family: Arial, sans-serif; background-color: #f4f4f4; }
        .container { max-width: 600px; margin: 0 auto; background: white; padding: 30px; }
        .header { background-color: #2c3e50; color: white; padding: 20px; text-align: center; }
        .book-info { background-color: #ecf0f1; padding: 15px; border-radius: 5px; margin: 20px 0; }
        .footer { text-align: center; color: #7f8c8d; font-size: 12px; margin-top: 30px; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>[DOCS] LibraryHub</h1>
            <p>Confirmation d'emprunt</p>
        </div>
        
        <p>Bonjour <strong th:text="${memberName}">Prénom</strong>,</p>
        <p>Votre emprunt a bien été enregistré. Voici les détails :</p>
        
        <div class="book-info">
            <p><strong>Livre :</strong> <span th:text="${bookTitle}">Titre du livre</span></p>
            <p><strong>Date d'emprunt :</strong> <span th:text="${loanDate}">01/01/2024</span></p>
            <p><strong>Date de retour prévue :</strong> <span th:text="${dueDate}">15/01/2024</span></p>
        </div>
        
        <p>Merci de retourner le livre avant la date prévue.</p>
        <p>Bonne lecture ! [GUIDE]</p>
        
        <div class="footer">
            <p>LibraryHub — Votre bibliothèque numérique</p>
        </div>
    </div>
</body>
</html>
```

---

### 14.2 `@Async` — Exécution Asynchrone

L'annotation `@Async` exécute une méthode dans un thread séparé. Essentiel pour les opérations longues (email, notifications...) qui ne doivent pas bloquer la réponse HTTP.

**Activez l'asynchrone :**
```java
@SpringBootApplication
@EnableAsync   // <- Activer @Async
public class LibraryhubApplication { ... }
```

**Configurer le pool de threads :**
```java
@Configuration
@EnableAsync
public class AsyncConfig {
    
    @Bean(name = "emailTaskExecutor")
    public Executor emailTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);           // Min 2 threads
        executor.setMaxPoolSize(10);           // Max 10 threads
        executor.setQueueCapacity(100);        // File d'attente de 100 tâches
        executor.setThreadNamePrefix("Email-"); // Nom des threads
        executor.initialize();
        return executor;
    }
}

// Utilisation dans EmailService
@Async("emailTaskExecutor")   // Utilise ce pool spécifique
public void sendLoanConfirmationEmail(Loan loan) { ... }
```

---

### 14.3 `@Scheduled` — Tâches Planifiées

**Activez le scheduling :**
```java
@SpringBootApplication
@EnableScheduling   // <- Activer @Scheduled
public class LibraryhubApplication { ... }
```

**Service de tâches planifiées :**
```java
// src/main/java/com/libraryhub/service/ScheduledTaskService.java
@Service
@RequiredArgsConstructor
@Slf4j
public class ScheduledTaskService {
    
    private final LoanRepository loanRepository;
    private final EmailService emailService;
    private final BookRepository bookRepository;
    
    // ─── Vérification quotidienne des emprunts en retard ─────────
    
    // Exécuté tous les jours à 9h00
    @Scheduled(cron = "0 0 9 * * *")
    // Format cron : secondes minutes heures jour-du-mois mois jour-de-la-semaine
    public void checkOverdueLoans() {
        log.info("[CLOCK_FACE_NINE_OCLOCK] Vérification des emprunts en retard...");
        
        List<Loan> overdueLoans = loanRepository.findByStatusAndDueDateBefore(
            LoanStatus.ACTIVE, 
            LocalDate.now()
        );
        
        overdueLoans.forEach(loan -> {
            // Envoyer un rappel
            emailService.sendOverdueReminderEmail(loan);
            // Marquer comme en retard
            loan.setStatus(LoanStatus.OVERDUE);
            loanRepository.save(loan);
        });
        
        log.info("[OK] {} emprunt(s) en retard traité(s)", overdueLoans.size());
    }
    
    // ─── Rappel J-2 avant la date de retour ──────────────────────
    
    // Tous les jours à 10h
    @Scheduled(cron = "0 0 10 * * *")
    public void sendReturnReminders() {
        LocalDate reminderDate = LocalDate.now().plusDays(2);
        
        List<Loan> soonDueLoans = loanRepository.findByStatusAndDueDateBefore(
            LoanStatus.ACTIVE,
            reminderDate.plusDays(1)   // Emprunts dus dans les 2 jours
        );
        
        soonDueLoans.stream()
            .filter(l -> l.getDueDate().equals(reminderDate))
            .forEach(loan -> {
                // Envoyer rappel "dans 2 jours"
                emailService.sendSimpleEmail(
                    loan.getMember().getEmail(),
                    "Rappel : Retour de livre dans 2 jours",
                    "N'oubliez pas de retourner '" + loan.getBook().getTitle() + "' avant le " + loan.getDueDate()
                );
            });
    }
    
    // ─── Génération du rapport hebdomadaire ───────────────────────
    
    // Tous les lundis à 8h
    @Scheduled(cron = "0 0 8 * * MON")
    public void generateWeeklyReport() {
        log.info("[GRAPHIQUE] Génération du rapport hebdomadaire...");
        
        long totalBooks = bookRepository.count();
        long availableBooks = bookRepository.countByCopiesAvailableGreaterThan(0);
        long activeLoans = loanRepository.countByStatus(LoanStatus.ACTIVE);
        long overdueLoans = loanRepository.countByStatus(LoanStatus.OVERDUE);
        
        log.info("[HAUSSE] Rapport hebdomadaire :");
        log.info("   Total livres : {}", totalBooks);
        log.info("   Livres disponibles : {}", availableBooks);
        log.info("   Emprunts actifs : {}", activeLoans);
        log.info("   Emprunts en retard : {}", overdueLoans);
        
        // Envoyer le rapport par email à l'admin...
    }
    
    // ─── Nettoyage des tokens expirés (si vous en avez) ──────────
    
    // Toutes les heures
    @Scheduled(fixedRate = 3600000)  // fixedRate : toutes les X millisecondes
    public void cleanupExpiredData() {
        log.debug("[NETTOYAGE] Nettoyage des données expirées...");
    }
    
    // 5 minutes après le démarrage, puis toutes les 30 minutes
    @Scheduled(initialDelay = 300000, fixedDelay = 1800000)
    public void periodicHealthCheck() {
        log.info("[BEATING_HEART] Vérification de santé périodique");
    }
}
```

**Expressions Cron — Aide-mémoire :**
```
Seconde  Minute  Heure  Jour/Mois  Mois  Jour/Semaine
   0       0      9        *        *        *        = Tous les jours à 9h00
   0       30     8        *        *        MON      = Tous les lundis à 8h30
   0       0      0        1        *        *        = Le 1er de chaque mois à minuit
   0       */15   *        *        *        *        = Toutes les 15 minutes
   0       0      9-17     *        *        MON-FRI  = Toutes les heures 9h-17h en semaine
```

---

## [IMPORTANT] Chapitre 15 — Pagination, Cache et Optimisations

### 15.1 Pagination et Tri avec Spring Data

Sans pagination, `GET /api/books` pourrait retourner 100 000 livres -> crash garanti.

```java
// Interface Repository avec Pageable
public interface BookRepository extends JpaRepository<Book, Long> {
    
    // Spring Data Page = résultats + métadonnées (total, pages...)
    Page<Book> findByTitleContainingIgnoreCase(String title, Pageable pageable);
    Page<Book> findByCopiesAvailableGreaterThan(int copies, Pageable pageable);
}
```

```java
// Service
@Transactional(readOnly = true)
public Page<Book> findAllBooks(int page, int size, String sortBy, String sortDir) {
    Sort sort = sortDir.equalsIgnoreCase("desc") 
        ? Sort.by(sortBy).descending() 
        : Sort.by(sortBy).ascending();
    
    Pageable pageable = PageRequest.of(page, size, sort);
    return bookRepository.findAll(pageable);
}

public Page<Book> searchBooks(String title, Pageable pageable) {
    return bookRepository.findByTitleContainingIgnoreCase(title, pageable);
}
```

```java
// Controller
@GetMapping
public ResponseEntity<Page<BookResponseDTO>> getAllBooks(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size,
        @RequestParam(defaultValue = "title") String sortBy,
        @RequestParam(defaultValue = "asc") String sortDir,
        @RequestParam(required = false) String title) {
    
    // Validation simple
    if (size > 100) size = 100;  // Limite max
    
    Pageable pageable = PageRequest.of(page, size, 
        Sort.by(sortDir.equals("desc") ? Sort.Order.desc(sortBy) : Sort.Order.asc(sortBy)));
    
    Page<Book> bookPage = (title != null) 
        ? bookService.searchBooks(title, pageable)
        : bookService.findAllBooks(page, size, sortBy, sortDir);
    
    // Convertir Page<Book> en Page<BookResponseDTO>
    Page<BookResponseDTO> responsePage = bookPage.map(bookMapper::toResponseDTO);
    
    return ResponseEntity.ok(responsePage);
}
```

**Réponse JSON avec pagination :**
```json
GET /api/books?page=0&size=5&sortBy=title&sortDir=asc

{
  "content": [
    { "id": 1, "title": "Clean Architecture", ... },
    { "id": 2, "title": "Clean Code", ... },
    { "id": 3, "title": "Design Patterns", ... },
    { "id": 4, "title": "Effective Java", ... },
    { "id": 5, "title": "Head First Java", ... }
  ],
  "pageable": {
    "sort": { "sorted": true, "direction": "ASC", "property": "title" },
    "pageNumber": 0,
    "pageSize": 5
  },
  "totalElements": 42,
  "totalPages": 9,
  "last": false,
  "first": true,
  "numberOfElements": 5,
  "empty": false
}
```

---

### 15.2 Cache avec Spring Cache

Le cache évite de recalculer les mêmes données répétées. Idéal pour les données peu changeantes.

**Activer le cache :**
```java
@SpringBootApplication
@EnableCaching   // <- Activer le cache
public class LibraryhubApplication { ... }
```

**Configurer le cache (Caffeine — recommandé) :**
```xml
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>
```

```properties
# application.properties
spring.cache.type=caffeine
spring.cache.caffeine.spec=maximumSize=500,expireAfterWrite=10m
```

**Utilisation dans le service :**
```java
@Service
@RequiredArgsConstructor
@CacheConfig(cacheNames = "books")  // Nom du cache pour cette classe
public class BookService {
    
    private final BookRepository bookRepository;
    
    // ─── @Cacheable : Met en cache le résultat ────────────────────
    
    @Cacheable(key = "#id")   // Clé du cache = id
    @Transactional(readOnly = true)
    public Book findBookById(Long id) {
        log.info("Chargement du livre depuis la BDD (id={})", id);
        // Ce log n'apparaît QUE si le résultat n'est pas en cache
        return bookRepository.findById(id)
            .orElseThrow(() -> new BookNotFoundException("Livre non trouvé : " + id));
    }
    
    @Cacheable(key = "'all'")
    @Transactional(readOnly = true)
    public List<Book> findAllBooks() {
        return bookRepository.findAll();
    }
    
    // ─── @CachePut : Met à jour le cache après modification ───────
    
    @CachePut(key = "#result.id")  // Met à jour le cache avec le livre retourné
    @Transactional
    public Book updateBook(Long id, Book bookDetails) {
        Book existing = findBookById(id);
        existing.setTitle(bookDetails.getTitle());
        // ... autres champs
        return bookRepository.save(existing);
    }
    
    // ─── @CacheEvict : Vide le cache ─────────────────────────────
    
    @CacheEvict(key = "#id")   // Supprime l'entrée du cache pour cet id
    @Transactional
    public void deleteBook(Long id) {
        Book book = findBookById(id);
        bookRepository.delete(book);
    }
    
    @CacheEvict(allEntries = true)   // Vide TOUT le cache "books"
    @Transactional
    public Book createBook(Book book) {
        // Après création, le cache "all" devient invalide
        return bookRepository.save(book);
    }
}
```

**Quand utiliser le cache :**
- [OK] Données lues très fréquemment et modifiées rarement (livres, catégories)
- [OK] Calculs coûteux (rapports, statistiques)
- [X] Données qui changent souvent (stock en temps réel, prix)
- [X] Données personnalisées par utilisateur (sans clé spécifique)

---

### 15.3 Optimisation des Requêtes JPA

#### Le problème N+1

```java
// [ATTENTION] Problème N+1 : Pour 100 livres, fait 101 requêtes SQL !
// 1 requête pour les livres + 1 par livre pour ses emprunts
List<Book> books = bookRepository.findAll();
books.forEach(book -> {
    int loanCount = book.getLoans().size();   // <- Déclenche une requête par livre !
});
```

**Solution : Fetch Join dans JPQL**
```java
@Query("SELECT DISTINCT b FROM Book b LEFT JOIN FETCH b.loans WHERE b.id IN :ids")
List<Book> findBooksWithLoans(@Param("ids") List<Long> ids);

// Ou avec EntityGraph
@EntityGraph(attributePaths = {"loans", "categories"})
@Query("SELECT b FROM Book b WHERE b.copiesAvailable > 0")
List<Book> findAvailableBooksWithDetails();
```

#### Projections — Sélectionner seulement les colonnes nécessaires

```java
// Interface de projection
public interface BookSummary {
    Long getId();
    String getTitle();
    String getAuthor();
    boolean isAvailable();
    
    // Valeur calculée dans la projection
    default boolean isAvailable() {
        return getCopiesAvailable() > 0;
    }
    
    int getCopiesAvailable();
}

// Repository
public interface BookRepository extends JpaRepository<Book, Long> {
    // Spring génère automatiquement la projection !
    List<BookSummary> findAllProjectedBy();
    // SQL : SELECT id, title, author, copies_available FROM books
    // (pas les autres colonnes !)
}
```

---

### 15.4 Documentation de l'API avec Swagger/OpenAPI

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

```java
// Configuration OpenAPI
@Configuration
public class OpenApiConfig {
    
    @Bean
    public OpenAPI libraryHubOpenAPI() {
        return new OpenAPI()
            .info(new Info()
                .title("LibraryHub API")
                .version("1.0.0")
                .description("API de gestion de bibliothèque")
                .contact(new Contact()
                    .name("LibraryHub Team")
                    .email("contact@libraryhub.com")))
            .addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
            .components(new Components()
                .addSecuritySchemes("Bearer Authentication", new SecurityScheme()
                    .type(SecurityScheme.Type.HTTP)
                    .scheme("bearer")
                    .bearerFormat("JWT")));
    }
}

// Annoter vos controllers pour une meilleure documentation
@RestController
@RequestMapping("/api/books")
@Tag(name = "Books", description = "Gestion des livres")
public class BookController {
    
    @GetMapping("/{id}")
    @Operation(summary = "Récupérer un livre par son ID")
    @ApiResponses({
        @ApiResponse(responseCode = "200", description = "Livre trouvé"),
        @ApiResponse(responseCode = "404", description = "Livre non trouvé")
    })
    public ResponseEntity<BookResponseDTO> getBookById(
            @Parameter(description = "ID du livre") @PathVariable Long id) {
        return ResponseEntity.ok(bookMapper.toResponseDTO(bookService.findBookById(id)));
    }
}
```

**Accédez à la documentation :** `http://localhost:8080/api/swagger-ui.html`

---

### [OK] Exercices du Chapitre 14 & 15

1. **Configurez** l'envoi d'email et testez avec un compte Mailtrap (sandbox gratuit)
2. **Créez** un scheduler qui envoie chaque matin la liste des emprunts du jour à l'admin
3. **Ajoutez** la pagination sur `GET /api/books` et testez avec Postman
4. **Activez** le cache sur `findBookById` et vérifiez avec les logs que le cache fonctionne
5. **Accédez** à la doc Swagger et testez vos endpoints directement depuis l'interface

---

*-> Prochain fichier : `10_deploiement_et_production.md`*


# [LIVRE] Chapitre 16 & 17 — Déploiement, Docker et Production

---

## [IMPORTANT] Chapitre 16 — Conteneurisation avec Docker

### 16.1 Pourquoi Docker ?

Sans Docker, vous avez le fameux problème : **"Ça marche sur ma machine !"**

Docker résout ce problème en encapsulant votre application ET son environnement dans une **image** portable. L'image contient : votre code, la JVM, les dépendances, la configuration — tout ce dont l'application a besoin pour tourner.

```
Sans Docker :                    Avec Docker :
Dev machine -> App               Image Docker -> Container (n'importe où)
"JDK 11 sur ma machine..."       "JDK 17, Spring Boot 3.2, même partout"
"Hmm ça marche pas en prod"      "Si ça tourne en dev, ça tourne en prod"
```

**Concepts clés :**
- **Dockerfile** : Recette pour construire l'image
- **Image** : Snapshot de votre application (comme un template)
- **Container** : Instance en cours d'exécution d'une image

---

### 16.2 Dockerfile — Optimisé pour Spring Boot

```dockerfile
# ─── ÉTAPE 1 : BUILD (avec Maven) ───────────────────────────────
# Multi-stage build : l'image finale ne contient PAS Maven (plus légère)
FROM eclipse-temurin:17-jdk-alpine AS builder

WORKDIR /app

# Copier d'abord le pom.xml séparément (optimisation du cache Docker)
# Si le code change mais pas pom.xml, Maven ne re-télécharge pas les dépendances
COPY pom.xml .
COPY .mvn .mvn
COPY mvnw .
RUN chmod +x mvnw && ./mvnw dependency:go-offline -B

# Maintenant copier le code source
COPY src src

# Compiler et créer le JAR (sans les tests pour la rapidité du build)
RUN ./mvnw package -DskipTests -B

# ─── ÉTAPE 2 : Extraire les couches (Spring Boot Layered JAR) ───
RUN java -Djarmode=layertools -jar target/libraryhub-*.jar extract

# ─── ÉTAPE 3 : IMAGE FINALE (légère — sans Maven, sans code source) ─
FROM eclipse-temurin:17-jre-alpine

# Utilisateur non-root pour la sécurité
RUN addgroup --system libraryhub && adduser --system --group libraryhub
USER libraryhub

WORKDIR /app

# Copier les couches dans l'ordre optimal (les moins changeantes en premier)
# Cela optimise le cache Docker lors des rebuilds
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./

# Port exposé
EXPOSE 8080

# Variables d'environnement par défaut
ENV SPRING_PROFILES_ACTIVE=prod
ENV JAVA_OPTS="-Xms256m -Xmx512m"

# Point d'entrée
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher"]
```

**Commandes Docker essentielles :**
```bash
# Construire l'image
docker build -t libraryhub:latest .
docker build -t libraryhub:1.0.0 .

# Lancer un container
docker run -p 8080:8080 \
  -e SPRING_PROFILES_ACTIVE=prod \
  -e DATABASE_URL=jdbc:postgresql://host.docker.internal:5432/librarydb \
  -e JWT_SECRET=mysecret \
  libraryhub:latest

# Voir les containers en cours
docker ps

# Logs d'un container
docker logs <container-id> -f

# Arrêter et supprimer
docker stop <container-id>
docker rm <container-id>
```

---

### 16.3 Docker Compose — Environnement Complet

```yaml
# docker-compose.yml — Développement local complet
version: '3.8'

services:
  
  # ─── Application Spring Boot ──────────────────────────────────
  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: libraryhub-app
    ports:
      - "8080:8080"
    environment:
      SPRING_PROFILES_ACTIVE: dev
      DATABASE_URL: jdbc:postgresql://postgres:5432/librarydb
      DATABASE_USERNAME: libraryuser
      DATABASE_PASSWORD: devpassword
      JWT_SECRET: devSecretKeyForLocalDevelopmentOnlyChangeInProduction
      SPRING_MAIL_HOST: mailhog
      SPRING_MAIL_PORT: 1025
    depends_on:
      postgres:
        condition: service_healthy
      mailhog:
        condition: service_started
    volumes:
      - app-logs:/var/log/libraryhub
    restart: unless-stopped
    
    # Health check de l'application
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/api/actuator/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s
  
  # ─── Base de données PostgreSQL ───────────────────────────────
  postgres:
    image: postgres:16-alpine
    container_name: libraryhub-db
    environment:
      POSTGRES_DB: librarydb
      POSTGRES_USER: libraryuser
      POSTGRES_PASSWORD: devpassword
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./docker/init.sql:/docker-entrypoint-initdb.d/init.sql  # Script d'init optionnel
    ports:
      - "5432:5432"   # Exposer pour DBeaver/pgAdmin en local
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U libraryuser -d librarydb"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped
  
  # ─── MailHog — Serveur SMTP local pour les emails ────────────
  mailhog:
    image: mailhog/mailhog:latest
    container_name: libraryhub-mail
    ports:
      - "1025:1025"   # SMTP
      - "8025:8025"   # Interface web pour voir les emails
    restart: unless-stopped
  
  # ─── Redis — Cache distribué (optionnel) ─────────────────────
  redis:
    image: redis:7-alpine
    container_name: libraryhub-cache
    ports:
      - "6379:6379"
    restart: unless-stopped
  
  # ─── PgAdmin — Interface web pour PostgreSQL ──────────────────
  pgadmin:
    image: dpage/pgadmin4:latest
    container_name: libraryhub-pgadmin
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@libraryhub.com
      PGADMIN_DEFAULT_PASSWORD: admin
    ports:
      - "5050:80"
    depends_on:
      - postgres
    restart: unless-stopped

volumes:
  postgres-data:
  app-logs:

networks:
  default:
    name: libraryhub-network
```

**Commandes Docker Compose :**
```bash
# Démarrer tout l'environnement
docker-compose up -d

# Voir les logs de tous les services
docker-compose logs -f

# Voir les logs d'un service spécifique
docker-compose logs -f app

# Arrêter et supprimer les containers (garder les volumes)
docker-compose down

# Arrêter ET supprimer les volumes (réinitialise la BDD)
docker-compose down -v

# Reconstruire l'image et relancer
docker-compose up -d --build
```

**Accès aux services après `docker-compose up` :**
- Application : `http://localhost:8080/api`
- PgAdmin : `http://localhost:5050`
- MailHog (emails) : `http://localhost:8025`
- Swagger : `http://localhost:8080/api/swagger-ui.html`

---

## [IMPORTANT] Chapitre 17 — Spring Boot Actuator et Production

### 17.1 Spring Boot Actuator

Actuator expose des endpoints de monitoring et de gestion de votre application.

**Ajoutez dans `pom.xml` :**
```xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- Micrometer + Prometheus (métriques) -->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
```

**Configuration Actuator :**
```properties
# Exposer les endpoints Actuator
management.endpoints.web.exposure.include=health,info,metrics,prometheus,env,loggers
management.endpoint.health.show-details=when-authorized  # Détails si authentifié
management.endpoint.health.probes.enabled=true            # Kubernetes probes
management.server.port=8081  # Port séparé pour Actuator (sécurité)

# Informations sur l'application
info.app.name=LibraryHub
info.app.version=@project.version@
info.app.description=Système de gestion de bibliothèque
```

**Endpoints Actuator disponibles :**

| Endpoint | URL | Description |
|---|---|---|
| Health | `/actuator/health` | État de santé de l'app |
| Info | `/actuator/info` | Infos de l'application |
| Metrics | `/actuator/metrics` | Métriques diverses |
| Prometheus | `/actuator/prometheus` | Format Prometheus |
| Env | `/actuator/env` | Variables d'environnement |
| Loggers | `/actuator/loggers` | Niveaux de log |
| Beans | `/actuator/beans` | Tous les Beans Spring |

**Health Check personnalisé :**
```java
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
    
    @Autowired
    private BookRepository bookRepository;
    
    @Override
    public Health health() {
        try {
            long count = bookRepository.count();
            return Health.up()
                .withDetail("bookCount", count)
                .withDetail("status", "Database accessible")
                .build();
        } catch (Exception e) {
            return Health.down()
                .withDetail("error", e.getMessage())
                .build();
        }
    }
}
```

**Réponse `/actuator/health` :**
```json
{
  "status": "UP",
  "components": {
    "database": {
      "status": "UP",
      "details": {
        "bookCount": 42,
        "status": "Database accessible"
      }
    },
    "diskSpace": {
      "status": "UP",
      "details": { "total": 250685575168, "free": 100000000000 }
    },
    "mail": { "status": "UP" }
  }
}
```

---

### 17.2 Métriques Personnalisées

```java
@Service
@RequiredArgsConstructor
public class BookService {
    
    private final MeterRegistry meterRegistry;
    private Counter bookCreatedCounter;
    private Counter loanCreatedCounter;
    
    @PostConstruct
    public void initMetrics() {
        bookCreatedCounter = Counter.builder("libraryhub.books.created")
            .description("Nombre de livres créés")
            .register(meterRegistry);
        
        loanCreatedCounter = Counter.builder("libraryhub.loans.created")
            .description("Nombre d'emprunts créés")
            .register(meterRegistry);
    }
    
    @Transactional
    public Book createBook(Book book) {
        Book saved = bookRepository.save(book);
        bookCreatedCounter.increment();   // <- Incrémenter le compteur
        return saved;
    }
}

// Timer pour mesurer la durée des opérations
@Timed(value = "libraryhub.book.search.duration", description = "Durée de recherche de livres")
public List<Book> searchByTitle(String title) {
    return bookRepository.findByTitleContainingIgnoreCase(title);
}
```

---

### 17.3 Flyway — Migrations de Base de Données

Flyway gère les changements de schéma de base de données de façon contrôlée.

```xml
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>
```

```properties
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
spring.flyway.baseline-on-migrate=true
spring.jpa.hibernate.ddl-auto=validate  # Flyway gère le schéma, pas Hibernate
```

**Structure des migrations :**
```
src/main/resources/db/migration/
├── V1__create_books_table.sql
├── V2__create_members_table.sql
├── V3__create_loans_table.sql
├── V4__add_categories.sql
└── V5__add_book_cover_url.sql
```

```sql
-- V1__create_books_table.sql
CREATE TABLE books (
    id BIGSERIAL PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    author VARCHAR(255) NOT NULL,
    isbn VARCHAR(13) UNIQUE NOT NULL,
    copies_available INT NOT NULL DEFAULT 0,
    publication_year INT,
    description TEXT,
    cover_image_url VARCHAR(500),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_books_title ON books(LOWER(title));
CREATE INDEX idx_books_author ON books(author);
```

```sql
-- V4__add_categories.sql
CREATE TABLE categories (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(100) UNIQUE NOT NULL,
    description TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE book_categories (
    book_id BIGINT NOT NULL,
    category_id BIGINT NOT NULL,
    PRIMARY KEY (book_id, category_id),
    FOREIGN KEY (book_id) REFERENCES books(id) ON DELETE CASCADE,
    FOREIGN KEY (category_id) REFERENCES categories(id)
);

-- Données initiales
INSERT INTO categories (name) VALUES 
    ('Programmation'), ('Architecture'), ('DevOps'), ('Design Patterns'), ('Management');
```

---

### 17.4 CI/CD avec GitHub Actions

Automatisez le build, les tests et le déploiement :

```yaml
# .github/workflows/ci-cd.yml
name: LibraryHub CI/CD

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}/libraryhub

jobs:
  
  # ─── Compilation et Tests ─────────────────────────────────────
  test:
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_DB: testdb
          POSTGRES_USER: testuser
          POSTGRES_PASSWORD: testpassword
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: 'maven'
      
      - name: Run Tests
        run: ./mvnw verify -B
        env:
          DATABASE_URL: jdbc:postgresql://localhost:5432/testdb
          DATABASE_USERNAME: testuser
          DATABASE_PASSWORD: testpassword
          SPRING_PROFILES_ACTIVE: test
      
      - name: Upload Test Report
        uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: test-results
          path: target/surefire-reports/
      
      - name: Upload Coverage Report
        uses: codecov/codecov-action@v3
        with:
          files: target/site/jacoco/jacoco.xml
  
  # ─── Build et Push Docker ─────────────────────────────────────
  build-and-push:
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main'   # Seulement sur main
    
    permissions:
      contents: read
      packages: write
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Extract metadata for Docker
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=sha,prefix=sha-
            type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
      
      - name: Build and push Docker 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
  
  # ─── Déploiement ──────────────────────────────────────────────
  deploy:
    runs-on: ubuntu-latest
    needs: build-and-push
    environment: production
    
    steps:
      - name: Deploy to server
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /opt/libraryhub
            docker-compose pull
            docker-compose up -d --no-deps app
            docker-compose exec app wget --quiet --tries=1 --spider \
              http://localhost:8080/api/actuator/health || exit 1
            echo "[OK] Déploiement réussi !"
```

---

### 17.5 Optimisations JVM pour la Production

```bash
# Démarrage optimisé pour la production
java \
  -server \
  -Xms512m \                        # Heap minimum 512MB
  -Xmx1024m \                       # Heap maximum 1GB
  -XX:+UseG1GC \                    # Garbage Collector G1 (recommandé)
  -XX:MaxGCPauseMillis=200 \        # Pause GC max 200ms
  -XX:+HeapDumpOnOutOfMemoryError \ # Dump si OutOfMemory
  -XX:HeapDumpPath=/var/log/libraryhub/heap-dump.hprof \
  -Dspring.profiles.active=prod \
  -jar libraryhub.jar
```

```dockerfile
# Dans le Dockerfile
ENV JAVA_OPTS="-server -Xms256m -Xmx512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher"]
```

---

### 17.6 Checklist de Production

Avant de déployer en production, vérifiez :

**Sécurité :**
- [ ] JWT secret fort (256 bits minimum) dans variable d'environnement
- [ ] Mots de passe hashés (BCrypt)
- [ ] HTTPS activé (Let's Encrypt ou certificat)
- [ ] Endpoints Actuator sécurisés ou sur port séparé
- [ ] `spring.jpa.show-sql=false`
- [ ] H2 console désactivée

**Base de données :**
- [ ] `ddl-auto=validate` (jamais `create-drop` en prod)
- [ ] Flyway pour les migrations
- [ ] Sauvegardes automatiques configurées
- [ ] Pool de connexions dimensionné (Hikari)

**Performance :**
- [ ] Cache configuré
- [ ] Pagination sur tous les endpoints liste
- [ ] Logs au niveau WARN/INFO (pas DEBUG)
- [ ] Logs vers fichier avec rotation

**Monitoring :**
- [ ] Actuator Health configuré
- [ ] Métriques Prometheus
- [ ] Alertes configurées (Grafana, Datadog...)
- [ ] Sentry ou équivalent pour les erreurs

**DevOps :**
- [ ] Docker Compose de production
- [ ] CI/CD pipeline fonctionnel
- [ ] Variables d'environnement dans CI/CD secrets
- [ ] Rollback plan

---

## [IMPORTANT] Bilan du Projet LibraryHub

[BRAVO] **Félicitations !** En construisant LibraryHub, vous avez maîtrisé :

| Compétence | Technologie |
|---|---|
| API REST complète | Spring MVC, @RestController |
| Persistance | JPA, Hibernate, Spring Data |
| Relations BDD | @OneToMany, @ManyToMany |
| Sécurité | Spring Security, JWT |
| Validation | Bean Validation, @Valid |
| Gestion d'erreurs | @ControllerAdvice, exceptions custom |
| Configuration | Profils, @ConfigurationProperties |
| Tests | JUnit 5, Mockito, MockMvc |
| Email | Spring Mail, Thymeleaf |
| Planification | @Scheduled, @Async |
| Pagination | PageRequest, Pageable |
| Cache | Spring Cache, Caffeine |
| Documentation | Swagger/OpenAPI |
| Monitoring | Spring Actuator, Micrometer |
| Conteneurisation | Docker, Docker Compose |
| CI/CD | GitHub Actions |
| Base de données | PostgreSQL, Flyway |

---

### [OK] Projet Final — Fonctionnalités à Implémenter

Pour valider complètement vos compétences, implémentez seul :

1. **Système de réservation** : Un membre peut réserver un livre non disponible et être notifié quand il revient
2. **Prolongation d'emprunt** : Un membre peut prolonger son emprunt une seule fois (+7 jours)
3. **Historique des emprunts** : Endpoint qui retourne l'historique paginé d'un membre
4. **Statistiques admin** : Dashboard avec le nombre d'emprunts par mois (dernier an)
5. **Export CSV** : Export de la liste des livres au format CSV
6. **Refresh token** : Implémenter un mécanisme de refresh JWT
7. **Rate limiting** : Limiter les tentatives de connexion (éviter les attaques brute-force)

---

*[DOCS] Bonne continuation dans votre aventure Spring Boot !*


# [RAPIDE] Projet Fil Rouge Spring Boot — Guide Complet pour Débutants

> **Projet : `LibraryHub`** — Une application de gestion de bibliothèque en ligne, construite pas à pas avec Spring Boot.

---

## [OBJECTIF] Objectif du Projet

Ce guide accompagne un étudiant **grand débutant** du zéro absolu jusqu'à une maîtrise solide de Spring Boot. Chaque chapitre s'appuie sur le projet concret **LibraryHub** pour ancrer la théorie dans la pratique.

**LibraryHub** est une API REST complète pour gérer :
- [DOCS] Des livres (CRUD complet)
- [UTILISATEUR] Des membres (inscription, connexion)
- [GUIDE] Des emprunts (avec dates, statuts)
- [SECURISE] La sécurité (JWT, rôles ADMIN / MEMBRE)
- [EMAIL] Des notifications (email automatique)

---

## [DOSSIER] Structure des Fichiers de ce Guide

| Fichier | Contenu | Chapitres |
|---|---|---|
| `01_introduction_et_setup.md` | Introduction, concepts clés, installation | Ch. 1–2 |
| `02_premiers_pas_spring_boot.md` | Structure projet, annotations de base, premier endpoint | Ch. 3–4 |
| `03_api_rest_et_couches.md` | Architecture en couches, Controller, Service, Repository | Ch. 5–6 |
| `04_base_de_donnees_jpa.md` | JPA, Hibernate, entités, relations, JPQL | Ch. 7–8 |
| `05_validation_et_exceptions.md` | Bean Validation, gestion globale des erreurs | Ch. 9 |
| `06_securite_spring_security.md` | Spring Security, JWT, rôles et permissions | Ch. 10–11 |
| `07_configuration_et_profils.md` | application.properties, profils, variables d'env | Ch. 12 |
| `08_tests.md` | Tests unitaires, d'intégration, MockMvc | Ch. 13 |
| `09_fonctionnalites_avancees.md` | Mail, scheduling, pagination, cache | Ch. 14–15 |
| `10_deploiement_et_production.md` | Docker, Actuator, monitoring, CI/CD | Ch. 16–17 |

---

## [CONSTRUCTION] Architecture Globale de LibraryHub

```
libraryhub/
├── src/
│   ├── main/
│   │   ├── java/com/libraryhub/
│   │   │   ├── config/          <- Configuration Spring
│   │   │   ├── controller/      <- Endpoints REST
│   │   │   ├── service/         <- Logique métier
│   │   │   ├── repository/      <- Accès base de données
│   │   │   ├── entity/          <- Entités JPA (tables)
│   │   │   ├── dto/             <- Objets de transfert
│   │   │   ├── exception/       <- Gestion des erreurs
│   │   │   └── security/        <- JWT, filtres
│   │   └── resources/
│   │       ├── application.properties
│   │       ├── application-dev.properties
│   │       └── application-prod.properties
│   └── test/
├── Dockerfile
├── docker-compose.yml
└── pom.xml
```

---

## [WORLD_MAP] Parcours d'Apprentissage

```
DÉBUTANT                    INTERMÉDIAIRE               AVANCÉ
    │                           │                           │
Ch.1-4                       Ch.5-9                     Ch.10-17
Setup &                   API REST &                 Sécurité,
Bases                     BDD & JPA                  Tests &
                                                      Déploiement
```

---

## [IDEE] Comment Utiliser ce Guide

1. **Lisez d'abord** la partie théorique de chaque section
2. **Codez** chaque exemple dans votre IDE
3. **Testez** avec Postman ou votre navigateur
4. **Complétez** les exercices proposés à la fin de chaque chapitre

> [ATTENTION] **Prérequis** : Notions de base en Java (classes, méthodes, interfaces). Pas besoin de connaître Spring au préalable.

---

## [OUTILS] Outils Nécessaires

- **JDK 17+** — [adoptium.net](https://adoptium.net)
- **Maven 3.8+** — Inclus dans IntelliJ
- **IntelliJ IDEA** (Community gratuite) — [jetbrains.com](https://jetbrains.com)
- **Postman** — Pour tester les API
- **Docker Desktop** — Pour le chapitre déploiement
- **Git** — Pour versionner votre code

---

*Bonne lecture et bon courage ! [COURS]*
