diff --git a/utils/src/main/java/software/amazon/awssdk/utils/cache/bounded/BoundedCache.java b/utils/src/main/java/software/amazon/awssdk/utils/cache/bounded/BoundedCache.java
new file mode 100644
index 000000000000..88c9a7da4af2
--- /dev/null
+++ b/utils/src/main/java/software/amazon/awssdk/utils/cache/bounded/BoundedCache.java
@@ -0,0 +1,146 @@
+/*
+ * 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.cache.bounded;
+
+import java.util.Iterator;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
+import software.amazon.awssdk.annotations.SdkProtectedApi;
+import software.amazon.awssdk.annotations.ThreadSafe;
+import software.amazon.awssdk.utils.Validate;
+
+/**
+ * A thread-safe cache implementation that returns the value for a specified key, retrieving it by either getting the stored
+ * value from the cache or using a supplied function to calculate that value and add it to the cache.
+ *
+ * When the cache is full, batch eviction of unspecified values will be performed, with a default evictionBatchSize of 10.
+ *
+ * The user can configure the maximum size of the cache, which is set to a default of 150.
+ *
+ * Keys must not be null, otherwise an error will be thrown. Null values are not cached.
+ */
+@SdkProtectedApi
+@ThreadSafe
+public final class BoundedCache {
+ private static final int DEFAULT_CACHE_SIZE = 150;
+ private static final int DEFAULT_EVICTION_BATCH_SIZE = 10;
+
+ private final ConcurrentHashMap cache;
+ private final Function valueMappingFunction;
+ private final int maxCacheSize;
+ private final int evictionBatchSize;
+ private final Object cacheLock;
+ private final AtomicInteger cacheSize;
+
+ private BoundedCache(Builder b) {
+ this.valueMappingFunction = b.mappingFunction;
+ this.maxCacheSize = b.maxSize != null ? Validate.isPositive(b.maxSize, "maxSize") : DEFAULT_CACHE_SIZE;
+ this.evictionBatchSize = b.evictionBatchSize != null ?
+ Validate.isPositive(b.evictionBatchSize, "evictionBatchSize") :
+ DEFAULT_EVICTION_BATCH_SIZE;
+ this.cache = new ConcurrentHashMap<>();
+ this.cacheLock = new Object();
+ this.cacheSize = new AtomicInteger();
+ }
+
+ /**
+ * Get a value based on the key. The key must not be null, otherwise an error is thrown.
+ * If the value exists in the cache, it's returned.
+ * Otherwise, the value is calculated based on the supplied function {@link Builder#builder(Function)}.
+ */
+ public V get(K key) {
+ Validate.paramNotNull(key, "key");
+ V value = cache.get(key);
+ if (value != null) {
+ return value;
+ }
+
+ V newValue = valueMappingFunction.apply(key);
+
+ // If the value is null, just return it without caching
+ if (newValue == null) {
+ return null;
+ }
+
+ synchronized (cacheLock) {
+ // Check again inside the synchronized block in case another thread added the value
+ value = cache.get(key);
+ if (value != null) {
+ return value;
+ }
+
+ if (cacheSize.get() >= maxCacheSize) {
+ cleanup();
+ }
+
+ cache.put(key, newValue);
+ cacheSize.incrementAndGet();
+ return newValue;
+ }
+ }
+
+ /**
+ * Clean up the cache by batch removing unspecified entries of evictionBatchSize
+ */
+ private void cleanup() {
+ Iterator iterator = cache.keySet().iterator();
+ int count = 0;
+ while (iterator.hasNext() && count < evictionBatchSize) {
+ iterator.next();
+ iterator.remove();
+ count++;
+ cacheSize.decrementAndGet();
+ }
+ }
+
+ public int size() {
+ return cacheSize.get();
+ }
+
+ public boolean containsKey(K key) {
+ return cache.containsKey(key);
+ }
+
+ public static BoundedCache.Builder builder(Function mappingFunction) {
+ return new Builder<>(mappingFunction);
+ }
+
+ public static final class Builder {
+
+ private final Function mappingFunction;
+ private Integer maxSize;
+ private Integer evictionBatchSize;
+
+ private Builder(Function mappingFunction) {
+ this.mappingFunction = mappingFunction;
+ }
+
+ public Builder maxSize(Integer maxSize) {
+ this.maxSize = maxSize;
+ return this;
+ }
+
+ public Builder evictionBatchSize(Integer evictionBatchSize) {
+ this.evictionBatchSize = evictionBatchSize;
+ return this;
+ }
+
+ public BoundedCache build() {
+ return new BoundedCache<>(this);
+ }
+ }
+}
\ No newline at end of file
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..3402124ca798
--- /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.bounded.BoundedCache;
+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;
+
+ /*
+ * Same value as default BoundedCache size. This contrasts to the default LruCache size of 100, since 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 BoundedCache cache;
+
+ private SdkUri() {
+ this.cache = BoundedCache.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;
+ }
+ }
+}
\ No newline at end of file
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..700d34649e88
--- /dev/null
+++ b/utils/src/test/java/software/amazon/awssdk/utils/SdkUriTest.java
@@ -0,0 +1,306 @@
+/*
+ * 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.bounded.BoundedCache;
+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(), BoundedCache.builder(UriConstructorArgs::newInstance)
+ .maxSize(100)
+ .evictionBatchSize(5)
+ .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 BoundedCache getCache() {
+ Field field = getCacheField();
+ field.setAccessible(true);
+ try {
+ return (BoundedCache) 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();
+ }
+}
\ No newline at end of file