From fcbef8490f350a2f5a22b6b5deb91e6c2677abd3 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 23 Aug 2026 20:59:45 +0200 Subject: [PATCH] feat: add domain exceptions and global error handler --- .../AvailabilityViolationException.java | 8 ++ .../common/ForbiddenOperationException.java | 8 ++ .../common/GlobalExceptionHandler.java | 86 +++++++++++++++++++ .../common/InvalidCredentialsException.java | 8 ++ .../common/InvalidReservationException.java | 8 ++ .../slotline/common/NotFoundException.java | 8 ++ .../common/SlotConflictException.java | 8 ++ .../slotline/common/SlugTakenException.java | 8 ++ .../common/UsernameTakenException.java | 8 ++ 9 files changed, 150 insertions(+) create mode 100644 src/main/java/com/slotline/common/AvailabilityViolationException.java create mode 100644 src/main/java/com/slotline/common/ForbiddenOperationException.java create mode 100644 src/main/java/com/slotline/common/GlobalExceptionHandler.java create mode 100644 src/main/java/com/slotline/common/InvalidCredentialsException.java create mode 100644 src/main/java/com/slotline/common/InvalidReservationException.java create mode 100644 src/main/java/com/slotline/common/NotFoundException.java create mode 100644 src/main/java/com/slotline/common/SlotConflictException.java create mode 100644 src/main/java/com/slotline/common/SlugTakenException.java create mode 100644 src/main/java/com/slotline/common/UsernameTakenException.java diff --git a/src/main/java/com/slotline/common/AvailabilityViolationException.java b/src/main/java/com/slotline/common/AvailabilityViolationException.java new file mode 100644 index 0000000..df0d815 --- /dev/null +++ b/src/main/java/com/slotline/common/AvailabilityViolationException.java @@ -0,0 +1,8 @@ +package com.slotline.common; + +public class AvailabilityViolationException extends RuntimeException { + + public AvailabilityViolationException(String message) { + super(message); + } +} diff --git a/src/main/java/com/slotline/common/ForbiddenOperationException.java b/src/main/java/com/slotline/common/ForbiddenOperationException.java new file mode 100644 index 0000000..e67ee90 --- /dev/null +++ b/src/main/java/com/slotline/common/ForbiddenOperationException.java @@ -0,0 +1,8 @@ +package com.slotline.common; + +public class ForbiddenOperationException extends RuntimeException { + + public ForbiddenOperationException(String message) { + super(message); + } +} diff --git a/src/main/java/com/slotline/common/GlobalExceptionHandler.java b/src/main/java/com/slotline/common/GlobalExceptionHandler.java new file mode 100644 index 0000000..37821fc --- /dev/null +++ b/src/main/java/com/slotline/common/GlobalExceptionHandler.java @@ -0,0 +1,86 @@ +package com.slotline.common; + +import com.slotline.recurrence.RecurrenceException; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; +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 java.util.List; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFound(NotFoundException exception, HttpServletRequest request) { + return problem(HttpStatus.NOT_FOUND, exception.getMessage(), request); + } + + @ExceptionHandler(InvalidCredentialsException.class) + public ResponseEntity handleInvalidCredentials(InvalidCredentialsException exception, + HttpServletRequest request) { + return problem(HttpStatus.UNAUTHORIZED, exception.getMessage(), request); + } + + @ExceptionHandler(ForbiddenOperationException.class) + public ResponseEntity handleForbidden(ForbiddenOperationException exception, + HttpServletRequest request) { + return problem(HttpStatus.FORBIDDEN, exception.getMessage(), request); + } + + @ExceptionHandler({InvalidReservationException.class, RecurrenceException.class}) + public ResponseEntity handleInvalidReservation(RuntimeException exception, + HttpServletRequest request) { + return problem(HttpStatus.BAD_REQUEST, exception.getMessage(), request); + } + + @ExceptionHandler(AvailabilityViolationException.class) + public ResponseEntity handleAvailability(AvailabilityViolationException exception, + HttpServletRequest request) { + return problem(HttpStatus.UNPROCESSABLE_ENTITY, exception.getMessage(), request); + } + + @ExceptionHandler({SlotConflictException.class, UsernameTakenException.class, SlugTakenException.class}) + public ResponseEntity handleConflict(RuntimeException exception, HttpServletRequest request) { + return problem(HttpStatus.CONFLICT, exception.getMessage(), request); + } + + @ExceptionHandler({ConcurrencyFailureException.class, DataIntegrityViolationException.class}) + public ResponseEntity handleConcurrentBooking(RuntimeException exception, + HttpServletRequest request) { + return problem(HttpStatus.CONFLICT, "The slot was booked by a concurrent request", request); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity handleUnreadable(HttpMessageNotReadableException exception, + HttpServletRequest request) { + return problem(HttpStatus.BAD_REQUEST, "Malformed request body", request); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidation(MethodArgumentNotValidException exception, + HttpServletRequest request) { + List violations = exception.getBindingResult().getFieldErrors().stream() + .map(error -> new FieldViolation(error.getField(), error.getDefaultMessage())) + .toList(); + ResponseEntity response = problem(HttpStatus.BAD_REQUEST, "Request validation failed", request); + response.getBody().setProperty("violations", violations); + return response; + } + + private ResponseEntity problem(HttpStatus status, String detail, HttpServletRequest request) { + ProblemDetail body = ProblemDetail.forStatusAndDetail(status, detail); + body.setTitle(status.getReasonPhrase()); + body.setProperty("path", request.getRequestURI()); + return ResponseEntity.status(status).body(body); + } + + private record FieldViolation(String field, String message) { + } +} diff --git a/src/main/java/com/slotline/common/InvalidCredentialsException.java b/src/main/java/com/slotline/common/InvalidCredentialsException.java new file mode 100644 index 0000000..1c08952 --- /dev/null +++ b/src/main/java/com/slotline/common/InvalidCredentialsException.java @@ -0,0 +1,8 @@ +package com.slotline.common; + +public class InvalidCredentialsException extends RuntimeException { + + public InvalidCredentialsException() { + super("Invalid username or password"); + } +} diff --git a/src/main/java/com/slotline/common/InvalidReservationException.java b/src/main/java/com/slotline/common/InvalidReservationException.java new file mode 100644 index 0000000..6c89f5b --- /dev/null +++ b/src/main/java/com/slotline/common/InvalidReservationException.java @@ -0,0 +1,8 @@ +package com.slotline.common; + +public class InvalidReservationException extends RuntimeException { + + public InvalidReservationException(String message) { + super(message); + } +} diff --git a/src/main/java/com/slotline/common/NotFoundException.java b/src/main/java/com/slotline/common/NotFoundException.java new file mode 100644 index 0000000..3204c4e --- /dev/null +++ b/src/main/java/com/slotline/common/NotFoundException.java @@ -0,0 +1,8 @@ +package com.slotline.common; + +public class NotFoundException extends RuntimeException { + + public NotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/slotline/common/SlotConflictException.java b/src/main/java/com/slotline/common/SlotConflictException.java new file mode 100644 index 0000000..9b7ac4d --- /dev/null +++ b/src/main/java/com/slotline/common/SlotConflictException.java @@ -0,0 +1,8 @@ +package com.slotline.common; + +public class SlotConflictException extends RuntimeException { + + public SlotConflictException(String message) { + super(message); + } +} diff --git a/src/main/java/com/slotline/common/SlugTakenException.java b/src/main/java/com/slotline/common/SlugTakenException.java new file mode 100644 index 0000000..14a6e67 --- /dev/null +++ b/src/main/java/com/slotline/common/SlugTakenException.java @@ -0,0 +1,8 @@ +package com.slotline.common; + +public class SlugTakenException extends RuntimeException { + + public SlugTakenException(String slug) { + super("Organization slug '" + slug + "' is already taken"); + } +} diff --git a/src/main/java/com/slotline/common/UsernameTakenException.java b/src/main/java/com/slotline/common/UsernameTakenException.java new file mode 100644 index 0000000..5099a1e --- /dev/null +++ b/src/main/java/com/slotline/common/UsernameTakenException.java @@ -0,0 +1,8 @@ +package com.slotline.common; + +public class UsernameTakenException extends RuntimeException { + + public UsernameTakenException(String username) { + super("Username '" + username + "' is already taken"); + } +}