video-surveillance-app/src/main/java/me/belkacemi/iotalarm/presentation/advice/GlobalExceptionHandler.java

94 lines
4.2 KiB
Java
Raw Normal View History

package me.belkacemi.iotalarm.presentation.advice;
import me.belkacemi.iotalarm.domain.exception.DomainException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
import org.springframework.web.ErrorResponse;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.net.URI;
import java.util.stream.Collectors;
/**
* Traduit les exceptions en reponses HTTP, au format RFC 7807 (ProblemDetail).
*
* <p>C'est ici, et nulle part ailleurs, que le domaine rencontre HTTP. Une
* {@link DomainException} ne sait pas ce qu'est un code 409 - et c'est
* precisement ce qui permet de la lever depuis l'agregat sans le coupler au web.</p>
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* Regle metier violee : armer une alarme deja armee, acquitter deux fois.
* 409 et non 400 - la requete est bien formee, c'est l'etat qui s'y oppose.
*/
@ExceptionHandler(DomainException.class)
public ProblemDetail onDomainException(DomainException exception) {
log.info("Regle metier refusee : {}", exception.getMessage());
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.CONFLICT, exception.getMessage());
problem.setTitle("Regle metier violee");
problem.setType(URI.create("urn:iot-alarm:domain-rule-violation"));
return problem;
}
/** Donnee invalide a la frontiere : type de capteur inconnu, deviceId malforme. */
@ExceptionHandler(IllegalArgumentException.class)
public ProblemDetail onIllegalArgument(IllegalArgumentException exception) {
log.info("Requete rejetee : {}", exception.getMessage());
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST, exception.getMessage());
problem.setTitle("Requete invalide");
problem.setType(URI.create("urn:iot-alarm:invalid-request"));
return problem;
}
/** Echec de Bean Validation : champs manquants ou hors bornes. */
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail onValidationError(MethodArgumentNotValidException exception) {
String details = exception.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + " : " + error.getDefaultMessage())
.collect(Collectors.joining(", "));
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST, details);
problem.setTitle("Validation echouee");
problem.setType(URI.create("urn:iot-alarm:validation-failed"));
return problem;
}
/**
* Filet de securite. On journalise la stacktrace mais on ne la renvoie pas :
* l'endpoint d'ingestion est expose publiquement.
*
* <p>Les exceptions que Spring sait deja traduire implementent
* {@link ErrorResponse} et portent leur propre statut : ressource absente
* (404), methode non supportee (405), media non supporte (415). Les avaler
* dans un 500 generique transformerait une URL inconnue en panne serveur -
* et, plus grave, noierait les vraies pannes au milieu de ces faux
* positifs dans les logs et les alertes.</p>
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ProblemDetail> onUnexpectedError(Exception exception) {
if (exception instanceof ErrorResponse errorResponse) {
return ResponseEntity.status(errorResponse.getStatusCode())
.body(errorResponse.getBody());
}
log.error("Erreur inattendue", exception);
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.INTERNAL_SERVER_ERROR,
"Une erreur interne est survenue.");
problem.setTitle("Erreur interne");
return ResponseEntity.internalServerError().body(problem);
}
}