1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.bremersee.exception;
18
19 import static java.util.Objects.isNull;
20 import static java.util.Objects.nonNull;
21 import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation;
22 import static org.springframework.util.ClassUtils.getUserClass;
23
24 import java.lang.reflect.Method;
25 import java.time.OffsetDateTime;
26 import java.time.ZoneOffset;
27 import java.util.Arrays;
28 import java.util.Optional;
29 import java.util.stream.Collectors;
30 import lombok.AccessLevel;
31 import lombok.Getter;
32 import org.bremersee.exception.RestApiExceptionMapperProperties.ExceptionMappingConfig;
33 import org.bremersee.exception.annotation.ErrorCode;
34 import org.bremersee.exception.model.Handler;
35 import org.bremersee.exception.model.RestApiException;
36 import org.bremersee.exception.model.StackTraceItem;
37 import org.springframework.http.HttpStatus;
38 import org.springframework.http.HttpStatusCode;
39 import org.springframework.lang.Nullable;
40 import org.springframework.util.ObjectUtils;
41 import org.springframework.web.bind.annotation.ResponseStatus;
42 import org.springframework.web.method.HandlerMethod;
43 import org.springframework.web.server.ResponseStatusException;
44
45
46
47
48
49
50 public class RestApiExceptionMapperForWeb implements RestApiExceptionMapper {
51
52 @Getter(AccessLevel.PROTECTED)
53 private final RestApiExceptionMapperProperties properties;
54
55 @Getter(AccessLevel.PROTECTED)
56 private final String applicationName;
57
58
59
60
61
62
63
64 public RestApiExceptionMapperForWeb(
65 RestApiExceptionMapperProperties properties,
66 String applicationName) {
67 this.properties = properties;
68 this.applicationName = applicationName;
69 }
70
71
72
73
74
75
76
77
78 protected HttpStatusCode detectHttpStatus(Throwable exception, Object handler) {
79 return Optional.of(exception)
80 .flatMap(exc -> {
81 if (exc instanceof HttpStatusAware hsa) {
82 return Optional.of(HttpStatusCode.valueOf(hsa.status()));
83 }
84 if (exc instanceof ResponseStatusException rse) {
85 return Optional.of(rse.getStatusCode());
86 }
87 return Optional.empty();
88 })
89 .or(() -> findHandlerMethod(handler)
90 .map(method -> findMergedAnnotation(method, ResponseStatus.class))
91 .map(ResponseStatus::code))
92 .or(() -> findHandlerClass(handler)
93 .map(handlerClass -> findMergedAnnotation(handlerClass, ResponseStatus.class))
94 .map(ResponseStatus::code))
95 .or(() -> Optional
96 .ofNullable(findMergedAnnotation(exception.getClass(), ResponseStatus.class))
97 .map(ResponseStatus::code))
98 .or(() -> fromStatus(properties.findExceptionMapping(exception).getStatus()))
99 .orElse(HttpStatus.INTERNAL_SERVER_ERROR);
100 }
101
102
103
104
105
106
107
108 protected Optional<HttpStatusCode> fromStatus(Integer status) {
109 return Optional.ofNullable(status)
110 .map(HttpStatus::valueOf);
111 }
112
113
114
115
116
117
118
119
120 @Nullable
121 protected String getError(Throwable throwable, HttpStatusCode httpStatusCode) {
122 if ((throwable instanceof ResponseStatusException rse)
123 && !(ObjectUtils.isEmpty(rse.getReason()))) {
124 return rse.getReason();
125 }
126 if (httpStatusCode instanceof HttpStatus hs) {
127 return hs.getReasonPhrase();
128 }
129 return null;
130 }
131
132 @Override
133 public RestApiException build(
134 Throwable exception,
135 String requestPath,
136 Object handler) {
137
138 HttpStatusCode httpStatus = detectHttpStatus(exception, handler);
139
140 RestApiException restApiException = new RestApiException();
141
142 restApiException.setTimestamp(OffsetDateTime.now(ZoneOffset.UTC));
143
144 restApiException.setStatus(httpStatus.value());
145
146 restApiException.setError(getError(exception, httpStatus));
147
148 ExceptionMappingConfig config = getProperties().findExceptionMappingConfig(exception);
149
150 restApiException = setErrorCode(restApiException, exception, handler, config);
151
152 restApiException = setMessage(restApiException, exception, handler, config);
153
154 restApiException = setClassName(restApiException, exception, config);
155
156 restApiException = setApplication(restApiException, config);
157
158 restApiException = setPath(restApiException, requestPath, config);
159
160 restApiException = setHandler(restApiException, handler, config);
161
162 restApiException = setStackTrace(restApiException, exception.getStackTrace(), config);
163
164 return setCause(restApiException, exception, config);
165 }
166
167
168
169
170
171
172
173 protected Optional<Class<?>> findHandlerClass(Object handler) {
174 return Optional.ofNullable(handler)
175 .map(h -> {
176 if (h instanceof HandlerMethod) {
177 return ((HandlerMethod) h).getBean().getClass();
178 }
179 return (h instanceof Class) ? (Class<?>) h : h.getClass();
180 });
181 }
182
183
184
185
186
187
188
189 protected Optional<Method> findHandlerMethod(Object handler) {
190 return Optional.ofNullable(handler)
191 .filter(h -> h instanceof HandlerMethod)
192 .map(h -> ((HandlerMethod) h).getMethod());
193 }
194
195
196
197
198
199
200
201
202
203
204 protected RestApiException setErrorCode(
205 RestApiException restApiException,
206 Throwable exception,
207 Object handler,
208 ExceptionMappingConfig config) {
209
210 return Optional.of(exception)
211 .filter(exc -> (exc instanceof ErrorCodeAware) && !config.getEvaluateAnnotationFirst())
212 .map(exc -> ((ErrorCodeAware) exc).getErrorCode())
213 .or(() -> findHandlerMethod(handler)
214 .map(method -> findMergedAnnotation(method, ErrorCode.class))
215 .map(ErrorCode::value))
216 .or(() -> findHandlerClass(handler)
217 .map(handlerClass -> findMergedAnnotation(handlerClass, ErrorCode.class))
218 .map(ErrorCode::value))
219 .or(() -> Optional
220 .ofNullable(findMergedAnnotation(getUserClass(exception), ErrorCode.class))
221 .map(ErrorCode::value))
222 .or(() -> Optional.ofNullable(getProperties().findExceptionMapping(exception).getCode()))
223 .filter(errorCode -> !errorCode.isBlank())
224 .map(errorCode -> restApiException.toBuilder()
225 .errorCode(errorCode)
226 .errorCodeInherited(false)
227 .build())
228 .orElse(restApiException);
229 }
230
231
232
233
234
235
236
237
238
239
240 protected RestApiException setMessage(
241 RestApiException restApiException,
242 Throwable exception,
243 Object handler,
244 ExceptionMappingConfig config) {
245
246 if (!config.getIncludeMessage()) {
247 return restApiException;
248 }
249 return Optional.ofNullable(exception.getMessage())
250 .filter(msg -> !msg.isBlank() && !config.getEvaluateAnnotationFirst())
251 .or(() -> findHandlerMethod(handler)
252 .map(method -> findMergedAnnotation(method, ResponseStatus.class))
253 .map(ResponseStatus::reason))
254 .or(() -> findHandlerClass(handler)
255 .map(handlerClass -> findMergedAnnotation(handlerClass, ResponseStatus.class))
256 .map(ResponseStatus::reason))
257 .or(() -> Optional
258 .ofNullable(findMergedAnnotation(getUserClass(exception), ResponseStatus.class))
259 .map(ResponseStatus::reason))
260 .or(() -> Optional.ofNullable(getProperties().findExceptionMapping(exception).getMessage()))
261 .map(msg -> restApiException.toBuilder().message(msg).build())
262 .orElse(restApiException);
263 }
264
265
266
267
268
269
270
271
272
273 protected RestApiException setClassName(
274 RestApiException restApiException,
275 Throwable exception,
276 ExceptionMappingConfig config) {
277
278 if (!config.getIncludeException()) {
279 return restApiException;
280 }
281 return restApiException.toBuilder()
282 .exception(getUserClass(exception).getName())
283 .build();
284 }
285
286
287
288
289
290
291
292
293 protected RestApiException setApplication(
294 RestApiException restApiException,
295 ExceptionMappingConfig config) {
296
297 if (!config.getIncludeApplicationName()) {
298 return restApiException;
299 }
300 return restApiException.toBuilder().application(getApplicationName()).build();
301 }
302
303
304
305
306
307
308
309
310
311 protected RestApiException setPath(
312 RestApiException restApiException,
313 String path,
314 ExceptionMappingConfig config) {
315
316 if (!config.getIncludePath() || isNull(path)) {
317 return restApiException;
318 }
319 return restApiException.toBuilder().path(path).build();
320 }
321
322
323
324
325
326
327
328
329
330 protected RestApiException setHandler(
331 RestApiException restApiException,
332 Object handler,
333 ExceptionMappingConfig config) {
334
335 if (!config.getIncludeHandler() || isNull(handler)) {
336 return restApiException;
337 }
338 return findHandlerMethod(handler)
339 .map(method -> Handler.builder()
340 .methodName(method.getName())
341 .methodParameterTypes(Arrays.stream(method.getParameterTypes())
342 .map(Class::getName)
343 .collect(Collectors.toList()))
344 .build())
345 .flatMap(h -> findHandlerClass(handler)
346 .map(cls -> h.toBuilder().className(cls.getName()).build()))
347 .map(h -> restApiException.toBuilder().handler(h).build())
348 .orElse(restApiException);
349 }
350
351
352
353
354
355
356
357
358
359 protected RestApiException setStackTrace(
360 RestApiException restApiException,
361 StackTraceElement[] stackTrace,
362 ExceptionMappingConfig config) {
363
364 if (!config.getIncludeStackTrace() || isNull(stackTrace) || stackTrace.length == 0) {
365 return restApiException;
366 }
367 return restApiException.toBuilder()
368 .stackTrace(Arrays.stream(stackTrace)
369 .map(st -> StackTraceItem
370 .builder()
371 .declaringClass(st.getClassName())
372 .fileName(st.getFileName())
373 .lineNumber(st.getLineNumber())
374 .methodName(st.getMethodName())
375 .build())
376 .collect(Collectors.toList()))
377 .build();
378 }
379
380
381
382
383
384
385
386
387
388 protected RestApiException setCause(
389 RestApiException restApiException,
390 Throwable exception,
391 ExceptionMappingConfig config) {
392
393 return Optional.ofNullable(exception)
394 .filter(exc -> exc instanceof RestApiExceptionAware)
395 .map(exc -> ((RestApiExceptionAware) exc).getRestApiException())
396 .map(cause -> reconfigureRestApiException(cause, config))
397 .or(() -> Optional.ofNullable(exception)
398 .map(Throwable::getCause)
399 .map(cause -> {
400 RestApiException rae = new RestApiException();
401 rae = setErrorCode(rae, cause, null, config);
402 rae = setMessage(rae, cause, null, config);
403 rae = setClassName(rae, cause, config);
404 rae = setStackTrace(rae, cause.getStackTrace(), config);
405 rae = setCause(rae, cause.getCause(), config);
406 return rae;
407 }))
408 .map(cause -> {
409 RestApiException.RestApiExceptionBuilder builder = restApiException.toBuilder();
410 String causeErrorCode = cause.getErrorCode();
411 if (nonNull(causeErrorCode) && !causeErrorCode.isBlank()) {
412 builder = builder
413 .errorCode(causeErrorCode)
414 .errorCodeInherited(true);
415 }
416 if (config.getIncludeCause()) {
417 builder = builder.cause(cause);
418 }
419 return builder.build();
420 })
421 .orElse(restApiException);
422 }
423
424
425
426
427
428
429
430
431 protected RestApiException reconfigureRestApiException(
432 RestApiException source,
433 ExceptionMappingConfig config) {
434
435 RestApiException target = new RestApiException();
436 target.setId(source.getId());
437 target.setTimestamp(source.getTimestamp());
438 target.setStatus(source.getStatus());
439 target.setError(source.getError());
440 if (nonNull(source.getErrorCode()) && !source.getErrorCode().isBlank()) {
441 target.setErrorCode(source.getErrorCode());
442 target.setErrorCodeInherited(source.getErrorCodeInherited());
443 }
444 if (config.getIncludeMessage()) {
445 target.setMessage(source.getMessage());
446 }
447 if (config.getIncludeException()) {
448 target.setException(source.getException());
449 }
450 if (config.getIncludeApplicationName()) {
451 target.setApplication(source.getApplication());
452 }
453 if (config.getIncludePath()) {
454 target.setPath(source.getPath());
455 }
456 if (config.getIncludeHandler()) {
457 target.setHandler(source.getHandler());
458 }
459 if (config.getIncludeStackTrace()) {
460 target.setStackTrace(source.getStackTrace());
461 }
462 if (config.getIncludeCause() && nonNull(source.getCause())) {
463 target.setCause(reconfigureRestApiException(source.getCause(), config));
464 }
465 source.furtherDetails().forEach(target::furtherDetails);
466 return target;
467 }
468
469 }