Skip to content

Better arn parsing #6163

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
64 changes: 64 additions & 0 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,70 @@ 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.
*/
public static Optional<Arn> tryFromString(String arn) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It contains a lot of duplicate code. Can we extract common code?

One idea is to create a helper method that takes a boolean for throwing behavior 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))) {
return Optional.empty();
}

int partitionColonIndex = arn.indexOf(':', arnColonIndex + 1);
if (partitionColonIndex < 0) {
return Optional.empty();
}

String partition = arn.substring(arnColonIndex + 1, partitionColonIndex);

int serviceColonIndex = arn.indexOf(':', partitionColonIndex + 1);
if (serviceColonIndex < 0) {
return Optional.empty();
}
String service = arn.substring(partitionColonIndex + 1, serviceColonIndex);

int regionColonIndex = arn.indexOf(':', serviceColonIndex + 1);
if (regionColonIndex < 0) {
return Optional.empty();
}
String region = arn.substring(serviceColonIndex + 1, regionColonIndex);

int accountColonIndex = arn.indexOf(':', regionColonIndex + 1);
if (accountColonIndex < 0) {
return Optional.empty();
}
String accountId = arn.substring(regionColonIndex + 1, accountColonIndex);

String resource = arn.substring(accountColonIndex + 1);
if (resource.isEmpty()) {
return Optional.empty();
}


Arn resultArn = builder()
.partition(partition)
.service(service)
.region(region)
.accountId(accountId)
.resource(resource)
.build();

return Optional.of(resultArn);
}

/**
* 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 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);
});
}
Comment on lines +388 to +392
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I thought it would no longer throw exception, did I miss anything?


}
Loading