View Javadoc
1   package com.github.davidmoten.aws.lw.client;
2   
3   import java.io.IOException;
4   import java.io.UncheckedIOException;
5   import java.util.Arrays;
6   import java.util.Collection;
7   import java.util.HashSet;
8   import java.util.Optional;
9   import java.util.Set;
10  import java.util.concurrent.TimeUnit;
11  import java.util.function.Function;
12  import java.util.function.Predicate;
13  
14  import com.github.davidmoten.aws.lw.client.internal.Clock;
15  import com.github.davidmoten.aws.lw.client.internal.Environment;
16  import com.github.davidmoten.aws.lw.client.internal.ExceptionFactoryExtended;
17  import com.github.davidmoten.aws.lw.client.internal.Retries;
18  import com.github.davidmoten.aws.lw.client.internal.util.Preconditions;
19  
20  public final class Client {
21  
22      private final Clock clock;
23      private final String serviceName;
24      private final Optional<String> region;
25      private final Credentials credentials;
26      private final HttpClient httpClient;
27      private final int connectTimeoutMs;
28      private final int readTimeoutMs;
29      private final ExceptionFactory exceptionFactory;
30      private final BaseUrlFactory baseUrlFactory;
31      private final Retries<ResponseInputStream> retries;
32  
33      private Client(Clock clock, String serviceName, Optional<String> region, Credentials credentials,
34              HttpClient httpClient, int connectTimeoutMs, int readTimeoutMs, ExceptionFactory exceptionFactory,
35              BaseUrlFactory baseUrlFactory, Retries<ResponseInputStream> retries) {
36          this.clock = clock;
37          this.serviceName = serviceName;
38          this.region = region;
39          this.credentials = credentials;
40          this.httpClient = httpClient;
41          this.connectTimeoutMs = connectTimeoutMs;
42          this.readTimeoutMs = readTimeoutMs;
43          this.exceptionFactory = exceptionFactory;
44          this.baseUrlFactory = baseUrlFactory;
45          this.retries = retries;
46      }
47  
48      public static Builder service(String serviceName) {
49          Preconditions.checkNotNull(serviceName);
50          return new Builder(serviceName);
51      }
52  
53      ///////////////////////////////////////////////////
54      //
55      // Convenience methods for a few common services
56      // Use service(serviceName) method for the rest
57      //
58      ///////////////////////////////////////////////////
59  
60      public static Builder s3() {
61          return service("s3");
62      }
63  
64      public static Builder sqs() {
65          return service("sqs");
66      }
67  
68      public static Builder iam() {
69          return service("iam");
70      }
71  
72      public static Builder ec2() {
73          return service("ec2");
74      }
75  
76      public static Builder sns() {
77          return service("sns");
78      }
79  
80      public static Builder lambda() {
81          return service("lambda");
82      }
83  
84      ///////////////////////////////////////////////////
85  
86      String serviceName() {
87          return serviceName;
88      }
89  
90      public Optional<String> region() {
91          return region;
92      }
93  
94      Credentials credentials() {
95          return credentials;
96      }
97  
98      HttpClient httpClient() {
99          return httpClient;
100     }
101 
102     Clock clock() {
103         return clock;
104     }
105 
106     ExceptionFactory exceptionFactory() {
107         return exceptionFactory;
108     }
109 
110     BaseUrlFactory baseUrlFactory() {
111         return baseUrlFactory;
112     }
113 
114     int connectTimeoutMs() {
115         return connectTimeoutMs;
116     }
117 
118     int readTimeoutMs() {
119         return readTimeoutMs;
120     }
121 
122     Retries<ResponseInputStream> retries() {
123         return retries;
124     }
125 
126     public Request url(String url) {
127         Preconditions.checkNotNull(url);
128         return new Request(this, url);
129     }
130 
131     /**
132      * Specify the path (can include query starting with ? at end of final segment).
133      * 
134      * @param segments that will be joined together with the '/' character
135      * @return request
136      */
137     public Request path(String... segments) {
138         Preconditions.checkNotNull(segments, "segments cannot be null");
139         return new Request(this, null, segments);
140     }
141 
142     public Request query(String name, String value) {
143         Preconditions.checkNotNull(name, "name cannot be null");
144         Preconditions.checkNotNull(value, "value cannot be null");
145         return path("").query(name, value);
146     }
147 
148     public Request attributePrefix(String attributePrefix) {
149         return path("").attributePrefix(attributePrefix);
150     }
151 
152     public Request attribute(String name, String value) {
153         Preconditions.checkNotNull(name, "name cannot be null");
154         Preconditions.checkNotNull(value, "value cannot be null");
155         return path("").attribute(name, value);
156     }
157 
158     public static final class Builder {
159 
160         // from
161         // https://docs.aws.amazon.com/sdkref/latest/guide/feature-retry-behavior.html
162         private static final Set<Integer> RETRY_STATUS_CODES = new HashSet<>( //
163                 Arrays.asList( //
164                         400, // BAD_REQUEST
165                         403, // FORBIDDEN
166                         408, // REQUEST_TIMEOUT
167                         429, // TOO_MANY_REQUESTS
168                         500, // INTERNAL_SERVER_ERROR
169                         502, // BAD_GATEWAY
170                         503, // SERVICE_UNAVAILABLE
171                         509 // BANDWIDTH_LIMIT_EXCEEDED
172                 ));
173         
174 
175         private final String serviceName;
176         private Optional<String> region = Optional.empty();
177         private String accessKey;
178         private Credentials credentials;
179         private HttpClient httpClient = HttpClient.defaultClient();
180         private int connectTimeoutMs = 30000;
181         private int readTimeoutMs = 300000;
182         private ExceptionFactory exceptionFactory = ExceptionFactory.DEFAULT;
183         private Clock clock = Clock.DEFAULT;
184         private Environment environment = Environment.instance();
185         private BaseUrlFactory baseUrlFactory = BaseUrlFactory.DEFAULT;
186         private Retries<ResponseInputStream> retries = Retries.create(
187                 ris -> RETRY_STATUS_CODES.contains(ris.statusCode()), //
188         t -> t instanceof IOException || t instanceof UncheckedIOException);
189 
190         private Builder(String serviceName) {
191             this.serviceName = serviceName;
192         }
193 
194         // VisibleForTesting
195         Builder environment(Environment environment) {
196             Preconditions.checkNotNull(environment, "environment cannot be null");
197             this.environment = environment;
198             return this;
199         }
200 
201         public Builder4 defaultClient() {
202             return regionFromEnvironment().credentialsFromEnvironment();
203         }
204 
205         public Builder4 from(Client client) {
206             Preconditions.checkNotNull(client, "client cannot be null");
207             this.region = client.region;
208             this.credentials = client.credentials;
209             this.httpClient = client.httpClient;
210             this.connectTimeoutMs = client.connectTimeoutMs;
211             this.readTimeoutMs = client.readTimeoutMs;
212             this.exceptionFactory = client.exceptionFactory;
213             return new Builder4(this);
214         }
215 
216         public Builder2 regionFromEnvironment() {
217             return region(environment.get("AWS_REGION"));
218         }
219 
220         public Builder2 region(Optional<String> region) {
221             Preconditions.checkNotNull(region, "region cannot be null");
222             this.region = region;
223             return new Builder2(this);
224         }
225 
226         public Builder2 region(String region) {
227             Preconditions.checkNotNull(region, "region cannot be null");
228             return region(Optional.of(region));
229         }
230 
231         public Builder2 regionNone() {
232             return region(Optional.empty());
233         }
234     }
235 
236     public static final class Builder2 {
237         private final Builder b;
238 
239         private Builder2(Builder b) {
240             this.b = b;
241         }
242 
243         public Builder4 credentialsFromEnvironment() {
244             b.credentials = b.environment.credentials();
245             return new Builder4(b);
246         }
247 
248         public Builder4 credentialsFromSystemProperties() {
249             return credentials(Credentials.fromSystemProperties());
250         }
251 
252         public Builder3 accessKey(String accessKey) {
253             Preconditions.checkNotNull(accessKey);
254             b.accessKey = accessKey;
255             return new Builder3(b);
256         }
257 
258         public Builder4 credentials(Credentials credentials) {
259             Preconditions.checkNotNull(credentials);
260             b.credentials = credentials;
261             return new Builder4(b);
262         }
263     }
264 
265     public static final class Builder3 {
266         private final Builder b;
267 
268         private Builder3(Builder b) {
269             this.b = b;
270         }
271 
272         public Builder4 secretKey(String secretKey) {
273             Preconditions.checkNotNull(secretKey);
274             b.credentials = Credentials.of(b.accessKey, secretKey);
275             return new Builder4(b);
276         }
277     }
278 
279     public static final class Builder4 {
280         private final Builder b;
281 
282         private Builder4(Builder b) {
283             this.b = b;
284         }
285 
286         public Builder4 baseUrlFactory(BaseUrlFactory factory) {
287             b.baseUrlFactory = factory;
288             return this;
289         }
290 
291         public Builder4 httpClient(HttpClient httpClient) {
292             b.httpClient = httpClient;
293             return this;
294         }
295 
296         public Builder4 retryInitialInterval(long duration, TimeUnit unit) {
297             Preconditions.checkArgument(duration >= 0, "duration cannot be negative");
298             Preconditions.checkNotNull(unit, "unit cannot be null");
299             b.retries = b.retries.withInitialIntervalMs(unit.toMillis(duration));
300             return this;
301         }
302 
303         public Builder4 retryMaxAttempts(int maxAttempts) {
304             Preconditions.checkArgument(maxAttempts >= 0, "maxAttempts cannot be negative");
305             b.retries = b.retries.withMaxAttempts( maxAttempts);
306             return this;
307         }
308 
309         public Builder4 retryBackoffFactor(double factor) {
310             Preconditions.checkArgument(factor >= 0, "backoffFactor cannot be negative");
311             b.retries = b.retries.withBackoffFactor(factor);
312             return this;
313         }
314 
315         public Builder4 retryMaxInterval(long duration, TimeUnit unit) {
316             Preconditions.checkArgument(duration >= 0, "duration cannot be negative");
317             Preconditions.checkNotNull(unit, "unit cannot be null");
318             b.retries = b.retries.withMaxIntervalMs(unit.toMillis(duration));
319             return this;
320         }
321         
322         /**
323          * Sets the level of randomness applied to the next retry interval. The next
324          * calculated retry interval is multiplied by
325          * {@code (1 - jitter * Math.random())}. A value of zero means no jitter, 1
326          * means max jitter.
327          * 
328          * @param jitter level of randomness applied to the retry interval
329          * @return this
330          */
331         public Builder4 retryJitter(double jitter) {
332             Preconditions.checkArgument(jitter >= 0 && jitter <= 1, "jitter must be between 0 and 1");
333             b.retries = b.retries.withJitter(jitter);
334             return this;
335         }
336         
337         public Builder4 retryCondition(Predicate<? super ResponseInputStream> shouldRetry) {
338             Preconditions.checkNotNull(shouldRetry, "shouldRetry cannot be null");
339             b.retries = b.retries.withValueShouldRetry(shouldRetry);
340             return this;
341         }
342 
343         public Builder4 retryStatusCodes(Integer... statusCodes) {
344             return retryStatusCodes(Arrays.asList(statusCodes));
345         }
346         
347         public Builder4 retryStatusCodes(Collection<Integer> statusCodes) {
348             Preconditions.checkNotNull(statusCodes, "statusCodes cannot be null");
349             Set<Integer> set = new HashSet<>(statusCodes);
350             return retryCondition(ris -> set.contains(ris.statusCode()));
351         }
352         
353         /**
354          * Default behaviour is to retry IOException and UncheckedIOException.
355          * 
356          * @param shouldRetry returns true if should retry
357          * @return this
358          */
359         public Builder4 retryException(Predicate<? super Throwable> shouldRetry) {
360             Preconditions.checkNotNull(shouldRetry, "shouldRetry cannot be null");
361             b.retries = b.retries.withThrowableShouldRetry(shouldRetry);
362             return this;
363         }
364         
365         public Builder4 connectTimeout(long duration, TimeUnit unit) {
366             Preconditions.checkArgument(duration >= 0, "duration cannot be negative");
367             Preconditions.checkNotNull(unit, "unit cannot be null");
368             b.connectTimeoutMs = (int) unit.toMillis(duration);
369             return this;
370         }
371 
372         public Builder4 readTimeout(long duration, TimeUnit unit) {
373             Preconditions.checkArgument(duration >= 0, "duration cannot be negative");
374             Preconditions.checkNotNull(unit, "unit cannot be null");
375             b.readTimeoutMs = (int) unit.toMillis(duration);
376             return this;
377         }
378 
379         public Builder4 exceptionFactory(ExceptionFactory exceptionFactory) {
380             b.exceptionFactory = exceptionFactory;
381             return this;
382         }
383 
384         public Builder4 exception(Predicate<? super Response> predicate,
385                 Function<? super Response, ? extends RuntimeException> factory) {
386             b.exceptionFactory = new ExceptionFactoryExtended(b.exceptionFactory, predicate, factory);
387             return this;
388         }
389 
390         public Builder4 clock(Clock clock) {
391             b.clock = clock;
392             return this;
393         }
394 
395         public Client build() {
396             return new Client(b.clock, b.serviceName, b.region, b.credentials, b.httpClient, b.connectTimeoutMs,
397                     b.readTimeoutMs, b.exceptionFactory, b.baseUrlFactory, b.retries);
398         }
399     }
400 
401 }