View Javadoc
1   /*
2    * Copyright 2019 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 java.util.ArrayList;
20  import java.util.List;
21  import lombok.AllArgsConstructor;
22  import lombok.EqualsAndHashCode;
23  import lombok.Getter;
24  import lombok.NoArgsConstructor;
25  import lombok.Setter;
26  import lombok.ToString;
27  import org.springframework.boot.context.properties.ConfigurationProperties;
28  import org.springframework.http.HttpStatus;
29  
30  /**
31   * Configuration properties for the rest api exception handler or resolver.
32   *
33   * @author Christian Bremer
34   */
35  @ConfigurationProperties(prefix = "bremersee.exception-mapping")
36  @Getter
37  @Setter
38  @ToString
39  @EqualsAndHashCode
40  public class RestApiExceptionMapperProperties {
41  
42    /**
43     * The request paths the handler is responsible for. Default is empty.
44     */
45    private List<String> apiPaths = new ArrayList<>();
46  
47    /**
48     * The default values of a rest api exception that will be set, if a value is not detected. The default values are:
49     * <table style="border: 1px solid">
50     * <thead>
51     * <tr>
52     * <th style="border: 1px solid">attribute</th>
53     * <th style="border: 1px solid">value</th>
54     * </tr>
55     * </thead>
56     * <tbody>
57     * <tr>
58     * <td style="border: 1px solid">status</td>
59     * <td style="border: 1px solid">500</td>
60     * </tr>
61     * <tr>
62     * <td style="border: 1px solid">message</td>
63     * <td style="border: 1px solid">Internal Server Error</td>
64     * </tr>
65     * <tr>
66     * <td style="border: 1px solid">code</td>
67     * <td style="border: 1px solid">UNSPECIFIED</td>
68     * </tr>
69     * </tbody>
70     * </table>
71     */
72    private ExceptionMapping defaultExceptionMapping;
73  
74    /**
75     * Values ​​of errors whose values ​​can not be determined automatically. The name of the exception can be a package,
76     * too (e. g. org.bremersee.foobar.*).
77     *
78     * <p>Examples application.yml:
79     * <pre>
80     * bremersee:
81     *   exception-mapping:
82     *     exception-mappings:
83     *     - exception-class-name: org.springframework.security.access.AccessDeniedException
84     *       status: 403
85     *       message: Forbidden
86     *       code: XYZ:0815
87     *     - exception-class-name: javax.persistence.EntityNotFoundException
88     *       status: 404
89     *       message: Not Found
90     *       code: GEN:404
91     * </pre>
92     */
93    private List<ExceptionMapping> exceptionMappings = new ArrayList<>();
94  
95    /**
96     * The default configuration of the exception mapping.
97     *
98     * <table style="border: 1px solid">
99     * <thead>
100    * <tr>
101    * <th style="border: 1px solid">attribute</th>
102    * <th style="border: 1px solid">value</th>
103    * </tr>
104    * </thead>
105    * <tbody>
106    * <tr>
107    * <td style="border: 1px solid">includeExceptionClassName</td>
108    * <td style="border: 1px solid">true</td>
109    * </tr>
110    * <tr>
111    * <td style="border: 1px solid">includeApplicationName</td>
112    * <td style="border: 1px solid">true</td>
113    * </tr>
114    * <tr>
115    * <td style="border: 1px solid">includePath</td>
116    * <td style="border: 1px solid">true</td>
117    * </tr>
118    * <tr>
119    * <td style="border: 1px solid">includeHandler</td>
120    * <td style="border: 1px solid">false</td>
121    * </tr>
122    * <tr>
123    * <td style="border: 1px solid">includeStackTrace</td>
124    * <td style="border: 1px solid">false</td>
125    * </tr>
126    * <tr>
127    * <td style="border: 1px solid">includeCause</td>
128    * <td style="border: 1px solid">true</td>
129    * </tr>
130    * <tr>
131    * <td style="border: 1px solid">evaluateAnnotationFirst</td>
132    * <td style="border: 1px solid">false</td>
133    * </tr>
134    * </tbody>
135    * </table>
136    *
137    * <p>If two values of a key are available (e. g. per annotation (see {@link
138    * org.springframework.web.bind.annotation.ResponseStatus}) and member attribute) it is possible
139    * with {@code evaluateAnnotationFirst} to specify which one should be used.
140    */
141   private ExceptionMappingConfig defaultExceptionMappingConfig;
142 
143   /**
144    * Specifies mapping configuration per exception class.  The name of the exception can be a package, too (e. g.
145    * org.bremersee.foobar.*).
146    *
147    * <p>Examples application.yml:
148    * <pre>
149    * bremersee:
150    *   exception-mapping:
151    *     exception-mapping-configs:
152    *     - exception-class-name: org.springframework.security.access.AccessDeniedException
153    *       include-exception-class-name: false
154    *       include-handler: true
155    *       include-cause: true
156    *     - exception-class-name: org.springframework.*
157    *       include-exception-class-name: true
158    *       include-handler: true
159    *       include-cause: false
160    * </pre>
161    */
162   private List<ExceptionMappingConfig> exceptionMappingConfigs = new ArrayList<>();
163 
164   /**
165    * Instantiates rest api exception mapper properties.
166    */
167   public RestApiExceptionMapperProperties() {
168 
169     defaultExceptionMapping = new ExceptionMapping();
170     defaultExceptionMapping.setCode(null);
171     defaultExceptionMapping.setMessage(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase());
172     defaultExceptionMapping.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
173     defaultExceptionMapping.setExceptionClassName("*");
174 
175     defaultExceptionMappingConfig = new ExceptionMappingConfig();
176 
177     exceptionMappings.add(new ExceptionMapping(
178         IllegalArgumentException.class.getName(),
179         HttpStatus.BAD_REQUEST,
180         null));
181 
182     exceptionMappings.add(new ExceptionMapping(
183         "org.springframework.security.access.AccessDeniedException",
184         HttpStatus.FORBIDDEN,
185         null));
186 
187     exceptionMappings.add(new ExceptionMapping(
188         "javax.persistence.EntityNotFoundException",
189         HttpStatus.NOT_FOUND,
190         null));
191   }
192 
193   /**
194    * Find exception mapping.
195    *
196    * @param throwable the throwable
197    * @return the exception mapping
198    */
199   @SuppressWarnings("WeakerAccess")
200   public ExceptionMapping findExceptionMapping(Throwable throwable) {
201     return getExceptionMappings()
202         .stream()
203         .filter(exceptionMapping -> matches(
204             throwable, exceptionMapping.getExceptionClassName()))
205         .findFirst()
206         .orElseGet(this::getDefaultExceptionMapping);
207   }
208 
209   /**
210    * Find exception mapping config.
211    *
212    * @param throwable the throwable
213    * @return the exception mapping config
214    */
215   @SuppressWarnings("WeakerAccess")
216   public ExceptionMappingConfig findExceptionMappingConfig(
217       final Throwable throwable) {
218 
219     return getExceptionMappingConfigs()
220         .stream()
221         .filter(exceptionMappingConfig -> matches(
222             throwable, exceptionMappingConfig.getExceptionClassName()))
223         .findFirst()
224         .orElseGet(this::getDefaultExceptionMappingConfig);
225   }
226 
227   private boolean matches(final Throwable throwable, final String exceptionClassName) {
228     if (throwable == null || exceptionClassName == null) {
229       return false;
230     }
231     if (throwable.getClass().getName().equals(exceptionClassName)) {
232       return true;
233     }
234     if (exceptionClassName.endsWith(".*")) {
235       final String packagePrefix = exceptionClassName.substring(0, exceptionClassName.length() - 1);
236       if (throwable.getClass().getName().startsWith(packagePrefix)) {
237         return true;
238       }
239     }
240     if (matches(throwable.getCause(), exceptionClassName)) {
241       return true;
242     }
243     return matches(throwable.getClass().getSuperclass(), exceptionClassName);
244   }
245 
246   private boolean matches(final Class<?> exceptionClass, final String exceptionClassName) {
247     if (exceptionClass == null || exceptionClassName == null) {
248       return false;
249     }
250     if (exceptionClass.getName().equals(exceptionClassName)) {
251       return true;
252     }
253     if (exceptionClassName.endsWith(".*")) {
254       final String packagePrefix = exceptionClassName.substring(0, exceptionClassName.length() - 1);
255       if (exceptionClass.getName().startsWith(packagePrefix)) {
256         return true;
257       }
258     }
259     return matches(exceptionClass.getSuperclass(), exceptionClassName);
260   }
261 
262   /**
263    * The exception mapping.
264    */
265   @ToString
266   @EqualsAndHashCode
267   @NoArgsConstructor
268   @AllArgsConstructor
269   public static class ExceptionMapping {
270 
271     /**
272      * Instantiates a new exception mapping.
273      *
274      * @param exceptionClassName the exception class name
275      * @param httpStatus the http status
276      * @param code the code
277      */
278     @SuppressWarnings("WeakerAccess")
279     public ExceptionMapping(String exceptionClassName, HttpStatus httpStatus, String code) {
280       this.exceptionClassName = exceptionClassName;
281       if (httpStatus != null) {
282         this.status = httpStatus.value();
283         this.message = httpStatus.getReasonPhrase();
284       }
285       this.code = code;
286     }
287 
288     @Getter
289     @Setter
290     private String exceptionClassName;
291 
292     @Setter
293     private int status;
294 
295     @Getter
296     @Setter
297     private String message;
298 
299     @Getter
300     @Setter
301     private String code;
302 
303     /**
304      * Gets status.
305      *
306      * @return the status
307      */
308     public int getStatus() {
309       if (HttpStatus.resolve(status) == null) {
310         return HttpStatus.INTERNAL_SERVER_ERROR.value();
311       }
312       return status;
313     }
314 
315   }
316 
317   /**
318    * The exception mapping config.
319    */
320   @SuppressWarnings("WeakerAccess")
321   @ToString
322   @EqualsAndHashCode
323   @NoArgsConstructor
324   @AllArgsConstructor
325   public static class ExceptionMappingConfig {
326 
327     @Getter
328     @Setter
329     private String exceptionClassName;
330 
331     @Getter
332     @Setter
333     private boolean includeExceptionClassName = true;
334 
335     @Getter
336     @Setter
337     private boolean includeApplicationName = true;
338 
339     @Getter
340     @Setter
341     private boolean includePath = true;
342 
343     @Getter
344     @Setter
345     private boolean includeHandler = false;
346 
347     @Getter
348     @Setter
349     private boolean includeStackTrace = false;
350 
351     @Getter
352     @Setter
353     private boolean includeCause = true;
354 
355     @Getter
356     @Setter
357     private boolean evaluateAnnotationFirst = false;
358 
359   }
360 
361 }