Skip to content

Commit fbdc7d3

Browse files
authored
Better arn parsing (#6163)
* adding new non-exception-based arn parsing logic * fixing test cases and checkstyle * better javadoc * refactoring code
1 parent d73091d commit fbdc7d3

File tree

3 files changed

+144
-13
lines changed

3 files changed

+144
-13
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "AWS SDK for Java v2",
4+
"contributor": "",
5+
"description": "Adding a new method of constructing ARNs without exceptions as control flow"
6+
}

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

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,23 @@ 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+
* When successful, the resource is accessible entirely as a string through
146+
* {@link #resourceAsString()}. Where correctly formatted, a parsed resource
147+
* containing resource type, resource and qualifier is available through
148+
* {@link #resource()}.
149+
*
150+
* @param arn A string containing an ARN to parse.
151+
* @return An {@link Optional} containing the parsed {@link Arn} if valid, or empty if invalid.
152+
* @throws IllegalArgumentException if the ARN contains empty partition or service fields
153+
*/
154+
public static Optional<Arn> tryFromString(String arn) {
155+
return parseArn(arn, false);
156+
}
157+
141158
/**
142159
* Parses a given string into an {@link Arn}. The resource is accessible entirely as a
143160
* string through {@link #resourceAsString()}. Where correctly formatted, a parsed
@@ -148,47 +165,75 @@ public static Builder builder() {
148165
* @return {@link Arn} - A modeled Arn.
149166
*/
150167
public static Arn fromString(String arn) {
168+
return parseArn(arn, true).orElseThrow(() -> new IllegalArgumentException("ARN parsing failed"));
169+
}
170+
171+
private static Optional<Arn> parseArn(String arn, boolean throwOnError) {
172+
if (arn == null) {
173+
return Optional.empty();
174+
}
175+
151176
int arnColonIndex = arn.indexOf(':');
152177
if (arnColonIndex < 0 || !"arn".equals(arn.substring(0, arnColonIndex))) {
153-
throw new IllegalArgumentException("Malformed ARN - doesn't start with 'arn:'");
178+
if (throwOnError) {
179+
throw new IllegalArgumentException("Malformed ARN - doesn't start with 'arn:'");
180+
}
181+
return Optional.empty();
154182
}
155183

156184
int partitionColonIndex = arn.indexOf(':', arnColonIndex + 1);
157185
if (partitionColonIndex < 0) {
158-
throw new IllegalArgumentException("Malformed ARN - no AWS partition specified");
186+
if (throwOnError) {
187+
throw new IllegalArgumentException("Malformed ARN - no AWS partition specified");
188+
}
189+
return Optional.empty();
159190
}
160191
String partition = arn.substring(arnColonIndex + 1, partitionColonIndex);
161192

162193
int serviceColonIndex = arn.indexOf(':', partitionColonIndex + 1);
163194
if (serviceColonIndex < 0) {
164-
throw new IllegalArgumentException("Malformed ARN - no service specified");
195+
if (throwOnError) {
196+
throw new IllegalArgumentException("Malformed ARN - no service specified");
197+
}
198+
return Optional.empty();
165199
}
166200
String service = arn.substring(partitionColonIndex + 1, serviceColonIndex);
167201

168202
int regionColonIndex = arn.indexOf(':', serviceColonIndex + 1);
169203
if (regionColonIndex < 0) {
170-
throw new IllegalArgumentException("Malformed ARN - no AWS region partition specified");
204+
if (throwOnError) {
205+
throw new IllegalArgumentException("Malformed ARN - no AWS region partition specified");
206+
}
207+
return Optional.empty();
171208
}
172209
String region = arn.substring(serviceColonIndex + 1, regionColonIndex);
173210

174211
int accountColonIndex = arn.indexOf(':', regionColonIndex + 1);
175212
if (accountColonIndex < 0) {
176-
throw new IllegalArgumentException("Malformed ARN - no AWS account specified");
213+
if (throwOnError) {
214+
throw new IllegalArgumentException("Malformed ARN - no AWS account specified");
215+
}
216+
return Optional.empty();
177217
}
178218
String accountId = arn.substring(regionColonIndex + 1, accountColonIndex);
179219

180220
String resource = arn.substring(accountColonIndex + 1);
181221
if (resource.isEmpty()) {
182-
throw new IllegalArgumentException("Malformed ARN - no resource specified");
222+
if (throwOnError) {
223+
throw new IllegalArgumentException("Malformed ARN - no resource specified");
224+
}
225+
return Optional.empty();
183226
}
184227

185-
return Arn.builder()
186-
.partition(partition)
187-
.service(service)
188-
.region(region)
189-
.accountId(accountId)
190-
.resource(resource)
191-
.build();
228+
Arn resultArn = builder()
229+
.partition(partition)
230+
.service(service)
231+
.region(region)
232+
.accountId(accountId)
233+
.resource(resource)
234+
.build();
235+
236+
return Optional.of(resultArn);
192237
}
193238

194239
@Override

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

Lines changed: 80 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,78 @@ 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+
Arguments.of("Basic resource", "arn:aws:s3:us-east-1:12345678910:myresource"),
324+
Arguments.of("Minimal requirements", "arn:aws:foobar:::myresource"),
325+
Arguments.of("Qualified resource", "arn:aws:s3:us-east-1:12345678910:myresource:foobar:1"),
326+
Arguments.of("Minimal resources", "arn:aws:s3:::bucket"),
327+
Arguments.of("Without region", "arn:aws:iam::123456789012:root"),
328+
Arguments.of("Resource type and resource", "arn:aws:s3:us-east-1:12345678910:bucket:foobar"),
329+
Arguments.of("Resource type And resource and qualifier",
330+
"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 slash",
333+
"arn:aws:s3:us-east-1:12345678910:bucket/foobar/1"),
334+
Arguments.of("Without region", "arn:aws:s3::123456789012:myresource"),
335+
Arguments.of("Without accountId", "arn:aws:s3:us-east-1::myresource"),
336+
Arguments.of("Resource with dots", "arn:aws:s3:us-east-1:12345678910:myresource:foobar.1")
337+
);
338+
}
339+
340+
private static Stream<Arguments> invalidArnTestCases() {
341+
return Stream.of(
342+
Arguments.of("Without resource", "arn:aws:s3:us-east-1:12345678910:"),
343+
Arguments.of("Invalid arn", "arn:aws:"),
344+
Arguments.of("Doesn't start with arn", "fakearn:aws:"),
345+
Arguments.of("Invalid without partition", "arn:"),
346+
Arguments.of("Invalid without service", "arn:aws:"),
347+
Arguments.of("Invalid without region", "arn:aws:s3:"),
348+
Arguments.of("Invalid without accountId", "arn:aws:s3:us-east-1:"),
349+
Arguments.of("Null Arn", null)
350+
);
351+
}
352+
353+
private static Stream<Arguments> exceptionThrowingArnTestCases() {
354+
return Stream.of(
355+
Arguments.of("Valid without partition", "arn::s3:us-east-1:12345678910:myresource"),
356+
Arguments.of("Valid without service", "arn:aws::us-east-1:12345678910:myresource")
357+
);
358+
}
359+
360+
@ParameterizedTest(name = "{0}")
361+
@MethodSource("validArnTestCases")
362+
public void optionalArnFromString_ValidArns_ReturnsPopulatedOptional(String testName, String arnString) {
363+
Optional<Arn> optionalArn = Arn.tryFromString(arnString);
364+
365+
assertThat(optionalArn).isPresent();
366+
367+
Arn expectedArn = Arn.fromString(arnString);
368+
Arn actualArn = optionalArn.get();
369+
370+
assertThat(actualArn.partition()).isEqualTo(expectedArn.partition());
371+
assertThat(actualArn.service()).isEqualTo(expectedArn.service());
372+
assertThat(actualArn.region()).isEqualTo(expectedArn.region());
373+
assertThat(actualArn.accountId()).isEqualTo(expectedArn.accountId());
374+
assertThat(actualArn.resourceAsString()).isEqualTo(expectedArn.resourceAsString());
375+
376+
assertThat(actualArn.toString()).isEqualTo(arnString);
377+
}
378+
379+
@ParameterizedTest(name = "{0}")
380+
@MethodSource("invalidArnTestCases")
381+
public void optionalArnFromString_InvalidArns_ReturnsEmptyOptional(String testName, String arnString) {
382+
Optional<Arn> optionalArn = Arn.tryFromString(arnString);
383+
assertThat(optionalArn).isEmpty();
384+
}
385+
386+
@ParameterizedTest(name = "{0}")
387+
@MethodSource("exceptionThrowingArnTestCases")
388+
public void tryFromString_InvalidArns_ShouldThrowExceptions(String testName, String arnString) {
389+
assertThrows(IllegalArgumentException.class, () -> {
390+
Arn.tryFromString(arnString);
391+
});
392+
}
393+
314394
}

0 commit comments

Comments
 (0)