From 3c3508a956d651c11fa2852af80bb587a71060b0 Mon Sep 17 00:00:00 2001 From: nenzen Date: Sat, 10 Jan 2026 16:57:34 +0100 Subject: [PATCH 1/6] Add StudentportalenException base class --- .../exception/StudentportalenException.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 bff/src/main/java/se/su/dsv/studentportalen/bff/exception/StudentportalenException.java diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/StudentportalenException.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/StudentportalenException.java new file mode 100644 index 0000000..1fbcadc --- /dev/null +++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/StudentportalenException.java @@ -0,0 +1,34 @@ +package se.su.dsv.studentportalen.bff.exception; + +import org.springframework.http.HttpStatus; + +/** + * Base exception for all Studentportalen BFF errors. + * Subclasses define specific error types that map to RFC 7807 Problem Details. + */ +public abstract class StudentportalenException extends RuntimeException { + + protected StudentportalenException(String message) { + super(message); + } + + protected StudentportalenException(String message, Throwable cause) { + super(message, cause); + } + + /** + * The error type URI for RFC 7807 Problem Details. + * Example: "studentportalen:validation-error" + */ + public abstract String getType(); + + /** + * A short, human-readable summary of the problem type. + */ + public abstract String getTitle(); + + /** + * The HTTP status code for this error. + */ + public abstract HttpStatus getStatus(); +} -- 2.39.5 From 09931c5170ce9b6a6e3ed9e945f604b96789da6e Mon Sep 17 00:00:00 2001 From: nenzen Date: Fri, 30 Jan 2026 07:38:01 +0100 Subject: [PATCH 2/6] Add ValidationException with field violations --- .../bff/exception/ValidationException.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ValidationException.java diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ValidationException.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ValidationException.java new file mode 100644 index 0000000..24b245e --- /dev/null +++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ValidationException.java @@ -0,0 +1,47 @@ +package se.su.dsv.studentportalen.bff.exception; + +import org.springframework.http.HttpStatus; + +import java.util.List; +import java.util.Map; + +/** + * Exception for validation errors, including field-level violations. + * Used for both BFF validation failures and mapped Daisy validation errors. + */ +public class ValidationException extends StudentportalenException { + + private final Map> violations; + + public ValidationException(String message) { + this(message, Map.of()); + } + + public ValidationException(String message, Map> violations) { + super(message); + this.violations = violations != null ? Map.copyOf(violations) : Map.of(); + } + + @Override + public String getType() { + return "studentportalen:validation-error"; + } + + @Override + public String getTitle() { + return "Validation failed"; + } + + @Override + public HttpStatus getStatus() { + return HttpStatus.BAD_REQUEST; + } + + /** + * Field-level validation errors. + * Keys are field names, values are lists of error messages. + */ + public Map> getViolations() { + return violations; + } +} -- 2.39.5 From 42abc19ffa615ffcbdfb54815f26f3d605c94b4b Mon Sep 17 00:00:00 2001 From: nenzen Date: Sat, 10 Jan 2026 16:57:55 +0100 Subject: [PATCH 3/6] Add ForbiddenException --- .../bff/exception/ForbiddenException.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ForbiddenException.java diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ForbiddenException.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ForbiddenException.java new file mode 100644 index 0000000..4063508 --- /dev/null +++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ForbiddenException.java @@ -0,0 +1,33 @@ +package se.su.dsv.studentportalen.bff.exception; + +import org.springframework.http.HttpStatus; + +/** + * Exception for authorization failures. + * Thrown when the user is authenticated but not allowed to perform the action. + */ +public class ForbiddenException extends StudentportalenException { + + public ForbiddenException(String message) { + super(message); + } + + public ForbiddenException(String message, Throwable cause) { + super(message, cause); + } + + @Override + public String getType() { + return "studentportalen:forbidden"; + } + + @Override + public String getTitle() { + return "Access denied"; + } + + @Override + public HttpStatus getStatus() { + return HttpStatus.FORBIDDEN; + } +} -- 2.39.5 From 8d04c5efdc6ec7763026044c80963c6324e23acf Mon Sep 17 00:00:00 2001 From: nenzen Date: Sat, 10 Jan 2026 16:58:02 +0100 Subject: [PATCH 4/6] Add ServiceUnavailableException --- .../ServiceUnavailableException.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ServiceUnavailableException.java diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ServiceUnavailableException.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ServiceUnavailableException.java new file mode 100644 index 0000000..b57c0c7 --- /dev/null +++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ServiceUnavailableException.java @@ -0,0 +1,33 @@ +package se.su.dsv.studentportalen.bff.exception; + +import org.springframework.http.HttpStatus; + +/** + * Exception for backend service unavailability. + * Thrown when a backend system (e.g., Daisy) is unreachable or returns 5xx errors. + */ +public class ServiceUnavailableException extends StudentportalenException { + + public ServiceUnavailableException(String message) { + super(message); + } + + public ServiceUnavailableException(String message, Throwable cause) { + super(message, cause); + } + + @Override + public String getType() { + return "studentportalen:service-unavailable"; + } + + @Override + public String getTitle() { + return "Service unavailable"; + } + + @Override + public HttpStatus getStatus() { + return HttpStatus.SERVICE_UNAVAILABLE; + } +} -- 2.39.5 From 03679231236d69d9e9e2adc9748aae826ec3ad8f Mon Sep 17 00:00:00 2001 From: nenzen Date: Fri, 30 Jan 2026 07:38:51 +0100 Subject: [PATCH 5/6] Add GlobalExceptionHandler --- bff/pom.xml | 4 + .../bff/exception/GlobalExceptionHandler.java | 85 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 bff/src/main/java/se/su/dsv/studentportalen/bff/exception/GlobalExceptionHandler.java diff --git a/bff/pom.xml b/bff/pom.xml index cfd48dd..32b3a1d 100644 --- a/bff/pom.xml +++ b/bff/pom.xml @@ -32,6 +32,10 @@ org.springframework.boot spring-boot-starter-oauth2-client + + org.springframework.boot + spring-boot-starter-validation + diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/GlobalExceptionHandler.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..ac53673 --- /dev/null +++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/GlobalExceptionHandler.java @@ -0,0 +1,85 @@ +package se.su.dsv.studentportalen.bff.exception; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Centralized exception handler for all REST controllers. + * Returns RFC 7807 Problem Details for all error responses. + */ +@RestControllerAdvice +public class GlobalExceptionHandler { + + private static final Logger LOG = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + /** + * Handles Jakarta Bean Validation errors (missing/invalid fields). + * Triggered when @Valid fails on request body. + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public ProblemDetail handleValidation(MethodArgumentNotValidException ex) { + Map> violations = ex.getBindingResult().getFieldErrors().stream() + .collect(Collectors.groupingBy( + FieldError::getField, + Collectors.mapping( + error -> error.getDefaultMessage() != null ? error.getDefaultMessage() : "Invalid value", + Collectors.toList() + ) + )); + + LOG.warn("Validation failed: {}", violations); + + ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST); + problem.setType(URI.create("studentportalen:validation-error")); + problem.setTitle("Validation failed"); + problem.setDetail("One or more fields are invalid"); + problem.setProperty("violations", violations); + return problem; + } + + /** + * Handles business exceptions from the BFF. + * These include mapped errors from backend systems like Daisy. + */ + @ExceptionHandler(StudentportalenException.class) + public ProblemDetail handleBusinessError(StudentportalenException ex) { + LOG.warn("Business error: {} - {}", ex.getType(), ex.getMessage()); + + ProblemDetail problem = ProblemDetail.forStatus(ex.getStatus()); + problem.setType(URI.create(ex.getType())); + problem.setTitle(ex.getTitle()); + problem.setDetail(ex.getMessage()); + + if (ex instanceof ValidationException ve && !ve.getViolations().isEmpty()) { + problem.setProperty("violations", ve.getViolations()); + } + + return problem; + } + + /** + * Handles unexpected errors. + * Logs the full stack trace but returns a generic message to the client. + */ + @ExceptionHandler(Exception.class) + public ProblemDetail handleUnexpected(Exception ex) { + LOG.error("Unexpected error", ex); + + ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR); + problem.setType(URI.create("studentportalen:internal-error")); + problem.setTitle("An unexpected error occurred"); + problem.setDetail("Please try again later"); + return problem; + } +} -- 2.39.5 From 9e63eb64066ee04dbae68cd02a10b939e6f019ff Mon Sep 17 00:00:00 2001 From: nenzen Date: Fri, 30 Jan 2026 11:42:17 +0100 Subject: [PATCH 6/6] Prettier and checkstyle --- .../bff/exception/ForbiddenException.java | 36 ++--- .../bff/exception/GlobalExceptionHandler.java | 129 ++++++++++-------- .../ServiceUnavailableException.java | 36 ++--- .../exception/StudentportalenException.java | 38 +++--- .../bff/exception/ValidationException.java | 60 ++++---- 5 files changed, 156 insertions(+), 143 deletions(-) diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ForbiddenException.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ForbiddenException.java index 4063508..ada8cde 100644 --- a/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ForbiddenException.java +++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ForbiddenException.java @@ -8,26 +8,26 @@ import org.springframework.http.HttpStatus; */ public class ForbiddenException extends StudentportalenException { - public ForbiddenException(String message) { - super(message); - } + public ForbiddenException(String message) { + super(message); + } - public ForbiddenException(String message, Throwable cause) { - super(message, cause); - } + public ForbiddenException(String message, Throwable cause) { + super(message, cause); + } - @Override - public String getType() { - return "studentportalen:forbidden"; - } + @Override + public String getType() { + return "studentportalen:forbidden"; + } - @Override - public String getTitle() { - return "Access denied"; - } + @Override + public String getTitle() { + return "Access denied"; + } - @Override - public HttpStatus getStatus() { - return HttpStatus.FORBIDDEN; - } + @Override + public HttpStatus getStatus() { + return HttpStatus.FORBIDDEN; + } } diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/GlobalExceptionHandler.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/GlobalExceptionHandler.java index ac53673..f54ddd9 100644 --- a/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/GlobalExceptionHandler.java +++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/GlobalExceptionHandler.java @@ -1,5 +1,9 @@ package se.su.dsv.studentportalen.bff.exception; +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; @@ -9,11 +13,6 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import java.net.URI; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - /** * Centralized exception handler for all REST controllers. * Returns RFC 7807 Problem Details for all error responses. @@ -21,65 +20,77 @@ import java.util.stream.Collectors; @RestControllerAdvice public class GlobalExceptionHandler { - private static final Logger LOG = LoggerFactory.getLogger(GlobalExceptionHandler.class); + private static final Logger LOG = LoggerFactory.getLogger( + GlobalExceptionHandler.class + ); - /** - * Handles Jakarta Bean Validation errors (missing/invalid fields). - * Triggered when @Valid fails on request body. - */ - @ExceptionHandler(MethodArgumentNotValidException.class) - public ProblemDetail handleValidation(MethodArgumentNotValidException ex) { - Map> violations = ex.getBindingResult().getFieldErrors().stream() - .collect(Collectors.groupingBy( - FieldError::getField, - Collectors.mapping( - error -> error.getDefaultMessage() != null ? error.getDefaultMessage() : "Invalid value", - Collectors.toList() - ) - )); + /** + * Handles Jakarta Bean Validation errors (missing/invalid fields). + * Triggered when @Valid fails on request body. + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public ProblemDetail handleValidation(MethodArgumentNotValidException ex) { + Map> violations = ex + .getBindingResult() + .getFieldErrors() + .stream() + .collect( + Collectors.groupingBy( + FieldError::getField, + Collectors.mapping( + error -> + error.getDefaultMessage() != null + ? error.getDefaultMessage() + : "Invalid value", + Collectors.toList() + ) + ) + ); - LOG.warn("Validation failed: {}", violations); + LOG.warn("Validation failed: {}", violations); - ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST); - problem.setType(URI.create("studentportalen:validation-error")); - problem.setTitle("Validation failed"); - problem.setDetail("One or more fields are invalid"); - problem.setProperty("violations", violations); - return problem; + ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST); + problem.setType(URI.create("studentportalen:validation-error")); + problem.setTitle("Validation failed"); + problem.setDetail("One or more fields are invalid"); + problem.setProperty("violations", violations); + return problem; + } + + /** + * Handles business exceptions from the BFF. + * These include mapped errors from backend systems like Daisy. + */ + @ExceptionHandler(StudentportalenException.class) + public ProblemDetail handleBusinessError(StudentportalenException ex) { + LOG.warn("Business error: {} - {}", ex.getType(), ex.getMessage()); + + ProblemDetail problem = ProblemDetail.forStatus(ex.getStatus()); + problem.setType(URI.create(ex.getType())); + problem.setTitle(ex.getTitle()); + problem.setDetail(ex.getMessage()); + + if (ex instanceof ValidationException ve && !ve.getViolations().isEmpty()) { + problem.setProperty("violations", ve.getViolations()); } - /** - * Handles business exceptions from the BFF. - * These include mapped errors from backend systems like Daisy. - */ - @ExceptionHandler(StudentportalenException.class) - public ProblemDetail handleBusinessError(StudentportalenException ex) { - LOG.warn("Business error: {} - {}", ex.getType(), ex.getMessage()); + return problem; + } - ProblemDetail problem = ProblemDetail.forStatus(ex.getStatus()); - problem.setType(URI.create(ex.getType())); - problem.setTitle(ex.getTitle()); - problem.setDetail(ex.getMessage()); + /** + * Handles unexpected errors. + * Logs the full stack trace but returns a generic message to the client. + */ + @ExceptionHandler(Exception.class) + public ProblemDetail handleUnexpected(Exception ex) { + LOG.error("Unexpected error", ex); - if (ex instanceof ValidationException ve && !ve.getViolations().isEmpty()) { - problem.setProperty("violations", ve.getViolations()); - } - - return problem; - } - - /** - * Handles unexpected errors. - * Logs the full stack trace but returns a generic message to the client. - */ - @ExceptionHandler(Exception.class) - public ProblemDetail handleUnexpected(Exception ex) { - LOG.error("Unexpected error", ex); - - ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR); - problem.setType(URI.create("studentportalen:internal-error")); - problem.setTitle("An unexpected error occurred"); - problem.setDetail("Please try again later"); - return problem; - } + ProblemDetail problem = ProblemDetail.forStatus( + HttpStatus.INTERNAL_SERVER_ERROR + ); + problem.setType(URI.create("studentportalen:internal-error")); + problem.setTitle("An unexpected error occurred"); + problem.setDetail("Please try again later"); + return problem; + } } diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ServiceUnavailableException.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ServiceUnavailableException.java index b57c0c7..d160d1e 100644 --- a/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ServiceUnavailableException.java +++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ServiceUnavailableException.java @@ -8,26 +8,26 @@ import org.springframework.http.HttpStatus; */ public class ServiceUnavailableException extends StudentportalenException { - public ServiceUnavailableException(String message) { - super(message); - } + public ServiceUnavailableException(String message) { + super(message); + } - public ServiceUnavailableException(String message, Throwable cause) { - super(message, cause); - } + public ServiceUnavailableException(String message, Throwable cause) { + super(message, cause); + } - @Override - public String getType() { - return "studentportalen:service-unavailable"; - } + @Override + public String getType() { + return "studentportalen:service-unavailable"; + } - @Override - public String getTitle() { - return "Service unavailable"; - } + @Override + public String getTitle() { + return "Service unavailable"; + } - @Override - public HttpStatus getStatus() { - return HttpStatus.SERVICE_UNAVAILABLE; - } + @Override + public HttpStatus getStatus() { + return HttpStatus.SERVICE_UNAVAILABLE; + } } diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/StudentportalenException.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/StudentportalenException.java index 1fbcadc..193f2a0 100644 --- a/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/StudentportalenException.java +++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/StudentportalenException.java @@ -8,27 +8,27 @@ import org.springframework.http.HttpStatus; */ public abstract class StudentportalenException extends RuntimeException { - protected StudentportalenException(String message) { - super(message); - } + protected StudentportalenException(String message) { + super(message); + } - protected StudentportalenException(String message, Throwable cause) { - super(message, cause); - } + protected StudentportalenException(String message, Throwable cause) { + super(message, cause); + } - /** - * The error type URI for RFC 7807 Problem Details. - * Example: "studentportalen:validation-error" - */ - public abstract String getType(); + /** + * The error type URI for RFC 7807 Problem Details. + * Example: "studentportalen:validation-error" + */ + public abstract String getType(); - /** - * A short, human-readable summary of the problem type. - */ - public abstract String getTitle(); + /** + * A short, human-readable summary of the problem type. + */ + public abstract String getTitle(); - /** - * The HTTP status code for this error. - */ - public abstract HttpStatus getStatus(); + /** + * The HTTP status code for this error. + */ + public abstract HttpStatus getStatus(); } diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ValidationException.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ValidationException.java index 24b245e..1d69220 100644 --- a/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ValidationException.java +++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ValidationException.java @@ -1,9 +1,8 @@ package se.su.dsv.studentportalen.bff.exception; -import org.springframework.http.HttpStatus; - import java.util.List; import java.util.Map; +import org.springframework.http.HttpStatus; /** * Exception for validation errors, including field-level violations. @@ -11,37 +10,40 @@ import java.util.Map; */ public class ValidationException extends StudentportalenException { - private final Map> violations; + private final Map> violations; - public ValidationException(String message) { - this(message, Map.of()); - } + public ValidationException(String message) { + this(message, Map.of()); + } - public ValidationException(String message, Map> violations) { - super(message); - this.violations = violations != null ? Map.copyOf(violations) : Map.of(); - } + public ValidationException( + String message, + Map> violations + ) { + super(message); + this.violations = violations != null ? Map.copyOf(violations) : Map.of(); + } - @Override - public String getType() { - return "studentportalen:validation-error"; - } + @Override + public String getType() { + return "studentportalen:validation-error"; + } - @Override - public String getTitle() { - return "Validation failed"; - } + @Override + public String getTitle() { + return "Validation failed"; + } - @Override - public HttpStatus getStatus() { - return HttpStatus.BAD_REQUEST; - } + @Override + public HttpStatus getStatus() { + return HttpStatus.BAD_REQUEST; + } - /** - * Field-level validation errors. - * Keys are field names, values are lists of error messages. - */ - public Map> getViolations() { - return violations; - } + /** + * Field-level validation errors. + * Keys are field names, values are lists of error messages. + */ + public Map> getViolations() { + return violations; + } } -- 2.39.5