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.http;
17
18 import static software.amazon.awssdk.utils.CollectionUtils.deepCopyMap;
19 import static software.amazon.awssdk.utils.CollectionUtils.deepUnmodifiableMap;
20
21 import java.util.ArrayList;
22 import java.util.LinkedHashMap;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Optional;
26 import java.util.TreeMap;
27 import java.util.function.Consumer;
28 import software.amazon.awssdk.annotations.Immutable;
29 import software.amazon.awssdk.annotations.SdkInternalApi;
30 import software.amazon.awssdk.utils.CollectionUtils;
31 import software.amazon.awssdk.utils.StringUtils;
32 import software.amazon.awssdk.utils.ToString;
33 import software.amazon.awssdk.utils.Validate;
34 import software.amazon.awssdk.utils.http.SdkHttpUtils;
35
36 /**
37  * Internal implementation of {@link SdkHttpFullRequest}, buildable via {@link SdkHttpFullRequest#builder()}. Provided to HTTP
38  * implementation to execute a request.
39  */

40 @SdkInternalApi
41 @Immutable
42 final class DefaultSdkHttpFullRequest implements SdkHttpFullRequest {
43     private final String protocol;
44     private final String host;
45     private final Integer port;
46     private final String path;
47     private final Map<String, List<String>> queryParameters;
48     private final SdkHttpMethod httpMethod;
49     private final Map<String, List<String>> headers;
50     private final ContentStreamProvider contentStreamProvider;
51
52     private DefaultSdkHttpFullRequest(Builder builder) {
53         this.protocol = standardizeProtocol(builder.protocol);
54         this.host = Validate.paramNotNull(builder.host, "host");
55         this.port = standardizePort(builder.port);
56         this.path = standardizePath(builder.path);
57         this.httpMethod = Validate.paramNotNull(builder.httpMethod, "method");
58         this.contentStreamProvider = builder.contentStreamProvider;
59
60         this.queryParameters = builder.queryParametersAreFromToBuilder
61                                ? builder.queryParameters
62                                : deepUnmodifiableMap(builder.queryParameters, () -> new LinkedHashMap<>());
63         this.headers = builder.headersAreFromToBuilder
64                        ? builder.headers
65                        : deepUnmodifiableMap(builder.headers, () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER));
66     }
67
68     private String standardizeProtocol(String protocol) {
69         Validate.paramNotNull(protocol, "protocol");
70
71         String standardizedProtocol = StringUtils.lowerCase(protocol);
72         Validate.isTrue(standardizedProtocol.equals("http") || standardizedProtocol.equals("https"),
73                         "Protocol must be 'http' or 'https', but was %s", protocol);
74
75         return standardizedProtocol;
76     }
77
78     private String standardizePath(String path) {
79         if (StringUtils.isEmpty(path)) {
80             return "";
81         }
82
83         StringBuilder standardizedPath = new StringBuilder();
84
85         // Path must always start with '/'
86         if (!path.startsWith("/")) {
87             standardizedPath.append('/');
88         }
89
90         standardizedPath.append(path);
91
92         return standardizedPath.toString();
93     }
94
95     private Integer standardizePort(Integer port) {
96         Validate.isTrue(port == null || port >= -1,
97                         "Port must be positive (or null/-1 to indicate no port), but was '%s'", port);
98
99         if (port != null && port == -1) {
100             return null;
101         }
102
103         return port;
104     }
105
106     @Override
107     public String protocol() {
108         return protocol;
109     }
110
111     @Override
112     public String host() {
113         return host;
114     }
115
116     @Override
117     public int port() {
118         return Optional.ofNullable(port).orElseGet(() -> SdkHttpUtils.standardPort(protocol()));
119     }
120
121     @Override
122     public Map<String, List<String>> headers() {
123         return headers;
124     }
125
126     @Override
127     public String encodedPath() {
128         return path;
129     }
130
131     @Override
132     public Map<String, List<String>> rawQueryParameters() {
133         return queryParameters;
134     }
135
136     @Override
137     public SdkHttpMethod method() {
138         return httpMethod;
139     }
140
141     @Override
142     public Optional<ContentStreamProvider> contentStreamProvider() {
143         return Optional.ofNullable(contentStreamProvider);
144     }
145
146     @Override
147     public SdkHttpFullRequest.Builder toBuilder() {
148         return new Builder(this);
149     }
150
151     @Override
152     public String toString() {
153         return ToString.builder("DefaultSdkHttpFullRequest")
154                        .add("httpMethod", httpMethod)
155                        .add("protocol", protocol)
156                        .add("host", host)
157                        .add("port", port)
158                        .add("encodedPath", path)
159                        .add("headers", headers.keySet())
160                        .add("queryParameters", queryParameters.keySet())
161                        .build();
162     }
163
164     /**
165      * Builder for a {@link DefaultSdkHttpFullRequest}.
166      */

167     static final class Builder implements SdkHttpFullRequest.Builder {
168         private String protocol;
169         private String host;
170         private Integer port;
171         private String path;
172
173         private boolean queryParametersAreFromToBuilder;
174         private Map<String, List<String>> queryParameters;
175
176         private SdkHttpMethod httpMethod;
177
178         private boolean headersAreFromToBuilder;
179         private Map<String, List<String>> headers;
180
181         private ContentStreamProvider contentStreamProvider;
182
183         Builder() {
184             queryParameters = new LinkedHashMap<>();
185             queryParametersAreFromToBuilder = false;
186             headers = new LinkedHashMap<>();
187             headersAreFromToBuilder = false;
188         }
189
190         Builder(DefaultSdkHttpFullRequest request) {
191             queryParameters = request.queryParameters;
192             queryParametersAreFromToBuilder = true;
193             headers = request.headers;
194             headersAreFromToBuilder = true;
195             protocol = request.protocol;
196             host = request.host;
197             port = request.port;
198             path = request.path;
199             httpMethod = request.httpMethod;
200             contentStreamProvider = request.contentStreamProvider;
201         }
202
203         @Override
204         public String protocol() {
205             return protocol;
206         }
207
208         @Override
209         public SdkHttpFullRequest.Builder protocol(String protocol) {
210             this.protocol = protocol;
211             return this;
212         }
213
214         @Override
215         public String host() {
216             return host;
217         }
218
219         @Override
220         public SdkHttpFullRequest.Builder host(String host) {
221             this.host = host;
222             return this;
223         }
224
225         @Override
226         public Integer port() {
227             return port;
228         }
229
230         @Override
231         public SdkHttpFullRequest.Builder port(Integer port) {
232             this.port = port;
233             return this;
234         }
235
236         @Override
237         public DefaultSdkHttpFullRequest.Builder encodedPath(String path) {
238             this.path = path;
239             return this;
240         }
241
242         @Override
243         public String encodedPath() {
244             return path;
245         }
246
247         @Override
248         public DefaultSdkHttpFullRequest.Builder putRawQueryParameter(String paramName, List<String> paramValues) {
249             copyQueryParamsIfNeeded();
250             this.queryParameters.put(paramName, new ArrayList<>(paramValues));
251             return this;
252         }
253
254         @Override
255         public SdkHttpFullRequest.Builder appendRawQueryParameter(String paramName, String paramValue) {
256             copyQueryParamsIfNeeded();
257             this.queryParameters.computeIfAbsent(paramName, k -> new ArrayList<>()).add(paramValue);
258             return this;
259         }
260
261         @Override
262         public DefaultSdkHttpFullRequest.Builder rawQueryParameters(Map<String, List<String>> queryParameters) {
263             this.queryParameters = CollectionUtils.deepCopyMap(queryParameters, () -> new LinkedHashMap<>());
264             queryParametersAreFromToBuilder = false;
265             return this;
266         }
267
268         @Override
269         public Builder removeQueryParameter(String paramName) {
270             copyQueryParamsIfNeeded();
271             this.queryParameters.remove(paramName);
272             return this;
273         }
274
275         @Override
276         public Builder clearQueryParameters() {
277             this.queryParameters = new LinkedHashMap<>();
278             queryParametersAreFromToBuilder = false;
279             return this;
280         }
281
282         private void copyQueryParamsIfNeeded() {
283             if (queryParametersAreFromToBuilder) {
284                 queryParametersAreFromToBuilder = false;
285                 this.queryParameters = deepCopyMap(queryParameters);
286             }
287         }
288
289         @Override
290         public Map<String, List<String>> rawQueryParameters() {
291             return CollectionUtils.unmodifiableMapOfLists(queryParameters);
292         }
293
294         @Override
295         public DefaultSdkHttpFullRequest.Builder method(SdkHttpMethod httpMethod) {
296             this.httpMethod = httpMethod;
297             return this;
298         }
299
300         @Override
301         public SdkHttpMethod method() {
302             return httpMethod;
303         }
304
305         @Override
306         public DefaultSdkHttpFullRequest.Builder putHeader(String headerName, List<String> headerValues) {
307             copyHeadersIfNeeded();
308             this.headers.put(headerName, new ArrayList<>(headerValues));
309             return this;
310         }
311
312         @Override
313         public SdkHttpFullRequest.Builder appendHeader(String headerName, String headerValue) {
314             copyHeadersIfNeeded();
315             this.headers.computeIfAbsent(headerName, k -> new ArrayList<>()).add(headerValue);
316             return this;
317         }
318
319         @Override
320         public DefaultSdkHttpFullRequest.Builder headers(Map<String, List<String>> headers) {
321             this.headers = CollectionUtils.deepCopyMap(headers);
322             headersAreFromToBuilder = false;
323             return this;
324         }
325
326         @Override
327         public SdkHttpFullRequest.Builder removeHeader(String headerName) {
328             copyHeadersIfNeeded();
329             this.headers.remove(headerName);
330             return this;
331         }
332
333         @Override
334         public SdkHttpFullRequest.Builder clearHeaders() {
335             this.headers = new LinkedHashMap<>();
336             headersAreFromToBuilder = false;
337             return this;
338         }
339
340         @Override
341         public Map<String, List<String>> headers() {
342             return CollectionUtils.unmodifiableMapOfLists(this.headers);
343         }
344
345         private void copyHeadersIfNeeded() {
346             if (headersAreFromToBuilder) {
347                 headersAreFromToBuilder = false;
348                 this.headers = deepCopyMap(headers);
349             }
350         }
351
352         @Override
353         public DefaultSdkHttpFullRequest.Builder contentStreamProvider(ContentStreamProvider contentStreamProvider) {
354             this.contentStreamProvider = contentStreamProvider;
355             return this;
356         }
357
358         public ContentStreamProvider contentStreamProvider() {
359             return contentStreamProvider;
360         }
361
362         @Override
363         public SdkHttpFullRequest.Builder copy() {
364             return build().toBuilder();
365         }
366
367         @Override
368         public SdkHttpFullRequest.Builder applyMutation(Consumer<SdkHttpRequest.Builder> mutator) {
369             mutator.accept(this);
370             return this;
371         }
372
373         @Override
374         public DefaultSdkHttpFullRequest build() {
375             return new DefaultSdkHttpFullRequest(this);
376         }
377     }
378
379 }
380