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