Skip to content

Commit 8ae1dad

Browse files
committed
adding new non-exception-based arn parsing logic
1 parent 75604a3 commit 8ae1dad

File tree

2 files changed

+175
-0
lines changed
  • core/arns/src

2 files changed

+175
-0
lines changed

core/arns/src/main/java/software/amazon/awssdk/arns/Arn.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,71 @@ public static Builder builder() {
138138
return new DefaultBuilder();
139139
}
140140

141+
/**
142+
* Attempts to parse the given string into an {@link Arn}. If the input string is not a valid ARN,
143+
* this method returns {@link Optional#empty()} instead of throwing an exception.
144+
* <p>
145+
* Returns an empty Optional if the input string is not a valid ARN.
146+
* When successful, the resource is accessible entirely as a string through
147+
* {@link #resourceAsString()}. Where correctly formatted, a parsed resource
148+
* containing resource type, resource and qualifier is available through
149+
* {@link #resource()}.
150+
*
151+
* @param arn A string containing an ARN to parse.
152+
* @return An {@link Optional} containing the parsed {@link Arn} if valid, or empty if invalid.
153+
*/
154+
public static Optional<Arn> tryFromString(String arn) {
155+
if (arn == null) {
156+
return Optional.empty();
157+
}
158+
159+
int arnColonIndex = arn.indexOf(':');
160+
if (arnColonIndex < 0 || !"arn".equals(arn.substring(0, arnColonIndex))) {
161+
return Optional.empty();
162+
}
163+
164+
int partitionColonIndex = arn.indexOf(':', arnColonIndex + 1);
165+
if (partitionColonIndex < 0) {
166+
return Optional.empty();
167+
}
168+
169+
String partition = arn.substring(arnColonIndex + 1, partitionColonIndex);
170+
171+
int serviceColonIndex = arn.indexOf(':', partitionColonIndex + 1);
172+
if (serviceColonIndex < 0) {
173+
return Optional.empty();
174+
}
175+
String service = arn.substring(partitionColonIndex + 1, serviceColonIndex);
176+
177+
int regionColonIndex = arn.indexOf(':', serviceColonIndex + 1);
178+
if (regionColonIndex < 0) {
179+
return Optional.empty();
180+
}
181+
String region = arn.substring(serviceColonIndex + 1, regionColonIndex);
182+
183+
int accountColonIndex = arn.indexOf(':', regionColonIndex + 1);
184+
if (accountColonIndex < 0) {
185+
return Optional.empty();
186+
}
187+
String accountId = arn.substring(regionColonIndex + 1, accountColonIndex);
188+
189+
String resource = arn.substring(accountColonIndex + 1);
190+
if (resource.isEmpty()) {
191+
return Optional.empty();
192+
}
193+
194+
195+
Arn resultArn = builder()
196+
.partition(partition)
197+
.service(service)
198+
.region(region)
199+
.accountId(accountId)
200+
.resource(resource)
201+
.build();
202+
203+
return Optional.of(resultArn);
204+
}
205+
141206
/**
142207
* Parses a given string into an {@link Arn}. The resource is accessible entirely as a
143208
* string through {@link #resourceAsString()}. Where correctly formatted, a parsed

core/arns/src/test/java/software/amazon/awssdk/arns/ArnTest.java

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@
1717

1818
import static org.assertj.core.api.Assertions.assertThat;
1919
import static org.assertj.core.api.Assertions.assertThatThrownBy;
20+
import static org.junit.jupiter.api.Assertions.assertThrows;
2021

22+
import java.util.Optional;
23+
import java.util.stream.Stream;
2124
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.params.ParameterizedTest;
26+
import org.junit.jupiter.params.provider.Arguments;
27+
import org.junit.jupiter.params.provider.MethodSource;
2228

2329
public class ArnTest {
2430

@@ -311,4 +317,108 @@ public void invalidArnWithoutAccountId_ThrowsIllegalArgumentException() {
311317
String arnString = "arn:aws:s3:us-east-1:";
312318
assertThatThrownBy(() -> Arn.fromString(arnString)).hasMessageContaining("Malformed ARN");
313319
}
320+
321+
private static Stream<Arguments> validArnTestCases() {
322+
return Stream.of(
323+
// Test case name, ARN string
324+
Arguments.of("Basic Resource", "arn:aws:s3:us-east-1:12345678910:myresource"),
325+
Arguments.of("Minimal Requirements", "arn:aws:foobar:::myresource"),
326+
Arguments.of("Qualified Resource", "arn:aws:s3:us-east-1:12345678910:myresource:foobar:1"),
327+
Arguments.of("Minimal Resources", "arn:aws:s3:::bucket"),
328+
Arguments.of("Without Region", "arn:aws:iam::123456789012:root"),
329+
Arguments.of("Resource Type And Resource", "arn:aws:s3:us-east-1:12345678910:bucket:foobar"),
330+
Arguments.of("Resource Type And Resource And Qualifier", "arn:aws:s3:us-east-1:12345678910:bucket:foobar:1"),
331+
Arguments.of("Resource Type And Resource With Slash", "arn:aws:s3:us-east-1:12345678910:bucket/foobar"),
332+
Arguments.of("Resource Type And Resource And Qualifier With Slash", "arn:aws:s3:us-east-1:12345678910:bucket/foobar/1"),
333+
Arguments.of("Without Region", "arn:aws:s3::123456789012:myresource"),
334+
Arguments.of("Without AccountId", "arn:aws:s3:us-east-1::myresource"),
335+
Arguments.of("Resource Containing Dots", "arn:aws:s3:us-east-1:12345678910:myresource:foobar.1")
336+
);
337+
}
338+
339+
private static Stream<Arguments> invalidArnTestCases() {
340+
return Stream.of(
341+
// Test case name, ARN string
342+
Arguments.of("Without Partition", "arn::s3:us-east-1:12345678910:myresource"),
343+
Arguments.of("Without Service", "arn:aws::us-east-1:12345678910:myresource"),
344+
Arguments.of("Without Resource", "arn:aws:s3:us-east-1:12345678910:"),
345+
Arguments.of("Invalid ARN", "arn:aws:"),
346+
Arguments.of("Doesn't Start With ARN", "fakearn:aws:"),
347+
Arguments.of("Invalid Without Partition", "arn:"),
348+
Arguments.of("Invalid Without Service", "arn:aws:"),
349+
Arguments.of("Invalid Without Region", "arn:aws:s3:"),
350+
Arguments.of("Invalid Without AccountId", "arn:aws:s3:us-east-1:")
351+
);
352+
}
353+
354+
private static Stream<Arguments> exceptionThrowingArnTestCases() {
355+
return Stream.of(
356+
Arguments.of("Without Partition", "arn::s3:us-east-1:12345678910:myresource"),
357+
Arguments.of("Without Service", "arn:aws::us-east-1:12345678910:myresource")
358+
);
359+
}
360+
361+
@ParameterizedTest(name = "{0}")
362+
@MethodSource("validArnTestCases")
363+
public void optionalArnFromString_ValidArns_ReturnsPopulatedOptional(String testName, String arnString) {
364+
Optional<Arn> optionalArn = Arn.tryFromString(arnString);
365+
366+
assertThat(optionalArn).isPresent();
367+
368+
// Compare with the original fromString implementation
369+
Arn expectedArn = Arn.fromString(arnString);
370+
Arn actualArn = optionalArn.get();
371+
372+
assertThat(actualArn.partition()).isEqualTo(expectedArn.partition());
373+
assertThat(actualArn.service()).isEqualTo(expectedArn.service());
374+
assertThat(actualArn.region()).isEqualTo(expectedArn.region());
375+
assertThat(actualArn.accountId()).isEqualTo(expectedArn.accountId());
376+
assertThat(actualArn.resourceAsString()).isEqualTo(expectedArn.resourceAsString());
377+
378+
// Verify the ARN string representation matches
379+
assertThat(actualArn.toString()).isEqualTo(arnString);
380+
}
381+
382+
@ParameterizedTest(name = "{0}")
383+
@MethodSource("invalidArnTestCases")
384+
public void optionalArnFromString_InvalidArns_ReturnsEmptyOptional(String testName, String arnString) {
385+
Optional<Arn> optionalArn = Arn.tryFromString(arnString);
386+
assertThat(optionalArn).isEmpty();
387+
}
388+
389+
@ParameterizedTest(name = "{0}")
390+
@MethodSource("exceptionThrowingArnTestCases")
391+
public void tryFromString_InvalidArns_ShouldThrowExceptions(String testName, String arnString) {
392+
assertThrows(IllegalArgumentException.class, () -> {
393+
Arn.tryFromString(arnString);
394+
});
395+
}
396+
397+
@Test
398+
public void optionalArnFromString_NullInput_ReturnsEmptyOptional() {
399+
Optional<Arn> optionalArn = Arn.tryFromString(null);
400+
assertThat(optionalArn).isEmpty();
401+
}
402+
403+
@ParameterizedTest(name = "Resource parsing: {0}")
404+
@MethodSource("validArnTestCases")
405+
public void tryFromString_ResourceParsing_MatchesOriginalImplementation(String testName, String arnString) {
406+
// Skip test cases that would throw exceptions in the resource parsing
407+
if (arnString.contains("bucket:") || arnString.contains("bucket/")) {
408+
Optional<Arn> optionalArn = Arn.tryFromString(arnString);
409+
assertThat(optionalArn).isPresent();
410+
411+
Arn expectedArn = Arn.fromString(arnString);
412+
Arn actualArn = optionalArn.get();
413+
414+
// Verify resource parsing
415+
ArnResource expectedResource = expectedArn.resource();
416+
ArnResource actualResource = actualArn.resource();
417+
418+
assertThat(actualResource.resourceType()).isEqualTo(expectedResource.resourceType());
419+
assertThat(actualResource.resource()).isEqualTo(expectedResource.resource());
420+
assertThat(actualResource.qualifier()).isEqualTo(expectedResource.qualifier());
421+
}
422+
}
423+
314424
}

0 commit comments

Comments
 (0)