GlobalExceptionHandler.java
package cl.prgm.portaltrip.infrastructure.web.exception;
import java.util.List;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import cl.prgm.portaltrip.domain.exception.DomainValidationException;
import cl.prgm.portaltrip.domain.exception.DuplicateUserException;
import cl.prgm.portaltrip.domain.exception.IdempotencyConflictException;
import cl.prgm.portaltrip.domain.exception.InsufficientBalanceException;
import cl.prgm.portaltrip.domain.exception.InvalidCredentialsException;
import cl.prgm.portaltrip.domain.exception.InvalidReservationStateException;
import cl.prgm.portaltrip.domain.exception.PortalStipendCooldownException;
import cl.prgm.portaltrip.domain.exception.ResourceNotFoundException;
import cl.prgm.portaltrip.infrastructure.web.dto.ApiResponseDto;
import cl.prgm.portaltrip.infrastructure.web.dto.BalanceErrorDto;
import jakarta.validation.ConstraintViolationException;
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiResponseDto<Void>> handleNotFound(ResourceNotFoundException exception) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponseDto.error(HttpStatus.NOT_FOUND, exception.getMessage()));
}
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ApiResponseDto<Void>> handleInvalidJson(HttpMessageNotReadableException exception) {
return ResponseEntity.badRequest()
.body(ApiResponseDto.error(HttpStatus.BAD_REQUEST, "A valid JSON request body is required"));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponseDto<Void>> handleValidation(MethodArgumentNotValidException exception) {
String message = exception.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining("; "));
if (message.isBlank()) {
message = "Validation failed";
}
return ResponseEntity.badRequest()
.body(ApiResponseDto.error(HttpStatus.BAD_REQUEST, message));
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ApiResponseDto<Void>> handleConstraint(ConstraintViolationException exception) {
String message = exception.getConstraintViolations().stream()
.map(violation -> violation.getPropertyPath() + ": " + violation.getMessage())
.collect(Collectors.joining("; "));
if (message.isBlank()) {
message = "Validation failed";
}
return ResponseEntity.badRequest()
.body(ApiResponseDto.error(HttpStatus.BAD_REQUEST, message));
}
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ApiResponseDto<Void>> handleTypeMismatch(MethodArgumentTypeMismatchException exception) {
return ResponseEntity.badRequest()
.body(ApiResponseDto.error(
HttpStatus.BAD_REQUEST,
"Invalid value for parameter '" + exception.getName() + "'"));
}
@ExceptionHandler(ServletRequestBindingException.class)
public ResponseEntity<ApiResponseDto<Void>> handleRequestBinding(ServletRequestBindingException exception) {
return ResponseEntity.badRequest()
.body(ApiResponseDto.error(HttpStatus.BAD_REQUEST, exception.getMessage()));
}
@ExceptionHandler(NoResourceFoundException.class)
public ResponseEntity<ApiResponseDto<Void>> handleNoResource() {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponseDto.error(HttpStatus.NOT_FOUND, "Resource not found"));
}
@ExceptionHandler(DomainValidationException.class)
public ResponseEntity<ApiResponseDto<List<String>>> handleDomainValidation(DomainValidationException exception) {
return ResponseEntity.unprocessableEntity()
.body(ApiResponseDto.error(
HttpStatus.UNPROCESSABLE_ENTITY,
"Validation failed",
exception.errors()));
}
@ExceptionHandler(InvalidReservationStateException.class)
public ResponseEntity<ApiResponseDto<Void>> handleInvalidState(InvalidReservationStateException exception) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(ApiResponseDto.error(HttpStatus.CONFLICT, exception.getMessage()));
}
@ExceptionHandler({DuplicateUserException.class, IdempotencyConflictException.class})
public ResponseEntity<ApiResponseDto<Void>> handleConflict(RuntimeException exception) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(ApiResponseDto.error(HttpStatus.CONFLICT, exception.getMessage()));
}
@ExceptionHandler(InvalidCredentialsException.class)
public ResponseEntity<ApiResponseDto<Void>> handleInvalidCredentials(InvalidCredentialsException exception) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(ApiResponseDto.error(HttpStatus.UNAUTHORIZED, exception.getMessage()));
}
@ExceptionHandler(InsufficientBalanceException.class)
public ResponseEntity<ApiResponseDto<BalanceErrorDto>> handleInsufficientBalance(
InsufficientBalanceException exception) {
return ResponseEntity.unprocessableEntity()
.body(ApiResponseDto.error(
HttpStatus.UNPROCESSABLE_ENTITY,
exception.getMessage(),
new BalanceErrorDto(exception.requiredBalance(), exception.currentBalance())));
}
@ExceptionHandler(PortalStipendCooldownException.class)
public ResponseEntity<ApiResponseDto<Void>> handlePortalStipendCooldown(
PortalStipendCooldownException exception) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.body(ApiResponseDto.error(HttpStatus.TOO_MANY_REQUESTS, exception.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponseDto<Void>> handleUnexpected(Exception exception) {
LOGGER.error("Unhandled server error", exception);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponseDto.error(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error"));
}
}