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.unmarshall;
17
18 import java.io.IOException;
19 import java.util.List;
20 import java.util.Optional;
21 import java.util.function.Function;
22 import java.util.function.Supplier;
23 import software.amazon.awssdk.annotations.SdkProtectedApi;
24 import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
25 import software.amazon.awssdk.awscore.exception.AwsServiceException;
26 import software.amazon.awssdk.core.SdkBytes;
27 import software.amazon.awssdk.core.SdkPojo;
28 import software.amazon.awssdk.core.http.HttpResponseHandler;
29 import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
30 import software.amazon.awssdk.http.SdkHttpFullResponse;
31 import software.amazon.awssdk.protocols.core.ExceptionMetadata;
32 import software.amazon.awssdk.protocols.query.internal.unmarshall.AwsXmlErrorUnmarshaller;
33 import software.amazon.awssdk.utils.Pair;
34
35 /**
36  * Error unmarshaller for Query/EC2/XML based protocols. Some examples of error responses from
37  * the various protocols are below.
38  *
39  * <h3>Legacy Query (SimpleDB/EC2)</h3>
40  * <pre>
41  * {@code
42  * <Response>
43  *    <Errors>
44  *       <Error>
45  *          <Code>MissingParameter</Code>
46  *          <Message>The request must contain the parameter DomainName</Message>
47  *          <BoxUsage>0.0055590278</BoxUsage>
48  *       </Error>
49  *    </Errors>
50  *    <RequestID>ad3280dd-5ac1-efd1-b9b0-a86969a9234d</RequestID>
51  * </Response>
52  * }
53  * </pre>
54  *
55  * <h3>Traditional Query/Rest-XML (Cloudfront)</h3>
56  * <pre>
57  * {@code
58  * <ErrorResponse xmlns="http://cloudfront.amazonaws.com/doc/2017-10-30/">
59  *    <Error>
60  *       <Type>Sender</Type>
61  *       <Code>MalformedInput</Code>
62  *       <Message>Invalid XML document</Message>
63  *    </Error>
64  *    <RequestId>7c8da4af-de44-11e8-a60e-1b2014315455</RequestId>
65  * </ErrorResponse>
66  * }
67  * </pre>
68  *
69  * <h3>Amazon S3</h3>
70  * <pre>
71  * {@code
72  * <Error>
73  *    <Code>NoSuchBucket</Code>
74  *    <Message>The specified bucket does not exist</Message>
75  *    <BucketName>flajdfadjfladjf</BucketName>
76  *    <RequestId>D9DBB9F267849CA3</RequestId>
77  *    <HostId>fn8B1fUvWzg7I3CIeMT4UMqCZDF4+QO1JlbOJlQAVOosACZsLWv/K2dapVncz34a2mArhp11PjI=</HostId>
78  * </Error>
79  * }
80  * </pre>
81  */

82 @SdkProtectedApi
83 public final class AwsXmlErrorProtocolUnmarshaller implements HttpResponseHandler<AwsServiceException> {
84
85     private final AwsXmlErrorUnmarshaller awsXmlErrorUnmarshaller;
86     private final Function<XmlElement, Optional<XmlElement>> errorRootExtractor;
87
88     private AwsXmlErrorProtocolUnmarshaller(Builder builder) {
89         this.errorRootExtractor = builder.errorRootExtractor;
90         this.awsXmlErrorUnmarshaller = AwsXmlErrorUnmarshaller.builder()
91                                                               .defaultExceptionSupplier(builder.defaultExceptionSupplier)
92                                                               .exceptions(builder.exceptions)
93                                                               .errorUnmarshaller(builder.errorUnmarshaller)
94                                                               .build();
95     }
96
97     @Override
98     public AwsServiceException handle(SdkHttpFullResponse response, ExecutionAttributes executionAttributes) {
99         Pair<XmlElement, SdkBytes> xmlAndBytes = parseXml(response);
100         XmlElement document = xmlAndBytes.left();
101         Optional<XmlElement> errorRoot = errorRootExtractor.apply(document);
102         return awsXmlErrorUnmarshaller.unmarshall(document, errorRoot, Optional.of(xmlAndBytes.right()), response,
103                                                   executionAttributes);
104     }
105
106     /**
107      * Parse the response content into an XML document. If we fail to read the content or the XML is malformed
108      * we still return an empty {@link XmlElement} so that unmarshalling can proceed. In those failure
109      * cases we can't parse out the error code so we'd unmarshall into a generic service exception.
110      *
111      * @param response HTTP response.
112      * @return Pair of the parsed {@link XmlElement} and the raw bytes of the response.
113      */

114     private Pair<XmlElement, SdkBytes> parseXml(SdkHttpFullResponse response) {
115         SdkBytes bytes = getResponseBytes(response);
116         try {
117             return Pair.of(XmlDomParser.parse(bytes.asInputStream()), bytes);
118         } catch (Exception e) {
119             return Pair.of(XmlElement.empty(), bytes);
120         }
121     }
122
123     /**
124      * Buffers the response content into an {@link SdkBytes} object. Used to fill the {@link AwsErrorDetails}. If
125      * an {@link IOException} occurs then this will return {@link #emptyXmlBytes()} so that unmarshalling can proceed.
126      *
127      * @param response HTTP response.
128      * @return Raw bytes of response.
129      */

130     private SdkBytes getResponseBytes(SdkHttpFullResponse response) {
131         try {
132             return response.content()
133                            .map(SdkBytes::fromInputStream)
134                            .orElseGet(this::emptyXmlBytes);
135         } catch (Exception e) {
136             return emptyXmlBytes();
137         }
138     }
139
140     /**
141      * @return Dummy XML document to allow unmarshalling when response can't be read/parsed.
142      */

143     private SdkBytes emptyXmlBytes() {
144         return SdkBytes.fromUtf8String("<eof/>");
145     }
146
147
148     /**
149      * @return New Builder instance.
150      */

151     public static Builder builder() {
152         return new Builder();
153     }
154
155     /**
156      * Builder for {@link AwsXmlErrorProtocolUnmarshaller}.
157      */

158     public static final class Builder {
159
160         private List<ExceptionMetadata> exceptions;
161         private Supplier<SdkPojo> defaultExceptionSupplier;
162         private Function<XmlElement, Optional<XmlElement>> errorRootExtractor;
163         private XmlErrorUnmarshaller errorUnmarshaller;
164
165         private Builder() {
166         }
167
168         /**
169          * List of {@link ExceptionMetadata} to represent the modeled exceptions for the service.
170          * For AWS services the error type is a string representing the type of the modeled exception.
171          *
172          * @return This builder for method chaining.
173          */

174         public Builder exceptions(List<ExceptionMetadata> exceptions) {
175             this.exceptions = exceptions;
176             return this;
177         }
178
179         /**
180          * Default exception type if "error code" does not match any known modeled exception. This is the generated
181          * base exception for the service (i.e. DynamoDbException).
182          *
183          * @return This builder for method chaining.
184          */

185         public Builder defaultExceptionSupplier(Supplier<SdkPojo> defaultExceptionSupplier) {
186             this.defaultExceptionSupplier = defaultExceptionSupplier;
187             return this;
188         }
189
190         /**
191          * Extracts the <Error/> element from the top level XML document. Different protocols have slightly
192          * different formats for where the Error element is located.  The error root of the XML document contains
193          * the code, message and any modeled fields of the exception. See javadocs of
194          * {@link AwsXmlErrorProtocolUnmarshaller} for examples.
195          *
196          * @param errorRootExtractor Function that extracts the <Error/> element from the root XML document.
197          * @return This builder for method chaining.
198          */

199         public Builder errorRootExtractor(Function<XmlElement, Optional<XmlElement>> errorRootExtractor) {
200             this.errorRootExtractor = errorRootExtractor;
201             return this;
202         }
203
204         /**
205          * The unmarshaller to use. The unmarshaller only unmarshalls any modeled fields of the exception,
206          * additional metadata is extracted by {@link AwsXmlErrorProtocolUnmarshaller}.
207          *
208          * @param errorUnmarshaller Error unmarshaller to use.
209          * @return This builder for method chaining.
210          */

211         public Builder errorUnmarshaller(XmlErrorUnmarshaller errorUnmarshaller) {
212             this.errorUnmarshaller = errorUnmarshaller;
213             return this;
214         }
215
216         /**
217          * @return New instance of {@link AwsXmlErrorProtocolUnmarshaller}.
218          */

219         public AwsXmlErrorProtocolUnmarshaller build() {
220             return new AwsXmlErrorProtocolUnmarshaller(this);
221         }
222     }
223 }
224