2026-09-12 17:32:45 +02:00
|
|
|
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;
|
2026-09-12 18:08:34 +02:00
|
|
|
import org.springframework.http.ResponseEntity;
|
|
|
|
|
import org.springframework.web.ErrorResponse;
|
2026-09-12 17:32:45 +02:00
|
|
|
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.
|
2026-09-12 18:08:34 +02:00
|
|
|
*
|
|
|
|
|
* <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>
|
2026-09-12 17:32:45 +02:00
|
|
|
*/
|
|
|
|
|
@ExceptionHandler(Exception.class)
|
2026-09-12 18:08:34 +02:00
|
|
|
public ResponseEntity<ProblemDetail> onUnexpectedError(Exception exception) {
|
|
|
|
|
if (exception instanceof ErrorResponse errorResponse) {
|
|
|
|
|
return ResponseEntity.status(errorResponse.getStatusCode())
|
|
|
|
|
.body(errorResponse.getBody());
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-12 17:32:45 +02:00
|
|
|
log.error("Erreur inattendue", exception);
|
|
|
|
|
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
|
|
|
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
|
|
|
"Une erreur interne est survenue.");
|
|
|
|
|
problem.setTitle("Erreur interne");
|
2026-09-12 18:08:34 +02:00
|
|
|
return ResponseEntity.internalServerError().body(problem);
|
2026-09-12 17:32:45 +02:00
|
|
|
}
|
|
|
|
|
}
|