package dev.lions.unionflow.server.exception; import dev.lions.unionflow.server.service.SystemLoggingService; import jakarta.inject.Inject; import jakarta.ws.rs.ForbiddenException; import jakarta.ws.rs.NotAllowedException; import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriInfo; import jakarta.ws.rs.ext.ExceptionMapper; import jakarta.ws.rs.ext.Provider; import lombok.extern.slf4j.Slf4j; import java.io.PrintWriter; import java.io.StringWriter; /** * Exception Mapper global pour capturer toutes les exceptions non gérées * et les persister dans system_logs. * * @author UnionFlow Team * @version 1.0 * @since 2026-03-15 */ @Slf4j @Provider public class GlobalExceptionMapper implements ExceptionMapper { @Inject SystemLoggingService systemLoggingService; @Context UriInfo uriInfo; @Override public Response toResponse(Throwable exception) { // Déterminer le code HTTP int statusCode = determineStatusCode(exception); // Les exceptions métier 4xx (404, 401, 403) sont des cas normaux : // on les logue en DEBUG/WARN, sans stack trace et sans persister en system_logs. if (isExpectedClientError(exception)) { log.debug("Expected client error [{}]: {}", statusCode, exception.getMessage()); return buildErrorResponse(exception, statusCode); } // Pour toute autre exception (5xx ou inattendue) : log ERROR + persistance log.error("Unhandled exception", exception); // Récupérer l'endpoint (safe pour les tests unitaires) String endpoint = "unknown"; try { if (uriInfo != null) { endpoint = uriInfo.getPath(); } } catch (Exception e) { // Ignore - pas de contexte REST (ex: test unitaire) } // Générer le message et le stacktrace String message = exception.getMessage() != null ? exception.getMessage() : exception.getClass().getSimpleName(); String stacktrace = getStackTrace(exception); // Persister dans system_logs (ne pas laisser ça crasher le mapper) // Note: systemLoggingService est @Transactional → ne peut pas être appelé depuis // le thread IO de Vert.x (ex: exceptions levées avant le dispatch vers worker thread) try { systemLoggingService.logError( determineSource(exception), message, stacktrace, "system", "unknown", "/" + endpoint, statusCode ); } catch (IllegalStateException e) { // BlockingOperationNotAllowedException est une IllegalStateException : // le mapper est appelé depuis le thread IO, on ne peut pas démarrer une transaction JTA. log.debug("Cannot persist error log from IO thread ({}): {}", e.getClass().getSimpleName(), message); } catch (Exception e) { log.warn("Failed to log error to system_logs", e); } // Retourner une réponse HTTP appropriée return buildErrorResponse(exception, statusCode); } /** * Retourne {@code true} pour les exceptions 4xx qui représentent * des cas métier attendus (ressource absente, accès refusé, session expirée). * Ces exceptions ne doivent pas être loguées ERROR ni persistées. */ private boolean isExpectedClientError(Throwable exception) { return exception instanceof NotFoundException || exception instanceof ForbiddenException || exception instanceof NotAuthorizedException || exception instanceof NotAllowedException; } private int determineStatusCode(Throwable exception) { if (exception instanceof WebApplicationException webAppException) { return webAppException.getResponse().getStatus(); } if (exception instanceof IllegalArgumentException || exception instanceof IllegalStateException) { return Response.Status.BAD_REQUEST.getStatusCode(); } if (exception instanceof SecurityException) { return Response.Status.FORBIDDEN.getStatusCode(); } return Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(); } private String determineSource(Throwable exception) { String className = exception.getClass().getSimpleName(); if (className.contains("Database") || className.contains("SQL") || className.contains("Persistence")) { return "Database"; } if (className.contains("Security") || className.contains("Auth")) { return "Auth"; } if (className.contains("Validation")) { return "Validation"; } return "API"; } private String getStackTrace(Throwable exception) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); exception.printStackTrace(pw); return sw.toString(); } private Response buildErrorResponse(Throwable exception, int statusCode) { String message = statusCode >= 500 ? "Internal server error" : (exception.getMessage() != null ? exception.getMessage() : "An error occurred"); return Response.status(statusCode) .entity(java.util.Map.of( "error", message, "status", statusCode, "timestamp", java.time.LocalDateTime.now().toString() )) .build(); } }