1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.util.StringUtils.hasText;
22
23 import com.fasterxml.jackson.databind.ObjectMapper;
24 import com.fasterxml.jackson.dataformat.xml.XmlMapper;
25 import java.nio.charset.Charset;
26 import java.nio.charset.StandardCharsets;
27 import java.time.OffsetDateTime;
28 import java.util.Optional;
29 import lombok.extern.slf4j.Slf4j;
30 import org.bremersee.exception.model.RestApiException;
31 import org.springframework.http.HttpHeaders;
32 import org.springframework.http.HttpStatus;
33 import org.springframework.http.HttpStatusCode;
34 import org.springframework.http.MediaType;
35 import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
36 import org.springframework.util.ObjectUtils;
37
38
39
40
41
42
43 @Slf4j
44 public class RestApiExceptionParserImpl implements RestApiExceptionParser {
45
46 private final ObjectMapper objectMapper;
47
48 private final XmlMapper xmlMapper;
49
50 private final Charset defaultCharset;
51
52
53
54
55 public RestApiExceptionParserImpl() {
56 this(new Jackson2ObjectMapperBuilder());
57 }
58
59
60
61
62
63
64 public RestApiExceptionParserImpl(Charset defaultCharset) {
65 this(new Jackson2ObjectMapperBuilder(), defaultCharset);
66 }
67
68
69
70
71
72
73 public RestApiExceptionParserImpl(Jackson2ObjectMapperBuilder objectMapperBuilder) {
74 this(objectMapperBuilder.build(), objectMapperBuilder.createXmlMapper(true).build());
75 }
76
77
78
79
80
81
82
83 public RestApiExceptionParserImpl(
84 Jackson2ObjectMapperBuilder objectMapperBuilder,
85 Charset charset) {
86 this(objectMapperBuilder.build(), objectMapperBuilder.createXmlMapper(true).build(), charset);
87 }
88
89
90
91
92
93
94
95 public RestApiExceptionParserImpl(ObjectMapper objectMapper, XmlMapper xmlMapper) {
96 this(objectMapper, xmlMapper, null);
97 }
98
99
100
101
102
103
104
105
106 public RestApiExceptionParserImpl(
107 ObjectMapper objectMapper,
108 XmlMapper xmlMapper,
109 Charset defaultCharset) {
110 this.objectMapper = objectMapper;
111 this.xmlMapper = xmlMapper;
112 this.defaultCharset = Optional.ofNullable(defaultCharset)
113 .orElse(StandardCharsets.UTF_8);
114 }
115
116 private ObjectMapper getJsonMapper() {
117 return objectMapper;
118 }
119
120 private XmlMapper getXmlMapper() {
121 return xmlMapper;
122 }
123
124
125
126
127
128
129
130 Optional<ObjectMapper> getObjectMapper(RestApiResponseType responseType) {
131 if (responseType == RestApiResponseType.JSON) {
132 return Optional.of(getJsonMapper());
133 }
134 if (responseType == RestApiResponseType.XML) {
135 return Optional.of(getXmlMapper());
136 }
137 return Optional.empty();
138 }
139
140
141
142
143
144
145 protected Charset getDefaultCharset() {
146 return defaultCharset;
147 }
148
149 @Override
150 public RestApiException parseException(
151 byte[] response,
152 HttpStatusCode httpStatus,
153 HttpHeaders headers) {
154
155 String responseStr;
156 if (isNull(response) || response.length == 0) {
157 responseStr = null;
158 } else {
159 responseStr = new String(response, getContentTypeCharset(headers.getContentType()));
160 }
161 return parseException(responseStr, httpStatus, headers);
162 }
163
164 @Override
165 public RestApiException parseException(
166 String response,
167 HttpStatusCode httpStatus,
168 HttpHeaders headers) {
169
170 RestApiResponseType responseType = RestApiResponseType
171 .detectByContentType(headers.getContentType());
172 return Optional.ofNullable(response)
173 .filter(res -> !res.isBlank())
174 .flatMap(res -> getObjectMapper(responseType).flatMap(om -> {
175 try {
176 return Optional.of(om.readValue(res, RestApiException.class));
177 } catch (Exception ignored) {
178 log.debug("Response is not a 'RestApiException' as {}.", responseType.name());
179 return Optional.empty();
180 }
181 }))
182 .map(restApiException -> {
183 restApiException.setStatus(httpStatus.value());
184 if ((httpStatus instanceof HttpStatus status)
185 && ObjectUtils.isEmpty(restApiException.getError())) {
186 restApiException.setError(status.getReasonPhrase());
187 }
188 return restApiException;
189 })
190 .orElseGet(() -> getRestApiExceptionFromHeaders(response, httpStatus, headers));
191 }
192
193
194
195
196
197
198
199
200
201 protected RestApiException getRestApiExceptionFromHeaders(
202 String response,
203 HttpStatusCode httpStatus,
204 HttpHeaders httpHeaders) {
205
206 RestApiException restApiException = new RestApiException();
207
208 String tmp = httpHeaders.getFirst(RestApiExceptionConstants.ID_HEADER_NAME);
209 if (hasText(tmp)) {
210 restApiException.setId(tmp);
211 }
212
213 tmp = httpHeaders.getFirst(RestApiExceptionConstants.TIMESTAMP_HEADER_NAME);
214 restApiException.setTimestamp(parseErrorTimestamp(tmp));
215
216 tmp = httpHeaders.getFirst(RestApiExceptionConstants.CODE_HEADER_NAME);
217 if (hasText(tmp)) {
218 restApiException.setErrorCode(tmp);
219
220 tmp = httpHeaders.getFirst(RestApiExceptionConstants.CODE_INHERITED_HEADER_NAME);
221 if (hasText(tmp)) {
222 restApiException.setErrorCodeInherited(Boolean.valueOf(tmp));
223 }
224 }
225
226 tmp = httpHeaders.getFirst(RestApiExceptionConstants.MESSAGE_HEADER_NAME);
227 if (hasText(tmp)) {
228 restApiException.setMessage(tmp);
229 } else {
230 restApiException.setMessage(response);
231 }
232
233 tmp = httpHeaders.getFirst(RestApiExceptionConstants.EXCEPTION_HEADER_NAME);
234 if (hasText(tmp)) {
235 restApiException.setException(tmp);
236 }
237
238 tmp = httpHeaders.getFirst(RestApiExceptionConstants.APPLICATION_HEADER_NAME);
239 if (hasText(tmp)) {
240 restApiException.setApplication(tmp);
241 }
242
243 tmp = httpHeaders.getFirst(RestApiExceptionConstants.PATH_HEADER_NAME);
244 if (hasText(tmp)) {
245 restApiException.setPath(tmp);
246 }
247
248 restApiException.setStatus(httpStatus.value());
249 if (httpStatus instanceof HttpStatus status) {
250 restApiException.setError(status.getReasonPhrase());
251 }
252
253 return restApiException;
254 }
255
256
257
258
259
260
261
262 protected Charset getContentTypeCharset(MediaType contentType) {
263 return Optional.ofNullable(contentType)
264 .map(ct -> {
265 RestApiResponseType responseType = RestApiResponseType.detectByContentType(ct);
266 if (RestApiResponseType.JSON == responseType) {
267 return StandardCharsets.UTF_8;
268 }
269 Charset charset = ct.getCharset();
270 if (isNull(charset) && RestApiResponseType.XML == responseType) {
271 return StandardCharsets.UTF_8;
272 }
273 return charset;
274 })
275 .orElseGet(this::getDefaultCharset);
276 }
277
278
279
280
281
282
283
284 protected OffsetDateTime parseErrorTimestamp(String value) {
285 OffsetDateTime time = null;
286 if (nonNull(value)) {
287 try {
288 time = OffsetDateTime.parse(value, RestApiExceptionConstants.TIMESTAMP_FORMATTER);
289 } catch (Exception e) {
290 log.debug("Parsing timestamp failed, timestamp = '{}'.", value);
291 }
292 }
293 return time;
294 }
295
296 }