diff --git a/.changes/next-release/feature-AWSSDKforJavav2-b405876.json b/.changes/next-release/feature-AWSSDKforJavav2-b405876.json
new file mode 100644
index 000000000000..bb9d5276a1ac
--- /dev/null
+++ b/.changes/next-release/feature-AWSSDKforJavav2-b405876.json
@@ -0,0 +1,6 @@
+{
+ "type": "feature",
+ "category": "Amazon DyanmoDB",
+ "contributor": "",
+ "description": "Enable caching calls to URI constructors for account-id based endpoints"
+}
diff --git a/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml b/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml
index e123f641d076..7206acceb577 100644
--- a/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml
+++ b/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml
@@ -359,4 +359,8 @@
+
+
+
+
diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java
index 32cecd79feb5..4215717c88e6 100644
--- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java
+++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java
@@ -350,6 +350,11 @@ public class CustomizationConfig {
*/
private boolean enableFastUnmarshaller;
+ /**
+ * A boolean flag to indicate if the code-generated endpoint providers class should cache the calls to URI constructors.
+ */
+ private boolean enableEndpointProviderUriCaching;
+
private CustomizationConfig() {
}
@@ -924,4 +929,12 @@ public boolean getEnableFastUnmarshaller() {
public void setEnableFastUnmarshaller(boolean enableFastUnmarshaller) {
this.enableFastUnmarshaller = enableFastUnmarshaller;
}
+
+ public boolean getEnableEndpointProviderUriCaching() {
+ return enableEndpointProviderUriCaching;
+ }
+
+ public void setEnableEndpointProviderUriCaching(boolean enableEndpointProviderUriCaching) {
+ this.enableEndpointProviderUriCaching = enableEndpointProviderUriCaching;
+ }
}
diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/CodeGeneratorVisitor.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/CodeGeneratorVisitor.java
index 4cd94ace20ad..30f2e457275a 100644
--- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/CodeGeneratorVisitor.java
+++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/CodeGeneratorVisitor.java
@@ -26,21 +26,25 @@
import software.amazon.awssdk.awscore.endpoints.authscheme.SigV4aAuthScheme;
import software.amazon.awssdk.codegen.model.config.customization.KeyTypePair;
import software.amazon.awssdk.endpoints.Endpoint;
+import software.amazon.awssdk.utils.uri.SdkUri;
public class CodeGeneratorVisitor extends WalkRuleExpressionVisitor {
private final CodeBlock.Builder builder;
private final RuleRuntimeTypeMirror typeMirror;
private final SymbolTable symbolTable;
private final Map knownEndpointAttributes;
+ private final boolean endpointCaching;
public CodeGeneratorVisitor(RuleRuntimeTypeMirror typeMirror,
SymbolTable symbolTable,
Map knownEndpointAttributes,
- CodeBlock.Builder builder) {
+ CodeBlock.Builder builder,
+ boolean endpointCaching) {
this.builder = builder;
this.symbolTable = symbolTable;
this.knownEndpointAttributes = knownEndpointAttributes;
this.typeMirror = typeMirror;
+ this.endpointCaching = endpointCaching;
}
@Override
@@ -274,7 +278,11 @@ private void codegenTreeBody(RuleSetExpression expr) {
@Override
public Void visitEndpointExpression(EndpointExpression e) {
builder.add("return $T.endpoint(", typeMirror.rulesResult().type());
- builder.add("$T.builder().url($T.create(", Endpoint.class, URI.class);
+ if (endpointCaching) {
+ builder.add("$T.builder().url($T.getInstance().create(", Endpoint.class, SdkUri.class);
+ } else {
+ builder.add("$T.builder().url($T.create(", Endpoint.class, URI.class);
+ }
e.url().accept(this);
builder.add("))");
e.headers().accept(this);
diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/EndpointProviderSpec2.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/EndpointProviderSpec2.java
index adbed805cc40..f066cca43585 100644
--- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/EndpointProviderSpec2.java
+++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/EndpointProviderSpec2.java
@@ -222,10 +222,12 @@ private MethodSpec.Builder methodBuilderForRule(RuleSetExpression expr) {
}
private void codegenExpr(RuleExpression expr, CodeBlock.Builder builder) {
+ boolean useEndpointCaching = intermediateModel.getCustomizationConfig().getEnableEndpointProviderUriCaching();
CodeGeneratorVisitor visitor = new CodeGeneratorVisitor(typeMirror,
utils.symbolTable(),
knownEndpointAttributes,
- builder);
+ builder,
+ useEndpointCaching);
expr.accept(visitor);
}
diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java
index 308aa69ea487..617963937a05 100644
--- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java
+++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java
@@ -159,6 +159,28 @@ public static IntermediateModel queryServiceModelsWithOverrideKnowProperties() {
return new IntermediateModelBuilder(models).build();
}
+ public static IntermediateModel queryServiceModelsWithUriCache() {
+ File serviceModel = new File(ClientTestModels.class.getResource("client/c2j/query/service-2.json").getFile());
+ File customizationModel =
+ new File(ClientTestModels.class.getResource("client/c2j/query/customization-uri-cache.config").getFile());
+ File waitersModel = new File(ClientTestModels.class.getResource("client/c2j/query/waiters-2.json").getFile());
+ File endpointRuleSetModel =
+ new File(ClientTestModels.class.getResource("client/c2j/query/endpoint-rule-set.json").getFile());
+ File endpointTestsModel =
+ new File(ClientTestModels.class.getResource("client/c2j/query/endpoint-tests.json").getFile());
+
+ C2jModels models = C2jModels
+ .builder()
+ .serviceModel(getServiceModel(serviceModel))
+ .customizationConfig(getCustomizationConfig(customizationModel))
+ .waitersModel(getWaiters(waitersModel))
+ .endpointRuleSetModel(getEndpointRuleSet(endpointRuleSetModel))
+ .endpointTestSuiteModel(getEndpointTestSuite(endpointTestsModel))
+ .build();
+
+ return new IntermediateModelBuilder(models).build();
+ }
+
public static IntermediateModel queryServiceModelsEndpointAuthParamsWithAllowList() {
File serviceModel = new File(ClientTestModels.class.getResource("client/c2j/query/service-2.json").getFile());
File customizationModel =
diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/EndpointProviderCompiledRulesClassSpecTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/EndpointProviderCompiledRulesClassSpecTest.java
index 3ee5c8757a37..bcabddafd773 100644
--- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/EndpointProviderCompiledRulesClassSpecTest.java
+++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/EndpointProviderCompiledRulesClassSpecTest.java
@@ -37,4 +37,11 @@ void knowPropertiesOverride() {
new EndpointProviderSpec2(ClientTestModels.queryServiceModelsWithOverrideKnowProperties());
assertThat(endpointProviderSpec, generatesTo("endpoint-provider-know-prop-override-class.java"));
}
+
+ @Test
+ void endpointProviderClassWithUriCache() {
+ ClassSpec endpointProviderSpec =
+ new EndpointProviderSpec2(ClientTestModels.queryServiceModelsWithUriCache());
+ assertThat(endpointProviderSpec, generatesTo("endpoint-provider-uri-cache-class.java"));
+ }
}
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/customization-uri-cache.config b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/customization-uri-cache.config
new file mode 100644
index 000000000000..75393cba13cf
--- /dev/null
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/customization-uri-cache.config
@@ -0,0 +1,37 @@
+{
+ "authPolicyActions" : {
+ "skip" : true
+ },
+ "skipEndpointTests": {
+ "test case 4": "Does not work"
+ },
+ "endpointParameters": {
+ "CustomEndpointArray": {
+ "required": false,
+ "documentation": "Parameter from the customization config",
+ "type": "StringArray"
+ },
+ "ArnList": {
+ "required": false,
+ "documentation": "Parameter from the customization config",
+ "type": "StringArray"
+ }
+ },
+ "customOperationContextParams": [
+ {
+ "operationName": "OperationWithCustomizedOperationContextParam",
+ "operationContextParamsMap": {
+ "customEndpointArray": {
+ "path": "ListMember.StringList[*].LeafString"
+ }
+ }
+ }
+ ],
+ "preClientExecutionRequestCustomizer": {
+ "OperationWithCustomMember": {
+ "methodName": "dummyRequestModifier",
+ "className": "software.amazon.awssdk.codegen.internal.UtilsTest"
+ }
+ },
+ "enableEndpointProviderUriCaching": true
+}
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-uri-cache-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-uri-cache-class.java
new file mode 100644
index 000000000000..62fc9c73f908
--- /dev/null
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-uri-cache-class.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.services.query.endpoints.internal;
+
+import java.util.Arrays;
+import java.util.concurrent.CompletableFuture;
+import software.amazon.awssdk.annotations.Generated;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.awscore.endpoints.AwsEndpointAttribute;
+import software.amazon.awssdk.awscore.endpoints.authscheme.SigV4AuthScheme;
+import software.amazon.awssdk.awscore.endpoints.authscheme.SigV4aAuthScheme;
+import software.amazon.awssdk.core.exception.SdkClientException;
+import software.amazon.awssdk.endpoints.Endpoint;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.query.endpoints.QueryEndpointParams;
+import software.amazon.awssdk.services.query.endpoints.QueryEndpointProvider;
+import software.amazon.awssdk.utils.CompletableFutureUtils;
+import software.amazon.awssdk.utils.Validate;
+import software.amazon.awssdk.utils.uri.SdkUri;
+
+@Generated("software.amazon.awssdk:codegen")
+@SdkInternalApi
+public final class DefaultQueryEndpointProvider implements QueryEndpointProvider {
+ @Override
+ public CompletableFuture resolveEndpoint(QueryEndpointParams params) {
+ Validate.notNull(params.region(), "Parameter 'region' must not be null");
+ try {
+ RuleResult result = endpointRule0(params, new LocalState(params.region()));
+ if (result.canContinue()) {
+ throw SdkClientException.create("Rule engine did not reach an error or endpoint result");
+ }
+ if (result.isError()) {
+ String errorMsg = result.error();
+ if (errorMsg.contains("Invalid ARN") && errorMsg.contains(":s3:::")) {
+ errorMsg += ". Use the bucket name instead of simple bucket ARNs in GetBucketLocationRequest.";
+ }
+ throw SdkClientException.create(errorMsg);
+ }
+ return CompletableFuture.completedFuture(result.endpoint());
+ } catch (Exception error) {
+ return CompletableFutureUtils.failedFuture(error);
+ }
+ }
+
+ private static RuleResult endpointRule0(QueryEndpointParams params, LocalState locals) {
+ return endpointRule1(params, locals);
+ }
+
+ private static RuleResult endpointRule1(QueryEndpointParams params, LocalState locals) {
+ RulePartition partitionResult = null;
+ if ((partitionResult = RulesFunctions.awsPartition(locals.region())) != null) {
+ locals = locals.toBuilder().partitionResult(partitionResult).build();
+ RuleResult result = endpointRule2(params, locals);
+ if (result.isResolved()) {
+ return result;
+ }
+ result = endpointRule6(params, locals);
+ if (result.isResolved()) {
+ return result;
+ }
+ result = endpointRule11(params, locals);
+ if (result.isResolved()) {
+ return result;
+ }
+ return endpointRule12(params, locals);
+ }
+ return RuleResult.carryOn();
+ }
+
+ private static RuleResult endpointRule2(QueryEndpointParams params, LocalState locals) {
+ if (params.endpointId() != null) {
+ RuleResult result = endpointRule3(params, locals);
+ if (result.isResolved()) {
+ return result;
+ }
+ result = endpointRule4(params, locals);
+ if (result.isResolved()) {
+ return result;
+ }
+ return endpointRule5(params, locals);
+ }
+ return RuleResult.carryOn();
+ }
+
+ private static RuleResult endpointRule3(QueryEndpointParams params, LocalState locals) {
+ if (params.useFipsEndpoint() != null && params.useFipsEndpoint()) {
+ return RuleResult.error("FIPS endpoints not supported with multi-region endpoints");
+ }
+ return RuleResult.carryOn();
+ }
+
+ private static RuleResult endpointRule4(QueryEndpointParams params, LocalState locals) {
+ if (params.useFipsEndpoint() == null && params.useDualStackEndpoint() != null && params.useDualStackEndpoint()) {
+ return RuleResult
+ .endpoint(Endpoint
+ .builder()
+ .url(SdkUri.getInstance().create("https://" + params.endpointId() + ".query."
+ + locals.partitionResult().dualStackDnsSuffix()))
+ .putAttribute(
+ AwsEndpointAttribute.AUTH_SCHEMES,
+ Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
+ .signingRegionSet(Arrays.asList("*")).build())).build());
+ }
+ return RuleResult.carryOn();
+ }
+
+ private static RuleResult endpointRule5(QueryEndpointParams params, LocalState locals) {
+ return RuleResult.endpoint(Endpoint
+ .builder()
+ .url(SdkUri.getInstance().create("https://" + params.endpointId() + ".query." + locals.partitionResult().dnsSuffix()))
+ .putAttribute(
+ AwsEndpointAttribute.AUTH_SCHEMES,
+ Arrays.asList(SigV4aAuthScheme.builder().signingName("query").signingRegionSet(Arrays.asList("*"))
+ .build())).build());
+ }
+
+ private static RuleResult endpointRule6(QueryEndpointParams params, LocalState locals) {
+ if (RulesFunctions.isValidHostLabel(locals.region(), false)) {
+ RuleResult result = endpointRule7(params, locals);
+ if (result.isResolved()) {
+ return result;
+ }
+ result = endpointRule8(params, locals);
+ if (result.isResolved()) {
+ return result;
+ }
+ result = endpointRule9(params, locals);
+ if (result.isResolved()) {
+ return result;
+ }
+ return endpointRule10(params, locals);
+ }
+ return RuleResult.carryOn();
+ }
+
+ private static RuleResult endpointRule7(QueryEndpointParams params, LocalState locals) {
+ if (params.useFipsEndpoint() != null && params.useFipsEndpoint() && params.useDualStackEndpoint() == null) {
+ return RuleResult.endpoint(Endpoint
+ .builder()
+ .url(SdkUri.getInstance().create("https://query-fips." + locals.region() + "." + locals.partitionResult().dnsSuffix()))
+ .putAttribute(
+ AwsEndpointAttribute.AUTH_SCHEMES,
+ Arrays.asList(SigV4aAuthScheme.builder().signingName("query").signingRegionSet(Arrays.asList("*"))
+ .build())).build());
+ }
+ return RuleResult.carryOn();
+ }
+
+ private static RuleResult endpointRule8(QueryEndpointParams params, LocalState locals) {
+ if (params.useDualStackEndpoint() != null && params.useDualStackEndpoint() && params.useFipsEndpoint() == null) {
+ return RuleResult.endpoint(Endpoint
+ .builder()
+ .url(SdkUri.getInstance().create("https://query." + locals.region() + "." + locals.partitionResult().dualStackDnsSuffix()))
+ .putAttribute(
+ AwsEndpointAttribute.AUTH_SCHEMES,
+ Arrays.asList(SigV4aAuthScheme.builder().signingName("query").signingRegionSet(Arrays.asList("*"))
+ .build(), SigV4AuthScheme.builder().signingName("query").signingRegion(locals.region())
+ .build())).build());
+ }
+ return RuleResult.carryOn();
+ }
+
+ private static RuleResult endpointRule9(QueryEndpointParams params, LocalState locals) {
+ if (params.useDualStackEndpoint() != null && params.useFipsEndpoint() != null && params.useDualStackEndpoint()
+ && params.useFipsEndpoint()) {
+ return RuleResult
+ .endpoint(Endpoint
+ .builder()
+ .url(SdkUri.getInstance().create("https://query-fips." + locals.region() + "."
+ + locals.partitionResult().dualStackDnsSuffix()))
+ .putAttribute(
+ AwsEndpointAttribute.AUTH_SCHEMES,
+ Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
+ .signingRegionSet(Arrays.asList("*")).build())).build());
+ }
+ return RuleResult.carryOn();
+ }
+
+ private static RuleResult endpointRule10(QueryEndpointParams params, LocalState locals) {
+ return RuleResult.endpoint(Endpoint.builder()
+ .url(SdkUri.getInstance().create("https://query." + locals.region() + "." + locals.partitionResult().dnsSuffix())).build());
+ }
+
+ private static RuleResult endpointRule11(QueryEndpointParams params, LocalState locals) {
+ return RuleResult.error(locals.region() + " is not a valid HTTP host-label");
+ }
+
+ private static RuleResult endpointRule12(QueryEndpointParams params, LocalState locals) {
+ if (params.useFipsEndpoint() == null && params.useDualStackEndpoint() != null && params.useDualStackEndpoint()
+ && params.arnList() != null) {
+ String firstArn = null;
+ RuleArn parsedArn = null;
+ if ((firstArn = RulesFunctions.listAccess(params.arnList(), 0)) != null) {
+ locals = locals.toBuilder().firstArn(firstArn).build();
+ } else {
+ return RuleResult.carryOn();
+ }
+ if ((parsedArn = RulesFunctions.awsParseArn(locals.firstArn())) != null) {
+ locals = locals.toBuilder().parsedArn(parsedArn).build();
+ return RuleResult.endpoint(Endpoint
+ .builder()
+ .url(SdkUri.getInstance().create("https://" + params.endpointId() + ".query."
+ + locals.partitionResult().dualStackDnsSuffix()))
+ .putAttribute(
+ AwsEndpointAttribute.AUTH_SCHEMES,
+ Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
+ .signingRegionSet(Arrays.asList("*")).build())).build());
+ }
+ }
+ return RuleResult.carryOn();
+ }
+
+ @Override
+ public boolean equals(Object rhs) {
+ return rhs != null && getClass().equals(rhs.getClass());
+ }
+
+ @Override
+ public int hashCode() {
+ return getClass().hashCode();
+ }
+
+ private static final class LocalState {
+ private final String region;
+
+ private final RulePartition partitionResult;
+
+ private final String firstArn;
+
+ private final RuleArn parsedArn;
+
+ LocalState() {
+ this.region = null;
+ this.partitionResult = null;
+ this.firstArn = null;
+ this.parsedArn = null;
+ }
+
+ LocalState(Region region) {
+ if (region != null) {
+ this.region = region.id();
+ } else {
+ this.region = null;
+ }
+ this.partitionResult = null;
+ this.firstArn = null;
+ this.parsedArn = null;
+ }
+
+ LocalState(LocalStateBuilder builder) {
+ this.region = builder.region;
+ this.partitionResult = builder.partitionResult;
+ this.firstArn = builder.firstArn;
+ this.parsedArn = builder.parsedArn;
+ }
+
+ public String region() {
+ return this.region;
+ }
+
+ public RulePartition partitionResult() {
+ return this.partitionResult;
+ }
+
+ public String firstArn() {
+ return this.firstArn;
+ }
+
+ public RuleArn parsedArn() {
+ return this.parsedArn;
+ }
+
+ public LocalStateBuilder toBuilder() {
+ return new LocalStateBuilder(this);
+ }
+ }
+
+ private static final class LocalStateBuilder {
+ private String region;
+
+ private RulePartition partitionResult;
+
+ private String firstArn;
+
+ private RuleArn parsedArn;
+
+ LocalStateBuilder() {
+ this.region = null;
+ this.partitionResult = null;
+ this.firstArn = null;
+ this.parsedArn = null;
+ }
+
+ LocalStateBuilder(LocalState locals) {
+ this.region = locals.region;
+ this.partitionResult = locals.partitionResult;
+ this.firstArn = locals.firstArn;
+ this.parsedArn = locals.parsedArn;
+ }
+
+ public LocalStateBuilder region(String value) {
+ this.region = value;
+ return this;
+ }
+
+ public LocalStateBuilder partitionResult(RulePartition value) {
+ this.partitionResult = value;
+ return this;
+ }
+
+ public LocalStateBuilder firstArn(String value) {
+ this.firstArn = value;
+ return this;
+ }
+
+ public LocalStateBuilder parsedArn(RuleArn value) {
+ this.parsedArn = value;
+ return this;
+ }
+
+ LocalState build() {
+ return new LocalState(this);
+ }
+ }
+}
diff --git a/core/auth-crt/src/main/java/software/amazon/awssdk/authcrt/signer/internal/CrtHttpRequestConverter.java b/core/auth-crt/src/main/java/software/amazon/awssdk/authcrt/signer/internal/CrtHttpRequestConverter.java
index 89f3541348cd..2047e88f5bad 100644
--- a/core/auth-crt/src/main/java/software/amazon/awssdk/authcrt/signer/internal/CrtHttpRequestConverter.java
+++ b/core/auth-crt/src/main/java/software/amazon/awssdk/authcrt/signer/internal/CrtHttpRequestConverter.java
@@ -36,6 +36,7 @@
import software.amazon.awssdk.http.SdkHttpFullRequest;
import software.amazon.awssdk.utils.StringUtils;
import software.amazon.awssdk.utils.http.SdkHttpUtils;
+import software.amazon.awssdk.utils.uri.SdkUri;
@SdkInternalApi
public final class CrtHttpRequestConverter {
@@ -77,7 +78,7 @@ public SdkHttpFullRequest crtRequestToHttp(SdkHttpFullRequest inputRequest, Http
String portString = SdkHttpUtils.isUsingStandardPort(builder.protocol(), builder.port()) ? "" : ":" + builder.port();
String encodedPath = encodedPathFromCrtFormat(inputRequest.encodedPath(), signedCrtRequest.getEncodedPath());
String fullUriString = builder.protocol() + "://" + builder.host() + portString + encodedPath;
- fullUri = new URI(fullUriString);
+ fullUri = SdkUri.getInstance().newUri(fullUriString);
} catch (URISyntaxException e) {
return null;
}
diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoint/AwsClientEndpointProvider.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoint/AwsClientEndpointProvider.java
index fbee6e0ec47f..0bb5119b369e 100644
--- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoint/AwsClientEndpointProvider.java
+++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoint/AwsClientEndpointProvider.java
@@ -41,6 +41,7 @@
import software.amazon.awssdk.utils.ToString;
import software.amazon.awssdk.utils.Validate;
import software.amazon.awssdk.utils.internal.SystemSettingUtils;
+import software.amazon.awssdk.utils.uri.SdkUri;
/**
* An implementation of {@link ClientEndpointProvider} that loads the default client endpoint from:
@@ -238,7 +239,7 @@ private Optional clientEndpointFromServiceMetadata(Builder build
.region(builder.region)
.tags(endpointTags)
.build());
- URI endpoint = URI.create(builder.protocol + "://" + endpointWithoutProtocol);
+ URI endpoint = SdkUri.getInstance().create(builder.protocol + "://" + endpointWithoutProtocol);
if (endpoint.getHost() == null) {
String error = "Configured region (" + builder.region + ") and tags (" + endpointTags + ") resulted in "
+ "an invalid URI: " + endpoint + ". This is usually caused by an invalid region "
@@ -260,7 +261,7 @@ private Optional clientEndpointFromServiceMetadata(Builder build
private Optional createUri(String source, Optional uri) {
return uri.map(u -> {
try {
- URI parsedUri = new URI(uri.get());
+ URI parsedUri = SdkUri.getInstance().newUri(uri.get());
log.trace(() -> "Client endpoint was loaded from the " + source + ": " + parsedUri);
return parsedUri;
} catch (URISyntaxException e) {
diff --git a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/util/CrtHttpRequestConverter.java b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/util/CrtHttpRequestConverter.java
index 0485cf128887..fd0bb010fd8f 100644
--- a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/util/CrtHttpRequestConverter.java
+++ b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/util/CrtHttpRequestConverter.java
@@ -31,6 +31,7 @@
import software.amazon.awssdk.http.auth.aws.crt.internal.io.CrtInputStream;
import software.amazon.awssdk.utils.StringUtils;
import software.amazon.awssdk.utils.http.SdkHttpUtils;
+import software.amazon.awssdk.utils.uri.SdkUri;
@SdkInternalApi
public final class CrtHttpRequestConverter {
@@ -73,7 +74,7 @@ public static SdkHttpRequest toRequest(SdkHttpRequest request, HttpRequest crtRe
String portString = SdkHttpUtils.isUsingStandardPort(builder.protocol(), builder.port()) ? "" : ":" + builder.port();
String encodedPath = encodedPathFromCrtFormat(request.encodedPath(), crtRequest.getEncodedPath());
String fullUriString = builder.protocol() + "://" + builder.host() + portString + encodedPath;
- fullUri = new URI(fullUriString);
+ fullUri = SdkUri.getInstance().newUri(fullUriString);
} catch (URISyntaxException e) {
throw new RuntimeException("Full URI could not be formed.", e);
}
diff --git a/core/regions/src/main/java/software/amazon/awssdk/regions/internal/util/ServiceMetadataUtils.java b/core/regions/src/main/java/software/amazon/awssdk/regions/internal/util/ServiceMetadataUtils.java
index 87e1d9f89f37..19810aedfd43 100644
--- a/core/regions/src/main/java/software/amazon/awssdk/regions/internal/util/ServiceMetadataUtils.java
+++ b/core/regions/src/main/java/software/amazon/awssdk/regions/internal/util/ServiceMetadataUtils.java
@@ -25,6 +25,7 @@
import software.amazon.awssdk.utils.Pair;
import software.amazon.awssdk.utils.StringUtils;
import software.amazon.awssdk.utils.Validate;
+import software.amazon.awssdk.utils.uri.SdkUri;
@SdkInternalApi
public class ServiceMetadataUtils {
@@ -38,7 +39,8 @@ public static URI endpointFor(String hostname,
String endpointPrefix,
String region,
String dnsSuffix) {
- return URI.create(StringUtils.replaceEach(hostname, SEARCH_LIST, new String[] { endpointPrefix, region, dnsSuffix }));
+ return SdkUri.getInstance().create(
+ StringUtils.replaceEach(hostname, SEARCH_LIST, new String[] {endpointPrefix, region, dnsSuffix }));
}
public static Region signingRegion(ServiceEndpointKey key,
diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/util/MetricUtils.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/util/MetricUtils.java
index ed2c85f86943..d30e66692368 100644
--- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/util/MetricUtils.java
+++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/util/MetricUtils.java
@@ -38,6 +38,7 @@
import software.amazon.awssdk.metrics.NoOpMetricCollector;
import software.amazon.awssdk.metrics.SdkMetric;
import software.amazon.awssdk.utils.Pair;
+import software.amazon.awssdk.utils.uri.SdkUri;
/**
* Utility methods for working with metrics.
@@ -112,7 +113,8 @@ public static void collectServiceEndpointMetrics(MetricCollector metricCollector
// Only interested in the service endpoint so don't include any path, query, or fragment component
URI requestUri = httpRequest.getUri();
try {
- URI serviceEndpoint = new URI(requestUri.getScheme(), requestUri.getAuthority(), null, null, null);
+ URI serviceEndpoint = SdkUri.getInstance().newUri(
+ requestUri.getScheme(), requestUri.getAuthority(), null, null, null);
metricCollector.reportMetric(CoreMetric.SERVICE_ENDPOINT, serviceEndpoint);
} catch (URISyntaxException e) {
// This should not happen since getUri() should return a valid URI
diff --git a/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpRequest.java b/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpRequest.java
index bb34909b5f36..72bcea0299e6 100644
--- a/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpRequest.java
+++ b/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpRequest.java
@@ -29,6 +29,7 @@
import software.amazon.awssdk.utils.builder.CopyableBuilder;
import software.amazon.awssdk.utils.builder.ToCopyableBuilder;
import software.amazon.awssdk.utils.http.SdkHttpUtils;
+import software.amazon.awssdk.utils.uri.SdkUri;
/**
* An immutable HTTP request without access to the request body. {@link SdkHttpFullRequest} should be used when access to a
@@ -154,7 +155,7 @@ default URI getUri() {
// Do not include the port in the URI when using the default port for the protocol.
String portString = SdkHttpUtils.isUsingStandardPort(protocol(), port()) ? "" : ":" + port();
- return URI.create(protocol() + "://" + host() + portString + encodedPath() + encodedQueryString);
+ return SdkUri.getInstance().create(protocol() + "://" + host() + portString + encodedPath() + encodedQueryString);
}
/**
diff --git a/http-clients/apache-client/src/main/java/software/amazon/awssdk/http/apache/internal/impl/ApacheHttpRequestFactory.java b/http-clients/apache-client/src/main/java/software/amazon/awssdk/http/apache/internal/impl/ApacheHttpRequestFactory.java
index cfb22343ba3f..1c77e8738f3e 100644
--- a/http-clients/apache-client/src/main/java/software/amazon/awssdk/http/apache/internal/impl/ApacheHttpRequestFactory.java
+++ b/http-clients/apache-client/src/main/java/software/amazon/awssdk/http/apache/internal/impl/ApacheHttpRequestFactory.java
@@ -42,6 +42,7 @@
import software.amazon.awssdk.http.apache.internal.utils.ApacheUtils;
import software.amazon.awssdk.utils.StringUtils;
import software.amazon.awssdk.utils.http.SdkHttpUtils;
+import software.amazon.awssdk.utils.uri.SdkUri;
/**
* Responsible for creating Apache HttpClient 4 request objects.
@@ -80,7 +81,7 @@ private URI sanitizeUri(SdkHttpRequest request) {
String portString = SdkHttpUtils.isUsingStandardPort(protocol, port) ?
"" : ":" + port;
- return URI.create(protocol + "://" + request.host() + portString + newPath + encodedQueryString);
+ return SdkUri.getInstance().create(protocol + "://" + request.host() + portString + newPath + encodedQueryString);
}
return request.getUri();
diff --git a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java
index 76af9bc6d8f2..2df865f0fa0b 100644
--- a/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java
+++ b/http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientBase.java
@@ -44,6 +44,7 @@
import software.amazon.awssdk.utils.IoUtils;
import software.amazon.awssdk.utils.Logger;
import software.amazon.awssdk.utils.SdkAutoCloseable;
+import software.amazon.awssdk.utils.uri.SdkUri;
/**
* Common functionality and configuration for the CRT Http clients.
@@ -162,8 +163,8 @@ HttpClientConnectionManager getOrCreateConnectionPool(URI uri) {
}
URI poolKey(SdkHttpRequest sdkRequest) {
- return invokeSafely(() -> new URI(sdkRequest.protocol(), null, sdkRequest.host(),
- sdkRequest.port(), null, null, null));
+ return invokeSafely(() -> SdkUri.getInstance().newUri(sdkRequest.protocol(), null, sdkRequest.host(),
+ sdkRequest.port(), null, null, null));
}
@Override
diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClient.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClient.java
index b28b32da1df7..f8a5b99809ca 100644
--- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClient.java
+++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/NettyNioAsyncHttpClient.java
@@ -60,6 +60,7 @@
import software.amazon.awssdk.utils.AttributeMap;
import software.amazon.awssdk.utils.Either;
import software.amazon.awssdk.utils.Validate;
+import software.amazon.awssdk.utils.uri.SdkUri;
/**
* An implementation of {@link SdkAsyncHttpClient} that uses a Netty non-blocking HTTP client to communicate with the service.
@@ -169,8 +170,8 @@ private SdkEventLoopGroup eventLoopGroup(DefaultBuilder builder) {
}
private static URI poolKey(SdkHttpRequest sdkRequest) {
- return invokeSafely(() -> new URI(sdkRequest.protocol(), null, sdkRequest.host(),
- sdkRequest.port(), null, null, null));
+ return invokeSafely(() -> SdkUri.getInstance().newUri(sdkRequest.protocol(), null, sdkRequest.host(),
+ sdkRequest.port(), null, null, null));
}
private SslProvider resolveSslProvider(DefaultBuilder builder) {
diff --git a/services/dynamodb/src/main/resources/codegen-resources/dynamodb/customization.config b/services/dynamodb/src/main/resources/codegen-resources/dynamodb/customization.config
index a7b525068c07..b777570861ea 100644
--- a/services/dynamodb/src/main/resources/codegen-resources/dynamodb/customization.config
+++ b/services/dynamodb/src/main/resources/codegen-resources/dynamodb/customization.config
@@ -36,5 +36,6 @@
"customRetryStrategy" : "software.amazon.awssdk.services.dynamodb.DynamoDbRetryPolicy",
"enableEndpointDiscoveryMethodRequired": true,
"enableGenerateCompiledEndpointRules": true,
- "enableFastUnmarshaller": false
+ "enableFastUnmarshaller": false,
+ "enableEndpointProviderUriCaching": true
}
diff --git a/utils/pom.xml b/utils/pom.xml
index 23bce848125b..b209b2f9f770 100644
--- a/utils/pom.xml
+++ b/utils/pom.xml
@@ -130,6 +130,11 @@
rxjava
test
+
+ nl.jqno.equalsverifier
+ equalsverifier
+ test
+
diff --git a/utils/src/main/java/software/amazon/awssdk/utils/cache/lru/LruCache.java b/utils/src/main/java/software/amazon/awssdk/utils/cache/lru/LruCache.java
index df7bc222d261..e5bc23dca833 100644
--- a/utils/src/main/java/software/amazon/awssdk/utils/cache/lru/LruCache.java
+++ b/utils/src/main/java/software/amazon/awssdk/utils/cache/lru/LruCache.java
@@ -168,6 +168,10 @@ public int size() {
return cache.size();
}
+ public boolean containsKey(K key) {
+ return cache.containsKey(key);
+ }
+
public static LruCache.Builder builder(Function supplier) {
return new Builder<>(supplier);
}
diff --git a/utils/src/main/java/software/amazon/awssdk/utils/uri/SdkUri.java b/utils/src/main/java/software/amazon/awssdk/utils/uri/SdkUri.java
new file mode 100644
index 000000000000..c7102a8dd9a3
--- /dev/null
+++ b/utils/src/main/java/software/amazon/awssdk/utils/uri/SdkUri.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.utils.uri;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Objects;
+import software.amazon.awssdk.annotations.SdkProtectedApi;
+import software.amazon.awssdk.utils.Lazy;
+import software.amazon.awssdk.utils.Logger;
+import software.amazon.awssdk.utils.cache.lru.LruCache;
+import software.amazon.awssdk.utils.uri.internal.UriConstructorArgs;
+
+/**
+ * Global cache for account-id based URI. Prevent calling new URI constructor for the same string, which can cause performance
+ * issues with some uri pattern. Do not directly depend on this class, it will be removed in the future.
+ */
+@SdkProtectedApi
+public final class SdkUri {
+ private static final Logger log = Logger.loggerFor(SdkUri.class);
+
+ private static final String HTTPS_PREFIX = "https://";
+ private static final String HTTP_PREFIX = "http://";
+ private static final int MAX_INT_DIGITS_BASE_10 = 10;
+
+ /*
+ * The default LRUCache size is 100, but for a single service call we cache at least 3 different URIs so the cache size is
+ * increased a bit to account for the different URIs.
+ */
+ private static final int CACHE_SIZE = 150;
+
+ private static final Lazy INSTANCE = new Lazy<>(SdkUri::new);
+
+ private final LruCache cache;
+
+ private SdkUri() {
+ this.cache = LruCache.builder(UriConstructorArgs::newInstance)
+ .maxSize(CACHE_SIZE)
+ .build();
+ }
+
+ public static SdkUri getInstance() {
+ return INSTANCE.getValue();
+ }
+
+ public URI create(String s) {
+ if (!isAccountIdUri(s)) {
+ log.trace(() -> "skipping cache for uri " + s);
+ return URI.create(s);
+ }
+ StringConstructorArgs key = new StringConstructorArgs(s);
+ boolean containsK = cache.containsKey(key);
+ URI uri = cache.get(key);
+ logCacheUsage(containsK, uri);
+ return uri;
+ }
+
+ public URI newUri(String s) throws URISyntaxException {
+ if (!isAccountIdUri(s)) {
+ log.trace(() -> "skipping cache for uri " + s);
+ return new URI(s);
+ }
+ try {
+ StringConstructorArgs key = new StringConstructorArgs(s);
+ boolean containsK = cache.containsKey(key);
+ URI uri = cache.get(key);
+ logCacheUsage(containsK, uri);
+ return uri;
+ } catch (IllegalArgumentException e) {
+ // URI.create() wraps the URISyntaxException thrown by new URI in a IllegalArgumentException, we need to unwrap it
+ if (e.getCause() instanceof URISyntaxException) {
+ throw (URISyntaxException) e.getCause();
+ }
+ throw e;
+ }
+ }
+
+ public URI newUri(String scheme,
+ String userInfo, String host, int port,
+ String path, String query, String fragment) throws URISyntaxException {
+ if (!isAccountIdUri(host)) {
+ log.trace(() -> "skipping cache for host " + host);
+ return new URI(scheme, userInfo, host, port, path, query, fragment);
+ }
+ try {
+ HostConstructorArgs key = new HostConstructorArgs(scheme, userInfo, host, port, path, query, fragment);
+ boolean containsK = cache.containsKey(key);
+ URI uri = cache.get(key);
+ logCacheUsage(containsK, uri);
+ return uri;
+ } catch (IllegalArgumentException e) {
+ if (e.getCause() instanceof URISyntaxException) {
+ throw (URISyntaxException) e.getCause();
+ }
+ throw e;
+ }
+ }
+
+ public URI newUri(String scheme,
+ String authority,
+ String path, String query, String fragment) throws URISyntaxException {
+ if (!isAccountIdUri(authority)) {
+ log.trace(() -> "skipping cache for authority " + authority);
+ return new URI(scheme, authority, path, query, fragment);
+ }
+ try {
+ AuthorityConstructorArgs key = new AuthorityConstructorArgs(scheme, authority, path, query, fragment);
+ boolean containsK = cache.containsKey(key);
+ URI uri = cache.get(key);
+ logCacheUsage(containsK, uri);
+ return uri;
+ } catch (IllegalArgumentException e) {
+ if (e.getCause() instanceof URISyntaxException) {
+ throw (URISyntaxException) e.getCause();
+ }
+ throw e;
+ }
+ }
+
+ /*
+ * Best-effort check for uri string being account-id based.
+ *
+ * The troublesome uris are of the form 'https://123456789012.ddb.us-east-1.amazonaws.com' The heuristic chosen to detect such
+ * candidate URI is to check the first char after the scheme, and then the char 10 places further down the string. If both
+ * are digits, there is a potential for that string to represent a number that would exceed the value of Integer.MAX_VALUE,
+ * which would cause the performance degradation observed with such URIs.
+ */
+ private boolean isAccountIdUri(String s) {
+ int firstCharAfterScheme = 0;
+ if (s.startsWith(HTTPS_PREFIX)) {
+ firstCharAfterScheme = HTTPS_PREFIX.length();
+ } else if (s.startsWith(HTTP_PREFIX)) {
+ firstCharAfterScheme = HTTP_PREFIX.length();
+ }
+
+ if (s.length() > firstCharAfterScheme + MAX_INT_DIGITS_BASE_10) {
+ return Character.isDigit(s.charAt(firstCharAfterScheme))
+ && Character.isDigit(s.charAt(firstCharAfterScheme + MAX_INT_DIGITS_BASE_10));
+ }
+ return false;
+ }
+
+ private void logCacheUsage(boolean containsKey, URI uri) {
+ log.trace(() -> "URI cache size: " + cache.size());
+ if (containsKey) {
+ log.trace(() -> "Using cached uri for " + uri.toString());
+ } else {
+ log.trace(() -> "Cache empty for " + uri.toString());
+ }
+ }
+
+ private static final class StringConstructorArgs implements UriConstructorArgs {
+ private final String str;
+
+ private StringConstructorArgs(String str) {
+ this.str = str;
+ }
+
+ @Override
+ public URI newInstance() {
+ return URI.create(str);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ StringConstructorArgs that = (StringConstructorArgs) o;
+ return Objects.equals(str, that.str);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(str);
+ }
+ }
+
+ private static final class HostConstructorArgs implements UriConstructorArgs {
+ private final String scheme;
+ private final String userInfo;
+ private final String host;
+ private final int port;
+ private final String path;
+ private final String query;
+ private final String fragment;
+
+ private HostConstructorArgs(String scheme,
+ String userInfo, String host, int port,
+ String path, String query, String fragment) {
+ this.scheme = scheme;
+ this.userInfo = userInfo;
+ this.host = host;
+ this.port = port;
+ this.path = path;
+ this.query = query;
+ this.fragment = fragment;
+ }
+
+ @Override
+ public URI newInstance() {
+ try {
+ return new URI(scheme, userInfo, host, port, path, query, fragment);
+ } catch (URISyntaxException x) {
+ throw new IllegalArgumentException(x.getMessage(), x);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ HostConstructorArgs that = (HostConstructorArgs) o;
+ return port == that.port && Objects.equals(scheme, that.scheme) && Objects.equals(userInfo, that.userInfo)
+ && Objects.equals(host, that.host) && Objects.equals(path, that.path) && Objects.equals(query, that.query)
+ && Objects.equals(fragment, that.fragment);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hashCode(scheme);
+ result = 31 * result + Objects.hashCode(userInfo);
+ result = 31 * result + Objects.hashCode(host);
+ result = 31 * result + port;
+ result = 31 * result + Objects.hashCode(path);
+ result = 31 * result + Objects.hashCode(query);
+ result = 31 * result + Objects.hashCode(fragment);
+ return result;
+ }
+ }
+
+ private static final class AuthorityConstructorArgs implements UriConstructorArgs {
+ private final String scheme;
+ private final String authority;
+ private final String path;
+ private final String query;
+ private final String fragment;
+
+ private AuthorityConstructorArgs(String scheme, String authority, String path, String query, String fragment) {
+ this.scheme = scheme;
+ this.authority = authority;
+ this.path = path;
+ this.query = query;
+ this.fragment = fragment;
+ }
+
+ @Override
+ public URI newInstance() {
+ try {
+ return new URI(scheme, authority, path, query, fragment);
+ } catch (URISyntaxException x) {
+ throw new IllegalArgumentException(x.getMessage(), x);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ AuthorityConstructorArgs that = (AuthorityConstructorArgs) o;
+ return Objects.equals(scheme, that.scheme) && Objects.equals(authority, that.authority)
+ && Objects.equals(path, that.path) && Objects.equals(query, that.query)
+ && Objects.equals(fragment, that.fragment);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hashCode(scheme);
+ result = 31 * result + Objects.hashCode(authority);
+ result = 31 * result + Objects.hashCode(path);
+ result = 31 * result + Objects.hashCode(query);
+ result = 31 * result + Objects.hashCode(fragment);
+ return result;
+ }
+ }
+}
diff --git a/utils/src/main/java/software/amazon/awssdk/utils/uri/internal/UriConstructorArgs.java b/utils/src/main/java/software/amazon/awssdk/utils/uri/internal/UriConstructorArgs.java
new file mode 100644
index 000000000000..86251e30d42d
--- /dev/null
+++ b/utils/src/main/java/software/amazon/awssdk/utils/uri/internal/UriConstructorArgs.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.utils.uri.internal;
+
+import java.net.URI;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+
+/**
+ * Represent the different constructor to the URI class used by the SDK. Implementation of this interface are able to create new
+ * URIs based on the different arguments passed to classes to them.
+ *
+ * @see URI#create(String)
+ * @see URI#URI(String, String, String, String, String)
+ * @see URI#URI(String, String, String, int, String, String, String)
+ */
+@SdkInternalApi
+public interface UriConstructorArgs {
+
+ /**
+ * Creates a new instance of the URI. Can return a new instance everytime it is called.
+ *
+ * @return a new URI instance
+ */
+ URI newInstance();
+}
diff --git a/utils/src/test/java/software/amazon/awssdk/utils/SdkUriTest.java b/utils/src/test/java/software/amazon/awssdk/utils/SdkUriTest.java
new file mode 100644
index 000000000000..71bb6a0b7a4f
--- /dev/null
+++ b/utils/src/test/java/software/amazon/awssdk/utils/SdkUriTest.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.utils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.lang.reflect.Field;
+import java.net.URI;
+import java.net.URISyntaxException;
+import nl.jqno.equalsverifier.EqualsVerifier;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.junit.platform.commons.util.ReflectionUtils;
+import org.opentest4j.AssertionFailedError;
+import software.amazon.awssdk.utils.cache.lru.LruCache;
+import software.amazon.awssdk.utils.uri.SdkUri;
+import software.amazon.awssdk.utils.uri.internal.UriConstructorArgs;
+
+class SdkUriTest {
+
+ @AfterEach
+ void resetCache() throws IllegalAccessException {
+ Field cacheField = getCacheField();
+ cacheField.setAccessible(true);
+ cacheField.set(SdkUri.getInstance(), LruCache.builder(UriConstructorArgs::newInstance)
+ .maxSize(100)
+ .build());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"https://123456789012.ddb.us-east-1.amazonaws.com",
+ "http://123456789012.ddb.us-east-1.amazonaws.com"})
+ void multipleCreate_simpleURI_SameStringConstructor_ShouldCacheOnlyOnce(String strURI) {
+ URI uri = SdkUri.getInstance().create(strURI);
+ String scheme = strURI.startsWith("https") ? "https" : "http";
+ assertThat(uri).hasHost("123456789012.ddb.us-east-1.amazonaws.com")
+ .hasScheme(scheme)
+ .hasNoParameters()
+ .hasNoPort()
+ .hasNoQuery();
+ assertThat(getCache().size()).isEqualTo(1);
+ URI uri2 = SdkUri.getInstance().create(strURI);
+ assertThat(getCache().size()).isEqualTo(1);
+ assertThat(uri).isSameAs(uri2);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"http", "https"})
+ void multipleCreate_FullUri_SameConstructor_ShouldCacheOnlyOne(String scheme) {
+ String strURI = scheme + "://123456789012.ddb.us-east-1.amazonaws.com:322/some/path?foo=bar#test";
+ URI uri = SdkUri.getInstance().create(strURI);
+ assertThat(uri).hasHost("123456789012.ddb.us-east-1.amazonaws.com")
+ .hasScheme(scheme)
+ .hasNoUserInfo()
+ .hasPort(322)
+ .hasPath("/some/path")
+ .hasQuery("foo=bar")
+ .hasFragment("test");
+
+ assertThat(getCache().size()).isEqualTo(1);
+ URI uri2 = SdkUri.getInstance().create(strURI);
+ assertThat(getCache().size()).isEqualTo(1);
+ assertThat(uri).isSameAs(uri2);
+
+ }
+
+ @Test
+ void multipleCreate_withDifferentStringConstructor_shouldCacheOnlyOnce() {
+ String[] strURIs = {
+ "https://123456789012.ddb.us-east-1.amazonaws.com",
+ "https://123456789013.ddb.us-east-1.amazonaws.com",
+ "https://123456789014.ddb.us-east-1.amazonaws.com",
+ "https://123456789015.ddb.us-east-1.amazonaws.com",
+ "https://123456789016.ddb.us-east-1.amazonaws.com",
+ "https://123456789017.ddb.us-east-1.amazonaws.com",
+ "https://123456789018.ddb.us-east-1.amazonaws.com",
+ "https://123456789019.ddb.us-east-1.amazonaws.com",
+ };
+ for (String uri : strURIs) {
+ URI u = SdkUri.getInstance().create(uri);
+ }
+ assertThat(getCache().size()).isEqualTo(8);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"http", "https"})
+ void multipleNewUriWithNulls_SameAuthorityConstructor_ShouldCacheOnlyOnce(String scheme) throws URISyntaxException {
+ String strURI = "123456789012.ddb.us-east-1.amazonaws.com";
+ URI uri = SdkUri.getInstance().newUri(scheme, strURI, null, null, null);
+ assertThat(uri).hasHost("123456789012.ddb.us-east-1.amazonaws.com")
+ .hasScheme(scheme)
+ .hasNoParameters()
+ .hasNoPort()
+ .hasNoQuery();
+ assertThat(getCache().size()).isEqualTo(1);
+ URI uri2 = SdkUri.getInstance().newUri(scheme, strURI, null, null, null);
+ assertThat(getCache().size()).isEqualTo(1);
+ assertThat(uri).isSameAs(uri2);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"http", "https"})
+ void multipleNewUri_SameAuthorityConstructor_ShouldCacheOnlyOnce(String scheme) throws URISyntaxException {
+ String strURI = "123456789012.ddb.us-east-1.amazonaws.com";
+ URI uri = SdkUri.getInstance().newUri(scheme, strURI, "/somePath/to/resource", "foo=bar", "test");
+ assertThat(uri).hasHost(strURI)
+ .hasPath("/somePath/to/resource")
+ .hasQuery("foo=bar")
+ .hasFragment("test")
+ .hasScheme(scheme);
+ assertThat(getCache().size()).isEqualTo(1);
+ URI uri2 = SdkUri.getInstance().newUri(scheme, strURI, "/somePath/to/resource", "foo=bar", "test");
+ assertThat(getCache().size()).isEqualTo(1);
+ assertThat(uri).isSameAs(uri2);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"http", "https"})
+ void multipleNewUri_DifferentAuthorityConstructor_ShouldCacheAll(String scheme) throws URISyntaxException {
+ String strURI = "123456789012.ddb.us-east-1.amazonaws.com";
+ URI uri = SdkUri.getInstance().newUri(scheme, strURI, "/somePath/to/resource", "foo=bar", "test");
+ assertThat(uri).hasHost(strURI)
+ .hasPath("/somePath/to/resource")
+ .hasQuery("foo=bar")
+ .hasFragment("test")
+ .hasScheme(scheme);
+ assertThat(getCache().size()).isEqualTo(1);
+ URI uri2 = SdkUri.getInstance().newUri(scheme, strURI, "/some/otherPath/to/resource", null, "test2");
+ assertThat(getCache().size()).isEqualTo(2);
+ assertThat(uri).isNotSameAs(uri2);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"http", "https"})
+ void multipleNewUriWithNulls_SameHostConstructor_ShouldCacheOnlyOnce(String scheme) throws URISyntaxException {
+ String strURI = "123456789012.ddb.us-east-1.amazonaws.com";
+ URI uri = SdkUri.getInstance().newUri(scheme, null, strURI, 322, null, null, null);
+ assertThat(uri).hasHost("123456789012.ddb.us-east-1.amazonaws.com")
+ .hasNoParameters()
+ .hasPort(322)
+ .hasNoQuery();
+ assertThat(getCache().size()).isEqualTo(1);
+ URI uri2 = SdkUri.getInstance().newUri(scheme, null, strURI, 322, null, null, null);
+ assertThat(getCache().size()).isEqualTo(1);
+ assertThat(uri).isSameAs(uri2);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"http", "https"})
+ void multipleNewUri_SameHostConstructor_ShouldCacheOnlyOnce(String scheme) throws URISyntaxException {
+ String strURI = "123456789012.ddb.us-east-1.amazonaws.com";
+ URI uri = SdkUri.getInstance().newUri(scheme, "user1", strURI, 322, "/some/path", "foo=bar", "test");
+ assertThat(uri).hasHost("123456789012.ddb.us-east-1.amazonaws.com")
+ .hasScheme(scheme)
+ .hasUserInfo("user1")
+ .hasPort(322)
+ .hasPath("/some/path")
+ .hasQuery("foo=bar")
+ .hasFragment("test");
+ assertThat(getCache().size()).isEqualTo(1);
+ URI uri2 = SdkUri.getInstance().newUri(scheme, "user1", strURI, 322, "/some/path", "foo=bar", "test");
+ assertThat(getCache().size()).isEqualTo(1);
+ assertThat(uri).isSameAs(uri2);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"http", "https"})
+ void multipleNewUri_DifferentHostConstructor_ShouldCacheOnlyOnce(String scheme) throws URISyntaxException {
+ String strURI = "123456789012.ddb.us-east-1.amazonaws.com";
+ URI uri = SdkUri.getInstance().newUri(scheme, "user1", strURI, 322, "/some/path", "foo=bar", "test");
+ assertThat(uri).hasHost("123456789012.ddb.us-east-1.amazonaws.com")
+ .hasScheme(scheme)
+ .hasUserInfo("user1")
+ .hasPort(322)
+ .hasPath("/some/path")
+ .hasQuery("foo=bar")
+ .hasFragment("test");
+ assertThat(getCache().size()).isEqualTo(1);
+ URI uri2 = SdkUri.getInstance().newUri(scheme, "user1", strURI, 322, "/some/other/path", "foo=bar", "test2");
+ assertThat(getCache().size()).isEqualTo(2);
+ assertThat(uri).isNotSameAs(uri2);
+ }
+
+ @Test
+ void notCached_shouldCreateNewInstance() {
+ String strURI = "https://ddb.us-east-1.amazonaws.com";
+ URI uri = SdkUri.getInstance().create(strURI);
+ assertThat(uri).hasHost("ddb.us-east-1.amazonaws.com")
+ .hasNoParameters()
+ .hasNoPort()
+ .hasNoQuery();
+ assertThat(getCache().size()).isEqualTo(0);
+ URI uri2 = SdkUri.getInstance().create(strURI);
+ assertThat(getCache().size()).isEqualTo(0);
+ assertThat(uri).isNotSameAs(uri2);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"potatoes tomatoes", "123412341234 potatoes tomatoes"})
+ void malformedURI_shouldThrowsSameExceptionAsUriClass(String malformedUri) {
+
+ assertThatThrownBy(() -> SdkUri.getInstance().create(malformedUri))
+ .as("Malformed uri should throw IllegalArgumentException using the create method")
+ .isInstanceOf(IllegalArgumentException.class);
+ assertThat(getCache().size()).as("Cache should be empty if create URI fails")
+ .isEqualTo(0);
+
+ assertThatThrownBy(() -> SdkUri.getInstance().newUri(malformedUri))
+ .as("Malformed uri should throw URISyntaxException using the newURI method")
+ .isInstanceOf(URISyntaxException.class);
+ assertThat(getCache().size()).as("Cache should be empty if create URI fails")
+ .isEqualTo(0);
+
+ assertThatThrownBy(() -> SdkUri.getInstance().newUri("scheme", malformedUri, "path", "query", "fragment"))
+ .as("Malformed uri should throw URISyntaxException using the newURI with authority method")
+ .isInstanceOf(URISyntaxException.class);
+ assertThat(getCache().size()).as("Cache should be empty if create URI fails")
+ .isEqualTo(0);
+
+ assertThatThrownBy(() -> new URI("scheme", malformedUri, "path", "query", "fragment"))
+ .as("CONSTRUCTOR")
+ .isInstanceOf(URISyntaxException.class);
+ assertThat(getCache().size()).as("Cache should be empty if create URI fails")
+ .isEqualTo(0);
+
+
+ assertThatThrownBy(() -> SdkUri.getInstance().newUri("scheme", "userInfo", malformedUri,
+ 444, "path", "query", "fragment"))
+ .as("Malformed uri should throw URISyntaxException using the newURI with host method")
+ .isInstanceOf(URISyntaxException.class);
+ assertThat(getCache().size()).as("Cache should be empty if create URI fails")
+ .isEqualTo(0);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "http://123456789.ddb.com",
+ "https://123456789.ddb.com",
+ "123456789.ddb.com",
+ "http://123.ddb.com",
+ "https://123.ddb.com",
+ "123.ddb.com",
+ "http://123z.ddb.com",
+ "https://123z.ddb.com",
+ "123z.ddb.com",
+ "http://1",
+ "https://1",
+ "1",
+ "http://z",
+ "https://z",
+ "z"
+ })
+ void shouldNotCache_whenLeadingDigitsDoNotExceedIntegerMaxValue(String strURI) {
+ URI uri = SdkUri.getInstance().create(strURI);
+ assertThat(getCache().size()).isEqualTo(0);
+ URI uri2 = SdkUri.getInstance().create(strURI);
+ assertThat(getCache().size()).isEqualTo(0);
+ assertThat(uri).isNotSameAs(uri2);
+ }
+
+
+ private LruCache getCache() {
+ Field field = getCacheField();
+ field.setAccessible(true);
+ try {
+ return (LruCache) field.get(SdkUri.getInstance());
+ } catch (IllegalAccessException e) {
+ fail(e);
+ return null;
+ }
+ }
+
+ private Field getCacheField() {
+ return ReflectionUtils.streamFields(SdkUri.class,
+ f -> "cache".equals(f.getName()),
+ ReflectionUtils.HierarchyTraversalMode.TOP_DOWN)
+ .findFirst()
+ .orElseThrow(() -> new AssertionFailedError("Unexpected error - Could not find field "
+ + "'cache' in " + SdkUri.class.getName()));
+ }
+
+ @Test
+ void equals_hashCode() {
+ EqualsVerifier.forPackage("software.amazon.awssdk.utils.uri")
+ .except(SdkUri.class)
+ .verify();
+ }
+}