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 }