1 package com.github.davidmoten.aws.lw.client;
2
3 import java.nio.charset.StandardCharsets;
4 import java.util.ArrayList;
5 import java.util.Arrays;
6 import java.util.HashMap;
7 import java.util.List;
8 import java.util.Map;
9 import java.util.Optional;
10 import java.util.concurrent.TimeUnit;
11 import java.util.stream.Collectors;
12
13 import com.github.davidmoten.aws.lw.client.internal.Retries;
14 import com.github.davidmoten.aws.lw.client.internal.util.Preconditions;
15 import com.github.davidmoten.aws.lw.client.internal.util.Util;
16 import com.github.davidmoten.aws.lw.client.xml.XmlElement;
17
18 public final class Request {
19
20 private final Client client;
21 private Optional<String> region;
22 private String url;
23 private HttpMethod method = HttpMethod.GET;
24 private final Map<String, List<String>> headers = new HashMap<>();
25 private byte[] requestBody;
26 private int connectTimeoutMs;
27 private int readTimeoutMs;
28 private int attributeNumber = 1;
29 private Retries<ResponseInputStream> retries;
30 private String attributePrefix = "Attribute";
31 private String[] pathSegments;
32 private final List<NameValue> queries = new ArrayList<>();
33 private boolean signPayload = true;
34
35 Request(Client client, String url, String... pathSegments) {
36 this.client = client;
37 this.url = url;
38 this.pathSegments = pathSegments;
39 this.region = client.region();
40 this.connectTimeoutMs = client.connectTimeoutMs();
41 this.readTimeoutMs = client.readTimeoutMs();
42 this.retries = client.retries().copy();
43 }
44
45 public Request method(HttpMethod method) {
46 Preconditions.checkNotNull(method);
47 this.method = method;
48 return this;
49 }
50
51 public Request query(String name, String value) {
52 Preconditions.checkNotNull(name);
53 queries.add(new NameValue(name, value));
54 return this;
55 }
56
57 public Request query(String name) {
58 return query(name, null);
59 }
60
61 public Request attributePrefix(String attributePrefix) {
62 this.attributePrefix = attributePrefix;
63 this.attributeNumber = 1;
64 return this;
65 }
66
67 public Request attribute(String name, String value) {
68 int i = attributeNumber;
69 attributeNumber++;
70 return query(attributePrefix + "." + i + ".Name", name)
71 .query(attributePrefix + "." + i + ".Value", value);
72 }
73
74 public Request header(String name, String value) {
75 Preconditions.checkNotNull(name);
76 Preconditions.checkNotNull(value);
77 RequestHelper.put(headers, name, value);
78 return this;
79 }
80
81 public Request signPayload(boolean signPayload) {
82 this.signPayload = signPayload;
83 return this;
84 }
85
86 public Request unsignedPayload() {
87 return signPayload(false);
88 }
89
90
91
92
93
94
95
96
97
98
99 public Request metadata(String key, String value) {
100 Preconditions.checkNotNull(key);
101 Preconditions.checkNotNull(value);
102 return header("x-amz-meta-" + Util.canonicalMetadataKey(key), value);
103 }
104
105 public Request requestBody(byte[] requestBody) {
106 Preconditions.checkNotNull(requestBody);
107 this.requestBody = requestBody;
108 return this;
109 }
110
111 public Request requestBody(String requestBody) {
112 Preconditions.checkNotNull(requestBody);
113 return requestBody(requestBody.getBytes(StandardCharsets.UTF_8));
114 }
115
116 public Request region(String region) {
117 Preconditions.checkNotNull(region);
118 this.region = Optional.of(region);
119 return this;
120 }
121
122 public Request connectTimeout(long duration, TimeUnit unit) {
123 Preconditions.checkArgument(duration >= 0, "duration cannot be negative");
124 Preconditions.checkNotNull(unit, "unit cannot be null");
125 this.connectTimeoutMs = (int) unit.toMillis(duration);
126 return this;
127 }
128
129 public Request readTimeout(long duration, TimeUnit unit) {
130 Preconditions.checkArgument(duration >= 0, "duration cannot be negative");
131 Preconditions.checkNotNull(unit, "unit cannot be null");
132 this.readTimeoutMs = (int) unit.toMillis(duration);
133 return this;
134 }
135
136 public Request retryInitialInterval(long duration, TimeUnit unit) {
137 Preconditions.checkArgument(duration >= 0, "duration cannot be negative");
138 Preconditions.checkNotNull(unit, "unit cannot be null");
139 retries = retries.withInitialIntervalMs(unit.toMillis(duration));
140 return this;
141 }
142
143 public Request retryMaxAttempts(int maxAttempts) {
144 Preconditions.checkArgument(maxAttempts >=0, "retryMaxAttempts cannot be negative");
145 retries = retries.withMaxAttempts(maxAttempts);
146 return this;
147 }
148
149
150
151
152
153
154
155
156
157
158 public Request retryJitter(double jitter) {
159 Preconditions.checkArgument(jitter >= 0 && jitter <= 1, "jitter must be between 0 and 1");
160 retries = retries.withJitter(jitter);
161 return this;
162 }
163
164
165 public Request retryBackoffFactor(double factor) {
166 Preconditions.checkArgument(factor >=0, "retryBackoffFactor cannot be negative");
167 retries = retries.withBackoffFactor(factor);
168 return this;
169 }
170
171 public Request retryMaxInterval(long duration, TimeUnit unit) {
172 Preconditions.checkArgument(duration >= 0, "duration cannot be negative");
173 Preconditions.checkNotNull(unit, "unit cannot be null");
174 retries = retries.withMaxIntervalMs(unit.toMillis(duration));
175 return this;
176 }
177
178
179
180
181
182
183
184
185
186
187
188 public ResponseInputStream responseInputStream() {
189 String u = calculateUrl(url, client.serviceName(), region, queries, Arrays.asList(pathSegments),
190 client.baseUrlFactory());
191 return retries
192 .call(() -> RequestHelper.request(client.clock(), client.httpClient(), u, method,
193 RequestHelper.combineHeaders(headers), requestBody, client.serviceName(), region,
194 client.credentials(), connectTimeoutMs, readTimeoutMs, signPayload));
195 }
196
197
198
199
200
201
202
203
204
205 public Response response() {
206 ResponseInputStream r = responseInputStream();
207 final byte[] bytes;
208 if (hasBody(r)) {
209 bytes = Util.readBytesAndClose(r);
210 } else {
211 bytes = new byte[0];
212 }
213 return new Response(r.headers(), bytes, r.statusCode());
214 }
215
216
217
218
219
220
221
222
223
224
225 public Response responseExpectStatusCode(int expectedStatusCode) {
226 Response r = response();
227 if (r.statusCode() == expectedStatusCode) {
228 return r;
229 } else {
230 throw new ServiceException(r.statusCode(), r.contentUtf8());
231 }
232 }
233
234
235 static boolean hasBody(ResponseInputStream r) {
236 return r.header("Content-Length").isPresent()
237 || r.header("Transfer-Encoding").orElse("").equalsIgnoreCase("chunked");
238 }
239
240 private static String calculateUrl(String url, String serviceName, Optional<String> region, List<NameValue> queries,
241 List<String> pathSegments, BaseUrlFactory baseUrlFactory) {
242 String u = url;
243 if (u == null) {
244 String baseUrl = baseUrlFactory.create(serviceName, region);
245 Preconditions.checkNotNull(baseUrl, "baseUrl cannot be null");
246 u = trimAndEnsureHasTrailingSlash(baseUrl)
247 + pathSegments
248 .stream()
249 .map(x -> trimAndRemoveLeadingAndTrailingSlashes(x))
250 .collect(Collectors.joining("/"));
251 }
252
253 for (NameValue nv : queries) {
254 if (!u.contains("?")) {
255 u += "?";
256 }
257 if (!u.endsWith("?")) {
258 u += "&";
259 }
260 if (nv.value != null) {
261 u += Util.urlEncode(nv.name, false) + "=" + Util.urlEncode(nv.value, false);
262 } else {
263 u += Util.urlEncode(nv.name, false);
264 }
265 }
266 return u;
267 }
268
269
270 static String trimAndEnsureHasTrailingSlash(String s) {
271 String r = s.trim();
272 if (r.endsWith("/")) {
273 return r;
274 } else {
275 return r + "/";
276 }
277 }
278
279 public byte[] responseAsBytes() {
280 Response r = response();
281 Optional<? extends RuntimeException> exception = client.exceptionFactory().create(r);
282 if (!exception.isPresent()) {
283 return r.content();
284 } else {
285 throw exception.get();
286 }
287 }
288
289
290
291
292
293
294
295
296
297 public boolean exists() {
298 return response().exists();
299 }
300
301 public void execute() {
302 responseAsBytes();
303 }
304
305 public String responseAsUtf8() {
306 return new String(responseAsBytes(), StandardCharsets.UTF_8);
307 }
308
309 public XmlElement responseAsXml() {
310 return XmlElement.parse(responseAsUtf8());
311 }
312
313 public String presignedUrl(long expiryDuration, TimeUnit unit) {
314 String u = calculateUrl(url, client.serviceName(), region, queries, Arrays.asList(pathSegments),
315 client.baseUrlFactory());
316 return RequestHelper.presignedUrl(client.clock(), u, method.toString(), RequestHelper.combineHeaders(headers),
317 requestBody, client.serviceName(), region, client.credentials(), connectTimeoutMs, readTimeoutMs,
318 unit.toSeconds(expiryDuration), signPayload);
319 }
320
321
322 static String trimAndRemoveLeadingAndTrailingSlashes(String s) {
323 Preconditions.checkNotNull(s);
324 s = s.trim();
325 if (s.startsWith("/")) {
326 s = s.substring(1);
327 }
328 if (s.endsWith("/")) {
329 s = s.substring(0, s.length() - 1);
330 }
331 return s;
332 }
333
334 private static final class NameValue {
335 final String name;
336 final String value;
337
338 NameValue(String name, String value) {
339 this.name = name;
340 this.value = value;
341 }
342 }
343 }