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.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). * *
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.
*/ @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. */ @ExceptionHandler(Exception.class) public ProblemDetail onUnexpectedError(Exception exception) { log.error("Erreur inattendue", exception); ProblemDetail problem = ProblemDetail.forStatusAndDetail( HttpStatus.INTERNAL_SERVER_ERROR, "Une erreur interne est survenue."); problem.setTitle("Erreur interne"); return problem; } }