View Javadoc
1   /*
2    * Copyright 2019-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 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   * The default implementation of a http response parser that creates a {@link RestApiException}.
40   *
41   * @author Christian Bremer
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     * Instantiates a new rest api exception parser.
54     */
55    public RestApiExceptionParserImpl() {
56      this(new Jackson2ObjectMapperBuilder());
57    }
58  
59    /**
60     * Instantiates a new rest api exception parser.
61     *
62     * @param defaultCharset the default charset
63     */
64    public RestApiExceptionParserImpl(Charset defaultCharset) {
65      this(new Jackson2ObjectMapperBuilder(), defaultCharset);
66    }
67  
68    /**
69     * Instantiates a new rest api exception parser.
70     *
71     * @param objectMapperBuilder the object mapper builder
72     */
73    public RestApiExceptionParserImpl(Jackson2ObjectMapperBuilder objectMapperBuilder) {
74      this(objectMapperBuilder.build(), objectMapperBuilder.createXmlMapper(true).build());
75    }
76  
77    /**
78     * Instantiates a new rest api exception parser.
79     *
80     * @param objectMapperBuilder the object mapper builder
81     * @param charset the charset
82     */
83    public RestApiExceptionParserImpl(
84        Jackson2ObjectMapperBuilder objectMapperBuilder,
85        Charset charset) {
86      this(objectMapperBuilder.build(), objectMapperBuilder.createXmlMapper(true).build(), charset);
87    }
88  
89    /**
90     * Instantiates a new rest api exception parser.
91     *
92     * @param objectMapper the object mapper
93     * @param xmlMapper the xml mapper
94     */
95    public RestApiExceptionParserImpl(ObjectMapper objectMapper, XmlMapper xmlMapper) {
96      this(objectMapper, xmlMapper, null);
97    }
98  
99    /**
100    * Instantiates a new rest api exception parser.
101    *
102    * @param objectMapper the object mapper
103    * @param xmlMapper the xml mapper
104    * @param defaultCharset the default charset
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    * Gets object mapper.
126    *
127    * @param responseType the response type
128    * @return the object mapper
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    * Gets default charset.
142    *
143    * @return the default charset
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    * Gets rest api exception from headers.
195    *
196    * @param response the response
197    * @param httpStatus the http status
198    * @param httpHeaders the http headers
199    * @return the rest api exception from headers
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    * Gets content type charset.
258    *
259    * @param contentType the content type
260    * @return the content type charset
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    * Parse the 'timestamp' header value.
280    *
281    * @param value the 'timestamp' header value
282    * @return the timestamp
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 }