Skip to content

Commit b3cef76

Browse files
Adrian ClayORybak5
andauthored
Migrate from v1 to v2 of AWS SDK for Java (#849)
* Migrate from v1 to v2 of AWS SDK for Java v1 stops getting updates as of Dec 2025 * excluding intTests from unit tests * code test coverage threshold increase * adjusting tests to satisfy more permutations * increasing test coverage * adding s3mock library * checkstyle * code refactoring * specifying a region for S3Client * addressing pitest mutations * small code refactoring * refactoring to remove dependency on hardcoded region --------- Co-authored-by: hospel <[email protected]>
1 parent 875bff6 commit b3cef76

File tree

8 files changed

+192
-38
lines changed

8 files changed

+192
-38
lines changed

service/build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ dependencies {
5050
implementation 'org.springframework.boot:spring-boot-starter-logging'
5151

5252
// Infrastructure
53-
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.780'
53+
implementation 'software.amazon.awssdk:s3:2.28.29'
5454
implementation ('com.azure:azure-storage-blob:12.29.0')
5555
implementation 'org.apache.qpid:qpid-jms-client:2.6.1'
5656

@@ -72,6 +72,7 @@ dependencies {
7272
testImplementation 'org.wiremock:wiremock-standalone:3.9.2'
7373
testImplementation 'com.squareup.okhttp3:okhttp:4.12.0'
7474
testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'
75+
testImplementation 'io.findify:s3mock_2.13:0.2.6'
7576

7677
pitest 'com.arcmutate:base:1.3.1'
7778
pitest 'com.arcmutate:pitest-git-plugin:2.0.0'
@@ -178,3 +179,4 @@ sonar {
178179
bootJar {
179180
exclude("**/TransformJsonToXml*")
180181
}
182+

service/src/main/java/uk/nhs/adaptors/gp2gp/common/configuration/CustomTrustStore.java

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package uk.nhs.adaptors.gp2gp.common.configuration;
22

3+
import java.net.URI;
34
import java.security.KeyStore;
45
import java.security.cert.CertificateException;
56
import java.security.cert.X509Certificate;
@@ -11,10 +12,11 @@
1112

1213
import org.springframework.beans.factory.annotation.Autowired;
1314
import org.springframework.stereotype.Component;
14-
15-
import com.amazonaws.services.s3.AmazonS3;
16-
import com.amazonaws.services.s3.AmazonS3URI;
17-
import com.amazonaws.services.s3.model.GetObjectRequest;
15+
import software.amazon.awssdk.core.ResponseInputStream;
16+
import software.amazon.awssdk.services.s3.S3Uri;
17+
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
18+
import software.amazon.awssdk.services.s3.S3Client;
19+
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
1820

1921
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
2022
import lombok.NoArgsConstructor;
@@ -27,12 +29,13 @@
2729
@NoArgsConstructor
2830
public class CustomTrustStore {
2931
@Autowired(required = false)
30-
private AmazonS3 s3Client;
32+
private S3Client s3Client;
3133

3234
@SneakyThrows
3335
public void addToDefault(String trustStorePath, String trustStorePassword) {
3436
final X509TrustManager defaultTrustManager = getDefaultTrustManager();
35-
final X509TrustManager customTrustManager = getCustomDbTrustManager(new AmazonS3URI(trustStorePath), trustStorePassword);
37+
final var s3Uri = s3Client.utilities().parseUri(URI.create(trustStorePath));
38+
final X509TrustManager customTrustManager = getCustomDbTrustManager(s3Uri, trustStorePassword);
3639
X509TrustManager combinedTrustManager = new CombinedTrustManager(customTrustManager, defaultTrustManager);
3740

3841
SSLContext sslContext = SSLContext.getInstance("TLS");
@@ -56,15 +59,15 @@ private X509TrustManager getDefaultTrustManager() {
5659

5760
@SneakyThrows
5861
@SuppressFBWarnings("RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE")
59-
private X509TrustManager getCustomDbTrustManager(AmazonS3URI s3URI, String trustStorePassword) {
62+
protected X509TrustManager getCustomDbTrustManager(S3Uri s3Uri, String trustStorePassword) {
6063
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
6164
trustManagerFactory.init((KeyStore) null);
6265

63-
LOGGER.info("Loading custom KeyStore from '{}'", s3URI.toString());
64-
try (var s3Object = s3Client.getObject(new GetObjectRequest(s3URI.getBucket(), s3URI.getKey()));
65-
var content = s3Object.getObjectContent()) {
66+
LOGGER.info("Loading custom KeyStore from '{}'", s3Uri.toString());
67+
final var getObjectRequest = GetObjectRequest.builder().bucket(s3Uri.bucket().orElseThrow()).key(s3Uri.key().orElseThrow()).build();
68+
try (ResponseInputStream<GetObjectResponse> s3Object = s3Client.getObject(getObjectRequest)) {
6669
KeyStore customKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
67-
customKeyStore.load(content, trustStorePassword.toCharArray());
70+
customKeyStore.load(s3Object, trustStorePassword.toCharArray());
6871
trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
6972
trustManagerFactory.init(customKeyStore);
7073
}

service/src/main/java/uk/nhs/adaptors/gp2gp/common/storage/S3StorageConnector.java

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,41 @@
22

33
import java.io.InputStream;
44

5-
import com.amazonaws.services.s3.AmazonS3;
6-
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
7-
import com.amazonaws.services.s3.model.ObjectMetadata;
8-
import com.amazonaws.services.s3.model.S3Object;
5+
import software.amazon.awssdk.core.sync.RequestBody;
6+
import software.amazon.awssdk.services.s3.S3Client;
7+
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
8+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
9+
import software.amazon.awssdk.core.ResponseInputStream;
10+
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
911

1012
public class S3StorageConnector implements StorageConnector {
11-
private final AmazonS3 s3client;
13+
private final S3Client s3client;
1214
private final String bucketName;
1315

14-
protected S3StorageConnector(StorageConnectorConfiguration configuration) {
16+
protected S3StorageConnector(S3Client s3client, StorageConnectorConfiguration configuration) {
1517
this.bucketName = configuration.getContainerName();
16-
this.s3client = AmazonS3ClientBuilder
17-
.standard()
18-
.build();
18+
this.s3client = s3client;
1919
}
2020

2121
@Override
2222
public void uploadToStorage(InputStream is, long streamLength, String filename) throws StorageConnectorException {
2323
try {
24-
ObjectMetadata metadata = new ObjectMetadata();
25-
metadata.setContentLength(streamLength);
24+
final var putObjectRequest = PutObjectRequest.builder().bucket(bucketName).key(filename).build();
2625

2726
s3client.putObject(
28-
bucketName,
29-
filename,
30-
is,
31-
metadata
27+
putObjectRequest,
28+
RequestBody.fromInputStream(is, streamLength)
3229
);
3330
} catch (Exception exception) {
3431
throw new StorageConnectorException("Error occurred uploading to S3 Bucket", exception);
3532
}
3633
}
3734

3835
@Override
39-
public InputStream downloadFromStorage(String filename) throws StorageConnectorException {
36+
public ResponseInputStream<GetObjectResponse> downloadFromStorage(String filename) throws StorageConnectorException {
4037
try {
41-
S3Object s3Object = s3client.getObject(bucketName, filename);
42-
return s3Object.getObjectContent();
38+
final var request = GetObjectRequest.builder().bucket(bucketName).key(filename).build();
39+
return s3client.getObject(request);
4340
} catch (Exception exception) {
4441
throw new StorageConnectorException("Error occurred downloading from S3 Bucket", exception);
4542
}

service/src/main/java/uk/nhs/adaptors/gp2gp/common/storage/StorageConnectorConfiguration.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
import org.springframework.context.annotation.Bean;
66
import org.springframework.context.annotation.Configuration;
77

8-
import com.amazonaws.services.s3.AmazonS3;
9-
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
8+
import software.amazon.awssdk.services.s3.S3Client;
109

1110
import lombok.Getter;
1211
import lombok.Setter;
@@ -16,6 +15,7 @@
1615
@Configuration
1716
@ConfigurationProperties(prefix = "gp2gp.storage")
1817
public class StorageConnectorConfiguration {
18+
1919
private static final String S3_PREFIX = "s3";
2020

2121
private String type;
@@ -25,11 +25,9 @@ public class StorageConnectorConfiguration {
2525
private String trustStorePassword;
2626

2727
@Bean
28-
@SuppressWarnings("unused")
29-
public AmazonS3 getS3Client() {
28+
public S3Client getS3Client() {
3029
if (StringUtils.isNotBlank(trustStoreUrl) && trustStoreUrl.startsWith(S3_PREFIX)) {
31-
return AmazonS3ClientBuilder.standard()
32-
.build();
30+
return S3Client.builder().build();
3331
}
3432

3533
return null;

service/src/main/java/uk/nhs/adaptors/gp2gp/common/storage/StorageConnectorFactory.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package uk.nhs.adaptors.gp2gp.common.storage;
22

33
import org.springframework.beans.factory.FactoryBean;
4-
54
import lombok.Setter;
5+
import software.amazon.awssdk.services.s3.S3Client;
66

77
@Setter
88
public class StorageConnectorFactory implements FactoryBean<StorageConnector> {
@@ -15,7 +15,7 @@ public StorageConnector getObject() {
1515
if (storageConnector == null) {
1616
switch (StorageConnectorOptions.enumOf(configuration.getType())) {
1717
case S3:
18-
storageConnector = new S3StorageConnector(configuration);
18+
storageConnector = new S3StorageConnector(S3Client.builder().build(), configuration);
1919
break;
2020
case AZURE:
2121
storageConnector = new AzureStorageConnector();
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package uk.nhs.adaptors.gp2gp.common.configuration;
2+
3+
import io.findify.s3mock.S3Mock;
4+
import org.junit.jupiter.api.AfterAll;
5+
import org.junit.jupiter.api.BeforeAll;
6+
import org.junit.jupiter.api.Test;
7+
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
8+
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
9+
import software.amazon.awssdk.regions.Region;
10+
import software.amazon.awssdk.services.s3.S3Client;
11+
import software.amazon.awssdk.services.s3.S3Configuration;
12+
import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
13+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
14+
import java.io.File;
15+
import java.lang.reflect.Field;
16+
import java.net.URI;
17+
import static org.junit.jupiter.api.Assertions.assertNotNull;
18+
19+
class CustomTrustStoreTest {
20+
21+
public static final int PORT = 8001;
22+
private static S3Mock s3Mock;
23+
private static S3Client s3Client;
24+
private static final String BUCKET_NAME = "test-bucket";
25+
private static final String TRUSTSTORE_PATH = "test.jks";
26+
private static final String TRUSTSTORE_PASSWORD = "password";
27+
28+
private final CustomTrustStore customTrustStore = new CustomTrustStore();
29+
30+
@BeforeAll
31+
static void setUp() {
32+
s3Mock = new S3Mock.Builder().withPort(PORT).withInMemoryBackend().build();
33+
s3Mock.start();
34+
System.out.println("S3Mock started at http://localhost:" + PORT);
35+
36+
s3Client = S3Client.builder()
37+
.endpointOverride(URI.create("http://localhost:" + PORT))
38+
.credentialsProvider(StaticCredentialsProvider.create(
39+
AwsBasicCredentials.create("accessKey", "secretKey")))
40+
.serviceConfiguration(S3Configuration.builder().pathStyleAccessEnabled(true).build())
41+
.region(Region.EU_WEST_2)
42+
.build();
43+
44+
s3Client.createBucket(CreateBucketRequest.builder().bucket(BUCKET_NAME).build());
45+
46+
File trustStoreFile = new File("src/test/resources/test.jks");
47+
s3Client.putObject(PutObjectRequest.builder().bucket(BUCKET_NAME).key(TRUSTSTORE_PATH).build(),
48+
software.amazon.awssdk.core.sync.RequestBody.fromFile(trustStoreFile));
49+
}
50+
51+
@AfterAll
52+
static void tearDown() {
53+
s3Mock.shutdown();
54+
}
55+
56+
@Test
57+
void trustManagerLoadsSuccessfullyTest() throws NoSuchFieldException, IllegalAccessException {
58+
59+
Field s3ClientField = CustomTrustStore.class.getDeclaredField("s3Client");
60+
s3ClientField.setAccessible(true);
61+
s3ClientField.set(customTrustStore, s3Client);
62+
63+
String s3Uri = "s3://" + BUCKET_NAME + "/" + TRUSTSTORE_PATH;
64+
65+
var trustManager = customTrustStore.getCustomDbTrustManager(s3Client.utilities().parseUri(URI.create(s3Uri)), TRUSTSTORE_PASSWORD);
66+
67+
assertNotNull(trustManager, "Custom TrustManager wasn't loaded successfully!");
68+
}
69+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package uk.nhs.adaptors.gp2gp.common.storage;
2+
3+
import org.junit.jupiter.api.BeforeEach;
4+
import org.junit.jupiter.api.Test;
5+
import org.mockito.Mock;
6+
import org.mockito.MockitoAnnotations;
7+
import software.amazon.awssdk.core.ResponseInputStream;
8+
import software.amazon.awssdk.core.sync.RequestBody;
9+
import software.amazon.awssdk.regions.Region;
10+
import software.amazon.awssdk.services.s3.S3Client;
11+
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
12+
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
13+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
14+
15+
import java.io.ByteArrayInputStream;
16+
import java.io.InputStream;
17+
18+
import static org.junit.Assert.assertEquals;
19+
import static org.junit.Assert.assertThrows;
20+
import static org.junit.jupiter.api.Assertions.assertNotNull;
21+
import static org.mockito.ArgumentMatchers.any;
22+
import static org.mockito.ArgumentMatchers.eq;
23+
import static org.mockito.Mockito.mock;
24+
import static org.mockito.Mockito.times;
25+
import static org.mockito.Mockito.verify;
26+
import static org.mockito.Mockito.when;
27+
28+
class S3StorageConnectorTest {
29+
30+
private static final String FILE_NAME = "test-file.txt";
31+
private S3StorageConnector s3StorageConnector;
32+
private StorageConnectorConfiguration config;
33+
private static final long STREAM_LENGTH = 100L;
34+
35+
@Mock
36+
private S3Client mockS3Client;
37+
38+
@Mock
39+
private InputStream is;
40+
41+
@BeforeEach
42+
void setUp() {
43+
MockitoAnnotations.openMocks(this);
44+
45+
config = new StorageConnectorConfiguration();
46+
config.setContainerName("s3Bucket");
47+
48+
s3StorageConnector = new S3StorageConnector(mockS3Client, config);
49+
}
50+
51+
52+
@Test
53+
void expectExceptionWhenS3ClientCannotDeliverResponse() {
54+
S3StorageConnector storageConnector = new S3StorageConnector(S3Client.builder().region(Region.EU_WEST_2).build(), config);
55+
Exception exception = assertThrows(StorageConnectorException.class,
56+
() -> storageConnector.downloadFromStorage("s3File"));
57+
58+
assertEquals("Error occurred downloading from S3 Bucket", exception.getMessage());
59+
}
60+
61+
@Test
62+
void downloadFromStorageTest() {
63+
var mockResponse = mock(GetObjectResponse.class);
64+
var mockInputStream = new ByteArrayInputStream("dummy-content".getBytes());
65+
var mockResponseInputStream = new ResponseInputStream<>(mockResponse, mockInputStream);
66+
final var request = GetObjectRequest.builder().bucket(config.getContainerName()).key(FILE_NAME).build();
67+
68+
when(mockS3Client.getObject(request)).thenReturn(mockResponseInputStream);
69+
70+
var result = s3StorageConnector.downloadFromStorage(FILE_NAME);
71+
72+
assertNotNull(result);
73+
verify(mockS3Client).getObject(request);
74+
}
75+
76+
@Test
77+
void uploadToStorageTest() {
78+
final var expectedRequest = PutObjectRequest.builder().bucket(config.getContainerName()).key(FILE_NAME).build();
79+
80+
s3StorageConnector.uploadToStorage(is, STREAM_LENGTH, FILE_NAME);
81+
82+
verify(mockS3Client, times(1)).putObject(eq(expectedRequest), any(RequestBody.class));
83+
}
84+
85+
}
2.66 KB
Binary file not shown.

0 commit comments

Comments
 (0)