Aller au contenu

Feature 03 — Adhérent (Gestion des Exceptions)

Nous allons mettre en place la partie de gestion des Exceptions pour l’Adhérent (propriétaire). Elle pourra vous servir pour les autres entités et contrôleurs.

Reprenons le code de base de notre contrôleur : AdherentController

@RestController
@RequestMapping("/api/adherents")
public class AdherentController {

    private final AdherentService adherentService;

    public AdherentController(AdherentService adherentService) {
        this.adherentService = adherentService;
    }

    // Récupérer tous les adhérents
    @GetMapping
    public ResponseEntity<List<AdherentDto>> getAllAdherents() {
        List<AdherentDto> adherents = adherentService.getAllAdherents();
        return ResponseEntity.ok(adherents);
    }

    // Créer un adhérent
    @PostMapping
    public ResponseEntity<AdherentDto> createAdherent(@Valid @RequestBody AdherentCreateDto createDto) {
        AdherentDto savedAdherent = adherentService.createAdherent(createDto);
        return ResponseEntity.ok(savedAdherent);
    }

    // Récupérer un adhérent par ID
    @GetMapping("/{id}")
    public ResponseEntity<AdherentDto> getAdherent(@PathVariable Long id) {
        AdherentDto adherentDto = adherentService.getAdherentById(id);
        return ResponseEntity.ok(adherentDto);
    }

    // Mettre à jour un adhérent
    @PutMapping("/{id}")
    public ResponseEntity<AdherentDto> updateAdherent(
        @PathVariable Long id,
        @Valid @RequestBody AdherentUpdateDto updateDto) {
        AdherentDto updatedAdherent = adherentService.updateAdherent(id, updateDto);
        return ResponseEntity.ok(updatedAdherent);
    }
}

1) Exceptions Personnalisées

AdherentNotFoundException (Adhérent non trouvé)

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND) // Renvoie automatiquement un statut 404
public class AdherentNotFoundException extends RuntimeException {
    public AdherentNotFoundException(Long id) {
        super("Adhérent non trouvé avec l'ID : " + id);
    }

    public AdherentNotFoundException(String email) {
        super("Adhérent non trouvé avec l'email : " + email);
    }
}

AdherentAlreadyExistsException (Adhérent déjà existant)

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.CONFLICT) // Renvoie automatiquement un statut 409
public class AdherentAlreadyExistsException extends RuntimeException {
    public AdherentAlreadyExistsException(String email) {
        super("Un adhérent avec l'email " + email + " existe déjà.");
    }
}

AdherentInvalidException (Données d’adhérent invalides)

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.BAD_REQUEST) // Renvoie automatiquement un statut 400
public class AdherentInvalidException extends RuntimeException {
    public AdherentInvalidException(String message) {
        super(message);
    }
}

2) Classe ErrorResponse pour Structurer les Réponses d’Erreur

Cette classe permet de standardiser le format des réponses d’erreur renvoyées par l’API.

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

public record ErrorResponse(
    int status,
    String error,
    String message,
    Object details,  // Peut être une liste d'erreurs ou null
    LocalDateTime timestamp
) {
    // Constructeur pour les erreurs simples (sans détails)
    public ErrorResponse(int status, String error, String message, LocalDateTime timestamp) {
        this(status, error, message, null, timestamp);
    }

    // Constructeur pour les erreurs avec détails (ex : liste de validations)
    public ErrorResponse(int status, String error, String message, Object details, LocalDateTime timestamp) {
        this.status = status;
        this.error = error;
        this.message = message;
        this.details = details;
        this.timestamp = timestamp;
    }
}

3) Gestionnaire Global d’Exceptions (@RestControllerAdvice) déjà rédigé

Ce gestionnaire centralise la gestion des exceptions pour toute l’application.

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

@RestControllerAdvice // Intercepte les exceptions dans tous les contrôleurs
public class GlobalExceptionHandler {

    // Gestion des erreurs de validation (ex : @Valid échoue)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationExceptions(MethodArgumentNotValidException ex, WebRequest request) {
        List<String> errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(error -> error.getField() + ": " + error.getDefaultMessage())
            .collect(Collectors.toList());

        ErrorResponse errorResponse = new ErrorResponse(
            HttpStatus.BAD_REQUEST.value(),
            "Validation échouée",
            "Erreurs de validation dans la requête",
            errors,
            LocalDateTime.now()
        );

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

    // Gestion des adhérents non trouvés
    @ExceptionHandler(AdherentNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleAdherentNotFound(AdherentNotFoundException ex, WebRequest request) {
        ErrorResponse errorResponse = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            "Ressource non trouvée",
            ex.getMessage(),
            LocalDateTime.now()
        );
        return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
    }

    // Gestion des adhérents déjà existants
    @ExceptionHandler(AdherentAlreadyExistsException.class)
    public ResponseEntity<ErrorResponse> handleAdherentAlreadyExists(AdherentAlreadyExistsException ex, WebRequest request) {
        ErrorResponse errorResponse = new ErrorResponse(
            HttpStatus.CONFLICT.value(),
            "Conflit",
            ex.getMessage(),
            LocalDateTime.now()
        );
        return new ResponseEntity<>(errorResponse, HttpStatus.CONFLICT);
    }

    // Gestion des données d'adhérent invalides
    @ExceptionHandler(AdherentInvalidException.class)
    public ResponseEntity<ErrorResponse> handleAdherentInvalid(AdherentInvalidException ex, WebRequest request) {
        ErrorResponse errorResponse = new ErrorResponse(
            HttpStatus.BAD_REQUEST.value(),
            "Données invalides",
            ex.getMessage(),
            LocalDateTime.now()
        );
        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }

    // Gestion des exceptions génériques (pour éviter les erreurs 500 non gérées)
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleAllExceptions(Exception ex, WebRequest request) {
        ErrorResponse errorResponse = new ErrorResponse(
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            "Erreur interne",
            ex.getMessage(),
            LocalDateTime.now()
        );
        return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

4) Exemple d’Utilisation dans un Service (AdherentService)

Voici comment utiliser ces exceptions dans le service pour les adhérents.

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;

@Service
@Transactional
public class AdherentService {

    private final AdherentRepository adherentRepository;

    public AdherentService(AdherentRepository adherentRepository) {
        this.adherentRepository = adherentRepository;
    }

    // Créer un adhérent
    public AdherentDto createAdherent(AdherentCreateDto createDto) {
        // Vérifie si un adhérent avec le même email existe déjà
        if (adherentRepository.existsByEmail(createDto.email())) {
            throw new AdherentAlreadyExistsException(createDto.email());
        }

        Adherent adherent = new Adherent();
        adherent.setNom(createDto.nom());
        adherent.setAdresse(createDto.adresse());
        adherent.setTelephone(createDto.telephone());
        adherent.setEmail(createDto.email());

        Adherent savedAdherent = adherentRepository.save(adherent);
        return mapToDto(savedAdherent);
    }

    // Récupérer un adhérent par ID
    public AdherentDto getAdherentById(Long id) {
        Adherent adherent = adherentRepository.findById(id).orElseThrow(() -> new AdherentNotFoundException(id));
        return mapToDto(adherent);
    }

    // Récupérer un adhérent par email
    public AdherentDto getAdherentByEmail(String email) {
        Adherent adherent = adherentRepository.findByEmail(email).orElseThrow(() -> new AdherentNotFoundException(email));
        return mapToDto(adherent);
    }

    // Mettre à jour un adhérent
    public AdherentDto updateAdherent(Long id, AdherentUpdateDto updateDto) {
        Adherent adherent = adherentRepository.findById(id).orElseThrow(() -> new AdherentNotFoundException(id));

        // Vérifie si un autre adhérent utilise déjà le nouvel email
        if (updateDto.email() != null && !updateDto.email().equals(adherent.getEmail()) &&
            adherentRepository.existsByEmail(updateDto.email())) {
            throw new AdherentAlreadyExistsException(updateDto.email());
        }

        // Met à jour les champs fournis
        if (updateDto.nom() != null) {
            adherent.setNom(updateDto.nom());
        }
        if (updateDto.adresse() != null) {
            adherent.setAdresse(updateDto.adresse());
        }
        if (updateDto.telephone() != null) {
            adherent.setTelephone(updateDto.telephone());
        }
        if (updateDto.email() != null) {
            adherent.setEmail(updateDto.email());
        }

        Adherent updatedAdherent = adherentRepository.save(adherent);
        return mapToDto(updatedAdherent);
    }

    // Supprimer un adhérent
    public void deleteAdherent(Long id) {
        if (!adherentRepository.existsById(id)) {
            throw new AdherentNotFoundException(id);
        }
        adherentRepository.deleteById(id);
    }

    // Méthode utilitaire pour convertir Adherent en AdherentDto
    private AdherentDto mapToDto(Adherent adherent) {
        return new AdherentDto(
            adherent.getId(),
            adherent.getNom(),
            adherent.getAdresse(),
            adherent.getTelephone(),
            adherent.getEmail()
        );
    }
}

5) Exemple d’Utilisation dans un Contrôleur (AdherentController)

Voici comment le contrôleur peut utiliser ces exceptions et le service.

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;

@RestController
@RequestMapping("/api/adherents")
public class AdherentController {

    private final AdherentService adherentService;

    public AdherentController(AdherentService adherentService) {
        this.adherentService = adherentService;
    }

    // Créer un adhérent
    @PostMapping
    public ResponseEntity<AdherentDto> createAdherent(@Valid @RequestBody AdherentCreateDto createDto) {
        AdherentDto savedAdherent = adherentService.createAdherent(createDto);
        return ResponseEntity.status(HttpStatus.CREATED).body(savedAdherent);
    }

    // Récupérer un adhérent par ID
    @GetMapping("/{id}")
    public ResponseEntity<AdherentDto> getAdherent(@PathVariable Long id) {
        AdherentDto adherentDto = adherentService.getAdherentById(id);
        return ResponseEntity.ok(adherentDto);
    }

    // Récupérer un adhérent par email
    @GetMapping("/email/{email}")
    public ResponseEntity<AdherentDto> getAdherentByEmail(@PathVariable String email) {
        AdherentDto adherentDto = adherentService.getAdherentByEmail(email);
        return ResponseEntity.ok(adherentDto);
    }

    // Mettre à jour un adhérent
    @PutMapping("/{id}")
    public ResponseEntity<AdherentDto> updateAdherent(
        @PathVariable Long id,
        @Valid @RequestBody AdherentUpdateDto updateDto) {
        AdherentDto updatedAdherent = adherentService.updateAdherent(id, updateDto);
        return ResponseEntity.ok(updatedAdherent);
    }

    // Supprimer un adhérent
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteAdherent(@PathVariable Long id) {
        adherentService.deleteAdherent(id);
        return ResponseEntity.noContent().build();
    }
}

Exemples de Réponses d’Erreur

Voici à quoi ressembleront les réponses d’erreur pour les différentes exceptions :

Adhérent non trouvé (404)

{
  "status": 404,
  "error": "Ressource non trouvée",
  "message": "Adhérent non trouvé avec l'ID : 1",
  "timestamp": "2024-01-10T14:30:00"
}

Adhérent déjà existant (409)

{
  "status": 409,
  "error": "Conflit",
  "message": "Un adhérent avec l'email philippe@example.com existe déjà.",
  "timestamp": "2024-01-10T14:30:00"
}

Erreur de validation (400)

{
  "status": 400,
  "error": "Validation échouée",
  "message": "Erreurs de validation dans la requête",
  "details": [
    "nom: Le nom ne peut pas être vide",
    "email: L'email doit être valide"
  ],
  "timestamp": "2024-01-10T14:30:00"
}

Points Clés à Retenir

Exceptions personnalisées :

Gestion centralisée :

Validation :

Clarté et maintenabilité :

9) A Faire