GlobalExceptionHandler.java
package com.smartsupplypro.inventory.exception;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.server.ResponseStatusException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException;
/**
* Enterprise global exception handler for REST API standardization.
*
* <p>Provides unified error response structure, HTTP status code mapping, and consistent
* client experience across all API endpoints with comprehensive exception translation.
*
* <p><strong>Response Format</strong>: {@code {"error": "status_token", "message": "description"}}
*
* <p><strong>Status Mapping Strategy</strong>:
* <ul>
* <li><strong>400</strong> – Client validation errors, malformed requests, parameter issues</li>
* <li><strong>401</strong> – Authentication failures, missing credentials</li>
* <li><strong>403</strong> – Authorization failures, insufficient permissions</li>
* <li><strong>404</strong> – Resource not found, invalid endpoints</li>
* <li><strong>409</strong> – Business conflicts, constraint violations, concurrent updates</li>
* <li><strong>500</strong> – Unhandled server errors, system failures</li>
* </ul>
*
* <p>Key integrations: REST controllers, Service validation, Security framework, Data layer.
*
* @author Smart Supply Pro Development Team
* @version 1.0.0
* @since 1.0.0
*/
@Order(Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice
public class GlobalExceptionHandler {
/* =======================================================================
* Helpers
* ======================================================================= */
/**
* Constructs standardized JSON error response with HTTP status mapping.
*
* <p>Ensures consistent API contract with normalized error tokens and
* human-readable messages for reliable client error handling.
*
* @param status HTTP status code for response
* @param message descriptive error message for client
* @return standardized JSON error response entity
*/
private ResponseEntity<Map<String, String>> body(HttpStatus status, String message) {
Map<String, String> map = new HashMap<>();
map.put("error", errorCode(status));
map.put("message", nonEmpty(message, status.getReasonPhrase()));
map.put("timestamp", java.time.Instant.now().toString());
map.put("correlationId", generateCorrelationId());
return ResponseEntity.status(status)
.contentType(MediaType.APPLICATION_JSON)
.body(map);
}
/**
* Generates unique correlation ID for request tracking and debugging.
*
* @return formatted correlation ID
*/
private String generateCorrelationId() {
return "SSP-" + System.currentTimeMillis() + "-" +
java.util.concurrent.ThreadLocalRandom.current().nextInt(1000, 9999);
}
/**
* Sanitizes error messages to prevent sensitive information disclosure.
*
* @param message original error message
* @return sanitized message safe for client response
*/
private String sanitizeMessage(String message) {
if (message == null) return "Unknown error";
// Remove sensitive patterns
return message
.replaceAll("\\b[A-Za-z]:\\\\[\\w\\\\.-]+", "[PATH]")
.replaceAll("\\bcom\\.smartsupplypro\\.[\\w.]+", "[INTERNAL]")
.replaceAll("\\bSQL.*", "Database operation failed")
.replaceAll("\\bPassword.*", "Authentication failed")
.replaceAll("\\bToken.*", "Authentication failed")
.trim();
}
/** Normalize an HTTP status into a lowercase token (e.g., BAD_REQUEST → "bad_request"). */
private static String errorCode(HttpStatus status) {
return status.name().toLowerCase(); // "bad_request", "not_found", "conflict", etc.
}
private static String nonEmpty(String s, String fallback) {
return (s == null || s.isBlank()) ? fallback : s;
}
/* =======================================================================
* 400 Bad Request family
* ======================================================================= */
/**
* Handles custom application validation failures with business context.
*
* @param ex application-specific validation exception
* @return 400 Bad Request with validation details
*/
@ExceptionHandler(InvalidRequestException.class)
public ResponseEntity<Map<String, String>> handleInvalid(InvalidRequestException ex) {
String message = ex.hasFieldErrors() ?
"Validation failed: " + ex.getFieldErrors().size() + " field error(s)" :
nonEmpty(ex.getMessage(), "Invalid request");
// Use sanitized message for response
return body(HttpStatus.BAD_REQUEST, sanitizeMessage(message));
}
/**
* Bean validation failures on request bodies with enhanced field error extraction.
*
* @param ex Spring validation exception with field-level details
* @return 400 Bad Request with specific field validation errors
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
String first = ex.getBindingResult().getFieldErrors().stream()
.findFirst()
.map(fe -> fe.getField() + " " + fe.getDefaultMessage())
.orElse("Validation failed");
return body(HttpStatus.BAD_REQUEST, sanitizeMessage(first));
}
/**
* Constraint validation failures with enhanced error path extraction.
*
* @param ex constraint violation exception with validation details
* @return 400 Bad Request with constraint violation details
*/
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Map<String, String>> handleConstraintViolation(ConstraintViolationException ex) {
String first = ex.getConstraintViolations().stream()
.findFirst()
.map(v -> v.getPropertyPath() + " " + v.getMessage())
.orElse("Constraint violation");
return body(HttpStatus.BAD_REQUEST, sanitizeMessage(first));
}
/**
* JSON parsing failures with enhanced error context.
*
* @param ex HTTP message parsing exception
* @return 400 Bad Request with parsing error details
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<Map<String, String>> handleNotReadable(HttpMessageNotReadableException ex) {
return body(HttpStatus.BAD_REQUEST, sanitizeMessage("Request body is invalid or unreadable"));
}
/**
* Missing required request parameters with parameter identification.
*
* @param ex missing parameter exception with parameter details
* @return 400 Bad Request with missing parameter information
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity<Map<String, String>> handleMissingParam(MissingServletRequestParameterException ex) {
return body(HttpStatus.BAD_REQUEST, "Missing required parameter: " + ex.getParameterName());
}
/**
* Parameter type conversion failures with field identification.
*
* @param ex type mismatch exception with parameter details
* @return 400 Bad Request with type conversion error
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<Map<String, String>> handleTypeMismatch(MethodArgumentTypeMismatchException ex) {
return body(HttpStatus.BAD_REQUEST, "Invalid value for: " + ex.getName());
}
/* =======================================================================
* 401 / 403 Security
* ======================================================================= */
/**
* Authentication failures with secure error response for credential issues.
*
* @param ex Spring Security authentication exception
* @return 401 Unauthorized with generic authentication message
*/
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<Map<String, String>> handleAuthentication(AuthenticationException ex) {
return body(HttpStatus.UNAUTHORIZED, "Authentication required");
}
/**
* Authorization failures with role-based access control enforcement.
*
* @param ex Spring Security access denied exception
* @return 403 Forbidden with generic access denied message
*/
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<Map<String, String>> handleAccessDenied(AccessDeniedException ex) {
return body(HttpStatus.FORBIDDEN, "Access denied");
}
/* =======================================================================
* 404 Not Found
* ======================================================================= */
/**
* Resource existence validation failures with sanitized error messages.
*
* @param ex illegal argument exception indicating resource not found
* @return 404 Not Found with sanitized resource identification
*/
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Map<String, String>> handleIllegalArgument(IllegalArgumentException ex) {
return body(HttpStatus.NOT_FOUND, sanitizeMessage(nonEmpty(ex.getMessage(), "Resource not found")));
}
/**
* Repository lookup failures with enhanced error context.
*
* @param ex no such element exception from repository operations
* @return 404 Not Found with sanitized error details
*/
@ExceptionHandler(NoSuchElementException.class)
public ResponseEntity<Map<String, String>> handleNoSuchElement(NoSuchElementException ex) {
return body(HttpStatus.NOT_FOUND, sanitizeMessage(nonEmpty(ex.getMessage(), "Resource not found")));
}
/**
* Static resource access failures for missing web assets.
*
* @return 404 Not Found with no response body for resource efficiency
*/
@ExceptionHandler(org.springframework.web.servlet.resource.NoResourceFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public void notFound() { /* no body */ }
/* =======================================================================
* 409 Conflict
* ======================================================================= */
/**
* Handles enterprise duplicate resource violations with conflict resolution.
*
* @param ex business rule uniqueness constraint exception
* @return 409 Conflict with resource conflict details
*/
@ExceptionHandler(DuplicateResourceException.class)
public ResponseEntity<Map<String, String>> handleDuplicate(DuplicateResourceException ex) {
String message = ex.hasDetailedContext() ?
ex.getClientMessage() :
nonEmpty(ex.getMessage(), "Duplicate resource");
return body(HttpStatus.CONFLICT, sanitizeMessage(message));
}
/**
* Database constraint violations with enterprise conflict resolution.
*
* @param ex Spring Data integrity violation exception
* @return 409 Conflict with sanitized database constraint details
*/
@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<Map<String, String>> handleDataIntegrity(DataIntegrityViolationException ex) {
return body(HttpStatus.CONFLICT, sanitizeMessage("Data conflict"));
}
/**
* Business state violations with operational context preservation.
*
* <p>Handles enterprise business rule conflicts such as deleting suppliers with
* active inventory or state transition violations requiring specific conditions.</p>
*
* @param ex illegal state exception indicating business rule violation
* @return 409 Conflict with business context details
*/
@ExceptionHandler(IllegalStateException.class)
public ResponseEntity<Map<String, String>> handleIllegalState(IllegalStateException ex) {
return body(HttpStatus.CONFLICT, sanitizeMessage(nonEmpty(ex.getMessage(), "Business rule conflict")));
}
/**
* Concurrent modification conflicts with optimistic locking support.
*
* @param ex JPA optimistic locking failure exception
* @return 409 Conflict with concurrent update notification
*/
@ExceptionHandler(ObjectOptimisticLockingFailureException.class)
public ResponseEntity<Map<String, String>> handleOptimistic(ObjectOptimisticLockingFailureException ex) {
return body(HttpStatus.CONFLICT, "Concurrent update detected - please retry");
}
/* =======================================================================
* Pass-through (ResponseStatusException)
* ======================================================================= */
/**
* ResponseStatusException pass-through with status preservation and enhanced tracking.
*
* <p>Maintains original HTTP status codes while adding enterprise tracking capabilities
* for explicit controller-level exceptions and custom status scenarios.</p>
*
* @param ex response status exception with embedded HTTP status
* @param request HTTP servlet request for context extraction
* @return original status code with enhanced error response structure
*/
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<Map<String, String>> handleResponseStatus(ResponseStatusException ex,
HttpServletRequest request) {
Map<String, String> map = new HashMap<>();
map.put("error", ex.getStatusCode().toString().toLowerCase());
map.put("message", sanitizeMessage(nonEmpty(ex.getReason(), "Request failed")));
map.put("timestamp", java.time.Instant.now().toString());
map.put("correlationId", generateCorrelationId());
return ResponseEntity.status(ex.getStatusCode())
.contentType(MediaType.APPLICATION_JSON)
.body(map);
}
/* =======================================================================
* 500 Internal Server Error (safety net)
* ======================================================================= */
/**
* Enterprise safety net for unhandled exceptions with comprehensive error tracking.
*
* <p>Provides stable API contract while preventing sensitive information disclosure.
* Includes correlation tracking for debugging and monitoring integration.</p>
*
* @param ex any unhandled exception reaching the global handler
* @return 500 Internal Server Error with sanitized generic message
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, String>> handleAny(Exception ex) {
// Note: Consider adding ERROR-level logging with correlation ID for production monitoring
return body(HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected server error");
}
}