From 06656507ca295b761f43ef868c6f866c917a8e10 Mon Sep 17 00:00:00 2001
From: henridb <henridb@stud.ntnu.no>
Date: Wed, 17 Apr 2024 09:38:07 +0200
Subject: [PATCH] feat: add exception classes and handler class

---
 .../exception/ExceptionResponse.java          |  29 ++++
 .../exception/GlobalExceptionHandler.java     | 129 ++++++++++++++++++
 .../auth/InvalidCredentialsException.java     |  14 ++
 .../user/PermissionDeniedException.java       |  14 ++
 .../user/UserAlreadyExistsException.java      |  14 ++
 .../exception/user/UserNotFoundException.java |  21 +++
 .../user/UsernameTakenException.java          |  14 ++
 7 files changed, 235 insertions(+)
 create mode 100644 src/main/java/no/ntnu/idi/stud/savingsapp/exception/ExceptionResponse.java
 create mode 100644 src/main/java/no/ntnu/idi/stud/savingsapp/exception/GlobalExceptionHandler.java
 create mode 100644 src/main/java/no/ntnu/idi/stud/savingsapp/exception/auth/InvalidCredentialsException.java
 create mode 100644 src/main/java/no/ntnu/idi/stud/savingsapp/exception/user/PermissionDeniedException.java
 create mode 100644 src/main/java/no/ntnu/idi/stud/savingsapp/exception/user/UserAlreadyExistsException.java
 create mode 100644 src/main/java/no/ntnu/idi/stud/savingsapp/exception/user/UserNotFoundException.java
 create mode 100644 src/main/java/no/ntnu/idi/stud/savingsapp/exception/user/UsernameTakenException.java

diff --git a/src/main/java/no/ntnu/idi/stud/savingsapp/exception/ExceptionResponse.java b/src/main/java/no/ntnu/idi/stud/savingsapp/exception/ExceptionResponse.java
new file mode 100644
index 0000000..157d8f5
--- /dev/null
+++ b/src/main/java/no/ntnu/idi/stud/savingsapp/exception/ExceptionResponse.java
@@ -0,0 +1,29 @@
+package no.ntnu.idi.stud.savingsapp.exception;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+/**
+ * Represents an exception response containing status code and message.
+ */
+@Data
+@AllArgsConstructor
+public class ExceptionResponse {
+
+  private int status;
+  private String message;
+
+  /**
+   * Creates a ResponseEntity containing the exception response.
+   *
+   * @param status The HTTP status code.
+   * @param message The message describing the exception.
+   * @return A ResponseEntity containing the exception response.
+   */
+  protected static ResponseEntity<ExceptionResponse> toResponseEntity(HttpStatus status, String message) {
+    ExceptionResponse response = new ExceptionResponse(status.value(), message);
+    return ResponseEntity.status(status).body(response);
+  }
+}
diff --git a/src/main/java/no/ntnu/idi/stud/savingsapp/exception/GlobalExceptionHandler.java b/src/main/java/no/ntnu/idi/stud/savingsapp/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..007bdeb
--- /dev/null
+++ b/src/main/java/no/ntnu/idi/stud/savingsapp/exception/GlobalExceptionHandler.java
@@ -0,0 +1,129 @@
+package no.ntnu.idi.stud.savingsapp.exception;
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import no.ntnu.idi.stud.savingsapp.exception.auth.InvalidCredentialsException;
+import no.ntnu.idi.stud.savingsapp.exception.user.PermissionDeniedException;
+import no.ntnu.idi.stud.savingsapp.exception.user.UserAlreadyExistsException;
+import no.ntnu.idi.stud.savingsapp.exception.user.UserNotFoundException;
+import no.ntnu.idi.stud.savingsapp.exception.user.UsernameTakenException;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.HttpStatusCode;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.authentication.CredentialsExpiredException;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.context.request.WebRequest;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
+
+/**
+ * Global exception handler that handles various exceptions thrown by the application.
+ * Customizes the response for different types of exceptions.
+ */
+@ControllerAdvice
+public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
+
+  /**
+   * Handles validation errors for method arguments.
+   *
+   * @param e The MethodArgumentNotValidException containing validation errors.
+   * @param headers The headers for the response.
+   * @param status The HTTP status code.
+   * @param request The current web request.
+   * @return A ResponseEntity containing the error response.
+   */
+  @Override
+  protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException e, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
+    String error = e.getMessage();
+    FieldError fieldError = e.getFieldError();
+    if (fieldError != null) {
+      error = fieldError.getDefaultMessage();
+    }
+    ExceptionResponse response = new ExceptionResponse(HttpStatus.BAD_REQUEST.value(), error);
+    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
+  }
+
+  /**
+   * Handles exceptions by returning an {@link HttpStatus#BAD_REQUEST} response.
+   *
+   * @param e The exception.
+   * @return A ResponseEntity containing the error response.
+   */
+  @ExceptionHandler({ConstraintViolationException.class})
+  public ResponseEntity<ExceptionResponse> handleBadRequest(Exception e) {
+    String error = e.getMessage();
+    if (e instanceof ConstraintViolationException constraintViolationException) {
+      for (ConstraintViolation<?> violation : constraintViolationException.getConstraintViolations()) {
+        error = violation.getMessage();
+        break;
+      }
+    }
+    return ExceptionResponse.toResponseEntity(HttpStatus.BAD_REQUEST, error);
+  }
+
+  /**
+   * Handles exceptions by returning an {@link HttpStatus#UNAUTHORIZED} response.
+   *
+   * @param e The exception.
+   * @return A ResponseEntity containing the error response.
+   */
+  @ExceptionHandler({InvalidCredentialsException.class, AccessDeniedException.class,
+      AuthenticationException.class, CredentialsExpiredException.class, PermissionDeniedException.class})
+  public ResponseEntity<ExceptionResponse> handleUnauthorized(Exception e) {
+    return ExceptionResponse.toResponseEntity(HttpStatus.UNAUTHORIZED, e.getMessage());
+  }
+
+  /**
+   * Handles exceptions by returning an {@link HttpStatus#CONFLICT} response.
+   *
+   * @param e The exception.
+   * @return A ResponseEntity containing the error response.
+   */
+  @ExceptionHandler({UserAlreadyExistsException.class, UsernameTakenException.class})
+  public ResponseEntity<ExceptionResponse> handleConflict(Exception e) {
+    return ExceptionResponse.toResponseEntity(HttpStatus.CONFLICT, e.getMessage());
+  }
+
+  /**
+   * Handles exceptions by returning an {@link HttpStatus#NOT_FOUND} response.
+   *
+   * @param e The exception.
+   * @return A ResponseEntity containing the error response.
+   */
+  @ExceptionHandler({UserNotFoundException.class})
+  public ResponseEntity<ExceptionResponse> handleNotFound(Exception e) {
+    return ExceptionResponse.toResponseEntity(HttpStatus.NOT_FOUND, e.getMessage());
+  }
+
+  /**
+   * Handles remaining exceptions by returning an {@link HttpStatus#INTERNAL_SERVER_ERROR} response.
+   *
+   * @param e The exception.
+   * @return A ResponseEntity containing the error response.
+   */
+  @ExceptionHandler(Exception.class)
+  public ResponseEntity<ExceptionResponse> handleRemainingExceptions(Exception e) {
+    return ExceptionResponse.toResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
+  }
+
+  /**
+   * Customizes the default exception handler.
+   *
+   * @param e The exception.
+   * @param body The body of the response.
+   * @param headers The headers for the response.
+   * @param statusCode The HTTP status code.
+   * @param request The current web request.
+   * @return A ResponseEntity containing the error response.
+   */
+  @Override
+  protected ResponseEntity<Object> handleExceptionInternal(Exception e, Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) {
+    ExceptionResponse response = new ExceptionResponse(statusCode.value(), e.getMessage());
+    return super.handleExceptionInternal(e, response, headers, statusCode, request);
+  }
+}
diff --git a/src/main/java/no/ntnu/idi/stud/savingsapp/exception/auth/InvalidCredentialsException.java b/src/main/java/no/ntnu/idi/stud/savingsapp/exception/auth/InvalidCredentialsException.java
new file mode 100644
index 0000000..0950054
--- /dev/null
+++ b/src/main/java/no/ntnu/idi/stud/savingsapp/exception/auth/InvalidCredentialsException.java
@@ -0,0 +1,14 @@
+package no.ntnu.idi.stud.savingsapp.exception.auth;
+
+/**
+ * Exception thrown when authentication fails due to invalid credentials.
+ */
+public class InvalidCredentialsException extends RuntimeException {
+
+  /**
+   * Constructs an InvalidCredentialsException with the default message.
+   */
+  public InvalidCredentialsException() {
+    super("Invalid credentials");
+  }
+}
diff --git a/src/main/java/no/ntnu/idi/stud/savingsapp/exception/user/PermissionDeniedException.java b/src/main/java/no/ntnu/idi/stud/savingsapp/exception/user/PermissionDeniedException.java
new file mode 100644
index 0000000..9d7d389
--- /dev/null
+++ b/src/main/java/no/ntnu/idi/stud/savingsapp/exception/user/PermissionDeniedException.java
@@ -0,0 +1,14 @@
+package no.ntnu.idi.stud.savingsapp.exception.user;
+
+/**
+ * Exception thrown when a user attempts an action they don't have permission to.
+ */
+public final class PermissionDeniedException extends RuntimeException {
+
+  /**
+   * Constructs a PermissionDeniedException with the default message.
+   */
+  public PermissionDeniedException() {
+    super("Permission denied");
+  }
+}
diff --git a/src/main/java/no/ntnu/idi/stud/savingsapp/exception/user/UserAlreadyExistsException.java b/src/main/java/no/ntnu/idi/stud/savingsapp/exception/user/UserAlreadyExistsException.java
new file mode 100644
index 0000000..c5cc150
--- /dev/null
+++ b/src/main/java/no/ntnu/idi/stud/savingsapp/exception/user/UserAlreadyExistsException.java
@@ -0,0 +1,14 @@
+package no.ntnu.idi.stud.savingsapp.exception.user;
+
+/**
+ * Exception thrown when attempting to create a user that already exists in the system.
+ */
+public final class UserAlreadyExistsException extends RuntimeException {
+
+  /**
+   * Constructs a UserAlreadyExistsException with the default message.
+   */
+  public UserAlreadyExistsException() {
+    super("User already exists");
+  }
+}
diff --git a/src/main/java/no/ntnu/idi/stud/savingsapp/exception/user/UserNotFoundException.java b/src/main/java/no/ntnu/idi/stud/savingsapp/exception/user/UserNotFoundException.java
new file mode 100644
index 0000000..b6e2a7e
--- /dev/null
+++ b/src/main/java/no/ntnu/idi/stud/savingsapp/exception/user/UserNotFoundException.java
@@ -0,0 +1,21 @@
+package no.ntnu.idi.stud.savingsapp.exception.user;
+
+/**
+ * Exception thrown when attempting to retrieve a user that does not exist in the system.
+ */
+public final class UserNotFoundException extends RuntimeException {
+
+  /**
+   * Constructs a UserNotFoundException with the default message.
+   */
+  public UserNotFoundException() {
+    super("User not found");
+  }
+
+  /**
+   * Constructs a UserNotFoundException with the default message.
+   */
+  public UserNotFoundException(String string) {
+    super(string);
+  }
+}
diff --git a/src/main/java/no/ntnu/idi/stud/savingsapp/exception/user/UsernameTakenException.java b/src/main/java/no/ntnu/idi/stud/savingsapp/exception/user/UsernameTakenException.java
new file mode 100644
index 0000000..8c3bdfc
--- /dev/null
+++ b/src/main/java/no/ntnu/idi/stud/savingsapp/exception/user/UsernameTakenException.java
@@ -0,0 +1,14 @@
+package no.ntnu.idi.stud.savingsapp.exception.user;
+
+/**
+ * Exception thrown when attempting to create a user that already exists in the system.
+ */
+public final class UsernameTakenException extends RuntimeException {
+
+  /**
+   * Constructs a UserAlreadyExistsException with the default message.
+   */
+  public UsernameTakenException() {
+    super("Username is taken");
+  }
+}
-- 
GitLab