diff --git a/.changes/next-release/feature-AWSSDKforJavav2-1408e5f.json b/.changes/next-release/feature-AWSSDKforJavav2-1408e5f.json
new file mode 100644
index 000000000000..a326d8e4fd23
--- /dev/null
+++ b/.changes/next-release/feature-AWSSDKforJavav2-1408e5f.json
@@ -0,0 +1,6 @@
+{
+ "type": "feature",
+ "category": "AWS SDK for Java v2",
+ "contributor": "",
+ "description": "Adding a new method of constructing ARNs without exceptions as control flow"
+}
diff --git a/core/arns/src/main/java/software/amazon/awssdk/arns/Arn.java b/core/arns/src/main/java/software/amazon/awssdk/arns/Arn.java
index 18ac2c8f49f4..dada19e8557e 100644
--- a/core/arns/src/main/java/software/amazon/awssdk/arns/Arn.java
+++ b/core/arns/src/main/java/software/amazon/awssdk/arns/Arn.java
@@ -138,6 +138,23 @@ public static Builder builder() {
return new DefaultBuilder();
}
+ /**
+ * Attempts to parse the given string into an {@link Arn}. If the input string is not a valid ARN,
+ * this method returns {@link Optional#empty()} instead of throwing an exception.
+ *
+ * When successful, the resource is accessible entirely as a string through
+ * {@link #resourceAsString()}. Where correctly formatted, a parsed resource
+ * containing resource type, resource and qualifier is available through
+ * {@link #resource()}.
+ *
+ * @param arn A string containing an ARN to parse.
+ * @return An {@link Optional} containing the parsed {@link Arn} if valid, or empty if invalid.
+ * @throws IllegalArgumentException if the ARN contains empty partition or service fields
+ */
+ public static Optional tryFromString(String arn) {
+ return parseArn(arn, false);
+ }
+
/**
* Parses a given string into an {@link Arn}. The resource is accessible entirely as a
* string through {@link #resourceAsString()}. Where correctly formatted, a parsed
@@ -148,47 +165,75 @@ public static Builder builder() {
* @return {@link Arn} - A modeled Arn.
*/
public static Arn fromString(String arn) {
+ return parseArn(arn, true).orElseThrow(() -> new IllegalArgumentException("ARN parsing failed"));
+ }
+
+ private static Optional parseArn(String arn, boolean throwOnError) {
+ if (arn == null) {
+ return Optional.empty();
+ }
+
int arnColonIndex = arn.indexOf(':');
if (arnColonIndex < 0 || !"arn".equals(arn.substring(0, arnColonIndex))) {
- throw new IllegalArgumentException("Malformed ARN - doesn't start with 'arn:'");
+ if (throwOnError) {
+ throw new IllegalArgumentException("Malformed ARN - doesn't start with 'arn:'");
+ }
+ return Optional.empty();
}
int partitionColonIndex = arn.indexOf(':', arnColonIndex + 1);
if (partitionColonIndex < 0) {
- throw new IllegalArgumentException("Malformed ARN - no AWS partition specified");
+ if (throwOnError) {
+ throw new IllegalArgumentException("Malformed ARN - no AWS partition specified");
+ }
+ return Optional.empty();
}
String partition = arn.substring(arnColonIndex + 1, partitionColonIndex);
int serviceColonIndex = arn.indexOf(':', partitionColonIndex + 1);
if (serviceColonIndex < 0) {
- throw new IllegalArgumentException("Malformed ARN - no service specified");
+ if (throwOnError) {
+ throw new IllegalArgumentException("Malformed ARN - no service specified");
+ }
+ return Optional.empty();
}
String service = arn.substring(partitionColonIndex + 1, serviceColonIndex);
int regionColonIndex = arn.indexOf(':', serviceColonIndex + 1);
if (regionColonIndex < 0) {
- throw new IllegalArgumentException("Malformed ARN - no AWS region partition specified");
+ if (throwOnError) {
+ throw new IllegalArgumentException("Malformed ARN - no AWS region partition specified");
+ }
+ return Optional.empty();
}
String region = arn.substring(serviceColonIndex + 1, regionColonIndex);
int accountColonIndex = arn.indexOf(':', regionColonIndex + 1);
if (accountColonIndex < 0) {
- throw new IllegalArgumentException("Malformed ARN - no AWS account specified");
+ if (throwOnError) {
+ throw new IllegalArgumentException("Malformed ARN - no AWS account specified");
+ }
+ return Optional.empty();
}
String accountId = arn.substring(regionColonIndex + 1, accountColonIndex);
String resource = arn.substring(accountColonIndex + 1);
if (resource.isEmpty()) {
- throw new IllegalArgumentException("Malformed ARN - no resource specified");
+ if (throwOnError) {
+ throw new IllegalArgumentException("Malformed ARN - no resource specified");
+ }
+ return Optional.empty();
}
- return Arn.builder()
- .partition(partition)
- .service(service)
- .region(region)
- .accountId(accountId)
- .resource(resource)
- .build();
+ Arn resultArn = builder()
+ .partition(partition)
+ .service(service)
+ .region(region)
+ .accountId(accountId)
+ .resource(resource)
+ .build();
+
+ return Optional.of(resultArn);
}
@Override
diff --git a/core/arns/src/test/java/software/amazon/awssdk/arns/ArnTest.java b/core/arns/src/test/java/software/amazon/awssdk/arns/ArnTest.java
index 0e4bcfd68f0b..e95903e03cb9 100644
--- a/core/arns/src/test/java/software/amazon/awssdk/arns/ArnTest.java
+++ b/core/arns/src/test/java/software/amazon/awssdk/arns/ArnTest.java
@@ -17,8 +17,14 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import java.util.Optional;
+import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
public class ArnTest {
@@ -311,4 +317,78 @@ public void invalidArnWithoutAccountId_ThrowsIllegalArgumentException() {
String arnString = "arn:aws:s3:us-east-1:";
assertThatThrownBy(() -> Arn.fromString(arnString)).hasMessageContaining("Malformed ARN");
}
+
+ private static Stream validArnTestCases() {
+ return Stream.of(
+ Arguments.of("Basic resource", "arn:aws:s3:us-east-1:12345678910:myresource"),
+ Arguments.of("Minimal requirements", "arn:aws:foobar:::myresource"),
+ Arguments.of("Qualified resource", "arn:aws:s3:us-east-1:12345678910:myresource:foobar:1"),
+ Arguments.of("Minimal resources", "arn:aws:s3:::bucket"),
+ Arguments.of("Without region", "arn:aws:iam::123456789012:root"),
+ Arguments.of("Resource type and resource", "arn:aws:s3:us-east-1:12345678910:bucket:foobar"),
+ Arguments.of("Resource type And resource and qualifier",
+ "arn:aws:s3:us-east-1:12345678910:bucket:foobar:1"),
+ Arguments.of("Resource type And resource with slash", "arn:aws:s3:us-east-1:12345678910:bucket/foobar"),
+ Arguments.of("Resource type and resource and qualifier slash",
+ "arn:aws:s3:us-east-1:12345678910:bucket/foobar/1"),
+ Arguments.of("Without region", "arn:aws:s3::123456789012:myresource"),
+ Arguments.of("Without accountId", "arn:aws:s3:us-east-1::myresource"),
+ Arguments.of("Resource with dots", "arn:aws:s3:us-east-1:12345678910:myresource:foobar.1")
+ );
+ }
+
+ private static Stream invalidArnTestCases() {
+ return Stream.of(
+ Arguments.of("Without resource", "arn:aws:s3:us-east-1:12345678910:"),
+ Arguments.of("Invalid arn", "arn:aws:"),
+ Arguments.of("Doesn't start with arn", "fakearn:aws:"),
+ Arguments.of("Invalid without partition", "arn:"),
+ Arguments.of("Invalid without service", "arn:aws:"),
+ Arguments.of("Invalid without region", "arn:aws:s3:"),
+ Arguments.of("Invalid without accountId", "arn:aws:s3:us-east-1:"),
+ Arguments.of("Null Arn", null)
+ );
+ }
+
+ private static Stream exceptionThrowingArnTestCases() {
+ return Stream.of(
+ Arguments.of("Valid without partition", "arn::s3:us-east-1:12345678910:myresource"),
+ Arguments.of("Valid without service", "arn:aws::us-east-1:12345678910:myresource")
+ );
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("validArnTestCases")
+ public void optionalArnFromString_ValidArns_ReturnsPopulatedOptional(String testName, String arnString) {
+ Optional optionalArn = Arn.tryFromString(arnString);
+
+ assertThat(optionalArn).isPresent();
+
+ Arn expectedArn = Arn.fromString(arnString);
+ Arn actualArn = optionalArn.get();
+
+ assertThat(actualArn.partition()).isEqualTo(expectedArn.partition());
+ assertThat(actualArn.service()).isEqualTo(expectedArn.service());
+ assertThat(actualArn.region()).isEqualTo(expectedArn.region());
+ assertThat(actualArn.accountId()).isEqualTo(expectedArn.accountId());
+ assertThat(actualArn.resourceAsString()).isEqualTo(expectedArn.resourceAsString());
+
+ assertThat(actualArn.toString()).isEqualTo(arnString);
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("invalidArnTestCases")
+ public void optionalArnFromString_InvalidArns_ReturnsEmptyOptional(String testName, String arnString) {
+ Optional optionalArn = Arn.tryFromString(arnString);
+ assertThat(optionalArn).isEmpty();
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("exceptionThrowingArnTestCases")
+ public void tryFromString_InvalidArns_ShouldThrowExceptions(String testName, String arnString) {
+ assertThrows(IllegalArgumentException.class, () -> {
+ Arn.tryFromString(arnString);
+ });
+ }
+
}