Skip to content

Commit 2e33cdb

Browse files
refactor(config): update API error handling, security, OpenAPI, model mapping, and app properties
1 parent b435cfa commit 2e33cdb

6 files changed

Lines changed: 470 additions & 0 deletions

File tree

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package com.countyhospital.healthapi.common.dto;
2+
3+
import java.time.LocalDateTime;
4+
import java.util.ArrayList;
5+
import java.util.List;
6+
import java.util.Objects;
7+
8+
import com.fasterxml.jackson.annotation.JsonFormat;
9+
import com.fasterxml.jackson.annotation.JsonInclude;
10+
11+
import io.swagger.v3.oas.annotations.media.Schema;
12+
13+
@JsonInclude(JsonInclude.Include.NON_NULL)
14+
@Schema(description = "Standard API error response structure")
15+
public class ApiErrorResponse {
16+
17+
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
18+
@Schema(description = "Timestamp when the error occurred", example = "2025-11-15 10:30:00")
19+
private LocalDateTime timestamp;
20+
21+
@Schema(description = "HTTP status code", example = "400")
22+
private int status;
23+
24+
@Schema(description = "HTTP status description", example = "Bad Request")
25+
private String error;
26+
27+
@Schema(description = "Detailed error message", example = "Validation failed")
28+
private String message;
29+
30+
@Schema(description = "API path where the error occurred", example = "/api/patients")
31+
private String path;
32+
33+
@Schema(description = "List of field-specific validation errors")
34+
private List<ValidationError> validationErrors;
35+
36+
@Schema(description = "Debug message for developers (only in non-production)")
37+
private String debugMessage;
38+
39+
// Constructors
40+
public ApiErrorResponse() {
41+
this.timestamp = LocalDateTime.now();
42+
}
43+
44+
public ApiErrorResponse(int status, String error, String message, String path) {
45+
this();
46+
this.status = status;
47+
this.error = error;
48+
this.message = message;
49+
this.path = path;
50+
}
51+
52+
public ApiErrorResponse(int status, String error, String message, String path, String debugMessage) {
53+
this(status, error, message, path);
54+
this.debugMessage = debugMessage;
55+
}
56+
57+
// Builder pattern
58+
public static ApiErrorResponseBuilder builder() {
59+
return new ApiErrorResponseBuilder();
60+
}
61+
62+
// Getters and setters
63+
public LocalDateTime getTimestamp() { return timestamp; }
64+
public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
65+
66+
public int getStatus() { return status; }
67+
public void setStatus(int status) { this.status = status; }
68+
69+
public String getError() { return error; }
70+
public void setError(String error) { this.error = error; }
71+
72+
public String getMessage() { return message; }
73+
public void setMessage(String message) { this.message = message; }
74+
75+
public String getPath() { return path; }
76+
public void setPath(String path) { this.path = path; }
77+
78+
public List<ValidationError> getValidationErrors() { return validationErrors; }
79+
public void setValidationErrors(List<ValidationError> validationErrors) { this.validationErrors = validationErrors; }
80+
81+
public String getDebugMessage() { return debugMessage; }
82+
public void setDebugMessage(String debugMessage) { this.debugMessage = debugMessage; }
83+
84+
// Validation error
85+
public void addValidationError(String field, String message) {
86+
if (this.validationErrors == null) {
87+
this.validationErrors = new ArrayList<>();
88+
}
89+
this.validationErrors.add(new ValidationError(field, message));
90+
}
91+
92+
// Validation error inner class
93+
@JsonInclude(JsonInclude.Include.NON_NULL)
94+
public static class ValidationError {
95+
@Schema(description = "Field name that failed validation", example = "email")
96+
private final String field;
97+
98+
@Schema(description = "Validation error message", example = "Email must be valid")
99+
private final String message;
100+
101+
@Schema(description = "Rejected value", example = "invalid-email")
102+
private final Object rejectedValue;
103+
104+
public ValidationError(String field, String message) {
105+
this.field = field;
106+
this.message = message;
107+
this.rejectedValue = null;
108+
}
109+
110+
public ValidationError(String field, String message, Object rejectedValue) {
111+
this.field = field;
112+
this.message = message;
113+
this.rejectedValue = rejectedValue;
114+
}
115+
116+
public String getField() { return field; }
117+
public String getMessage() { return message; }
118+
public Object getRejectedValue() { return rejectedValue; }
119+
}
120+
121+
// Builder class
122+
public static class ApiErrorResponseBuilder {
123+
private int status;
124+
private String error;
125+
private String message;
126+
private String path;
127+
private String debugMessage;
128+
private List<ValidationError> validationErrors;
129+
130+
public ApiErrorResponseBuilder status(int status) {
131+
this.status = status;
132+
return this;
133+
}
134+
135+
public ApiErrorResponseBuilder error(String error) {
136+
this.error = error;
137+
return this;
138+
}
139+
140+
public ApiErrorResponseBuilder message(String message) {
141+
this.message = message;
142+
return this;
143+
}
144+
145+
public ApiErrorResponseBuilder path(String path) {
146+
this.path = path;
147+
return this;
148+
}
149+
150+
public ApiErrorResponseBuilder debugMessage(String debugMessage) {
151+
this.debugMessage = debugMessage;
152+
return this;
153+
}
154+
155+
public ApiErrorResponseBuilder validationErrors(List<ValidationError> validationErrors) {
156+
this.validationErrors = validationErrors;
157+
return this;
158+
}
159+
160+
public ApiErrorResponse build() {
161+
ApiErrorResponse response = new ApiErrorResponse(status, error, message, path, debugMessage);
162+
if (validationErrors != null) {
163+
response.setValidationErrors(validationErrors);
164+
}
165+
return response;
166+
}
167+
}
168+
169+
@Override
170+
public boolean equals(Object o) {
171+
if (this == o) return true;
172+
if (o == null || getClass() != o.getClass()) return false;
173+
ApiErrorResponse that = (ApiErrorResponse) o;
174+
return status == that.status &&
175+
Objects.equals(timestamp, that.timestamp) &&
176+
Objects.equals(error, that.error) &&
177+
Objects.equals(message, that.message) &&
178+
Objects.equals(path, that.path);
179+
}
180+
181+
@Override
182+
public int hashCode() {
183+
return Objects.hash(timestamp, status, error, message, path);
184+
}
185+
186+
@Override
187+
public String toString() {
188+
return "ApiErrorResponse{" +
189+
"timestamp=" + timestamp +
190+
", status=" + status +
191+
", error='" + error + '\'' +
192+
", message='" + message + '\'' +
193+
", path='" + path + '\'' +
194+
", validationErrors=" + validationErrors +
195+
'}';
196+
}
197+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.countyhospital.healthapi.common.exception;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
import org.springframework.http.HttpStatus;
9+
import org.springframework.http.ResponseEntity;
10+
import org.springframework.validation.FieldError;
11+
import org.springframework.web.bind.MethodArgumentNotValidException;
12+
import org.springframework.web.bind.annotation.ExceptionHandler;
13+
import org.springframework.web.bind.annotation.RestControllerAdvice;
14+
15+
@RestControllerAdvice
16+
public class GlobalExceptionHandler {
17+
18+
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
19+
20+
// Handle validation errors
21+
@ExceptionHandler(MethodArgumentNotValidException.class)
22+
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
23+
Map<String, String> errors = new HashMap<>();
24+
ex.getBindingResult().getAllErrors().forEach(error -> {
25+
String fieldName = error instanceof FieldError ? ((FieldError) error).getField() : error.getObjectName();
26+
String errorMessage = error.getDefaultMessage();
27+
errors.put(fieldName, errorMessage);
28+
});
29+
logger.error("Validation errors: {}", errors);
30+
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
31+
}
32+
33+
// Handle all other exceptions
34+
@ExceptionHandler(Exception.class)
35+
public ResponseEntity<Map<String, String>> handleAllExceptions(Exception ex) {
36+
logger.error("Unexpected error occurred: ", ex);
37+
Map<String, String> response = new HashMap<>();
38+
response.put("error", ex.getMessage());
39+
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
40+
}
41+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.countyhospital.healthapi.config;
2+
3+
import org.modelmapper.ModelMapper;
4+
import org.modelmapper.convention.MatchingStrategies;
5+
import org.springframework.context.annotation.Bean;
6+
import org.springframework.context.annotation.Configuration;
7+
8+
@Configuration
9+
public class ModelMapperConfig {
10+
11+
@Bean
12+
public ModelMapper modelMapper() {
13+
ModelMapper modelMapper = new ModelMapper();
14+
15+
modelMapper.getConfiguration()
16+
.setMatchingStrategy(MatchingStrategies.STRICT)
17+
.setFieldMatchingEnabled(true)
18+
.setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
19+
.setSkipNullEnabled(true);
20+
21+
return modelMapper;
22+
}
23+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.countyhospital.healthapi.config;
2+
3+
import java.util.List;
4+
5+
import org.springframework.beans.factory.annotation.Value;
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.context.annotation.Configuration;
8+
9+
import io.swagger.v3.oas.models.OpenAPI;
10+
import io.swagger.v3.oas.models.info.Contact;
11+
import io.swagger.v3.oas.models.info.Info;
12+
import io.swagger.v3.oas.models.info.License;
13+
import io.swagger.v3.oas.models.servers.Server;
14+
15+
@Configuration
16+
public class OpenApiConfig {
17+
18+
@Value("${app.version:1.0.0}")
19+
private String appVersion;
20+
21+
@Value("${app.environment:local}")
22+
private String environment;
23+
24+
@Bean
25+
public OpenAPI customOpenAPI() {
26+
return new OpenAPI()
27+
.info(new Info()
28+
.title("County Hospital Health API")
29+
.description(String.join("\n",
30+
"REST API for managing patient records and encounters for County Hospital.",
31+
"This API provides comprehensive healthcare data management including:",
32+
"- Patient registration and management",
33+
"- Encounter (visit) tracking",
34+
"- Advanced patient search capabilities",
35+
"- Secure data access",
36+
"",
37+
"## Key Features",
38+
"- **Patient Management**: Full CRUD operations for patient records",
39+
"- **Encounter Tracking**: Record and manage patient visits",
40+
"- **Advanced Search**: Flexible patient search by multiple criteria",
41+
"- **Validation**: Comprehensive input validation and error handling",
42+
"- **Security**: API key authentication (optional)",
43+
"- **Documentation**: Interactive API documentation",
44+
"",
45+
"## Healthcare Standards",
46+
"- FHIR-inspired data models",
47+
"- ISO date formats",
48+
"- Standard gender codes (MALE, FEMALE, OTHER, UNKNOWN)",
49+
"- Encounter classification (INPATIENT, OUTPATIENT, EMERGENCY, VIRTUAL)"
50+
))
51+
.version(appVersion)
52+
.contact(new Contact()
53+
.name("County Hospital IT Support")
54+
.email("it-support@countyhospital.org")
55+
.url("https://www.countyhospital.org"))
56+
.license(new License()
57+
.name("Hospital Internal Use")
58+
.url("https://www.countyhospital.org/license")))
59+
.servers(List.of(
60+
new Server()
61+
.url(environment.equals("prod") ?
62+
"https://api.countyhospital.org" :
63+
"http://localhost:8080")
64+
.description(environment.equals("prod") ?
65+
"Production Server" :
66+
"Local Development Server"),
67+
new Server()
68+
.url("https://staging.countyhospital.org")
69+
.description("Staging Server")
70+
));
71+
}
72+
}

0 commit comments

Comments
 (0)