View Javadoc
1   /*
2    * Copyright 2019-2022 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
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   * The implementation of a rest api exception mapper for spring web.
47   *
48   * @author Christian Bremer
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     * Instantiates a new rest api exception mapper.
60     *
61     * @param properties the properties
62     * @param applicationName the application name
63     */
64    public RestApiExceptionMapperForWeb(
65        RestApiExceptionMapperProperties properties,
66        String applicationName) {
67      this.properties = properties;
68      this.applicationName = applicationName;
69    }
70  
71    /**
72     * Detect http status http status.
73     *
74     * @param exception the exception
75     * @param handler the handler
76     * @return the http status
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    * From status optional.
104    *
105    * @param status the status
106    * @return the optional
107    */
108   protected Optional<HttpStatusCode> fromStatus(Integer status) {
109     return Optional.ofNullable(status)
110         .map(HttpStatus::valueOf);
111   }
112 
113   /**
114    * Gets error.
115    *
116    * @param throwable the throwable
117    * @param httpStatusCode the http status code
118    * @return the error
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    * Find the handler class.
169    *
170    * @param handler the handler
171    * @return the class
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    * Find the handler method.
185    *
186    * @param handler the handler
187    * @return the method
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    * Sets error code.
197    *
198    * @param restApiException the rest api exception
199    * @param exception the exception
200    * @param handler the handler
201    * @param config the config
202    * @return the error code
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    * Sets message.
233    *
234    * @param restApiException the rest api exception
235    * @param exception the exception
236    * @param handler the handler
237    * @param config the config
238    * @return the message
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    * Sets class name.
267    *
268    * @param restApiException the rest api exception
269    * @param exception the exception
270    * @param config the config
271    * @return the class name
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    * Sets application.
288    *
289    * @param restApiException the rest api exception
290    * @param config the config
291    * @return the application
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    * Sets path.
305    *
306    * @param restApiException the rest api exception
307    * @param path the path
308    * @param config the config
309    * @return the path
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    * Sets handler.
324    *
325    * @param restApiException the rest api exception
326    * @param handler the handler
327    * @param config the config
328    * @return the handler
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    * Sets stack trace.
353    *
354    * @param restApiException the rest api exception
355    * @param stackTrace the stack trace
356    * @param config the config
357    * @return the stack trace
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    * Sets cause.
382    *
383    * @param restApiException the rest api exception
384    * @param exception the exception
385    * @param config the config
386    * @return the cause
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    * Reconfigure rest api exception rest api exception.
426    *
427    * @param source the source
428    * @param config the config
429    * @return the rest api exception
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 }