1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.bremersee.exception.servlet;
18
19 import static java.util.Objects.requireNonNullElse;
20 import static org.springframework.core.annotation.AnnotationUtils.findAnnotation;
21 import static org.springframework.util.ObjectUtils.isEmpty;
22
23 import com.fasterxml.jackson.databind.ObjectMapper;
24 import com.fasterxml.jackson.dataformat.xml.XmlMapper;
25 import java.time.OffsetDateTime;
26 import java.time.ZoneOffset;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.function.Function;
30 import jakarta.servlet.http.HttpServletRequest;
31 import jakarta.servlet.http.HttpServletResponse;
32 import lombok.AccessLevel;
33 import lombok.Getter;
34 import lombok.Setter;
35 import lombok.extern.slf4j.Slf4j;
36 import org.bremersee.exception.RestApiExceptionConstants;
37 import org.bremersee.exception.RestApiExceptionMapper;
38 import org.bremersee.exception.RestApiResponseType;
39 import org.bremersee.exception.model.RestApiException;
40 import org.springframework.http.HttpStatus;
41 import org.springframework.http.MediaType;
42 import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
43 import org.springframework.http.server.ServletServerHttpRequest;
44 import org.springframework.lang.NonNull;
45 import org.springframework.util.AntPathMatcher;
46 import org.springframework.util.PathMatcher;
47 import org.springframework.web.bind.annotation.RestController;
48 import org.springframework.web.method.HandlerMethod;
49 import org.springframework.web.servlet.HandlerExceptionResolver;
50 import org.springframework.web.servlet.ModelAndView;
51 import org.springframework.web.servlet.view.AbstractView;
52 import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
53 import org.springframework.web.servlet.view.xml.MappingJackson2XmlView;
54 import org.springframework.web.util.WebUtils;
55
56
57
58
59
60
61 @Slf4j
62 public class ApiExceptionResolver implements HandlerExceptionResolver {
63
64
65
66
67 protected static final String MODEL_KEY = "error";
68
69 @Getter(AccessLevel.PROTECTED)
70 private final List<String> apiPaths;
71
72 @Getter(AccessLevel.PROTECTED)
73 @Setter
74 private PathMatcher pathMatcher = new AntPathMatcher();
75
76 @Getter(AccessLevel.PROTECTED)
77 @Setter
78 private Function<HttpServletRequest, String> restApiExceptionIdProvider;
79
80 @Getter(AccessLevel.PROTECTED)
81 private final RestApiExceptionMapper exceptionMapper;
82
83 @Getter(AccessLevel.PROTECTED)
84 private final ObjectMapper objectMapper;
85
86 @Getter(AccessLevel.PROTECTED)
87 private final XmlMapper xmlMapper;
88
89
90
91
92
93
94
95 public ApiExceptionResolver(
96 List<String> apiPaths,
97 RestApiExceptionMapper exceptionMapper) {
98 this(apiPaths, exceptionMapper, new Jackson2ObjectMapperBuilder());
99 }
100
101
102
103
104
105
106
107
108 public ApiExceptionResolver(
109 List<String> apiPaths,
110 RestApiExceptionMapper exceptionMapper,
111 Jackson2ObjectMapperBuilder objectMapperBuilder) {
112 this(
113 apiPaths,
114 exceptionMapper,
115 objectMapperBuilder.build(),
116 objectMapperBuilder.createXmlMapper(true).build());
117 }
118
119
120
121
122
123
124
125
126
127 public ApiExceptionResolver(
128 List<String> apiPaths,
129 RestApiExceptionMapper exceptionMapper,
130 ObjectMapper objectMapper,
131 XmlMapper xmlMapper) {
132 this.apiPaths = apiPaths;
133 this.exceptionMapper = exceptionMapper;
134 this.objectMapper = objectMapper;
135 this.xmlMapper = xmlMapper;
136 }
137
138 @Override
139 public ModelAndView resolveException(
140 @NonNull HttpServletRequest request,
141 @NonNull HttpServletResponse response,
142 Object handler,
143 @NonNull Exception ex) {
144
145 if (!isExceptionHandlerResponsible(request, handler)) {
146 return null;
147 }
148
149 RestApiException payload = exceptionMapper.build(ex, request.getRequestURI(), handler);
150 if (!isEmpty(restApiExceptionIdProvider)) {
151 payload.setId(restApiExceptionIdProvider.apply(request));
152 }
153
154 ServletServerHttpRequest httpRequest = new ServletServerHttpRequest(request);
155 List<MediaType> accepted = httpRequest.getHeaders().getAccept();
156 RestApiResponseType responseType = RestApiResponseType.detectByAccepted(accepted);
157
158 ModelAndView modelAndView;
159 switch (responseType) {
160 case JSON:
161 MappingJackson2JsonView mjv = new MappingJackson2JsonView(objectMapper);
162 mjv.setContentType(responseType.getContentTypeValue());
163 mjv.setPrettyPrint(true);
164 mjv.setModelKey(MODEL_KEY);
165 mjv.setExtractValueFromSingleKeyModel(true);
166 modelAndView = new ModelAndView(mjv, MODEL_KEY, payload);
167 break;
168
169 case XML:
170 MappingJackson2XmlView mxv = new MappingJackson2XmlView(xmlMapper);
171 mxv.setContentType(responseType.getContentTypeValue());
172 mxv.setPrettyPrint(true);
173 mxv.setModelKey(MODEL_KEY);
174 modelAndView = new ModelAndView(mxv, MODEL_KEY, payload);
175 break;
176
177 default:
178 modelAndView = new ModelAndView(new EmptyView(payload, responseType.getContentTypeValue()));
179 }
180
181 response.setContentType(responseType.getContentTypeValue());
182 int statusCode = requireNonNullElse(
183 payload.getStatus(),
184 HttpStatus.INTERNAL_SERVER_ERROR.value());
185 modelAndView.setStatus(HttpStatus.resolve(statusCode));
186 applyStatusCodeIfPossible(request, response, statusCode);
187 return modelAndView;
188 }
189
190
191
192
193
194
195
196
197 protected boolean isExceptionHandlerResponsible(
198 HttpServletRequest request,
199 Object handler) {
200
201 if (!isEmpty(apiPaths)) {
202 return apiPaths.stream().anyMatch(
203 s -> pathMatcher.match(s, request.getServletPath()));
204 }
205
206 if (isEmpty(handler)) {
207 return false;
208 }
209 Class<?> cls = handler instanceof HandlerMethod
210 ? ((HandlerMethod) handler).getBean().getClass()
211 : handler.getClass();
212 boolean result = !isEmpty(findAnnotation(cls, RestController.class));
213 if (log.isDebugEnabled()) {
214 log.debug("Is handler [" + handler + "] a rest controller? " + result);
215 }
216 return result;
217 }
218
219
220
221
222
223
224
225
226 protected final void applyStatusCodeIfPossible(
227 HttpServletRequest request,
228 HttpServletResponse response,
229 int statusCode) {
230
231 if (!WebUtils.isIncludeRequest(request)) {
232 if (log.isDebugEnabled()) {
233 log.debug("Applying HTTP status code " + statusCode);
234 }
235 response.setStatus(statusCode);
236 request.setAttribute(WebUtils.ERROR_STATUS_CODE_ATTRIBUTE, statusCode);
237 }
238 }
239
240
241
242
243 protected static class EmptyView extends AbstractView {
244
245
246
247
248 protected final RestApiException restApiException;
249
250
251
252
253
254
255
256 protected EmptyView(RestApiException payload, String contentType) {
257 this.restApiException = payload;
258 setContentType(contentType);
259 }
260
261 @Override
262 protected void renderMergedOutputModel(
263 @NonNull Map<String, Object> map,
264 @NonNull HttpServletRequest httpServletRequest,
265 @NonNull HttpServletResponse httpServletResponse) {
266
267 if (!isEmpty(restApiException.getId())) {
268 httpServletResponse.addHeader(
269 RestApiExceptionConstants.ID_HEADER_NAME,
270 restApiException.getId());
271 }
272
273 String timestamp;
274 if (!isEmpty(restApiException.getTimestamp())) {
275 timestamp = restApiException.getTimestamp()
276 .format(RestApiExceptionConstants.TIMESTAMP_FORMATTER);
277 } else {
278 timestamp = OffsetDateTime.now(ZoneOffset.UTC)
279 .format(RestApiExceptionConstants.TIMESTAMP_FORMATTER);
280 }
281 httpServletResponse.addHeader(RestApiExceptionConstants.TIMESTAMP_HEADER_NAME, timestamp);
282
283 if (!isEmpty(restApiException.getErrorCode())) {
284 httpServletResponse.addHeader(
285 RestApiExceptionConstants.CODE_HEADER_NAME,
286 restApiException.getErrorCode());
287 httpServletResponse.addHeader(
288 RestApiExceptionConstants.CODE_INHERITED_HEADER_NAME,
289 String.valueOf(restApiException.getErrorCodeInherited()));
290 }
291
292 if (!isEmpty(restApiException.getMessage())) {
293 httpServletResponse.addHeader(
294 RestApiExceptionConstants.MESSAGE_HEADER_NAME,
295 restApiException.getMessage());
296 }
297
298 if (!isEmpty(restApiException.getException())) {
299 httpServletResponse.addHeader(
300 RestApiExceptionConstants.EXCEPTION_HEADER_NAME,
301 restApiException.getException());
302 }
303
304 if (!isEmpty(restApiException.getApplication())) {
305 httpServletResponse.addHeader(
306 RestApiExceptionConstants.APPLICATION_HEADER_NAME,
307 restApiException.getApplication());
308 }
309
310 if (!isEmpty(restApiException.getPath())) {
311 httpServletResponse.addHeader(
312 RestApiExceptionConstants.PATH_HEADER_NAME,
313 restApiException.getPath());
314 }
315 }
316
317 }
318
319 }