Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/next-release/feature-AWSSDKforJavav2-1408e5f.json
Original file line number Diff line number Diff line change
@@ -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"
}
71 changes: 58 additions & 13 deletions core/arns/src/main/java/software/amazon/awssdk/arns/Arn.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* 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<Arn> 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
Expand All @@ -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<Arn> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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<Arguments> 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<Arguments> 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<Arguments> 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<Arn> 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<Arn> 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);
});
}

}
Loading