1010import java .time .Duration ;
1111import java .util .NoSuchElementException ;
1212import java .util .Set ;
13+ import java .util .function .Function ;
14+
1315import org .slf4j .Logger ;
1416import org .slf4j .LoggerFactory ;
1517import software .amazon .awssdk .auth .credentials .AwsCredentials ;
2527import software .amazon .awssdk .core .retry .conditions .RetryOnStatusCodeCondition ;
2628import software .amazon .awssdk .core .retry .conditions .RetryOnThrottlingCondition ;
2729import software .amazon .awssdk .http .HttpStatusCode ;
30+ import software .amazon .awssdk .http .SdkHttpResponse ;
2831import software .amazon .awssdk .regions .Region ;
2932import software .amazon .awssdk .services .s3 .S3AsyncClient ;
3033import software .amazon .awssdk .services .s3 .S3Client ;
@@ -96,16 +99,12 @@ public class S3ClientProvider {
9699 );
97100 }
98101
99- Logger logger = LoggerFactory .getLogger ("S3ClientStoreProvider" );
102+ static private final Logger logger = LoggerFactory .getLogger (S3ClientProvider . class );
100103
101104 public S3ClientProvider (S3NioSpiConfiguration c ) {
102105 this .configuration = (c == null ) ? new S3NioSpiConfiguration () : c ;
103106 }
104107
105- public S3ClientProvider () {
106- this (null );
107- }
108-
109108 public S3CrtAsyncClientBuilder asyncClientBuilder () {
110109 return asyncClientBuilder ;
111110 }
@@ -122,7 +121,7 @@ public void asyncClientBuilder(final S3CrtAsyncClientBuilder builder) {
122121 *
123122 * @return a S3Client not bound to a region
124123 */
125- public S3Client universalClient () {
124+ S3Client universalClient () {
126125 return universalClient (false );
127126 }
128127
@@ -134,7 +133,7 @@ public S3Client universalClient() {
134133 * @param <T> type of AwsClient
135134 * @return a S3Client not bound to a region
136135 */
137- public <T extends AwsClient > T universalClient (boolean async ) {
136+ <T extends AwsClient > T universalClient (boolean async ) {
138137 return (T )((async ) ? DEFAULT_ASYNC_CLIENT : DEFAULT_CLIENT );
139138 }
140139
@@ -161,54 +160,8 @@ protected S3AsyncClient generateAsyncClient(String bucket) {
161160 *
162161 * @return an S3 client appropriate for the region of the named bucket
163162 */
164- protected S3Client generateClient (String bucketName , S3Client locationClient ) {
165- logger .debug ("generating client for bucket: '{}'" , bucketName );
166- S3Client bucketSpecificClient = null ;
167-
168- if ((configuration .getEndpoint () == null ) || configuration .getEndpoint ().isBlank ()) {
169- //
170- // we try to locate a bucket only if no endpoint is provided, which
171- // means we are dealing with AWS S3 buckets
172- //
173- String bucketLocation = null ;
174- try {
175- logger .debug ("determining bucket location with getBucketLocation" );
176- bucketLocation = locationClient .getBucketLocation (builder -> builder .bucket (bucketName )).locationConstraintAsString ();
177- bucketSpecificClient = this .clientForRegion (bucketLocation );
178- } catch (S3Exception e ) {
179- if (e .statusCode () == 403 ) {
180- logger .debug ("Cannot determine location of '{}' bucket directly. Attempting to obtain bucket location with headBucket operation" , bucketName );
181- try {
182- final HeadBucketResponse headBucketResponse = locationClient .headBucket (builder -> builder .bucket (bucketName ));
183- bucketSpecificClient = this .clientForRegion (headBucketResponse .
184- sdkHttpResponse ()
185- .firstMatchingHeader ("x-amz-bucket-region" )
186- .orElseThrow (() -> new NoSuchElementException ("Head Bucket Response doesn't include the header 'x-amz-bucket-region'" )));
187- } catch (S3Exception e2 ) {
188- if (e2 .statusCode () == 301 ) {
189- bucketSpecificClient = this .clientForRegion (e2 .awsErrorDetails ().
190- sdkHttpResponse ()
191- .firstMatchingHeader ("x-amz-bucket-region" )
192- .orElseThrow (() -> new NoSuchElementException ("Head Bucket Response doesn't include the header 'x-amz-bucket-region'" )));
193- } else {
194- throw e2 ;
195- }
196- }
197- } else {
198- throw e ;
199- }
200- }
201-
202- //
203- // if here, no S3 nor other client has been created yet and we do not
204- // have a location; we'll let it figure out from the profile region
205- //
206- logger .warn ("Unable to determine the region of bucket: '{}'. Generating a client for the profile region." , bucketName );
207- }
208-
209- return (bucketSpecificClient != null )
210- ? bucketSpecificClient
211- : clientForRegion (configuration .getRegion ());
163+ S3Client generateSyncClient (String bucketName , S3Client locationClient ) {
164+ return getClientForBucket (bucketName , locationClient , this ::clientForRegion );
212165 }
213166
214167 /**
@@ -221,55 +174,77 @@ protected S3Client generateClient (String bucketName, S3Client locationClient) {
221174 *
222175 * @return an S3 client appropriate for the region of the named bucket
223176 */
224- protected S3AsyncClient generateAsyncClient (String bucketName , S3Client locationClient ) {
225- logger .debug ("generating asynchronous client for bucket: '{}'" , bucketName );
226- S3AsyncClient bucketSpecificClient = null ;
177+ S3AsyncClient generateAsyncClient (String bucketName , S3Client locationClient ) {
178+ return getClientForBucket (bucketName , locationClient , this ::asyncClientForRegion );
179+ }
180+
181+ private <T extends AwsClient > T getClientForBucket (
182+ String bucketName ,
183+ S3Client locationClient ,
184+ Function <String , T > getClientForRegion
185+ ) {
186+ logger .debug ("generating client for bucket: '{}'" , bucketName );
187+ T bucketSpecificClient = null ;
227188
228189 if ((configuration .getEndpoint () == null ) || configuration .getEndpoint ().isBlank ()) {
229190 //
230191 // we try to locate a bucket only if no endpoint is provided, which
231192 // means we are dealing with AWS S3 buckets
232193 //
233- String bucketLocation = null ;
234- try {
235- logger .debug ("determining bucket location with getBucketLocation" );
236- bucketLocation = locationClient .getBucketLocation (builder -> builder .bucket (bucketName )).locationConstraintAsString ();
237- bucketSpecificClient = this .asyncClientForRegion (bucketLocation );
238- } catch (S3Exception e ) {
239- if (e .statusCode () == 403 ) {
240- logger .debug ("Cannot determine location of '{}' bucket directly. Attempting to obtain bucket location with headBucket operation" , bucketName );
241- try {
242- final HeadBucketResponse headBucketResponse = locationClient .headBucket (builder -> builder .bucket (bucketName ));
243- bucketSpecificClient = this .asyncClientForRegion (headBucketResponse .sdkHttpResponse ()
244- .firstMatchingHeader ("x-amz-bucket-region" )
245- .orElseThrow (() -> new NoSuchElementException ("Head Bucket Response doesn't include the header 'x-amz-bucket-region'" )));
246- } catch (S3Exception e2 ) {
247- if (e2 .statusCode () == 301 ) {
248- bucketSpecificClient = this .asyncClientForRegion (e2
249- .awsErrorDetails ()
250- .sdkHttpResponse ()
251- .firstMatchingHeader ("x-amz-bucket-region" )
252- .orElseThrow (() -> new NoSuchElementException ("Head Bucket Response doesn't include the header 'x-amz-bucket-region'" ))
253- );
254- } else {
255- throw e2 ;
256- }
257- }
258- } else {
259- throw e ;
260- }
194+ String bucketLocation = determineBucketLocation (bucketName , locationClient );
195+
196+ if ( bucketLocation != null ) {
197+ bucketSpecificClient = getClientForRegion .apply (bucketLocation );
198+ } else {
199+ // if here, no S3 nor other client has been created yet, and we do not
200+ // have a location; we'll let it figure out from the profile region
201+ logger .warn ("Unable to determine the region of bucket: '{}'. Generating a client for the profile region." , bucketName );
261202 }
262-
263- //
264- // if here, no S3 nor other client has been created yet and we do not
265- // have a location; we'll let it figure out from the profile region
266- //
267- logger .warn ("Unable to determine the region of bucket: '{}'. Generating a client for the profile region." , bucketName );
268203 }
269204
270205 return (bucketSpecificClient != null )
271- ? bucketSpecificClient
272- : asyncClientForRegion (configuration .getRegion ());
206+ ? bucketSpecificClient
207+ : getClientForRegion .apply (configuration .getRegion ());
208+ }
209+
210+ private String determineBucketLocation (String bucketName , S3Client locationClient ) {
211+ try {
212+ return getBucketLocation (bucketName , locationClient );
213+ } catch (S3Exception e ) {
214+ if (isForbidden (e )) {
215+ return getBucketLocationFromHead (bucketName , locationClient );
216+ } else {
217+ throw e ;
218+ }
219+ }
220+ }
221+
222+ private String getBucketLocation (String bucketName , S3Client locationClient ) {
223+ logger .debug ("determining bucket location with getBucketLocation" );
224+ return locationClient .getBucketLocation (builder -> builder .bucket (bucketName )).locationConstraintAsString ();
225+ }
226+
227+ private String getBucketLocationFromHead (String bucketName , S3Client locationClient ) {
228+ try {
229+ logger .debug ("Cannot determine location of '{}' bucket directly. Attempting to obtain bucket location with headBucket operation" , bucketName );
230+ final HeadBucketResponse headBucketResponse = locationClient .headBucket (builder -> builder .bucket (bucketName ));
231+ return getBucketRegionFromResponse (headBucketResponse .sdkHttpResponse ());
232+ } catch (S3Exception e ) {
233+ if (isRedirect (e )) {
234+ return getBucketRegionFromResponse (e .awsErrorDetails ().sdkHttpResponse ());
235+ } else {
236+ throw e ;
237+ }
238+ }
239+ }
240+
241+ private boolean isForbidden (S3Exception e ) { return e .statusCode () == 403 ; }
242+ private boolean isRedirect (S3Exception e ) { return e .statusCode () == 301 ; }
243+
244+ private String getBucketRegionFromResponse (SdkHttpResponse response ) {
245+ return response .firstMatchingHeader ("x-amz-bucket-region" ).orElseThrow (() ->
246+ new NoSuchElementException ("Head Bucket Response doesn't include the header 'x-amz-bucket-region'" )
247+ );
273248 }
274249
275250 private S3Client clientForRegion (String regionName ) {
0 commit comments