|
31 | 31 | @Slf4j |
32 | 32 | public class GlobalExceptionHandler { |
33 | 33 |
|
| 34 | + /** Legacy JSF required-field bundle key, ported verbatim (Story 1.2, AD-8). */ |
| 35 | + private static final String REQUIRED_FIELD_KEY = "javax.faces.component.UIInput.REQUIRED"; |
| 36 | + |
34 | 37 | private final MessageSource messageSource; |
35 | 38 |
|
36 | 39 | public GlobalExceptionHandler(MessageSource messageSource) { |
@@ -216,6 +219,45 @@ public ResponseEntity<ProblemDetail> handleAccessDenied( |
216 | 219 | .body(problem); |
217 | 220 | } |
218 | 221 |
|
| 222 | + /** |
| 223 | + * Handles missing/blank/invalid required selection fields (UC-SEC-001 S04/S05/S08, Story 1.2). |
| 224 | + * Resolves the verbatim legacy required-field template |
| 225 | + * ({@code javax.faces.component.UIInput.REQUIRED = "{0}: Value is required."}) once per field — |
| 226 | + * passing the field label as the {@code {0}} argument (parameterized keys MUST get an args array) |
| 227 | + * — and returns ALL field messages together on one 400: {@code detail} joins the texts and the |
| 228 | + * {@code messages} extension property carries each {@code {key, text}} pair (the pinned shape the |
| 229 | + * frontend renders per field, mirroring {@code MessageInfo}). |
| 230 | + * |
| 231 | + * @param ex the exception carrying the ordered missing-field labels |
| 232 | + * @param request the current HTTP request |
| 233 | + * @return a {@link ProblemDetail} response with HTTP 400 status and a {@code messages} array |
| 234 | + */ |
| 235 | + @ExceptionHandler(FieldValuesRequiredException.class) |
| 236 | + public ResponseEntity<ProblemDetail> handleFieldValuesRequired( |
| 237 | + FieldValuesRequiredException ex, HttpServletRequest request) { |
| 238 | + log.debug("Required selection fields missing: {}", ex.getFieldLabels()); |
| 239 | + |
| 240 | + var messages = ex.getFieldLabels().stream() |
| 241 | + .map(label -> new FieldMessage( |
| 242 | + REQUIRED_FIELD_KEY, |
| 243 | + messageSource.getMessage( |
| 244 | + REQUIRED_FIELD_KEY, |
| 245 | + new Object[] {label}, |
| 246 | + REQUIRED_FIELD_KEY, |
| 247 | + LocaleContextHolder.getLocale()))) |
| 248 | + .toList(); |
| 249 | + |
| 250 | + ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST); |
| 251 | + problem.setTitle("Validation Failed"); |
| 252 | + problem.setDetail(messages.stream().map(FieldMessage::text).collect(Collectors.joining("; "))); |
| 253 | + problem.setInstance(URI.create(request.getRequestURI())); |
| 254 | + problem.setProperty("messages", messages); |
| 255 | + |
| 256 | + return ResponseEntity.badRequest() |
| 257 | + .contentType(MediaType.APPLICATION_PROBLEM_JSON) |
| 258 | + .body(problem); |
| 259 | + } |
| 260 | + |
219 | 261 | /** |
220 | 262 | * Handles a missing required request parameter (e.g. absent {@code millId}/{@code year}) and |
221 | 263 | * returns a 400 problem response. Without this handler these fall through to the generic 500 |
@@ -343,6 +385,15 @@ public ResponseEntity<ProblemDetail> handleGenericException( |
343 | 385 | .body(problem); |
344 | 386 | } |
345 | 387 |
|
| 388 | + /** |
| 389 | + * One resolved field-level message on a 400 {@code messages} array: the legacy bundle key plus |
| 390 | + * its resolved verbatim text (mirrors {@code MessageInfo}; pinned in Story 1.2's wire contract). |
| 391 | + * |
| 392 | + * @param key the legacy bundle key (e.g. {@code javax.faces.component.UIInput.REQUIRED}) |
| 393 | + * @param text the resolved verbatim text (e.g. {@code Mill: Value is required.}) |
| 394 | + */ |
| 395 | + public record FieldMessage(String key, String text) {} |
| 396 | + |
346 | 397 | /** |
347 | 398 | * Attempts to extract the most useful message from a DataIntegrityViolationException. |
348 | 399 | */ |
|
0 commit comments