feat: add domain exceptions and global error handler

This commit is contained in:
dualfroz committed 2026-08-23 20:59:45 +02:00
1 parent 49b1e28855
commit fcbef8490f
9 files changed
+150

No files matched your search

@@ -0,0 +1,8 @@
package com.slotline.common;
public class AvailabilityViolationException extends RuntimeException {
public AvailabilityViolationException(String message) {
super(message);
}
}
@@ -0,0 +1,8 @@
package com.slotline.common;
public class ForbiddenOperationException extends RuntimeException {
public ForbiddenOperationException(String message) {
super(message);
}
}
@@ -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<ProblemDetail> handleNotFound(NotFoundException exception, HttpServletRequest request) {
return problem(HttpStatus.NOT_FOUND, exception.getMessage(), request);
}
@ExceptionHandler(InvalidCredentialsException.class)
public ResponseEntity<ProblemDetail> handleInvalidCredentials(InvalidCredentialsException exception,
HttpServletRequest request) {
return problem(HttpStatus.UNAUTHORIZED, exception.getMessage(), request);
}
@ExceptionHandler(ForbiddenOperationException.class)
public ResponseEntity<ProblemDetail> handleForbidden(ForbiddenOperationException exception,
HttpServletRequest request) {
return problem(HttpStatus.FORBIDDEN, exception.getMessage(), request);
}
@ExceptionHandler({InvalidReservationException.class, RecurrenceException.class})
public ResponseEntity<ProblemDetail> handleInvalidReservation(RuntimeException exception,
HttpServletRequest request) {
return problem(HttpStatus.BAD_REQUEST, exception.getMessage(), request);
}
@ExceptionHandler(AvailabilityViolationException.class)
public ResponseEntity<ProblemDetail> handleAvailability(AvailabilityViolationException exception,
HttpServletRequest request) {
return problem(HttpStatus.UNPROCESSABLE_ENTITY, exception.getMessage(), request);
}
@ExceptionHandler({SlotConflictException.class, UsernameTakenException.class, SlugTakenException.class})
public ResponseEntity<ProblemDetail> handleConflict(RuntimeException exception, HttpServletRequest request) {
return problem(HttpStatus.CONFLICT, exception.getMessage(), request);
}
@ExceptionHandler({ConcurrencyFailureException.class, DataIntegrityViolationException.class})
public ResponseEntity<ProblemDetail> handleConcurrentBooking(RuntimeException exception,
HttpServletRequest request) {
return problem(HttpStatus.CONFLICT, "The slot was booked by a concurrent request", request);
}
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ProblemDetail> handleUnreadable(HttpMessageNotReadableException exception,
HttpServletRequest request) {
return problem(HttpStatus.BAD_REQUEST, "Malformed request body", request);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ProblemDetail> handleValidation(MethodArgumentNotValidException exception,
HttpServletRequest request) {
List<FieldViolation> violations = exception.getBindingResult().getFieldErrors().stream()
.map(error -> new FieldViolation(error.getField(), error.getDefaultMessage()))
.toList();
ResponseEntity<ProblemDetail> response = problem(HttpStatus.BAD_REQUEST, "Request validation failed", request);
response.getBody().setProperty("violations", violations);
return response;
}
private ResponseEntity<ProblemDetail> 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) {
}
}
@@ -0,0 +1,8 @@
package com.slotline.common;
public class InvalidCredentialsException extends RuntimeException {
public InvalidCredentialsException() {
super("Invalid username or password");
}
}
@@ -0,0 +1,8 @@
package com.slotline.common;
public class InvalidReservationException extends RuntimeException {
public InvalidReservationException(String message) {
super(message);
}
}
@@ -0,0 +1,8 @@
package com.slotline.common;
public class NotFoundException extends RuntimeException {
public NotFoundException(String message) {
super(message);
}
}
@@ -0,0 +1,8 @@
package com.slotline.common;
public class SlotConflictException extends RuntimeException {
public SlotConflictException(String message) {
super(message);
}
}
@@ -0,0 +1,8 @@
package com.slotline.common;
public class SlugTakenException extends RuntimeException {
public SlugTakenException(String slug) {
super("Organization slug '" + slug + "' is already taken");
}
}
@@ -0,0 +1,8 @@
package com.slotline.common;
public class UsernameTakenException extends RuntimeException {
public UsernameTakenException(String username) {
super("Username '" + username + "' is already taken");
}
}