Skip to content

Commit 4dc4560

Browse files
committed
Utility class addition POC
1 parent d7d00de commit 4dc4560

4 files changed

Lines changed: 391 additions & 3 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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.services.s3;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static software.amazon.awssdk.testutils.service.S3BucketUtils.temporaryBucketName;
20+
21+
import org.junit.AfterClass;
22+
import org.junit.BeforeClass;
23+
import org.junit.Test;
24+
import software.amazon.awssdk.core.sync.RequestBody;
25+
26+
public class S3UtilitiesConvenienceIntegrationTest extends S3IntegrationTestBase {
27+
28+
private static final String BUCKET = temporaryBucketName(S3UtilitiesConvenienceIntegrationTest.class);
29+
private static final String KEY = "test-key";
30+
31+
@BeforeClass
32+
public static void setupFixture() throws Exception {
33+
setUp();
34+
createBucket(BUCKET);
35+
s3.putObject(r -> r.bucket(BUCKET).key(KEY), RequestBody.fromString("hello"));
36+
}
37+
38+
@AfterClass
39+
public static void tearDown() {
40+
deleteBucketAndAllContents(BUCKET);
41+
}
42+
43+
@Test
44+
public void doesObjectExist_existingObject_returnsTrue() {
45+
assertThat(s3.utilities().doesObjectExist(BUCKET, KEY)).isTrue();
46+
}
47+
48+
@Test
49+
public void doesObjectExist_nonExistentObject_returnsFalse() {
50+
assertThat(s3.utilities().doesObjectExist(BUCKET, "no-such-key")).isFalse();
51+
}
52+
53+
@Test
54+
public void doesBucketExist_existingBucket_returnsTrue() {
55+
assertThat(s3.utilities().doesBucketExist(BUCKET)).isTrue();
56+
}
57+
58+
@Test
59+
public void doesBucketExist_nonExistentBucket_returnsFalse() {
60+
assertThat(s3.utilities().doesBucketExist("no-such-bucket-" + java.util.UUID.randomUUID())).isFalse();
61+
}
62+
}

services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java

Lines changed: 138 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import java.util.List;
2525
import java.util.Map;
2626
import java.util.Optional;
27+
import java.util.concurrent.CompletableFuture;
28+
import java.util.concurrent.CompletionException;
2729
import java.util.function.Consumer;
2830
import java.util.function.Supplier;
2931
import java.util.regex.Matcher;
@@ -41,6 +43,7 @@
4143
import software.amazon.awssdk.awscore.internal.defaultsmode.DefaultsModeConfiguration;
4244
import software.amazon.awssdk.core.ClientEndpointProvider;
4345
import software.amazon.awssdk.core.ClientType;
46+
import software.amazon.awssdk.core.SdkClient;
4447
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
4548
import software.amazon.awssdk.core.client.config.SdkClientConfiguration;
4649
import software.amazon.awssdk.core.client.config.SdkClientOption;
@@ -68,6 +71,10 @@
6871
import software.amazon.awssdk.services.s3.internal.endpoints.UseGlobalEndpointResolver;
6972
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
7073
import software.amazon.awssdk.services.s3.model.GetUrlRequest;
74+
import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
75+
import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
76+
import software.amazon.awssdk.services.s3.model.NoSuchBucketException;
77+
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
7178
import software.amazon.awssdk.utils.AttributeMap;
7279
import software.amazon.awssdk.utils.StringUtils;
7380
import software.amazon.awssdk.utils.Validate;
@@ -96,7 +103,8 @@
96103
* URL url = utilities.getUrl(request);
97104
* </pre>
98105
*
99-
* Note: This class does not make network calls.
106+
* Note: Most methods in this class do not make network calls. The exceptions are convenience methods like
107+
* {@link #doesObjectExist(String, String)} and {@link #doesBucketExist(String)}, which call S3 on your behalf.
100108
*/
101109
@Immutable
102110
@SdkPublicApi
@@ -112,6 +120,7 @@ public final class S3Utilities {
112120
private final boolean fipsEnabled;
113121
private final ExecutionInterceptorChain interceptorChain;
114122
private final UseGlobalEndpointResolver useGlobalEndpointResolver;
123+
private final SdkClient s3Client;
115124

116125
/**
117126
* SDK currently validates that region is present while constructing {@link S3Utilities} object.
@@ -143,6 +152,8 @@ private S3Utilities(Builder builder) {
143152
this.interceptorChain = createEndpointInterceptorChain();
144153

145154
this.useGlobalEndpointResolver = createUseGlobalEndpointResolver();
155+
156+
this.s3Client = builder.s3Client;
146157
}
147158

148159
private void resolveDualstackSetting(S3Configuration.Builder s3ConfigBuilder, Builder s3UtiltiesBuilder) {
@@ -177,11 +188,17 @@ public static Builder builder() {
177188
// Used by low-level client
178189
@SdkInternalApi
179190
static S3Utilities create(SdkClientConfiguration clientConfiguration) {
191+
return create(clientConfiguration, null);
192+
}
193+
194+
@SdkInternalApi
195+
static S3Utilities create(SdkClientConfiguration clientConfiguration, SdkClient s3Client) {
180196
S3Utilities.Builder builder = builder()
181197
.region(clientConfiguration.option(AwsClientOption.AWS_REGION))
182198
.s3Configuration((S3Configuration) clientConfiguration.option(SdkClientOption.SERVICE_CONFIGURATION))
183199
.profileFile(clientConfiguration.option(SdkClientOption.PROFILE_FILE_SUPPLIER))
184-
.profileName(clientConfiguration.option(SdkClientOption.PROFILE_NAME));
200+
.profileName(clientConfiguration.option(SdkClientOption.PROFILE_NAME))
201+
.s3Client(s3Client);
185202

186203
ClientEndpointProvider clientEndpoint = clientConfiguration.option(SdkClientOption.CLIENT_ENDPOINT_PROVIDER);
187204
if (clientEndpoint.isEndpointOverridden()) {
@@ -533,6 +550,115 @@ private UseGlobalEndpointResolver createUseGlobalEndpointResolver() {
533550
return new UseGlobalEndpointResolver(config);
534551
}
535552

553+
/**
554+
* Checks whether an object exists in S3. Returns {@code true} if the object exists, {@code false} if it does not.
555+
*
556+
* <p><b>Permissions:</b> This method returns {@code false} only when S3 responds with a 404 (NoSuchKey). If the
557+
* caller does not have {@code s3:ListBucket} permission on the bucket, S3 may return 403 instead of 404 for
558+
* non-existent objects, which will be thrown as an exception.
559+
*
560+
* @param bucket the bucket name
561+
* @param key the object key
562+
* @return {@code true} if the object exists, {@code false} otherwise
563+
* @throws S3Exception if S3 returns an error other than 404
564+
* @throws IllegalStateException if this {@link S3Utilities} was not created via {@link S3Client#utilities()}
565+
*/
566+
public boolean doesObjectExist(String bucket, String key) {
567+
Validate.notEmpty(bucket, "bucket must not be null or empty");
568+
Validate.notEmpty(key, "key must not be null or empty");
569+
Validate.validState(s3Client instanceof S3Client,
570+
"doesObjectExist requires a sync S3Client. Use S3Client.utilities() to create S3Utilities.");
571+
try {
572+
((S3Client) s3Client).headObject(HeadObjectRequest.builder().bucket(bucket).key(key).build());
573+
return true;
574+
} catch (NoSuchKeyException e) {
575+
return false;
576+
}
577+
}
578+
579+
/**
580+
* Checks whether a bucket exists in S3 and is accessible. Returns {@code true} if the bucket exists and the caller
581+
* has access, {@code false} if the bucket does not exist.
582+
*
583+
* <p><b>Permissions:</b> S3 returns 403 for buckets that exist but the caller does not have access to.
584+
* This means the method cannot distinguish between "bucket does not exist" and "bucket exists but access is denied"
585+
* — the 403 case will be thrown as an exception.
586+
*
587+
* @param bucket the bucket name
588+
* @return {@code true} if the bucket exists and is accessible, {@code false} if it does not exist
589+
* @throws S3Exception if S3 returns an error other than 404
590+
* @throws IllegalStateException if this {@link S3Utilities} was not created via {@link S3Client#utilities()}
591+
*/
592+
public boolean doesBucketExist(String bucket) {
593+
Validate.notEmpty(bucket, "bucket must not be null or empty");
594+
Validate.validState(s3Client instanceof S3Client,
595+
"doesBucketExist requires a sync S3Client. Use S3Client.utilities() to create S3Utilities.");
596+
try {
597+
((S3Client) s3Client).headBucket(HeadBucketRequest.builder().bucket(bucket).build());
598+
return true;
599+
} catch (NoSuchBucketException e) {
600+
return false;
601+
}
602+
}
603+
604+
/**
605+
* Async variant of {@link #doesObjectExist(String, String)}.
606+
*
607+
* <p>This method is only available when {@link S3Utilities} is created via {@link S3AsyncClient#utilities()}.
608+
*
609+
* @param bucket the bucket name
610+
* @param key the object key
611+
* @return a {@link CompletableFuture} that completes with {@code true} if the object exists, {@code false} otherwise
612+
* @throws S3Exception if S3 returns an error other than 404
613+
* @throws IllegalStateException if this {@link S3Utilities} was not created via {@link S3AsyncClient#utilities()}
614+
*/
615+
public CompletableFuture<Boolean> doesObjectExistAsync(String bucket, String key) {
616+
Validate.notEmpty(bucket, "bucket must not be null or empty");
617+
Validate.notEmpty(key, "key must not be null or empty");
618+
Validate.validState(s3Client instanceof S3AsyncClient,
619+
"doesObjectExistAsync requires an S3AsyncClient."
620+
+ " Use S3AsyncClient.utilities() to create S3Utilities.");
621+
return ((S3AsyncClient) s3Client).headObject(HeadObjectRequest.builder().bucket(bucket).key(key).build())
622+
.thenApply(r -> true)
623+
.exceptionally(t -> {
624+
Throwable cause = t instanceof CompletionException ? t.getCause() : t;
625+
if (cause instanceof NoSuchKeyException) {
626+
return false;
627+
}
628+
throw (cause instanceof RuntimeException)
629+
? (RuntimeException) cause
630+
: new RuntimeException(cause);
631+
});
632+
}
633+
634+
/**
635+
* Async variant of {@link #doesBucketExist(String)}.
636+
*
637+
* <p>This method is only available when {@link S3Utilities} is created via {@link S3AsyncClient#utilities()}.
638+
*
639+
* @param bucket the bucket name
640+
* @return a {@link CompletableFuture} that completes with {@code true} if the bucket exists, {@code false} otherwise
641+
* @throws S3Exception if S3 returns an error other than 404
642+
* @throws IllegalStateException if this {@link S3Utilities} was not created via {@link S3AsyncClient#utilities()}
643+
*/
644+
public CompletableFuture<Boolean> doesBucketExistAsync(String bucket) {
645+
Validate.notEmpty(bucket, "bucket must not be null or empty");
646+
Validate.validState(s3Client instanceof S3AsyncClient,
647+
"doesBucketExistAsync requires an S3AsyncClient."
648+
+ " Use S3AsyncClient.utilities() to create S3Utilities.");
649+
return ((S3AsyncClient) s3Client).headBucket(HeadBucketRequest.builder().bucket(bucket).build())
650+
.thenApply(r -> true)
651+
.exceptionally(t -> {
652+
Throwable cause = t instanceof CompletionException ? t.getCause() : t;
653+
if (cause instanceof NoSuchBucketException) {
654+
return false;
655+
}
656+
throw (cause instanceof RuntimeException)
657+
? (RuntimeException) cause
658+
: new RuntimeException(cause);
659+
});
660+
}
661+
536662
/**
537663
* Builder class to construct {@link S3Utilities} object
538664
*/
@@ -545,6 +671,7 @@ public static final class Builder {
545671
private String profileName;
546672
private Boolean dualstackEnabled;
547673
private Boolean fipsEnabled;
674+
private SdkClient s3Client;
548675

549676
private Builder() {
550677
}
@@ -646,6 +773,15 @@ private Builder profileName(String profileName) {
646773
return this;
647774
}
648775

776+
/**
777+
* The S3 client to use for convenience methods like {@link S3Utilities#doesObjectExist}. This is private and
778+
* only used when the utilities is created via {@link S3Client#utilities()}.
779+
*/
780+
private Builder s3Client(SdkClient s3Client) {
781+
this.s3Client = s3Client;
782+
return this;
783+
}
784+
649785
/**
650786
* Construct a {@link S3Utilities} object.
651787
*/

services/s3/src/main/resources/codegen-resources/customization.config

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,8 @@
260260
"utilitiesMethod": {
261261
"returnType": "software.amazon.awssdk.services.s3.S3Utilities",
262262
"createMethodParams": [
263-
"clientConfiguration"
263+
"clientConfiguration",
264+
"this"
264265
]
265266
},
266267
"additionalBuilderMethods": [

0 commit comments

Comments
 (0)