1 /*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
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 * A copy of the License is located at
7 *
8 * http://aws.amazon.com/apache2.0
9 *
10 * or in the "license" file accompanying this file. This file is distributed
11 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12 * express or implied. See the License for the specific language governing
13 * permissions and limitations under the License.
14 */
15
16 package software.amazon.awssdk.protocols.query.internal.unmarshall;
17
18 import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;
19
20 import java.time.Duration;
21 import java.util.List;
22 import java.util.Optional;
23 import java.util.function.Supplier;
24 import software.amazon.awssdk.annotations.SdkInternalApi;
25 import software.amazon.awssdk.awscore.AwsExecutionAttribute;
26 import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
27 import software.amazon.awssdk.awscore.exception.AwsServiceException;
28 import software.amazon.awssdk.core.SdkBytes;
29 import software.amazon.awssdk.core.SdkPojo;
30 import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
31 import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute;
32 import software.amazon.awssdk.http.SdkHttpFullResponse;
33 import software.amazon.awssdk.protocols.core.ExceptionMetadata;
34 import software.amazon.awssdk.protocols.query.unmarshall.XmlElement;
35 import software.amazon.awssdk.protocols.query.unmarshall.XmlErrorUnmarshaller;
36
37 /**
38 * Unmarshalls an AWS XML exception from parsed XML.
39 */
40 @SdkInternalApi
41 public final class AwsXmlErrorUnmarshaller {
42 private static final String X_AMZN_REQUEST_ID_HEADER = "x-amzn-RequestId";
43 private static final String X_AMZ_ID_2_HEADER = "x-amz-id-2";
44
45 private final List<ExceptionMetadata> exceptions;
46 private final Supplier<SdkPojo> defaultExceptionSupplier;
47
48 private final XmlErrorUnmarshaller errorUnmarshaller;
49
50 private AwsXmlErrorUnmarshaller(Builder builder) {
51 this.exceptions = builder.exceptions;
52 this.errorUnmarshaller = builder.errorUnmarshaller;
53 this.defaultExceptionSupplier = builder.defaultExceptionSupplier;
54 }
55
56 /**
57 * @return New Builder instance.
58 */
59 public static Builder builder() {
60 return new Builder();
61 }
62
63 /**
64 * Unmarshal an AWS XML exception
65 * @param documentRoot The parsed payload document
66 * @param errorRoot The specific element of the parsed payload document that contains the error to be marshalled
67 * or empty if it could not be located.
68 * @param documentBytes The raw bytes of the original payload document if they are available
69 * @param response The HTTP response object
70 * @param executionAttributes {@link ExecutionAttributes} for the current execution
71 * @return An {@link AwsServiceException} unmarshalled from the XML.
72 */
73 public AwsServiceException unmarshall(XmlElement documentRoot,
74 Optional<XmlElement> errorRoot,
75 Optional<SdkBytes> documentBytes,
76 SdkHttpFullResponse response,
77 ExecutionAttributes executionAttributes) {
78 String errorCode = getErrorCode(errorRoot);
79
80 AwsServiceException.Builder builder = errorRoot
81 .map(e -> invokeSafely(() -> unmarshallFromErrorCode(response, e, errorCode)))
82 .orElseGet(this::defaultException);
83
84 AwsErrorDetails awsErrorDetails =
85 AwsErrorDetails.builder()
86 .errorCode(errorCode)
87 .errorMessage(builder.message())
88 .rawResponse(documentBytes.orElse(null))
89 .sdkHttpResponse(response)
90 .serviceName(executionAttributes.getAttribute(AwsExecutionAttribute.SERVICE_NAME))
91 .build();
92
93 builder.requestId(getRequestId(response, documentRoot))
94 .extendedRequestId(getExtendedRequestId(response))
95 .statusCode(response.statusCode())
96 .clockSkew(getClockSkew(executionAttributes))
97 .awsErrorDetails(awsErrorDetails);
98
99 return builder.build();
100 }
101
102 private Duration getClockSkew(ExecutionAttributes executionAttributes) {
103 Integer timeOffset = executionAttributes.getAttribute(SdkExecutionAttribute.TIME_OFFSET);
104 return timeOffset == null ? null : Duration.ofSeconds(timeOffset);
105 }
106
107 /**
108 * @return Builder for the default service exception. Used when the error code doesn't match
109 * any known modeled exception or when we can't determine the error code.
110 */
111 private AwsServiceException.Builder defaultException() {
112 return (AwsServiceException.Builder) defaultExceptionSupplier.get();
113 }
114
115 /**
116 * Unmarshalls the XML into the appropriate modeled exception based on the error code. If the error code
117 * is not present or does not match any known exception we unmarshall into the base service exception.
118 *
119 * @param errorRoot Root of <Error/> element. Contains any modeled fields of the exception.
120 * @param errorCode Error code identifying the modeled exception.
121 * @return Unmarshalled exception builder.
122 */
123 private AwsServiceException.Builder unmarshallFromErrorCode(SdkHttpFullResponse response,
124 XmlElement errorRoot,
125 String errorCode) {
126 SdkPojo sdkPojo = exceptions.stream()
127 .filter(e -> e.errorCode().equals(errorCode))
128 .map(ExceptionMetadata::exceptionBuilderSupplier)
129 .findAny()
130 .orElse(defaultExceptionSupplier)
131 .get();
132
133 AwsServiceException.Builder builder =
134 ((AwsServiceException) errorUnmarshaller.unmarshall(sdkPojo, errorRoot, response)).toBuilder();
135 builder.message(getMessage(errorRoot));
136 return builder;
137 }
138
139 /**
140 * Extracts the error code (used to identify the modeled exception) from the <Error/>
141 * element.
142 *
143 * @param errorRoot Error element root.
144 * @return Error code or null if not present.
145 */
146 private String getErrorCode(Optional<XmlElement> errorRoot) {
147 return errorRoot.map(e -> e.getOptionalElementByName("Code")
148 .map(XmlElement::textContent)
149 .orElse(null))
150 .orElse(null);
151 }
152
153 /**
154 * Extracts the error message from the XML document. The message is in the <Error/>
155 * element for all services.
156 *
157 * @param errorRoot Error element root.
158 * @return Error message or null if not present.
159 */
160 private String getMessage(XmlElement errorRoot) {
161 return errorRoot.getOptionalElementByName("Message")
162 .map(XmlElement::textContent)
163 .orElse(null);
164 }
165
166 /**
167 * Extracts the request ID from the XML document. Request ID is a top level element
168 * for all protocols, it may be RequestId or RequestID depending on the service.
169 *
170 * @param document Root XML document.
171 * @return Request ID string or null if not present.
172 */
173 private String getRequestId(SdkHttpFullResponse response, XmlElement document) {
174 XmlElement requestId = document.getOptionalElementByName("RequestId")
175 .orElse(document.getElementByName("RequestID"));
176 return requestId != null ?
177 requestId.textContent() :
178 response.firstMatchingHeader(X_AMZN_REQUEST_ID_HEADER).orElse(null);
179 }
180
181 /**
182 * Extracts the extended request ID from the response headers.
183 *
184 * @param response The HTTP response object.
185 * @return Extended Request ID string or null if not present.
186 */
187 private String getExtendedRequestId(SdkHttpFullResponse response) {
188 return response.firstMatchingHeader(X_AMZ_ID_2_HEADER).orElse(null);
189 }
190
191 /**
192 * Builder for {@link AwsXmlErrorUnmarshaller}.
193 */
194 public static final class Builder {
195
196 private List<ExceptionMetadata> exceptions;
197 private Supplier<SdkPojo> defaultExceptionSupplier;
198 private XmlErrorUnmarshaller errorUnmarshaller;
199
200 private Builder() {
201 }
202
203 /**
204 * List of {@link ExceptionMetadata} to represent the modeled exceptions for the service.
205 * For AWS services the error type is a string representing the type of the modeled exception.
206 *
207 * @return This builder for method chaining.
208 */
209 public Builder exceptions(List<ExceptionMetadata> exceptions) {
210 this.exceptions = exceptions;
211 return this;
212 }
213
214 /**
215 * Default exception type if "error code" does not match any known modeled exception. This is the generated
216 * base exception for the service (i.e. DynamoDbException).
217 *
218 * @return This builder for method chaining.
219 */
220 public Builder defaultExceptionSupplier(Supplier<SdkPojo> defaultExceptionSupplier) {
221 this.defaultExceptionSupplier = defaultExceptionSupplier;
222 return this;
223 }
224
225 /**
226 * The unmarshaller to use. The unmarshaller only unmarshalls any modeled fields of the exception,
227 * additional metadata is extracted by {@link AwsXmlErrorUnmarshaller}.
228 *
229 * @param errorUnmarshaller Error unmarshaller to use.
230 * @return This builder for method chaining.
231 */
232 public Builder errorUnmarshaller(XmlErrorUnmarshaller errorUnmarshaller) {
233 this.errorUnmarshaller = errorUnmarshaller;
234 return this;
235 }
236
237 /**
238 * @return New instance of {@link AwsXmlErrorUnmarshaller}.
239 */
240 public AwsXmlErrorUnmarshaller build() {
241 return new AwsXmlErrorUnmarshaller(this);
242 }
243 }
244 }
245