View Javadoc
1   /*
2    * Copyright 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 java.util.List;
20  import org.immutables.value.Value;
21  import org.springframework.http.HttpStatus;
22  import org.springframework.lang.Nullable;
23  
24  /**
25   * The rest api exception mapper properties.
26   *
27   * @author Christian Bremer
28   */
29  @Value.Immutable
30  public interface RestApiExceptionMapperProperties {
31  
32    /**
33     * Creates rest api exception mapper properties builder.
34     *
35     * @return the builder
36     */
37    static ImmutableRestApiExceptionMapperProperties.Builder builder() {
38      return ImmutableRestApiExceptionMapperProperties.builder();
39    }
40  
41    /**
42     * Gets default exception mapping.
43     *
44     * @return the default exception mapping
45     */
46    @Value.Default
47    default ExceptionMapping getDefaultExceptionMapping() {
48      return ExceptionMapping.builder()
49          .message(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase())
50          .status(HttpStatus.INTERNAL_SERVER_ERROR.value())
51          .exceptionClassName("*")
52          .build();
53    }
54  
55    /**
56     * Gets exception mappings.
57     *
58     * @return the exception mappings
59     */
60    @Value.Default
61    default List<ExceptionMapping> getExceptionMappings() {
62      return List.of(
63          ExceptionMapping.builder()
64              .exceptionClassName(IllegalArgumentException.class.getName())
65              .status(HttpStatus.BAD_REQUEST.value())
66              .message(HttpStatus.BAD_REQUEST.getReasonPhrase())
67              .build(),
68          ExceptionMapping.builder()
69              .exceptionClassName("org.springframework.security.access.AccessDeniedException")
70              .status(HttpStatus.FORBIDDEN.value())
71              .message(HttpStatus.FORBIDDEN.getReasonPhrase())
72              .build(),
73          ExceptionMapping.builder()
74              .exceptionClassName("javax.persistence.EntityNotFoundException")
75              .status(HttpStatus.NOT_FOUND.value())
76              .message(HttpStatus.NOT_FOUND.getReasonPhrase())
77              .build()
78      );
79    }
80  
81    /**
82     * Gets default exception mapping config.
83     *
84     * @return the default exception mapping config
85     */
86    @Value.Default
87    default ExceptionMappingConfig getDefaultExceptionMappingConfig() {
88      return ExceptionMappingConfig.builder()
89          .exceptionClassName("*")
90          .build();
91    }
92  
93    /**
94     * Gets exception mapping configs.
95     *
96     * @return the exception mapping configs
97     */
98    @Value.Default
99    default List<ExceptionMappingConfig> getExceptionMappingConfigs() {
100     return List.of();
101   }
102 
103   /**
104    * Find exception mapping.
105    *
106    * @param throwable the throwable
107    * @return the exception mapping
108    */
109   default ExceptionMapping findExceptionMapping(Throwable throwable) {
110     return getExceptionMappings()
111         .stream()
112         .filter(exceptionMapping -> matches(
113             throwable, exceptionMapping.getExceptionClassName()))
114         .findFirst()
115         .orElseGet(this::getDefaultExceptionMapping);
116   }
117 
118   /**
119    * Find exception mapping config.
120    *
121    * @param throwable the throwable
122    * @return the exception mapping config
123    */
124   default ExceptionMappingConfig findExceptionMappingConfig(Throwable throwable) {
125     return getExceptionMappingConfigs()
126         .stream()
127         .filter(exceptionMappingConfig -> matches(
128             throwable, exceptionMappingConfig.getExceptionClassName()))
129         .findFirst()
130         .orElseGet(this::getDefaultExceptionMappingConfig);
131   }
132 
133   private boolean matches(Throwable throwable, String exceptionClassName) {
134     if (throwable == null || exceptionClassName == null) {
135       return false;
136     }
137     if (throwable.getClass().getName().equals(exceptionClassName)) {
138       return true;
139     }
140     if (exceptionClassName.endsWith(".*")) {
141       String packagePrefix = exceptionClassName.substring(0, exceptionClassName.length() - 1);
142       if (throwable.getClass().getName().startsWith(packagePrefix)) {
143         return true;
144       }
145     }
146     if (matches(throwable.getCause(), exceptionClassName)) {
147       return true;
148     }
149     return matches(throwable.getClass().getSuperclass(), exceptionClassName);
150   }
151 
152   private boolean matches(Class<?> exceptionClass, String exceptionClassName) {
153     if (exceptionClass == null || exceptionClassName == null) {
154       return false;
155     }
156     if (exceptionClass.getName().equals(exceptionClassName)) {
157       return true;
158     }
159     if (exceptionClassName.endsWith(".*")) {
160       String packagePrefix = exceptionClassName.substring(0, exceptionClassName.length() - 1);
161       if (exceptionClass.getName().startsWith(packagePrefix)) {
162         return true;
163       }
164     }
165     return matches(exceptionClass.getSuperclass(), exceptionClassName);
166   }
167 
168   /**
169    * The exception mapping.
170    */
171   @Value.Immutable
172   interface ExceptionMapping {
173 
174     /**
175      * Builder immutable exception mapping . builder.
176      *
177      * @return the immutable exception mapping . builder
178      */
179     static ImmutableExceptionMapping.Builder builder() {
180       return ImmutableExceptionMapping.builder();
181     }
182 
183     /**
184      * Gets exception class name.
185      *
186      * @return the exception class name
187      */
188     String getExceptionClassName();
189 
190     /**
191      * Gets status.
192      *
193      * @return the status
194      */
195     @Value.Default
196     default int getStatus() {
197       return HttpStatus.INTERNAL_SERVER_ERROR.value();
198     }
199 
200     /**
201      * Gets message.
202      *
203      * @return the message
204      */
205     @Nullable
206     String getMessage();
207 
208     /**
209      * Gets code.
210      *
211      * @return the code
212      */
213     @Nullable
214     String getCode();
215 
216   }
217 
218   /**
219    * The exception mapping config.
220    */
221   @Value.Immutable
222   interface ExceptionMappingConfig {
223 
224     /**
225      * Creates builder.
226      *
227      * @return the builder
228      */
229     static ImmutableExceptionMappingConfig.Builder builder() {
230       return ImmutableExceptionMappingConfig.builder();
231     }
232 
233     /**
234      * Gets exception class name.
235      *
236      * @return the exception class name
237      */
238     String getExceptionClassName();
239 
240     /**
241      * Determines whether to include the message into the http output or not.
242      *
243      * @return the boolean
244      */
245     @Value.Default
246     default Boolean getIncludeMessage() {
247       return true;
248     }
249 
250     /**
251      * Is include exception class name.
252      *
253      * @return the boolean
254      */
255     @Value.Default
256     default Boolean getIncludeException() {
257       return true;
258     }
259 
260     /**
261      * Is include application name.
262      *
263      * @return the boolean
264      */
265     @Value.Default
266     default Boolean getIncludeApplicationName() {
267       return true;
268     }
269 
270     /**
271      * Is include path.
272      *
273      * @return the boolean
274      */
275     @Value.Default
276     default Boolean getIncludePath() {
277       return true;
278     }
279 
280     /**
281      * Is include handler.
282      *
283      * @return the boolean
284      */
285     @Value.Default
286     default Boolean getIncludeHandler() {
287       return false;
288     }
289 
290     /**
291      * Is include stack trace.
292      *
293      * @return the boolean
294      */
295     @Value.Default
296     default Boolean getIncludeStackTrace() {
297       return false;
298     }
299 
300     /**
301      * Is include cause.
302      *
303      * @return the boolean
304      */
305     @Value.Default
306     default Boolean getIncludeCause() {
307       return false;
308     }
309 
310     /**
311      * Is evaluate annotation first.
312      *
313      * @return the boolean
314      */
315     @Value.Default
316     default Boolean getEvaluateAnnotationFirst() {
317       return false;
318     }
319 
320   }
321 
322 }