diff --git a/README.md b/README.md index e5730cf..b3e6ed5 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Spring Boot -Version +Version

@@ -125,6 +125,7 @@ Unlike other response wrapper libraries, this one offers: * ✅ **Native Spring Boot 3.x/4.x Auto-Configuration** - No manual setup required * ✅ **Zero-Boilerplate @AutoResponse** - Return raw objects, let the library wrap them automatically while preserving your HTTP Status codes. **Supports both Class-level and Method-level granularity.** * ✅ **Intelligent String Handling** - Safely wraps raw `String` returns into JSON without throwing `ClassCastException`. +* ✅ **Dynamic Exception Registry** - Seamlessly map 3rd-party exceptions (SQL, Security) to standard HTTP responses with a simple builder pattern. * ✅ **RFC 9457 ProblemDetail Support** - Industry-standard error responses (latest RFC) * ✅ **Opt-in Security Features** - Fine-grained control via field and **class-level** annotations (`@XssCheck`, `@AutoTrim`) * ✅ **Zero External Dependencies** - Pure Java implementation, won't conflict with your application @@ -134,29 +135,26 @@ Unlike other response wrapper libraries, this one offers: ## 🚀 Installation -### Maven (Latest - v1.4.0) +### Maven (Latest - v1.5.0) ```xml io.github.og4dev og4dev-spring-response - 1.4.0 + 1.5.0 - ``` -### Gradle (Latest - v1.4.0) +### Gradle (Latest - v1.5.0) ```gradle -implementation 'io.github.og4dev:og4dev-spring-response:1.4.0' - +implementation 'io.github.og4dev:og4dev-spring-response:1.5.0' ``` -### Gradle Kotlin DSL (Latest - v1.4.0) +### Gradle Kotlin DSL (Latest - v1.5.0) ```kotlin -implementation("io.github.og4dev:og4dev-spring-response:1.4.0") - +implementation("io.github.og4dev:og4dev-spring-response:1.5.0") ``` --- @@ -165,7 +163,7 @@ implementation("io.github.og4dev:og4dev-spring-response:1.4.0") The library is organized into six main packages: -``` +```text io.github.og4dev ├── advice/ │ └── GlobalResponseWrapper.java # Automatic response wrapper interceptor @@ -179,10 +177,10 @@ io.github.og4dev │ └── ApiResponse.java # Generic response wrapper ├── exception/ │ ├── ApiException.java # Abstract base for custom exceptions +│ ├── ApiExceptionRegistry.java # Dynamic mapping for 3rd-party exceptions │ └── GlobalExceptionHandler.java # RFC 9457 exception handler └── filter/ └── TraceIdFilter.java # Request trace ID generation - ``` ## 🎯 Quick Start @@ -201,10 +199,9 @@ public class UserController { return ApiResponse.success("User retrieved successfully", user); } } - ``` -### Method 2: Automatic Wrapping (New in v1.4.0) 🎁 +### Method 2: Automatic Wrapping (New in v1.5.0) 🎁 Tired of typing `ResponseEntity>`? Use `@AutoResponse`! You can apply it to the whole class, or just specific methods. @@ -232,10 +229,9 @@ public class UserController { @GetMapping("/greeting") public String greeting() { // Raw strings are safely converted to JSON ApiResponse too! - return "Hello World"; + return "Hello World"; } } - ``` **Both methods produce the exact same JSON:** @@ -243,7 +239,6 @@ public class UserController { ```json { "status": 200, - // or 201 for POST "message": "Success", "content": { "id": 1, @@ -251,22 +246,21 @@ public class UserController { }, "timestamp": "2026-02-28T10:30:45.123Z" } - ``` ## ⚙️ Auto-Configuration The library features **Spring Boot Auto-Configuration** for truly zero-config setup! -✅ **GlobalExceptionHandler** - Automatic exception handling -✅ **GlobalResponseWrapper** - Automatic payload wrapping via `@AutoResponse` +✅ **GlobalExceptionHandler** - Automatic exception handling +✅ **GlobalResponseWrapper** - Automatic payload wrapping via `@AutoResponse` ✅ **Security Customizers** - Jackson configuration for `@AutoTrim` and `@XssCheck` **No configuration needed!** Just add the dependency. ## 🎁 Opt-in Automatic Wrapping (@AutoResponse) -Introduced in **v1.4.0**, you can eliminate boilerplate code by letting the library wrap your controller responses automatically. +Introduced in **v1.5.0**, you can eliminate boilerplate code by letting the library wrap your controller responses automatically. ### Flexible Granularity: @@ -284,7 +278,7 @@ Introduced in **v1.4.0**, you can eliminate boilerplate code by letting the libr The library provides fine-grained security and data processing features through field-level annotations. By default, **fields are NOT modified** unless explicitly annotated. -*New in v1.4.0:* You can now apply `@AutoTrim` and `@XssCheck` to **entire classes** to protect all string fields at once! +*New in v1.5.0:* You can now apply `@AutoTrim` and `@XssCheck` to **entire classes** to protect all string fields at once! ### 1. Strict Property Validation 🛡️ (Automatic) @@ -297,7 +291,6 @@ Fail-fast HTML tag detection and rejection using regex pattern `(?s).*<\s*[a-zA- ```java @XssCheck private String comment; // Rejects "" - ``` ### 3. Opt-in String Trimming with @AutoTrim ✂️ @@ -307,10 +300,9 @@ Automatic whitespace removal for specific fields. ```java @AutoTrim private String username; // " john_doe " -> "john_doe" - ``` -### 4. Class-Level Protection (New in v1.4.0) 🛡️ +### 4. Class-Level Protection (New in v1.5.0) 🛡️ Apply annotations to the class level to automatically protect **ALL** string fields within that class! @@ -323,14 +315,13 @@ public class SecureRegistrationDTO { private String email; private String bio; } - ``` *(See full Security details in the Javadocs and examples above).* ## 🛡️ Built-in Exception Handling -The library includes a **production-ready `GlobalExceptionHandler**` that automatically handles 10 common exceptions using Spring Boot's **ProblemDetail (RFC 9457)** standard. +The library includes a **production-ready `GlobalExceptionHandler`** that automatically handles 10 common exceptions using Spring Boot's **ProblemDetail (RFC 9457)** standard. * **Automatic Logging:** SLF4J integration for all errors. * **Trace ID Consistency:** Logs and responses always have matching trace IDs. @@ -342,9 +333,30 @@ public class ResourceNotFoundException extends ApiException { super(String.format("%s not found with ID: %d", resource, id), HttpStatus.NOT_FOUND); } } +``` +### 🔄 Dynamic Exception Registry (New in v1.5.0) + +Seamlessly map 3rd-party or framework-specific exceptions (like `SQLException`, `MongoException`, or `AuthenticationException`) to standard `ProblemDetail` responses without writing custom `@ExceptionHandler` methods! + +Simply define a configuration bean to register your external exceptions: + +```java +@Configuration +public class ApiExceptionConfig { + @Bean + public ApiExceptionRegistry apiExceptionRegistry() { + return new ApiExceptionRegistry() + // Map Database Exceptions + .register(SQLException.class, HttpStatus.INTERNAL_SERVER_ERROR, "A database error occurred.") + // Map Security Exceptions + .register(AuthenticationException.class, HttpStatus.UNAUTHORIZED, "Invalid authentication token."); + } +} ``` +All registered exceptions will automatically be caught by the `GlobalExceptionHandler` and formatted into RFC 9457 compliant JSON responses with trace IDs. + ## 🌍 Real-World Examples ### Example 1: Clean CRUD Controller (Using Class-Level @AutoResponse) @@ -381,7 +393,6 @@ public class ProductController { // Returns empty content with 200 OK automatically } } - ``` ## 📚 API Reference @@ -390,26 +401,27 @@ public class ProductController { ## 📈 Version History -### 1.4.0 (February 2026) - Current Release +### 1.5.0 (April 2026) - **Current Release** ✨ **New Features & Improvements:** -* **@AutoResponse Annotation & GlobalResponseWrapper** -* Opt-in automatic response wrapping to eliminate boilerplate code. -* **Improved Granularity:** Fully supports both Class-level (`ElementType.TYPE`) and Method-level (`ElementType.METHOD`) placement for precision control over which endpoints are wrapped. -* Returns raw DTOs from controllers and automatically wraps them in `ApiResponse`. -* Preserves HTTP status codes from `@ResponseStatus`. -* Intelligently skips `ResponseEntity`, `ApiResponse`, and `ProblemDetail` to prevent double-wrapping. -* **Intelligent String Handling:** Uses Spring's `ObjectMapper` to safely serialize raw `String` returns to JSON, avoiding `ClassCastException` with native converters. +* **Dynamic Exception Registry (`ApiExceptionRegistry`)** + * Centralized mapping for 3rd-party exceptions (e.g., SQL, Mongo, Spring Security) without writing custom handlers. + * Thread-safe registry preserving insertion order for hierarchy-based exception catching. +* **@AutoResponse Annotation & GlobalResponseWrapper** + * Opt-in automatic response wrapping to eliminate boilerplate code. + * **Improved Granularity:** Fully supports both Class-level (`ElementType.TYPE`) and Method-level (`ElementType.METHOD`) placement for precision control over which endpoints are wrapped. + * Returns raw DTOs from controllers and automatically wraps them in `ApiResponse`. + * Preserves HTTP status codes from `@ResponseStatus`. + * Intelligently skips `ResponseEntity`, `ApiResponse`, and `ProblemDetail` to prevent double-wrapping. + * **Intelligent String Handling:** Uses Spring's `ObjectMapper` to safely serialize raw `String` returns to JSON, avoiding `ClassCastException` with native converters. * **Class-Level Security Annotations** -* `@AutoTrim` and `@XssCheck` can now be applied at the Class level (`ElementType.TYPE`) to automatically protect all String fields within the DTO at once. - + * `@AutoTrim` and `@XssCheck` can now be applied at the Class level (`ElementType.TYPE`) to automatically protect all String fields within the DTO at once. * **Documentation** -* `package-info.java` documentation added for the new `advice` package. - + * `package-info.java` documentation added for the new `advice` package. ### 1.3.0 (February 2026) diff --git a/pom.xml b/pom.xml index 8d66f6c..546b348 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.github.og4dev og4dev-spring-response - 1.4.0 + 1.5.0 OG4Dev Spring API Response A lightweight, zero-configuration REST API Response wrapper and Global Exception Handler (RFC 9457) for Spring Boot applications, maintained by OG4Dev. @@ -14,7 +14,7 @@ The Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt + https://www.apache.org/licenses/LICENSE-2.0.txt @@ -36,7 +36,7 @@ 17 17 - 4.0.3 + 4.0.5 UTF-8 diff --git a/src/main/java/io/github/og4dev/advice/GlobalResponseWrapper.java b/src/main/java/io/github/og4dev/advice/GlobalResponseWrapper.java index 8d4a2f9..2de90c3 100644 --- a/src/main/java/io/github/og4dev/advice/GlobalResponseWrapper.java +++ b/src/main/java/io/github/og4dev/advice/GlobalResponseWrapper.java @@ -19,79 +19,75 @@ import tools.jackson.databind.ObjectMapper; /** - * Global response interceptor that automatically wraps REST controller outputs into the - * standardized {@link ApiResponse} format. + * Spring {@link ResponseBodyAdvice} implementation that automatically encapsulates REST + * controller return values inside a standardized {@link ApiResponse} envelope. *

- * This wrapper is conditionally activated only for controllers or specific methods - * annotated with the {@link AutoResponse @AutoResponse} annotation. It provides a seamless - * developer experience by eliminating the need to manually return {@code ResponseEntity>} - * from every controller method. + * This wrapper is activated only for controllers or individual methods annotated + * with {@link AutoResponse @AutoResponse}. It intercepts the outgoing response body + * before Jackson serializes it, wraps the payload, and sends the resulting + * {@link ApiResponse} structure to the client — eliminating manual + * {@code ResponseEntity>} boilerplate. *

- *

Core Functionalities:

+ * + *

Core Behaviors

*
    - *
  • Automatic Encapsulation: Intercepts raw DTOs, Lists, or primitive responses and packages - * them into the {@code content} field of an {@code ApiResponse}.
  • - *
  • Status Code Preservation: Dynamically reads the current HTTP status of the response - * (e.g., set via {@code @ResponseStatus(HttpStatus.CREATED)}) and ensures it is accurately - * reflected in the final {@code ApiResponse}.
  • - *
  • String Payload Compatibility: Safely intercepts raw {@code String} returns and manually - * serializes them to prevent {@code ClassCastException} when Spring utilizes the {@code StringHttpMessageConverter}.
  • - *
  • Safety Mechanisms: Intelligently skips wrapping if the response is already formatted - * to prevent double-wrapping errors or interference with standard error handling protocols.
  • + *
  • Automatic Encapsulation — Raw DTOs, collections, and primitive values are + * placed in the {@code content} field of an {@link ApiResponse}.
  • + *
  • Status Code Preservation — The HTTP status already set on the response + * (e.g., via {@code @ResponseStatus(HttpStatus.CREATED)}) is read and reflected + * in the final {@code ApiResponse.status} field.
  • + *
  • Custom Message — The {@link AutoResponse#message()} value at method level + * takes precedence; falls back to the class-level value, then {@code "Success"}.
  • + *
  • String Payload Compatibility — Raw {@code String} returns are explicitly + * serialized via the injected {@code ObjectMapper} and the response + * {@code Content-Type} is forced to {@code application/json}, preventing + * {@code ClassCastException} with Spring's {@code StringHttpMessageConverter}.
  • + *
  • Double-Wrap Prevention — Already-formatted return types ({@link ApiResponse}, + * {@link ResponseEntity}, {@link ProblemDetail}) are skipped entirely.
  • *
* * @author Pasindu OG - * @version 1.4.0 + * @version 1.5.0 + * @since 1.4.0 * @see AutoResponse * @see ApiResponse - * @see ResponseBodyAdvice - * @since 1.4.0 + * @see org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice */ @RestControllerAdvice @SuppressWarnings("unused") -public @NullMarked class GlobalResponseWrapper implements ResponseBodyAdvice { +@NullMarked +public class GlobalResponseWrapper implements ResponseBodyAdvice { private final ObjectMapper objectMapper; /** - * Constructs a new {@code GlobalResponseWrapper} with the provided {@link ObjectMapper}. + * Constructs a {@code GlobalResponseWrapper} with the Jackson {@code ObjectMapper} + * used for explicit {@code String} payload serialization. * - * @param objectMapper The Jackson object mapper used for explicit string serialization. + * @param objectMapper the configured Jackson mapper injected by Spring; must not be {@code null} */ public GlobalResponseWrapper(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } /** - * Determines whether the current response should be intercepted and wrapped. + * Determines whether this advice should process the current response body. *

- * This method evaluates two main conditions before allowing the response to be wrapped: + * Returns {@code true} only when both of the following conditions are met: *

*
    - *
  1. Annotation Presence: The target controller class or the specific handler method - * must be annotated with {@link AutoResponse}.
  2. - *
  3. Type Exclusion: The return type must not be one of the explicitly excluded types.
  4. + *
  5. The controller class or the specific handler method is annotated with + * {@link AutoResponse}.
  6. + *
  7. The method's return type is not one of the explicitly excluded types: + * {@link ApiResponse}, {@link ResponseEntity}, or {@link ProblemDetail}.
  8. *
- *

- * To guarantee application stability and adherence to standard HTTP protocols, this method - * specifically excludes the following return types from being wrapped: - *

- *
    - *
  • {@link ApiResponse} - Prevents recursive double-wrapping (e.g., {@code ApiResponse>}).
  • - *
  • {@link ResponseEntity} - Skips manual responses to respect developer's explicit configurations.
  • - *
  • {@link ProblemDetail} - Excludes RFC 9457 error responses generated by exception handlers.
  • - *
- *

- * Note: Unlike standard wrappers, raw {@link String} payloads are supported and handled - * appropriately during the write phase. - *

* - * @param returnType The return type of the controller method. - * @param converterType The selected HTTP message converter. - * @return {@code true} if annotated with {@code @AutoResponse} and not an excluded type; {@code false} otherwise. + * @param returnType the return type descriptor of the handler method + * @param converterType the message converter selected by Spring MVC + * @return {@code true} if the response body should be wrapped; {@code false} otherwise */ @Override - public @NullMarked boolean supports(MethodParameter returnType, Class> converterType) { + public boolean supports(MethodParameter returnType, Class> converterType) { Class type = returnType.getParameterType(); boolean isExcludedType = ApiResponse.class.isAssignableFrom(type) || ResponseEntity.class.isAssignableFrom(type) || @@ -103,28 +99,30 @@ public GlobalResponseWrapper(ObjectMapper objectMapper) { } /** - * Intercepts the response body before it is written to the output stream and encapsulates it - * within an {@link ApiResponse}. + * Intercepts the outgoing response body and wraps it inside an {@link ApiResponse}. *

- * This method extracts the actual HTTP status code set on the current response (defaulting to 200 OK). - * Based on whether the status code represents a success (2xx) or another state, it dynamically - * assigns an appropriate message ("Success" or "Processed") to the API response. + * The HTTP status code already set on the servlet response is read and encoded in the + * {@code ApiResponse.status} field. For 2xx status codes the + * {@link AutoResponse#message()} value (method-level first, then class-level) is used + * as the message; for all other codes the message is {@code "Processed"}. *

*

- * Special String Handling: If the intercepted payload is a raw {@code String}, it is - * explicitly serialized to a JSON string using the configured {@link ObjectMapper}, and the - * response {@code Content-Type} is strictly set to {@code application/json}. This prevents - * standard message converter conflicts. + * Raw {@code String} handling: if the original body is a {@code String}, the + * resulting {@link ApiResponse} is serialized to a JSON string immediately using the + * injected {@code ObjectMapper} and the response {@code Content-Type} header is set to + * {@code application/json}. This prevents Spring from routing the value through the + * {@code StringHttpMessageConverter}, which would cause a {@code ClassCastException}. + * If serialization fails, the original string is returned unchanged as a fallback. *

* - * @param body The raw object returned by the controller method. - * @param returnType The return type of the controller method. - * @param selectedContentType The selected content type for the response. - * @param selectedConverterType The selected HTTP message converter. - * @param request The current server HTTP request. - * @param response The current server HTTP response. - * @return The newly wrapped {@code ApiResponse} object ready to be serialized, or a pre-serialized - * JSON {@code String} if the original payload was a raw string. + * @param body the value returned by the handler method; may be {@code null} + * @param returnType the return type descriptor of the handler method + * @param selectedContentType the content type selected by content negotiation + * @param selectedConverterType the message converter selected by Spring MVC + * @param request the current server-side HTTP request + * @param response the current server-side HTTP response + * @return the wrapped {@link ApiResponse} object, or a pre-serialized JSON + * {@code String} when the original body was a raw string */ @Override public @Nullable Object beforeBodyWrite(@Nullable Object body, MethodParameter returnType, MediaType selectedContentType, Class> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { @@ -134,7 +132,16 @@ public GlobalResponseWrapper(ObjectMapper objectMapper) { statusCode = serverHttpResponse.getServletResponse().getStatus(); } HttpStatus httpStatus = HttpStatus.valueOf(statusCode); - ApiResponse apiResponse = ApiResponse.status(httpStatus.is2xxSuccessful() ? "Success" : "Processed", body, httpStatus).getBody(); + + String responseMessage = "Success"; + AutoResponse methodAnnotation = returnType.getMethodAnnotation(AutoResponse.class); + if (methodAnnotation != null) { + responseMessage = methodAnnotation.message(); + } else { + AutoResponse classAnnotation = returnType.getDeclaringClass().getAnnotation(AutoResponse.class); + if (classAnnotation != null) responseMessage = classAnnotation.message(); + } + var apiResponse = ApiResponse.status(httpStatus.is2xxSuccessful() ? responseMessage : "Processed", body, httpStatus).getBody(); if (body instanceof String) { try { diff --git a/src/main/java/io/github/og4dev/advice/package-info.java b/src/main/java/io/github/og4dev/advice/package-info.java index 793e9b3..6f1bd74 100644 --- a/src/main/java/io/github/og4dev/advice/package-info.java +++ b/src/main/java/io/github/og4dev/advice/package-info.java @@ -1,30 +1,31 @@ /** - * Provides global advisory components for the OG4Dev Spring API Response Library. + * Global response-body advisory component for automatic API response wrapping. *

- * This package contains Spring {@link org.springframework.web.bind.annotation.RestControllerAdvice} - * implementations that intercept and modify HTTP responses and requests globally across the application. + * This package contains the {@link io.github.og4dev.advice.GlobalResponseWrapper}, a Spring + * {@link org.springframework.web.bind.annotation.RestControllerAdvice} implementation that + * intercepts outgoing HTTP response bodies and encapsulates them within the standardized + * {@link io.github.og4dev.dto.ApiResponse} structure before they are serialized and sent + * to the client. *

*

- * The primary component within this package is the {@link io.github.og4dev.advice.GlobalResponseWrapper}. - * It facilitates the seamless, opt-in encapsulation of standard controller return values into the - * standardized {@link io.github.og4dev.dto.ApiResponse} format, significantly reducing boilerplate code. - *

- *

Integration & Usage

- *

- * Components in this package are automatically registered via Spring Boot's auto-configuration - * mechanism ({@code ApiResponseAutoConfiguration}). Developers do not need to manually scan, import, - * or configure this package. + * The wrapper is activation-based: it only processes controllers or methods annotated + * with {@link io.github.og4dev.annotation.AutoResponse @AutoResponse}, leaving all other + * endpoints completely unaffected. *

+ * + *

Registration

*

- * To activate the response wrapping capabilities provided by this package, simply annotate - * target REST controllers or specific methods with the {@link io.github.og4dev.annotation.AutoResponse @AutoResponse} - * annotation. + * {@link io.github.og4dev.advice.GlobalResponseWrapper} is registered automatically as a + * Spring bean by + * {@link io.github.og4dev.config.ApiResponseAutoConfiguration#globalResponseWrapper(tools.jackson.databind.ObjectMapper)}. + * No manual configuration is needed. *

* * @author Pasindu OG - * @version 1.4.0 + * @version 1.5.0 + * @since 1.4.0 * @see io.github.og4dev.advice.GlobalResponseWrapper * @see io.github.og4dev.annotation.AutoResponse - * @since 1.4.0 + * @see io.github.og4dev.dto.ApiResponse */ package io.github.og4dev.advice; \ No newline at end of file diff --git a/src/main/java/io/github/og4dev/annotation/AutoResponse.java b/src/main/java/io/github/og4dev/annotation/AutoResponse.java index 3360904..01659c6 100644 --- a/src/main/java/io/github/og4dev/annotation/AutoResponse.java +++ b/src/main/java/io/github/og4dev/annotation/AutoResponse.java @@ -5,53 +5,99 @@ /** * Opt-in annotation to enable automatic API response wrapping for Spring REST controllers. *

- * When this annotation is applied to a {@link org.springframework.web.bind.annotation.RestController} - * class or a specific request mapping method, the {@link io.github.og4dev.advice.GlobalResponseWrapper} - * intercepts the returned object and automatically encapsulates it within the standardized - * {@link io.github.og4dev.dto.ApiResponse} format. + * When applied to a {@link org.springframework.web.bind.annotation.RestController} class or + * to a specific request-mapping method, {@link io.github.og4dev.advice.GlobalResponseWrapper} + * intercepts the returned object and encapsulates it within the standardized + * {@link io.github.og4dev.dto.ApiResponse} format before it is written to the HTTP response body. *

- *

Usage:

+ *

+ * This eliminates the need to manually wrap every return value in + * {@code ResponseEntity>}, reducing boilerplate while keeping the response + * contract consistent across the entire API surface. + *

+ * + *

Target Scopes

*
    - *
  • Class Level ({@link ElementType#TYPE}): Applies the wrapping behavior to all endpoint methods within the controller.
  • - *
  • Method Level ({@link ElementType#METHOD}): Applies the wrapping behavior only to the specific annotated method.
  • + *
  • Class Level ({@link ElementType#TYPE}): Applies automatic wrapping to + * all request-mapping methods within the annotated controller.
  • + *
  • Method Level ({@link ElementType#METHOD}): Applies automatic wrapping + * only to the specific annotated method, leaving others unaffected.
  • *
- *

Example:

+ * + *

Custom Response Message

+ *

+ * The {@link #message()} element controls the value of the {@code message} field in the + * produced {@link io.github.og4dev.dto.ApiResponse}. It defaults to {@code "Success"} and + * can be overridden at class or method level: + *

+ *
{@code
+ * @GetMapping("/{id}")
+ * @AutoResponse(message = "User retrieved successfully")
+ * public UserDto getUser(@PathVariable Long id) {
+ *     return userService.findById(id);
+ * }
+ * }
+ * + *

Usage Example

*
{@code
  * @RestController
  * @RequestMapping("/api/users")
- * @AutoResponse // All methods in this controller will be automatically wrapped
+ * @AutoResponse // All methods in this controller are automatically wrapped
  * public class UserController {
- * * @GetMapping("/{id}")
- * public UserDto getUser(@PathVariable Long id) {
- * // Returns: { "status": "Success", "content": { "id": 1, ... }, "timestamp": "..." }
- * return userService.findById(id);
- * }
- * * @PostMapping
- * @ResponseStatus(HttpStatus.CREATED)
- * // @AutoResponse can also be placed here for method-level granularity instead of class-level
- * public UserDto createUser(@RequestBody UserDto dto) {
- * return userService.create(dto);
- * }
+ *
+ *     @GetMapping("/{id}")
+ *     public UserDto getUser(@PathVariable Long id) {
+ *         // Response: { "status": 200, "message": "Success", "content": { ... }, "timestamp": "..." }
+ *         return userService.findById(id);
+ *     }
+ *
+ *     @PostMapping
+ *     @ResponseStatus(HttpStatus.CREATED)
+ *     public UserDto createUser(@RequestBody UserDto dto) {
+ *         // Response: { "status": 201, "message": "Success", "content": { ... }, "timestamp": "..." }
+ *         return userService.create(dto);
+ *     }
  * }
  * }
- *

Exclusions:

+ * + *

Excluded Return Types

*

- * To prevent errors and double-wrapping, the interceptor will safely ignore methods that return: + * The interceptor automatically skips wrapping for the following return types to prevent + * conflicts and double-wrapping: *

*
    - *
  • {@code ApiResponse} or {@code ResponseEntity} (Assumes the developer has explicitly formatted the response)
  • - *
  • {@code ProblemDetail} (RFC 9457 error responses managed by the global exception handler)
  • - *
  • {@code String} (Bypassed to avoid {@code ClassCastException} with Spring's internal string message converters)
  • + *
  • {@code ApiResponse} — already wrapped by the developer; wrapping again would produce + * {@code ApiResponse>}.
  • + *
  • {@code ResponseEntity} — the developer has explicitly configured the response; + * the wrapper respects that decision.
  • + *
  • {@code ProblemDetail} — RFC 9457 error responses produced by the global exception + * handler; must not be re-wrapped.
  • + *
  • {@code String} — handled with explicit JSON serialization via the injected + * {@code ObjectMapper} to avoid {@code ClassCastException} with Spring's + * {@code StringHttpMessageConverter}.
  • *
* * @author Pasindu OG - * @version 1.4.0 + * @version 1.5.0 + * @since 1.4.0 * @see io.github.og4dev.advice.GlobalResponseWrapper * @see io.github.og4dev.dto.ApiResponse - * @since 1.4.0 */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface AutoResponse { + + /** + * The message to include in the {@code message} field of the produced + * {@link io.github.og4dev.dto.ApiResponse}. + *

+ * When set at class level, all methods in the controller use this message unless + * overridden at the method level. When set at method level, it takes precedence + * over any class-level value. + *

+ * + * @return the response message; defaults to {@code "Success"} + */ + String message() default "Success"; } \ No newline at end of file diff --git a/src/main/java/io/github/og4dev/annotation/AutoTrim.java b/src/main/java/io/github/og4dev/annotation/AutoTrim.java index 768079e..2187e28 100644 --- a/src/main/java/io/github/og4dev/annotation/AutoTrim.java +++ b/src/main/java/io/github/og4dev/annotation/AutoTrim.java @@ -9,7 +9,7 @@ * Annotation to explicitly enable automatic string trimming during JSON deserialization. *

* By default, the OG4Dev Spring API Response library does NOT automatically trim strings. - * This annotation allows you to opt-in to automatic trimming for specific fields or entire + * This annotation allows you to opt in to automatic trimming for specific fields or entire * classes where removing leading and trailing whitespace is desired for data quality and consistency. *

*

@@ -111,7 +111,7 @@ *

* * @author Pasindu OG - * @version 1.4.0 + * @version 1.5.0 * @see io.github.og4dev.config.ApiResponseAutoConfiguration#strictJsonCustomizer() * @see io.github.og4dev.annotation.XssCheck * @see tools.jackson.databind.ValueDeserializer#createContextual diff --git a/src/main/java/io/github/og4dev/annotation/XssCheck.java b/src/main/java/io/github/og4dev/annotation/XssCheck.java index aadffea..a7af91b 100644 --- a/src/main/java/io/github/og4dev/annotation/XssCheck.java +++ b/src/main/java/io/github/og4dev/annotation/XssCheck.java @@ -9,7 +9,7 @@ * Annotation to explicitly enable XSS (Cross-Site Scripting) validation for string fields during JSON deserialization. *

* By default, the OG4Dev Spring API Response library does NOT perform XSS validation on strings. - * This annotation allows you to opt-in to automatic HTML/XML tag detection and rejection for specific fields + * This annotation allows you to opt in to automatic HTML/XML tag detection and rejection for specific fields * or entire classes where preventing malicious content injection is critical for security. *

*

@@ -94,7 +94,7 @@ *

  • Self-closing tags: {@code
    }, {@code }
  • *
  • Special tags: {@code }, {@code }, {@code }
  • *
  • Tags with attributes: {@code
    }, {@code }
  • - *
  • Multiline tags: Tags spanning multiple lines (DOTALL mode enabled)
  • + *
  • Multiline tags: Tags spanning multiple lines (DOT ALL mode enabled)
  • * *

    * What is NOT detected (safe to use): @@ -201,7 +201,7 @@ * * * @author Pasindu OG - * @version 1.4.0 + * @version 1.5.0 * @see io.github.og4dev.config.ApiResponseAutoConfiguration#strictJsonCustomizer() * @see io.github.og4dev.annotation.AutoTrim * @see tools.jackson.databind.ValueDeserializer#createContextual diff --git a/src/main/java/io/github/og4dev/annotation/package-info.java b/src/main/java/io/github/og4dev/annotation/package-info.java index cbc9df8..9e79781 100644 --- a/src/main/java/io/github/og4dev/annotation/package-info.java +++ b/src/main/java/io/github/og4dev/annotation/package-info.java @@ -152,12 +152,10 @@ *

    * * @author Pasindu OG - * @version 1.4.0 + * @version 1.5.0 * @see io.github.og4dev.annotation.AutoTrim * @see io.github.og4dev.annotation.XssCheck * @see io.github.og4dev.config.ApiResponseAutoConfiguration * @since 1.3.0 */ -package io.github.og4dev.annotation; - - +package io.github.og4dev.annotation; \ No newline at end of file diff --git a/src/main/java/io/github/og4dev/config/ApiResponseAutoConfiguration.java b/src/main/java/io/github/og4dev/config/ApiResponseAutoConfiguration.java index f5df5a7..3d10b4a 100644 --- a/src/main/java/io/github/og4dev/config/ApiResponseAutoConfiguration.java +++ b/src/main/java/io/github/og4dev/config/ApiResponseAutoConfiguration.java @@ -4,7 +4,9 @@ import io.github.og4dev.annotation.AutoResponse; import io.github.og4dev.annotation.AutoTrim; import io.github.og4dev.annotation.XssCheck; +import io.github.og4dev.exception.ApiExceptionRegistry; import io.github.og4dev.exception.GlobalExceptionHandler; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer; import org.springframework.context.annotation.Bean; @@ -64,7 +66,7 @@ * * * @author Pasindu OG - * @version 1.4.0 + * @version 1.5.0 * @see GlobalExceptionHandler * @see GlobalResponseWrapper * @see org.springframework.boot.autoconfigure.AutoConfiguration @@ -88,11 +90,13 @@ public ApiResponseAutoConfiguration() { * {@link org.springframework.web.bind.annotation.RestControllerAdvice} mechanism, * automatically converting various exceptions to RFC 9457 ProblemDetail responses. *

    - * * @return A new instance of {@link GlobalExceptionHandler} registered as a Spring bean. + * + * @param registry optional registry used to map external exception types + * @return A new instance of {@link GlobalExceptionHandler} registered as a Spring bean. */ @Bean - public GlobalExceptionHandler apiResponseAdvisor() { - return new GlobalExceptionHandler(); + public GlobalExceptionHandler apiResponseAdvisor(@Autowired(required = false) ApiExceptionRegistry registry) { + return new GlobalExceptionHandler(registry); } /** @@ -123,13 +127,13 @@ public GlobalExceptionHandler apiResponseAdvisor() { * @RequestMapping("/api/users") * @AutoResponse // Enables automatic wrapping for all methods in this controller * public class UserController { - * * @GetMapping("/{id}") + * @GetMapping("/{id}") * public UserDto getUser(@PathVariable Long id) { * // Simply return the DTO. It will be sent to the client as: * // { "status": "Success", "content": { "id": 1, "name": "..." }, "timestamp": "..." } * return userService.findById(id); * } - * * @PostMapping + * @PostMapping * @ResponseStatus(HttpStatus.CREATED) * public UserDto createUser(@RequestBody UserDto dto) { * // The 201 Created status will be preserved in the final ApiResponse @@ -163,103 +167,11 @@ public GlobalResponseWrapper globalResponseWrapper(ObjectMapper objectMapper) { * trimmed or XSS-validated unless explicitly annotated. *

    * - *

    Overview of Features

    - *

    - * This configuration provides four critical layers of protection and data processing: - *

    - *
      - *
    1. Strict Property Validation - Prevents mass assignment attacks (automatic)
    2. - *
    3. Case-Insensitive Enum Handling - Improves API usability (automatic)
    4. - *
    5. Opt-in XSS Prevention - Blocks HTML/XML injection attacks (requires {@code @XssCheck} on field or class)
    6. - *
    7. Opt-in String Trimming - Removes whitespace (requires {@code @AutoTrim} on field or class)
    8. - *
    - * - *

    Feature 1: Strict Property Validation

    - *

    - * Enables {@link DeserializationFeature#FAIL_ON_UNKNOWN_PROPERTIES} to reject JSON - * payloads containing unexpected fields. This prevents three critical security issues: - *

    - *
      - *
    • Mass Assignment Vulnerabilities: Attackers cannot inject fields like {@code isAdmin: true}
    • - *
    • Data Injection Attacks: Prevents modification of unintended database fields
    • - *
    • Client Errors: Detects typos early, preventing silent data loss
    • - *
    - * - *

    Feature 2: Case-Insensitive Enum Handling

    - *

    - * Enables {@link MapperFeature#ACCEPT_CASE_INSENSITIVE_ENUMS} for flexible enum - * deserialization. Clients can send enum values in any case format, improving API - * usability without compromising type safety or security. - *

    - * - *

    Feature 3: Opt-in XSS Prevention with @XssCheck

    - *

    - * Registers a custom {@link StdScalarDeserializer} ({@code AdvancedStringDeserializer}) - * that performs automatic HTML/XML tag detection and rejection at the deserialization layer - * for fields or classes annotated with {@link XssCheck @XssCheck}. This provides fail-fast security - * that prevents malicious content from ever entering your system. - *

    - * - *

    Feature 4: Opt-in String Trimming with @AutoTrim

    - *

    - * Automatically removes leading and trailing whitespace from string fields or entire classes annotated with - * {@link AutoTrim @AutoTrim}, improving data quality and preventing common user input errors. - *

    - * - *

    Combining Features (Class and Field Level)

    - *

    - * You can combine these annotations at both the class and field levels. Class-level annotations - * apply to all string fields within the class automatically: - *

    - *
    {@code
    -     * @AutoTrim // Automatically trims ALL strings in this class
    -     * public class SecureDTO {
    -     * @XssCheck
    -     * private String cleanInput;  // Both trimmed (from class scope) and XSS-validated
    -     * * private String email;       // Only trimmed (from class scope)
    -     * }
    -     * }
    - * - *

    Implementation Details

    - *

    - * This method registers an inner class {@code AdvancedStringDeserializer} that extends - * {@link StdScalarDeserializer}{@code }. The deserializer operates in different modes - * based on annotations: - *

    - *
      - *
    • Default Mode: No processing (preserves original value)
    • - *
    • Trim Mode: {@code shouldTrim = true} when {@code @AutoTrim} is present on the field or class
    • - *
    • XSS Mode: {@code shouldXssCheck = true} when {@code @XssCheck} is present on the field or class
    • - *
    • Combined Mode: Both trimming and validation when both annotations are present
    • - *
    - *

    - * The {@code createContextual()} method inspects each field's annotations, as well as its - * declaring class's annotations, during deserialization context creation and returns an - * appropriately configured deserializer instance. - *

    - * - *

    Null Value Handling

    - *

    - * Null values are preserved and never converted to empty strings. - *

    - * - *

    Performance Considerations

    - *

    - * The regex validation and trimming operations are highly optimized and add negligible overhead - * (typically {@code <1ms} per request). The contextual deserializer is created once per field - * during mapper initialization, not on every request, ensuring optimal runtime performance. - *

    - * - * @return A {@link JsonMapperBuilderCustomizer} that configures strict JSON processing - * with opt-in string validation via {@code @XssCheck}, opt-in trimming via {@code @AutoTrim}, - * unknown property rejection, and case-insensitive enum handling. + * @return A {@link JsonMapperBuilderCustomizer} that configures strict JSON processing. * @see DeserializationFeature#FAIL_ON_UNKNOWN_PROPERTIES * @see MapperFeature#ACCEPT_CASE_INSENSITIVE_ENUMS * @see io.github.og4dev.annotation.AutoTrim * @see io.github.og4dev.annotation.XssCheck - * @see JsonMapperBuilderCustomizer - * @see StdScalarDeserializer - * @see ValueDeserializer#createContextual(DeserializationContext, BeanProperty) * @since 1.1.0 */ @Bean @@ -267,53 +179,68 @@ public JsonMapperBuilderCustomizer strictJsonCustomizer() { return builder -> { builder.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); builder.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS); + SimpleModule stringTrimModule = new SimpleModule(); + stringTrimModule.addDeserializer(String.class, new AdvancedStringDeserializer()); + builder.addModules(stringTrimModule); + }; + } + + /** + * A specialized deserializer that applies opt-in string trimming and XSS validation. + * Extracted as a private static class to reduce cognitive complexity. + */ + private static class AdvancedStringDeserializer extends StdScalarDeserializer { + private final boolean shouldTrim; + private final boolean shouldXssCheck; - class AdvancedStringDeserializer extends StdScalarDeserializer { - private final boolean shouldTrim; - private final boolean shouldXssCheck; + public AdvancedStringDeserializer() { + super(String.class); + this.shouldTrim = false; + this.shouldXssCheck = false; + } - public AdvancedStringDeserializer() { - super(String.class); - this.shouldTrim = false; - this.shouldXssCheck = false; - } + public AdvancedStringDeserializer(boolean shouldTrim, boolean shouldXssCheck) { + super(String.class); + this.shouldTrim = shouldTrim; + this.shouldXssCheck = shouldXssCheck; + } - public AdvancedStringDeserializer(boolean shouldTrim, boolean shouldXssCheck) { - super(String.class); - this.shouldTrim = shouldTrim; - this.shouldXssCheck = shouldXssCheck; - } + @Override + public String deserialize(JsonParser p, DeserializationContext text) throws JacksonException { + String value = p.getValueAsString(); + if (value == null) { + return null; + } - @Override - public String deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { - String value = p.getValueAsString(); - if (value == null) return null; - String processedValue = shouldTrim ? value.trim() : value; - if (shouldXssCheck && processedValue.matches("(?s).*<\\s*[a-zA-Z/!].*")) throw new IllegalArgumentException("Security Error: HTML tags or XSS payloads are not allowed in the request."); - return processedValue; - } + String processedValue = shouldTrim ? value.trim() : value; + + if (shouldXssCheck && processedValue.matches("(?s).*<\\s*[a-zA-Z/!].*")) { + throw new IllegalArgumentException("Security Error: HTML tags or XSS payloads are not allowed in the request."); + } + + return processedValue; + } - @Override - public ValueDeserializer createContextual(DeserializationContext ct, BeanProperty property) throws JacksonException { - if (property != null) { - boolean trim = property.getAnnotation(AutoTrim.class) != null; - boolean xss = property.getAnnotation(XssCheck.class) != null; + @Override + public ValueDeserializer createContextual(DeserializationContext ct, BeanProperty property) throws JacksonException { + if (property == null) { + return this; + } - if ((!trim || !xss) && property.getMember() != null) { - Class declaringClass = property.getMember().getDeclaringClass(); - if (declaringClass != null) { - if (!trim) trim = declaringClass.getAnnotation(AutoTrim.class) != null; - if (!xss) xss = declaringClass.getAnnotation(XssCheck.class) != null; - } - } - return new AdvancedStringDeserializer(trim, xss); - } - return this; + boolean trim = property.getAnnotation(AutoTrim.class) != null; + boolean xss = property.getAnnotation(XssCheck.class) != null; + if (trim && xss) { + return new AdvancedStringDeserializer(true, true); + } + if (property.getMember() != null) { + Class declaringClass = property.getMember().getDeclaringClass(); + if (declaringClass != null) { + trim = trim || declaringClass.getAnnotation(AutoTrim.class) != null; + xss = xss || declaringClass.getAnnotation(XssCheck.class) != null; } } - stringTrimModule.addDeserializer(String.class, new AdvancedStringDeserializer()); - builder.addModules(stringTrimModule); - }; + return new AdvancedStringDeserializer(trim, xss); + } } } \ No newline at end of file diff --git a/src/main/java/io/github/og4dev/config/package-info.java b/src/main/java/io/github/og4dev/config/package-info.java index 3aedd22..950e854 100644 --- a/src/main/java/io/github/og4dev/config/package-info.java +++ b/src/main/java/io/github/og4dev/config/package-info.java @@ -12,7 +12,7 @@ *

    * * @author Pasindu OG - * @version 1.4.0 + * @version 1.5.0 * @since 1.0.0 */ package io.github.og4dev.config; \ No newline at end of file diff --git a/src/main/java/io/github/og4dev/dto/ApiResponse.java b/src/main/java/io/github/og4dev/dto/ApiResponse.java index 0c81a03..0fdcba6 100644 --- a/src/main/java/io/github/og4dev/dto/ApiResponse.java +++ b/src/main/java/io/github/og4dev/dto/ApiResponse.java @@ -49,7 +49,7 @@ * * @param the type of the response content (can be any Java type or Void for no content) * @author Pasindu OG - * @version 1.4.0 + * @version 1.5.0 * @since 1.0.0 * @see org.springframework.http.ResponseEntity * @see org.springframework.http.HttpStatus diff --git a/src/main/java/io/github/og4dev/dto/package-info.java b/src/main/java/io/github/og4dev/dto/package-info.java index 6c20b78..e56adf5 100644 --- a/src/main/java/io/github/og4dev/dto/package-info.java +++ b/src/main/java/io/github/og4dev/dto/package-info.java @@ -17,7 +17,7 @@ *

    * * @author Pasindu OG - * @version 1.4.0 + * @version 1.5.0 * @since 1.0.0 */ package io.github.og4dev.dto; \ No newline at end of file diff --git a/src/main/java/io/github/og4dev/exception/ApiException.java b/src/main/java/io/github/og4dev/exception/ApiException.java index 77cf7f4..10e38f1 100644 --- a/src/main/java/io/github/og4dev/exception/ApiException.java +++ b/src/main/java/io/github/og4dev/exception/ApiException.java @@ -38,7 +38,7 @@ * * * @author Pasindu OG - * @version 1.4.0 + * @version 1.5.0 * @since 1.0.0 * @see io.github.og4dev.exception.GlobalExceptionHandler * @see org.springframework.http.HttpStatus diff --git a/src/main/java/io/github/og4dev/exception/ApiExceptionRegistry.java b/src/main/java/io/github/og4dev/exception/ApiExceptionRegistry.java new file mode 100644 index 0000000..2ff4a28 --- /dev/null +++ b/src/main/java/io/github/og4dev/exception/ApiExceptionRegistry.java @@ -0,0 +1,105 @@ +package io.github.og4dev.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.util.Assert; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * A thread-safe registry for dynamically mapping third-party or framework-specific exceptions + * to standard HTTP statuses and user-friendly messages. + *

    + * This registry allows developers to handle external exceptions (for example, SQL, Mongo, or + * authentication exceptions) centrally without writing dedicated {@code @ExceptionHandler} + * methods for each type. + *

    + * + * @author Pasindu OG + * @version 1.5.0 + * @since 1.5.0 + */ +public class ApiExceptionRegistry { + + private final Map, ExceptionRule> registry = Collections.synchronizedMap(new LinkedHashMap<>()); + + /** + * Registers a custom or third-party exception type with a specific HTTP status and message. + *

    + * Registration order matters when both parent and child exception types are registered. + * The first assignable entry wins during lookup. + *

    + * + * @param exceptionClass the exception class to map + * @param status the HTTP status to return + * @param defaultMessage the fallback client-facing message + * @param the exception type + * @return this registry for fluent chaining + * @throws IllegalArgumentException if any argument is null or message is blank + */ + @SuppressWarnings("unused") + public ApiExceptionRegistry register(Class exceptionClass, HttpStatus status, String defaultMessage) { + Assert.notNull(exceptionClass, "Exception class must not be null"); + Assert.notNull(status, "HttpStatus must not be null"); + Assert.hasText(defaultMessage, "Default message must not be empty or null"); + + registry.put(exceptionClass, new ExceptionRule(status, defaultMessage)); + return this; + } + + /** + * Returns the first matching rule for the given thrown exception type. + * + * @param exceptionClass the thrown exception class + * @return matching rule or {@code null} when no mapping exists + */ + public ExceptionRule getRule(Class exceptionClass) { + if (exceptionClass == null) { + return null; + } + + synchronized (registry) { + for (Map.Entry, ExceptionRule> entry : registry.entrySet()) { + if (entry.getKey().isAssignableFrom(exceptionClass)) { + return entry.getValue(); + } + } + } + return null; + } + + /** + * Value object containing status and message for a registered exception mapping. + */ + @SuppressWarnings("ClassCanBeRecord") + public static class ExceptionRule { + private final HttpStatus status; + private final String message; + + /** + * Creates a new exception mapping rule. + * + * @param status mapped HTTP status + * @param message mapped client-facing message + */ + public ExceptionRule(HttpStatus status, String message) { + this.status = status; + this.message = message; + } + + /** + * @return mapped HTTP status + */ + public HttpStatus getStatus() { + return status; + } + + /** + * @return mapped client-facing message + */ + public String getMessage() { + return message; + } + } +} \ No newline at end of file diff --git a/src/main/java/io/github/og4dev/exception/GlobalExceptionHandler.java b/src/main/java/io/github/og4dev/exception/GlobalExceptionHandler.java index e2a3499..cc6b143 100644 --- a/src/main/java/io/github/og4dev/exception/GlobalExceptionHandler.java +++ b/src/main/java/io/github/og4dev/exception/GlobalExceptionHandler.java @@ -3,6 +3,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.http.HttpStatus; import org.springframework.http.ProblemDetail; @@ -34,28 +35,28 @@ * Supported Exception Types (10 handlers): *

    *
      - *
    1. General Exceptions - Catches all unhandled exceptions (HTTP 500)
    2. - *
    3. Validation Errors - {@code @Valid} annotation failures (HTTP 400)
    4. - *
    5. Type Mismatches - Method argument type conversion errors (HTTP 400)
    6. - *
    7. Malformed JSON - Invalid request body format (HTTP 400)
    8. - *
    9. Missing Parameters - Required {@code @RequestParam} missing (HTTP 400)
    10. - *
    11. 404 Not Found - Missing endpoints or resources (HTTP 404)
    12. - *
    13. Method Not Allowed - Unsupported HTTP methods (HTTP 405)
    14. - *
    15. Unsupported Media Type - Invalid Content-Type headers (HTTP 415)
    16. - *
    17. Null Pointer Exceptions - NullPointerException handling (HTTP 500)
    18. - *
    19. Custom API Exceptions - Domain-specific business logic errors (custom status)
    20. + *
    21. General Exceptions - Catches all unhandled exceptions (HTTP 500)
    22. + *
    23. Validation Errors - {@code @Valid} annotation failures (HTTP 400)
    24. + *
    25. Type Mismatches - Method argument type conversion errors (HTTP 400)
    26. + *
    27. Malformed JSON - Invalid request body format (HTTP 400)
    28. + *
    29. Missing Parameters - Required {@code @RequestParam} missing (HTTP 400)
    30. + *
    31. 404 Not Found - Missing endpoints or resources (HTTP 404)
    32. + *
    33. Method Not Allowed - Unsupported HTTP methods (HTTP 405)
    34. + *
    35. Unsupported Media Type - Invalid Content-Type headers (HTTP 415)
    36. + *
    37. Null Pointer Exceptions - NullPointerException handling (HTTP 500)
    38. + *
    39. Custom API Exceptions - Domain-specific business logic errors (custom status)
    40. *
    *

    * Error Response Format (RFC 9457 ProblemDetail): *

    *
      - *
    • type - URI reference identifying the problem type (defaults to "about:blank")
    • - *
    • title - Short, human-readable summary of the problem
    • - *
    • status - HTTP status code
    • - *
    • detail - Human-readable explanation specific to this occurrence
    • - *
    • traceId - Unique UUID for request correlation and debugging
    • - *
    • timestamp - RFC 3339 UTC timestamp
    • - *
    • errors - Validation field errors (for validation failures only)
    • + *
    • type - URI reference identifying the problem type (defaults to "about:blank")
    • + *
    • title - Short, human-readable summary of the problem
    • + *
    • status - HTTP status code
    • + *
    • detail - Human-readable explanation specific to this occurrence
    • + *
    • traceId - Unique UUID for request correlation and debugging
    • + *
    • timestamp - RFC 3339 UTC timestamp
    • + *
    • errors - Validation field errors (for validation failures only)
    • *
    *

    * Trace ID Management: All exception handlers ensure consistent trace IDs between logs @@ -70,12 +71,12 @@ * Logging: All exceptions are automatically logged with appropriate severity levels: *

    *
      - *
    • ERROR - General exceptions, null pointer exceptions
    • - *
    • WARN - Validation errors, type mismatches, business logic exceptions, 400/404/405/415 errors
    • + *
    • ERROR - General exceptions, null pointer exceptions
    • + *
    • WARN - Validation errors, type mismatches, business logic exceptions, 400/404/405/415 errors
    • *
    * * @author Pasindu OG - * @version 1.4.0 + * @version 1.5.0 * @since 1.0.0 * @see org.springframework.web.bind.annotation.RestControllerAdvice * @see org.springframework.http.ProblemDetail @@ -92,11 +93,15 @@ public class GlobalExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + private final ApiExceptionRegistry registry; + /** - * Default constructor for Spring bean instantiation. + * Constructor for Spring bean instantiation, accepting an optional ApiExceptionRegistry. + * + * @param registry the optional API exception registry for custom exception mapping */ - public GlobalExceptionHandler() { - // Default constructor for Spring bean instantiation + public GlobalExceptionHandler(@Autowired(required = false) ApiExceptionRegistry registry) { + this.registry = registry; } /** @@ -114,14 +119,16 @@ private String getOrGenerateTraceId() { } /** - * Handles all unhandled exceptions. + * Handles all unhandled exceptions, logs them with stack trace details, + * and dynamically maps them via ApiExceptionRegistry if configured. * * @param ex the exception - * @return ProblemDetail response with 500 status + * @return ProblemDetail response with appropriate status */ @ExceptionHandler(Exception.class) public ProblemDetail handleAllExceptions(Exception ex) { String traceId = getOrGenerateTraceId(); + StackTraceElement rootCause = ex.getStackTrace().length > 0 ? ex.getStackTrace()[0] : null; String className = (rootCause != null) ? rootCause.getClassName() : "Unknown Class"; int lineNumber = (rootCause != null) ? rootCause.getLineNumber() : -1; @@ -129,9 +136,21 @@ public ProblemDetail handleAllExceptions(Exception ex) { log.error("[TraceID: {}] Error in {}:{} - Message: {}", traceId, className, lineNumber, ex.getMessage()); + // Check if the exception is registered in the ApiExceptionRegistry + if (registry != null) { + ApiExceptionRegistry.ExceptionRule rule = registry.getRule(ex.getClass()); + if (rule != null) { + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(rule.getStatus(), rule.getMessage()); + problemDetail.setProperty("traceId", traceId); + problemDetail.setProperty("timestamp", Instant.now()); + return problemDetail; + } + } + + // Default Fallback ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail( HttpStatus.INTERNAL_SERVER_ERROR, - "Internal Server Error. Please contact technical support"); + "Internal Server Error. Please contact technical support."); problemDetail.setProperty("traceId", traceId); problemDetail.setProperty("timestamp", Instant.now()); return problemDetail; @@ -170,8 +189,14 @@ public ProblemDetail handleValidationExceptions(MethodArgumentNotValidException @ExceptionHandler(MethodArgumentTypeMismatchException.class) public ProblemDetail handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException ex) { String traceId = getOrGenerateTraceId(); + + // SonarQube Fix: Assigning getRequiredType() to a variable before checking for null + Class requiredType = ex.getRequiredType(); + String expectedType = (requiredType != null) ? requiredType.getSimpleName() : "Unknown"; + String errorMessage = String.format("Invalid value '%s' for parameter '%s'. Expected type: %s.", - ex.getValue(), ex.getName(), ex.getRequiredType() != null ? ex.getRequiredType().getSimpleName() : "Unknown"); + ex.getValue(), ex.getName(), expectedType); + log.warn("[TraceID: {}] Type mismatch error: {}", traceId, errorMessage); ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, errorMessage); problemDetail.setProperty("traceId", traceId); diff --git a/src/main/java/io/github/og4dev/exception/package-info.java b/src/main/java/io/github/og4dev/exception/package-info.java index 6e81223..e41ea33 100644 --- a/src/main/java/io/github/og4dev/exception/package-info.java +++ b/src/main/java/io/github/og4dev/exception/package-info.java @@ -26,8 +26,7 @@ * * * @author Pasindu OG - * @version 1.4.0 + * @version 1.5.0 * @since 1.0.0 */ package io.github.og4dev.exception; - diff --git a/src/main/java/io/github/og4dev/filter/TraceIdFilter.java b/src/main/java/io/github/og4dev/filter/TraceIdFilter.java index 658d146..aa67152 100644 --- a/src/main/java/io/github/og4dev/filter/TraceIdFilter.java +++ b/src/main/java/io/github/og4dev/filter/TraceIdFilter.java @@ -30,7 +30,7 @@ *
  • Compatible with microservices architectures
  • * *

    - * Usage: Register this filter as a Spring bean with highest precedence: + * Usage: Register this filter as a Spring bean with the highest precedence: *

    *
    {@code
      * @Configuration
    @@ -57,7 +57,7 @@
      * 

    * * @author Pasindu OG - * @version 1.4.0 + * @version 1.5.0 * @since 1.0.0 * @see org.springframework.web.filter.OncePerRequestFilter * @see org.slf4j.MDC @@ -96,4 +96,4 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse MDC.clear(); } } -} +} \ No newline at end of file diff --git a/src/main/java/io/github/og4dev/filter/package-info.java b/src/main/java/io/github/og4dev/filter/package-info.java index dcff3f0..4bbbf7c 100644 --- a/src/main/java/io/github/og4dev/filter/package-info.java +++ b/src/main/java/io/github/og4dev/filter/package-info.java @@ -21,8 +21,7 @@ *

    * * @author Pasindu OG - * @version 1.4.0 + * @version 1.5.0 * @since 1.0.0 */ package io.github.og4dev.filter; - diff --git a/src/main/java/io/github/og4dev/package-info.java b/src/main/java/io/github/og4dev/package-info.java index a471b48..efb2abf 100644 --- a/src/main/java/io/github/og4dev/package-info.java +++ b/src/main/java/io/github/og4dev/package-info.java @@ -10,8 +10,7 @@ *

    * * @author Pasindu OG - * @version 1.4.0 + * @version 1.5.0 * @since 1.0.0 */ package io.github.og4dev; -