From 5018dcb6a1a2f987b09678b8be73dc49fdcddd00 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Thu, 16 Jul 2026 12:21:17 -0700 Subject: [PATCH 1/5] [EndpointUri PR 1/4] Add EndpointUrl class (#7155) --- .../amazon/awssdk/endpoints/Endpoint.java | 61 +++- .../amazon/awssdk/endpoints/EndpointUrl.java | 343 ++++++++++++++++++ .../amazon/awssdk/endpoints/EndpointTest.java | 45 +++ .../awssdk/endpoints/EndpointUrlTest.java | 332 +++++++++++++++++ 4 files changed, 773 insertions(+), 8 deletions(-) create mode 100644 core/endpoints-spi/src/main/java/software/amazon/awssdk/endpoints/EndpointUrl.java create mode 100644 core/endpoints-spi/src/test/java/software/amazon/awssdk/endpoints/EndpointUrlTest.java diff --git a/core/endpoints-spi/src/main/java/software/amazon/awssdk/endpoints/Endpoint.java b/core/endpoints-spi/src/main/java/software/amazon/awssdk/endpoints/Endpoint.java index 7db593ba3376..0db92b5a7a2d 100644 --- a/core/endpoints-spi/src/main/java/software/amazon/awssdk/endpoints/Endpoint.java +++ b/core/endpoints-spi/src/main/java/software/amazon/awssdk/endpoints/Endpoint.java @@ -28,18 +28,34 @@ */ @SdkPublicApi public final class Endpoint { - private final URI url; + private final EndpointUrl endpointUrl; private final Map> headers; private final Map, Object> attributes; private Endpoint(BuilderImpl b) { - this.url = b.url; + this.endpointUrl = b.endpointUrl; this.headers = b.headers; this.attributes = b.attributes; } + /** + * Returns the URI. + * Delegates to {@link EndpointUrl#toUri()} which lazily constructs the URI. + * + * @deprecated Use {@link #endpointUrl()} instead, which provides direct access to URL components + * without the overhead of constructing a {@link URI}. + */ + @Deprecated public URI url() { - return url; + return endpointUrl.toUri(); + } + + /** + * Returns the {@link EndpointUrl} for efficient access to URL components + * without URI construction overhead. + */ + public EndpointUrl endpointUrl() { + return endpointUrl; } public Map> headers() { @@ -66,7 +82,11 @@ public boolean equals(Object o) { Endpoint endpoint = (Endpoint) o; - if (url != null ? !url.equals(endpoint.url) : endpoint.url != null) { + // ensures Endpoints built via url(URI) and + // endpointUrl(EndpointUrl) are equal when the URLs are equivalent (e.g., IPv6 bracket differences). + URI thisUrl = endpointUrl != null ? endpointUrl.toUri() : null; + URI thatUrl = endpoint.endpointUrl != null ? endpoint.endpointUrl.toUri() : null; + if (thisUrl != null ? !thisUrl.equals(thatUrl) : thatUrl != null) { return false; } if (headers != null ? !headers.equals(endpoint.headers) : endpoint.headers != null) { @@ -77,7 +97,9 @@ public boolean equals(Object o) { @Override public int hashCode() { - int result = url != null ? url.hashCode() : 0; + // Use toUri() for consistency with equals() + URI uri = endpointUrl != null ? endpointUrl.toUri() : null; + int result = uri != null ? uri.hashCode() : 0; result = 31 * result + (headers != null ? headers.hashCode() : 0); result = 31 * result + (attributes != null ? attributes.hashCode() : 0); return result; @@ -88,8 +110,24 @@ public static Builder builder() { } public interface Builder { + /** + * Sets the endpoint URL from a {@link URI}. + * Internally converts to an {@link EndpointUrl} via {@link EndpointUrl#fromUri(URI)}. + * + * @deprecated Use {@link #endpointUrl(EndpointUrl)} instead. + */ + @Deprecated Builder url(URI url); + /** + * Sets the endpoint URL from an {@link EndpointUrl} directly. + * This is the preferred path for code that already has an {@code EndpointUrl} + * (e.g., generated endpoint providers). + */ + default Builder endpointUrl(EndpointUrl endpointUrl) { + throw new UnsupportedOperationException(); + } + Builder putHeader(String name, String value); Builder putAttribute(EndpointAttributeKey key, T value); @@ -98,7 +136,7 @@ public interface Builder { } private static class BuilderImpl implements Builder { - private URI url; + private EndpointUrl endpointUrl; private final Map> headers = new HashMap<>(); private final Map, Object> attributes = new HashMap<>(); @@ -106,7 +144,7 @@ private BuilderImpl() { } private BuilderImpl(Endpoint e) { - this.url = e.url; + this.endpointUrl = e.endpointUrl; if (e.headers != null) { e.headers.forEach((n, v) -> { this.headers.put(n, new ArrayList<>(v)); @@ -115,9 +153,16 @@ private BuilderImpl(Endpoint e) { this.attributes.putAll(e.attributes); } + @SuppressWarnings("deprecation") @Override public Builder url(URI url) { - this.url = url; + this.endpointUrl = EndpointUrl.fromUri(url); + return this; + } + + @Override + public Builder endpointUrl(EndpointUrl endpointUrl) { + this.endpointUrl = endpointUrl; return this; } diff --git a/core/endpoints-spi/src/main/java/software/amazon/awssdk/endpoints/EndpointUrl.java b/core/endpoints-spi/src/main/java/software/amazon/awssdk/endpoints/EndpointUrl.java new file mode 100644 index 000000000000..b6b9594beb45 --- /dev/null +++ b/core/endpoints-spi/src/main/java/software/amazon/awssdk/endpoints/EndpointUrl.java @@ -0,0 +1,343 @@ +/* + * 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.endpoints; + +import java.net.URI; +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * A lightweight, immutable representation of a resolved endpoint URL that stores pre-parsed components + * (scheme, host, port, path) as strings, avoiding the cost of {@link URI} construction. + * + *

Three factory methods are provided: + *

    + *
  • {@link #fromString(String)} — parses a URL string using simple string operations (no URI construction)
  • + *
  • {@link #fromComponents(String, String, int, String)} — creates from individual components (no query/fragment)
  • + *
  • {@link #fromUri(URI)} — creates from an existing URI (pre-populates the cached URI field, preserves + * query and fragment)
  • + *
+ */ +@SdkPublicApi +public final class EndpointUrl { + + private final String scheme; + private final String host; + private final int port; + private final String encodedPath; + private final String queryAndFragment; + private final String rawUrl; + + // Inline lazy URI — avoids dependency on utils module's Lazy. + // Uses double-checked locking. + private volatile URI uri; + + private EndpointUrl(String scheme, String host, int port, String encodedPath, + String queryAndFragment, String rawUrl, URI uri) { + this.scheme = scheme; + this.host = host; + this.port = port; + this.encodedPath = encodedPath; + this.queryAndFragment = queryAndFragment; + this.rawUrl = rawUrl; + this.uri = uri; + } + + /** + * Parse a URL string into its components without constructing a {@link URI}. + * + *

Performs minimal string splitting only — no validation beyond checking for the {@code ://} separator. + * The original URL string is retained for faithful URI reconstruction via {@link #toUri()}. Path, query and fragment + * components (if present) MUST already be url encoded. + * + *

Expected format: {@code scheme://host[:port][/encodedPath][?query][#fragment]} + * + * @param url the URL string to parse + * @return a new {@code EndpointUrl} with pre-parsed components + * @throws IllegalArgumentException if the URL does not contain {@code ://} + */ + public static EndpointUrl fromString(String url) { + int schemeEnd = url.indexOf("://"); + if (schemeEnd < 0) { + throw new IllegalArgumentException("Invalid URL: missing '://' separator: " + url); + } + + String scheme = url.substring(0, schemeEnd); + int authorityStart = schemeEnd + 3; + + // Find where authority ends: first '/', '?', or '#' after authority start, or end of string. + // RFC 3986: authority is terminated by '/', '?', '#', or end of URI. + int pathStart = -1; + int len = url.length(); + for (int i = authorityStart; i < len; i++) { + char c = url.charAt(i); + if (c == '/' || c == '?' || c == '#') { + pathStart = i; + break; + } + } + + String authority; + String pathAndRest; + if (pathStart < 0) { + authority = url.substring(authorityStart); + pathAndRest = ""; + } else { + authority = url.substring(authorityStart, pathStart); + pathAndRest = url.substring(pathStart); + } + + // Separate path from query/fragment + String encodedPath; + String queryAndFragment; + int queryStart = pathAndRest.indexOf('?'); + int fragmentStart = pathAndRest.indexOf('#'); + int separatorPos = -1; + if (queryStart >= 0 && fragmentStart >= 0) { + separatorPos = Math.min(queryStart, fragmentStart); + } else if (queryStart >= 0) { + separatorPos = queryStart; + } else if (fragmentStart >= 0) { + separatorPos = fragmentStart; + } + + if (separatorPos >= 0) { + encodedPath = pathAndRest.substring(0, separatorPos); + queryAndFragment = pathAndRest.substring(separatorPos); + } else { + encodedPath = pathAndRest; + queryAndFragment = ""; + } + + // Split authority into host and optional port, handling IPv6 addresses + String host; + int port; + if (!authority.isEmpty() && authority.charAt(0) == '[') { + // IPv6: [::1]:8080 + int bracketEnd = authority.indexOf(']'); + host = authority.substring(0, bracketEnd + 1); + if (bracketEnd + 1 < authority.length() && authority.charAt(bracketEnd + 1) == ':') { + port = Integer.parseInt(authority.substring(bracketEnd + 2)); + } else { + port = -1; + } + } else { + int colonPos = authority.lastIndexOf(':'); + if (colonPos < 0) { + host = authority; + port = -1; + } else { + host = authority.substring(0, colonPos); + port = Integer.parseInt(authority.substring(colonPos + 1)); + } + } + + return new EndpointUrl(scheme, host, port, encodedPath, queryAndFragment, url, null); + } + + /** + * Create an {@code EndpointUrl} from individual components. + * + *

Used internally (e.g., by {@code addHostPrefix} and codegen) to avoid re-parsing. + * The {@code rawUrl} field is {@code null} in this case, so {@link #toUri()} reconstructs + * the URI from components. No query or fragment is included. + * + * @param scheme the URL scheme (e.g., "https") + * @param host the hostname (e.g., "s3.us-east-1.amazonaws.com") + * @param port the port number, or -1 if not specified + * @param encodedPath the encoded path (e.g., "/bucket/key"), or empty string if no path + * @return a new {@code EndpointUrl} + */ + public static EndpointUrl fromComponents(String scheme, String host, int port, String encodedPath) { + return new EndpointUrl(scheme, host, port, encodedPath, "", null, null); + } + + /** + * Create an {@code EndpointUrl} from individual components, including query and fragment. + * + *

This overload is used when reconstructing an EndpointUrl with modifications (e.g., host prefix) + * while preserving the original query and fragment components. + * + * @param scheme the URL scheme (e.g., "https") + * @param host the hostname (e.g., "s3.us-east-1.amazonaws.com") + * @param port the port number, or -1 if not specified + * @param encodedPath the encoded path (e.g., "/bucket/key"), or empty string if no path + * @param queryAndFragment the query and fragment string (e.g., "?key=value#section"), or empty string if none + * @return a new {@code EndpointUrl} + */ + public static EndpointUrl fromComponents(String scheme, String host, int port, String encodedPath, + String queryAndFragment) { + return new EndpointUrl(scheme, host, port, encodedPath, queryAndFragment, null, null); + } + + /** + * Create an {@code EndpointUrl} from an existing {@link URI}. + * + *

The URI field is pre-populated, so {@link #toUri()} + * returns the original URI instance without any additional construction. + * + * @param uri the URI to create from + * @return a new {@code EndpointUrl} with components extracted from the URI + */ + public static EndpointUrl fromUri(URI uri) { + String rawPath = uri.getRawPath(); + String queryAndFragment = buildQueryAndFragment(uri); + return new EndpointUrl( + uri.getScheme(), + uri.getHost(), + uri.getPort(), + rawPath != null ? rawPath : "", + queryAndFragment, + null, + uri + ); + } + + /** + * Returns the URL scheme (e.g., "https"). + */ + public String scheme() { + return scheme; + } + + /** + * Returns the hostname (e.g., "s3.us-east-1.amazonaws.com"). + */ + public String host() { + return host; + } + + /** + * Returns the port number, or -1 if not explicitly specified. + */ + public int port() { + return port; + } + + /** + * Returns the encoded path (e.g., "/bucket/key"), or an empty string if no path is present. + */ + public String encodedPath() { + return encodedPath; + } + + /** + * Returns the query and fragment portion of the URL (e.g., {@code "?key=value#section"}), + * or an empty string if neither is present. + */ + public String queryAndFragment() { + return queryAndFragment; + } + + /** + * Returns the {@link URI} representation. Lazily constructed on first call using double-checked locking. + * + *

When created via {@link #fromString(String)}, uses the original URL string for faithful reconstruction. + * When created via {@link #fromComponents(String, String, int, String)} reconstructs from components (no query/fragment). + * When created via {@link #fromUri(URI)}, returns the original URI instance. + * + * @return the URI representation of this endpoint URL + */ + public URI toUri() { + URI result = uri; + if (result == null) { + synchronized (this) { + result = uri; + if (result == null) { + if (rawUrl != null) { + result = URI.create(rawUrl); + } else { + StringBuilder sb = new StringBuilder(); + sb.append(scheme).append("://").append(host); + if (port >= 0) { + sb.append(':').append(port); + } + sb.append(encodedPath); + sb.append(queryAndFragment); + result = URI.create(sb.toString()); + } + uri = result; + } + } + } + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + EndpointUrl that = (EndpointUrl) o; + + if (port != that.port) { + return false; + } + if (scheme != null ? !scheme.equals(that.scheme) : that.scheme != null) { + return false; + } + if (host != null ? !host.equals(that.host) : that.host != null) { + return false; + } + if (encodedPath != null ? !encodedPath.equals(that.encodedPath) : that.encodedPath != null) { + return false; + } + return queryAndFragment.equals(that.queryAndFragment); + } + + @Override + public int hashCode() { + int result = scheme != null ? scheme.hashCode() : 0; + result = 31 * result + (host != null ? host.hashCode() : 0); + result = 31 * result + port; + result = 31 * result + (encodedPath != null ? encodedPath.hashCode() : 0); + result = 31 * result + queryAndFragment.hashCode(); + return result; + } + + @Override + public String toString() { + return "EndpointUrl(" + + "scheme=" + scheme + + ", host=" + host + + ", port=" + port + + ", encodedPath=" + encodedPath + + ", queryAndFragment=" + queryAndFragment + + ")"; + } + + /** + * Build the query and fragment string from a URI, or return empty string if neither is present. + */ + private static String buildQueryAndFragment(URI uri) { + String rawQuery = uri.getRawQuery(); + String rawFragment = uri.getRawFragment(); + if (rawQuery == null && rawFragment == null) { + return ""; + } + StringBuilder sb = new StringBuilder(); + if (rawQuery != null) { + sb.append('?').append(rawQuery); + } + if (rawFragment != null) { + sb.append('#').append(rawFragment); + } + return sb.toString(); + } +} diff --git a/core/endpoints-spi/src/test/java/software/amazon/awssdk/endpoints/EndpointTest.java b/core/endpoints-spi/src/test/java/software/amazon/awssdk/endpoints/EndpointTest.java index e6e3cb2a8983..25eb1729e846 100644 --- a/core/endpoints-spi/src/test/java/software/amazon/awssdk/endpoints/EndpointTest.java +++ b/core/endpoints-spi/src/test/java/software/amazon/awssdk/endpoints/EndpointTest.java @@ -88,4 +88,49 @@ public void toBuilder_attrsModified_notReflectedInOriginal() { assertThat(original.attribute(TEST_STRING_ATTR)).isEqualTo("foo"); } + + @Test + public void equality_acrossConstructionPaths() { + URI uri = URI.create("https://[::1]:8080/path?key=value#frag"); + + Endpoint viaUri = Endpoint.builder().url(uri).build(); + Endpoint viaFromString = Endpoint.builder() + .endpointUrl(EndpointUrl.fromString("https://[::1]:8080/path?key=value#frag")) + .build(); + Endpoint viaFromComponents = Endpoint.builder() + .endpointUrl(EndpointUrl.fromComponents("https", "[::1]", 8080, "/path", + "?key=value#frag")) + .build(); + + assertThat(viaUri).isEqualTo(viaFromString); + assertThat(viaUri).isEqualTo(viaFromComponents); + assertThat(viaUri.hashCode()).isEqualTo(viaFromString.hashCode()); + assertThat(viaUri.hashCode()).isEqualTo(viaFromComponents.hashCode()); + } + + @Test + public void inequality_differentUrlComponents() { + Endpoint endpoint1 = Endpoint.builder() + .endpointUrl(EndpointUrl.fromString("https://example.com/path?key=value")) + .build(); + Endpoint endpoint2 = Endpoint.builder() + .endpointUrl(EndpointUrl.fromString("https://example.com/path")) + .build(); + + assertThat(endpoint1).isNotEqualTo(endpoint2); + } + + @Test + public void endpointUrlAccessor_returnsCorrectComponents() { + Endpoint endpoint = Endpoint.builder() + .url(URI.create("https://s3.us-east-1.amazonaws.com:443/bucket")) + .build(); + + EndpointUrl endpointUrl = endpoint.endpointUrl(); + assertThat(endpointUrl.scheme()).isEqualTo("https"); + assertThat(endpointUrl.host()).isEqualTo("s3.us-east-1.amazonaws.com"); + assertThat(endpointUrl.port()).isEqualTo(443); + assertThat(endpointUrl.encodedPath()).isEqualTo("/bucket"); + assertThat(endpointUrl.queryAndFragment()).isEmpty(); + } } diff --git a/core/endpoints-spi/src/test/java/software/amazon/awssdk/endpoints/EndpointUrlTest.java b/core/endpoints-spi/src/test/java/software/amazon/awssdk/endpoints/EndpointUrlTest.java new file mode 100644 index 000000000000..2d6d1cf60614 --- /dev/null +++ b/core/endpoints-spi/src/test/java/software/amazon/awssdk/endpoints/EndpointUrlTest.java @@ -0,0 +1,332 @@ +/* + * 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.endpoints; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Tests for {@link EndpointUrl}. + */ +public class EndpointUrlTest { + + /** + * Representative AWS URLs used across parameterized tests. + */ + static List urls() { + return Arrays.asList( + "https://s3.us-east-1.amazonaws.com", + "https://s3.us-east-1.amazonaws.com/bucket/key", + "https://localhost:8080/path", + "https://s3.amazonaws.com/", + "https://[::1]:8080/path", + "https://dynamodb.us-west-2.amazonaws.com" + ); + } + + @ParameterizedTest + @MethodSource("urls") + void componentFidelity_scheme(String url) { + EndpointUrl endpointUrl = EndpointUrl.fromString(url); + URI uri = URI.create(url); + assertThat(endpointUrl.scheme()).isEqualTo(uri.getScheme()); + } + + /** + * Non-IPv6 URLs for host fidelity comparison against {@code URI.getHost()}. + * + *

IPv6 is excluded because {@code EndpointUrl.fromString()} always stores the host WITH brackets + * (e.g., "[::1]"), while {@code URI.getHost()} behavior for IPv6 varies across JDK versions + * (Java 8 strips brackets, Java 17+ keeps them). IPv6 host parsing is verified directly + * in the dedicated edge-case tests below.

+ */ + static List nonIpv6Urls() { + return Arrays.asList( + "https://s3.us-east-1.amazonaws.com", + "https://s3.us-east-1.amazonaws.com/bucket/key", + "https://localhost:8080/path", + "https://s3.amazonaws.com/", + "https://dynamodb.us-west-2.amazonaws.com" + ); + } + + @ParameterizedTest + @MethodSource("nonIpv6Urls") + void componentFidelity_host(String url) { + EndpointUrl endpointUrl = EndpointUrl.fromString(url); + URI uri = URI.create(url); + assertThat(endpointUrl.host()).isEqualTo(uri.getHost()); + } + + @ParameterizedTest + @MethodSource("urls") + void componentFidelity_port(String url) { + EndpointUrl endpointUrl = EndpointUrl.fromString(url); + URI uri = URI.create(url); + assertThat(endpointUrl.port()).isEqualTo(uri.getPort()); + } + + @ParameterizedTest + @MethodSource("urls") + void componentFidelity_encodedPath(String url) { + EndpointUrl endpointUrl = EndpointUrl.fromString(url); + URI uri = URI.create(url); + String expectedPath = uri.getRawPath() != null ? uri.getRawPath() : ""; + assertThat(endpointUrl.encodedPath()).isEqualTo(expectedPath); + } + + @ParameterizedTest + @MethodSource("urls") + void fromStringToUriRoundTrip(String url) { + assertThat(EndpointUrl.fromString(url).toUri()).isEqualTo(URI.create(url)); + } + + @ParameterizedTest + @MethodSource("urls") + void toUriCaching_fromString(String url) { + EndpointUrl endpointUrl = EndpointUrl.fromString(url); + URI first = endpointUrl.toUri(); + URI second = endpointUrl.toUri(); + assertThat(first).isSameAs(second); + } + + @Test + void toUriCaching_fromComponents() { + EndpointUrl endpointUrl = EndpointUrl.fromComponents("https", "s3.us-east-1.amazonaws.com", -1, ""); + URI first = endpointUrl.toUri(); + URI second = endpointUrl.toUri(); + assertThat(first).isSameAs(second); + } + + @Test + void toUriCaching_fromUri() { + URI original = URI.create("https://s3.us-east-1.amazonaws.com"); + EndpointUrl endpointUrl = EndpointUrl.fromUri(original); + URI first = endpointUrl.toUri(); + URI second = endpointUrl.toUri(); + assertThat(first).isSameAs(second); + } + + @ParameterizedTest + @MethodSource("urls") + void fromUriRoundTripIdentity(String url) { + URI original = URI.create(url); + assertThat(EndpointUrl.fromUri(original).toUri()).isSameAs(original); + } + + @Test + void malformedUrl_noSchemeSeparator() { + assertThatThrownBy(() -> EndpointUrl.fromString("not-a-url")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void malformedUrl_emptyString() { + assertThatThrownBy(() -> EndpointUrl.fromString("")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void malformedUrl_singleColon() { + assertThatThrownBy(() -> EndpointUrl.fromString("https:host")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void fromString_noPath_encodedPathIsEmpty() { + EndpointUrl endpointUrl = EndpointUrl.fromString("https://dynamodb.us-west-2.amazonaws.com"); + assertThat(endpointUrl.encodedPath()).isEmpty(); + } + + @Test + void fromString_trailingSlash_encodedPathIsSlash() { + EndpointUrl endpointUrl = EndpointUrl.fromString("https://s3.amazonaws.com/"); + assertThat(endpointUrl.encodedPath()).isEqualTo("/"); + } + + @Test + void fromString_ipv6WithPort() { + EndpointUrl endpointUrl = EndpointUrl.fromString("https://[::1]:8080/path"); + assertThat(endpointUrl.host()).isEqualTo("[::1]"); + assertThat(endpointUrl.port()).isEqualTo(8080); + assertThat(endpointUrl.encodedPath()).isEqualTo("/path"); + } + + @Test + void fromString_ipv6WithoutPort() { + EndpointUrl endpointUrl = EndpointUrl.fromString("https://[::1]/path"); + assertThat(endpointUrl.host()).isEqualTo("[::1]"); + assertThat(endpointUrl.port()).isEqualTo(-1); + assertThat(endpointUrl.encodedPath()).isEqualTo("/path"); + } + + @Test + void fromString_explicitPort() { + EndpointUrl endpointUrl = EndpointUrl.fromString("https://localhost:8080/path"); + assertThat(endpointUrl.scheme()).isEqualTo("https"); + assertThat(endpointUrl.host()).isEqualTo("localhost"); + assertThat(endpointUrl.port()).isEqualTo(8080); + assertThat(endpointUrl.encodedPath()).isEqualTo("/path"); + } + + @Test + void fromString_noPort() { + EndpointUrl endpointUrl = EndpointUrl.fromString("https://s3.us-east-1.amazonaws.com"); + assertThat(endpointUrl.port()).isEqualTo(-1); + } + + static List preParsedEquivalenceUrls() { + return Arrays.asList( + // Simple URL (no port, no path) + "https://s3.us-east-1.amazonaws.com", + // URL with port only + "https://runtime.sagemaker.us-east-1.amazonaws.com:8443", + // URL with path only + "https://places.geo.us-east-1.amazonaws.com/v2", + // URL with both port and path + "https://localhost:8443/v2/api", + // URL with trailing slash + "https://s3.amazonaws.com/", + // HTTP scheme with port and path + "http://localhost:8080/path", + // URL with deeper path + "https://s3.us-west-2.amazonaws.com/prefix/key" + ); + } + + @ParameterizedTest + @MethodSource("preParsedEquivalenceUrls") + void preParsedEquivalence_components(String url) { + EndpointUrl parsed = EndpointUrl.fromString(url); + EndpointUrl constructed = EndpointUrl.fromComponents(parsed.scheme(), parsed.host(), parsed.port(), parsed.encodedPath()); + + assertThat(constructed.scheme()).isEqualTo(parsed.scheme()); + assertThat(constructed.host()).isEqualTo(parsed.host()); + assertThat(constructed.port()).isEqualTo(parsed.port()); + assertThat(constructed.encodedPath()).isEqualTo(parsed.encodedPath()); + } + + @ParameterizedTest + @MethodSource("preParsedEquivalenceUrls") + void preParsedEquivalence_toUri(String url) { + EndpointUrl parsed = EndpointUrl.fromString(url); + EndpointUrl constructed = EndpointUrl.fromComponents(parsed.scheme(), parsed.host(), parsed.port(), parsed.encodedPath()); + + assertThat(constructed.toUri()).isEqualTo(parsed.toUri()); + } + + @ParameterizedTest + @MethodSource("preParsedEquivalenceUrls") + void preParsedEquivalence_objectEquality(String url) { + EndpointUrl parsed = EndpointUrl.fromString(url); + EndpointUrl constructed = EndpointUrl.fromComponents(parsed.scheme(), parsed.host(), parsed.port(), parsed.encodedPath()); + + assertThat(constructed).isEqualTo(parsed); + assertThat(constructed.hashCode()).isEqualTo(parsed.hashCode()); + } + + @Test + void fromString_withQueryOnly() { + EndpointUrl endpointUrl = EndpointUrl.fromString("https://example.com/path?key=value&foo=bar"); + assertThat(endpointUrl.scheme()).isEqualTo("https"); + assertThat(endpointUrl.host()).isEqualTo("example.com"); + assertThat(endpointUrl.port()).isEqualTo(-1); + assertThat(endpointUrl.encodedPath()).isEqualTo("/path"); + assertThat(endpointUrl.queryAndFragment()).isEqualTo("?key=value&foo=bar"); + } + + @Test + void fromString_withFragmentOnly() { + EndpointUrl endpointUrl = EndpointUrl.fromString("https://example.com/path#section"); + assertThat(endpointUrl.scheme()).isEqualTo("https"); + assertThat(endpointUrl.host()).isEqualTo("example.com"); + assertThat(endpointUrl.encodedPath()).isEqualTo("/path"); + assertThat(endpointUrl.queryAndFragment()).isEqualTo("#section"); + } + + @Test + void fromString_withQueryAndFragment() { + EndpointUrl endpointUrl = EndpointUrl.fromString("https://example.com/path?key=value#section"); + assertThat(endpointUrl.encodedPath()).isEqualTo("/path"); + assertThat(endpointUrl.queryAndFragment()).isEqualTo("?key=value#section"); + } + + @Test + void fromString_noQueryOrFragment() { + EndpointUrl endpointUrl = EndpointUrl.fromString("https://example.com/path"); + assertThat(endpointUrl.queryAndFragment()).isEmpty(); + } + + @Test + void fromString_queryWithNoPath() { + EndpointUrl endpointUrl = EndpointUrl.fromString("https://example.com?key=value"); + assertThat(endpointUrl.host()).isEqualTo("example.com"); + assertThat(endpointUrl.encodedPath()).isEmpty(); + assertThat(endpointUrl.queryAndFragment()).isEqualTo("?key=value"); + } + + @Test + void fromString_withQueryAndFragment_toUriRoundTrip() { + String url = "https://example.com/path?key=value#section"; + EndpointUrl endpointUrl = EndpointUrl.fromString(url); + assertThat(endpointUrl.toUri()).isEqualTo(URI.create(url)); + } + + @Test + void fromUri_withQueryAndFragment_preservesComponents() { + URI uri = URI.create("https://example.com/path?key=value#section"); + EndpointUrl endpointUrl = EndpointUrl.fromUri(uri); + assertThat(endpointUrl.scheme()).isEqualTo("https"); + assertThat(endpointUrl.host()).isEqualTo("example.com"); + assertThat(endpointUrl.encodedPath()).isEqualTo("/path"); + assertThat(endpointUrl.queryAndFragment()).isEqualTo("?key=value#section"); + assertThat(endpointUrl.toUri()).isSameAs(uri); + } + + @Test + void fromUri_noQueryOrFragment() { + URI uri = URI.create("https://example.com/path"); + EndpointUrl endpointUrl = EndpointUrl.fromUri(uri); + assertThat(endpointUrl.queryAndFragment()).isEmpty(); + } + + @Test + void fromComponents_withQueryAndFragment_toUriIncludesThem() { + EndpointUrl endpointUrl = EndpointUrl.fromComponents("https", "example.com", -1, "/path", "?key=value#section"); + assertThat(endpointUrl.toUri()).isEqualTo(URI.create("https://example.com/path?key=value#section")); + } + + @Test + void fromComponents_withQueryAndFragment_preservedInEquality() { + EndpointUrl with = EndpointUrl.fromComponents("https", "example.com", -1, "/path", "?key=value"); + EndpointUrl without = EndpointUrl.fromComponents("https", "example.com", -1, "/path"); + assertThat(with).isNotEqualTo(without); + } + + @Test + void fromComponents_withEmptyQueryAndFragment_equalsOfWithoutIt() { + EndpointUrl withEmpty = EndpointUrl.fromComponents("https", "example.com", -1, "/path", ""); + EndpointUrl without = EndpointUrl.fromComponents("https", "example.com", -1, "/path"); + assertThat(withEmpty).isEqualTo(without); + } +} From 97022a93c92be1272791510790da9705b6373ca9 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 17 Jul 2026 13:45:38 -0700 Subject: [PATCH 2/5] [EndpointUri PR 2/4] Switch runtime consumers to EndpointUrl (#7158) --- .../endpoints/AwsEndpointProviderUtils.java | 43 ++++++++-- .../AwsEndpointProviderUtilsTest.java | 86 +++++++++++++++++++ .../stages/EndpointResolutionStage.java | 19 ++-- .../stages/EndpointResolutionStageTest.java | 41 +++++++++ .../awssdk/services/s3/S3Utilities.java | 2 +- .../internal/signing/DefaultS3Presigner.java | 2 +- 6 files changed, 177 insertions(+), 16 deletions(-) diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoints/AwsEndpointProviderUtils.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoints/AwsEndpointProviderUtils.java index 4fd4c1c972d2..b2ee07acbf8b 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoints/AwsEndpointProviderUtils.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoints/AwsEndpointProviderUtils.java @@ -24,6 +24,7 @@ import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute; import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId; import software.amazon.awssdk.endpoints.Endpoint; +import software.amazon.awssdk.endpoints.EndpointUrl; import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.utils.HostnameValidator; @@ -114,11 +115,12 @@ public static Endpoint addHostPrefix(Endpoint endpoint, String prefix) { return endpoint; } validatePrefixIsHostNameCompliant(prefix); - URI originalUrl = endpoint.url(); - String newHost = prefix + endpoint.url().getHost(); - URI newUrl = invokeSafely(() -> new URI(originalUrl.getScheme(), null, newHost, originalUrl.getPort(), - originalUrl.getPath(), originalUrl.getQuery(), originalUrl.getFragment())); - return endpoint.toBuilder().url(newUrl).build(); + + EndpointUrl originalUrl = endpoint.endpointUrl(); + String newHost = prefix + originalUrl.host(); + EndpointUrl newUrl = EndpointUrl.fromComponents(originalUrl.scheme(), newHost, originalUrl.port(), + originalUrl.encodedPath(), originalUrl.queryAndFragment()); + return endpoint.toBuilder().endpointUrl(newUrl).build(); } /** @@ -139,7 +141,11 @@ public static Endpoint addHostPrefix(Endpoint endpoint, String prefix) { * However, we also pass the endpoint to provider as a parameter, and the resolver returns * {@code https://example.com/a/b}. This method takes care of combining the paths correctly so that the resulting * path is {@code https://example.com/a/b/c}. + * + * @deprecated Use {@link #setUri(SdkHttpRequest, URI, EndpointUrl)} instead for better performance by avoiding + * intermediate URI construction. */ + @Deprecated public static SdkHttpRequest setUri(SdkHttpRequest request, URI clientEndpoint, URI resolvedUri) { String clientEndpointPath = clientEndpoint.getRawPath(); String requestPath = request.encodedPath(); @@ -154,6 +160,33 @@ public static SdkHttpRequest setUri(SdkHttpRequest request, URI clientEndpoint, .encodedPath(finalPath).build(); } + /** + * Sets the request URI to the resolved endpoint URL returned by the endpoint provider, reading URL components + * directly from the {@link EndpointUrl} without constructing an intermediate {@link URI}. + *

+ * This overload has the same behavior as {@link #setUri(SdkHttpRequest, URI, URI)} but avoids the cost of + * {@link URI} construction, making it suitable for presigners and other code paths that resolve endpoints outside + * the main pipeline. + * + * @param request the current HTTP request + * @param clientEndpoint the endpoint configured on the client (may include a path prefix from endpoint override) + * @param resolvedUrl the resolved endpoint URL from the rules engine + * @return a new {@link SdkHttpRequest} with scheme, host, port, and path applied from the resolved URL + */ + public static SdkHttpRequest setUri(SdkHttpRequest request, URI clientEndpoint, EndpointUrl resolvedUrl) { + String clientEndpointPath = clientEndpoint.getRawPath(); + String requestPath = request.encodedPath(); + String resolvedUrlPath = resolvedUrl.encodedPath(); + + String finalPath = requestPath; + if (!resolvedUrlPath.equals(clientEndpointPath)) { + finalPath = combinePath(clientEndpointPath, requestPath, resolvedUrlPath); + } + + return request.toBuilder().protocol(resolvedUrl.scheme()).host(resolvedUrl.host()).port(resolvedUrl.port()) + .encodedPath(finalPath).build(); + } + /** * Constructs [resolved URI path]/[request path without client endpoint path]. Strips the client endpoint path from * the marshalled request path to isolate just the part added by the marshaller, then appends it to the resolved path. diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/endpoints/AwsEndpointProviderUtilsTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/endpoints/AwsEndpointProviderUtilsTest.java index 821e584f37a6..19541dc6a8aa 100644 --- a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/endpoints/AwsEndpointProviderUtilsTest.java +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/endpoints/AwsEndpointProviderUtilsTest.java @@ -26,6 +26,7 @@ import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute; import software.amazon.awssdk.core.useragent.BusinessMetricCollection; import software.amazon.awssdk.endpoints.Endpoint; +import software.amazon.awssdk.endpoints.EndpointUrl; import software.amazon.awssdk.http.SdkHttpMethod; import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.regions.Region; @@ -237,6 +238,91 @@ void setUri_noPathDifference_keepsRequestPath() { assertThat(result.encodedPath()).isEqualTo("/operation"); } + @Test + void setUri_endpointUrl_combinesPathsCorrectly() { + URI clientEndpoint = URI.create("https://override.example.com/a"); + EndpointUrl resolvedUrl = EndpointUrl.fromString("https://override.example.com/a/b"); + SdkHttpRequest request = SdkHttpRequest.builder() + .uri(URI.create("https://override.example.com/a/c")) + .method(SdkHttpMethod.GET) + .build(); + + assertThat(AwsEndpointProviderUtils.setUri(request, clientEndpoint, resolvedUrl).getUri().toString()) + .isEqualTo("https://override.example.com/a/b/c"); + } + + @Test + void setUri_endpointUrl_doubleSlash_combinesPathsCorrectly() { + URI clientEndpoint = URI.create("https://override.example.com/a"); + EndpointUrl resolvedUrl = EndpointUrl.fromString("https://override.example.com/a/b"); + SdkHttpRequest request = SdkHttpRequest.builder() + .uri(URI.create("https://override.example.com/a//c")) + .method(SdkHttpMethod.GET) + .build(); + + assertThat(AwsEndpointProviderUtils.setUri(request, clientEndpoint, resolvedUrl).getUri().toString()) + .isEqualTo("https://override.example.com/a/b//c"); + } + + @Test + void setUri_endpointUrl_withTrailingSlashNoPath_combinesPathsCorrectly() { + URI clientEndpoint = URI.create("https://override.example.com/"); + EndpointUrl resolvedUrl = EndpointUrl.fromString("https://override.example.com/"); + SdkHttpRequest request = SdkHttpRequest.builder() + .uri(URI.create("https://override.example.com//a")) + .method(SdkHttpMethod.GET) + .build(); + + assertThat(AwsEndpointProviderUtils.setUri(request, clientEndpoint, resolvedUrl).getUri().toString()) + .isEqualTo("https://override.example.com//a"); + } + + @Test + void setUri_endpointUrl_noPathDifference_keepsRequestPath() { + URI clientEndpoint = URI.create("https://example.com"); + EndpointUrl resolvedUrl = EndpointUrl.fromString("https://resolved.example.com"); + SdkHttpRequest request = SdkHttpRequest.builder() + .uri(URI.create("https://example.com/operation")) + .method(SdkHttpMethod.GET) + .build(); + + SdkHttpRequest result = AwsEndpointProviderUtils.setUri(request, clientEndpoint, resolvedUrl); + assertThat(result.host()).isEqualTo("resolved.example.com"); + assertThat(result.encodedPath()).isEqualTo("/operation"); + } + + @Test + void setUri_endpointUrl_withPort_setsPortCorrectly() { + URI clientEndpoint = URI.create("https://example.com:8080"); + EndpointUrl resolvedUrl = EndpointUrl.fromString("https://resolved.example.com:9090/path"); + SdkHttpRequest request = SdkHttpRequest.builder() + .uri(URI.create("https://example.com:8080/operation")) + .method(SdkHttpMethod.GET) + .build(); + + SdkHttpRequest result = AwsEndpointProviderUtils.setUri(request, clientEndpoint, resolvedUrl); + assertThat(result.host()).isEqualTo("resolved.example.com"); + assertThat(result.port()).isEqualTo(9090); + assertThat(result.protocol()).isEqualTo("https"); + assertThat(result.encodedPath()).isEqualTo("/path/operation"); + } + + @Test + void setUri_endpointUrl_producesIdenticalResultToUriOverload() { + URI clientEndpoint = URI.create("https://override.example.com/a"); + URI resolvedUri = URI.create("https://override.example.com/a/b"); + EndpointUrl resolvedUrl = EndpointUrl.fromUri(resolvedUri); + SdkHttpRequest request = SdkHttpRequest.builder() + .uri(URI.create("https://override.example.com/a/c")) + .method(SdkHttpMethod.GET) + .build(); + + SdkHttpRequest uriResult = AwsEndpointProviderUtils.setUri(request, clientEndpoint, resolvedUri); + SdkHttpRequest endpointUrlResult = AwsEndpointProviderUtils.setUri(request, clientEndpoint, resolvedUrl); + + assertThat(endpointUrlResult.getUri()).isEqualTo(uriResult.getUri()); + } + private static ClientEndpointProvider endpointProvider(boolean isEndpointOverridden) { return ClientEndpointProvider.create(URI.create("https://foo.aws"), isEndpointOverridden); } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/EndpointResolutionStage.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/EndpointResolutionStage.java index 0bf7ff20a0aa..6b3c99665b8e 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/EndpointResolutionStage.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/EndpointResolutionStage.java @@ -33,6 +33,7 @@ import software.amazon.awssdk.core.internal.http.pipeline.MutableRequestToRequestPipeline; import software.amazon.awssdk.core.metrics.CoreMetric; import software.amazon.awssdk.endpoints.Endpoint; +import software.amazon.awssdk.endpoints.EndpointUrl; import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.metrics.MetricCollector; import software.amazon.awssdk.utils.StringUtils; @@ -102,10 +103,10 @@ public SdkHttpFullRequest.Builder execute(SdkHttpFullRequest.Builder request, Re ClientEndpointProvider clientEndpointProvider = attrs.getAttribute(SdkInternalExecutionAttribute.CLIENT_ENDPOINT_PROVIDER); if (interceptorModifiedEndpoint(request, attrs)) { - applyResolvedPath(request, clientEndpointProvider.clientEndpoint(), endpoint.url()); + applyResolvedPath(request, clientEndpointProvider.clientEndpoint(), endpoint.endpointUrl()); return request; } - return setUri(request, clientEndpointProvider.clientEndpoint(), endpoint.url()); + return setUri(request, clientEndpointProvider.clientEndpoint(), endpoint.endpointUrl()); } /** @@ -131,19 +132,19 @@ private static boolean interceptorModifiedEndpoint(SdkHttpFullRequest.Builder re */ private static SdkHttpFullRequest.Builder setUri(SdkHttpFullRequest.Builder request, URI clientEndpoint, - URI resolvedUri) { - applyResolvedPath(request, clientEndpoint, resolvedUri); - return request.protocol(resolvedUri.getScheme()) - .host(resolvedUri.getHost()) - .port(resolvedUri.getPort()); + EndpointUrl resolvedUrl) { + applyResolvedPath(request, clientEndpoint, resolvedUrl); + return request.protocol(resolvedUrl.scheme()) + .host(resolvedUrl.host()) + .port(resolvedUrl.port()); } private static void applyResolvedPath(SdkHttpFullRequest.Builder request, URI clientEndpoint, - URI resolvedUri) { + EndpointUrl resolvedUrl) { String clientEndpointPath = clientEndpoint.getRawPath(); String requestPath = request.encodedPath(); - String resolvedUriPath = resolvedUri.getRawPath(); + String resolvedUriPath = resolvedUrl.encodedPath(); if (!resolvedUriPath.equals(clientEndpointPath)) { request.encodedPath(combinePath(clientEndpointPath, requestPath, resolvedUriPath)); diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/EndpointResolutionStageTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/EndpointResolutionStageTest.java index d13717389c5f..60c4b437e420 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/EndpointResolutionStageTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/EndpointResolutionStageTest.java @@ -273,4 +273,45 @@ void execute_endpointAlreadyResolved_skipsResolution() throws Exception { assertThat(result).isSameAs(request); assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.RESOLVED_ENDPOINT)).isSameAs(preResolved); } + + @Test + void execute_resolvedEndpointWithPort_appliesPort() throws Exception { + Endpoint endpoint = Endpoint.builder().url(URI.create("https://resolved.amazonaws.com:8443/v2")).build(); + SdkHttpFullRequest.Builder request = SdkHttpFullRequest.builder() + .method(SdkHttpMethod.GET) + .protocol("https") + .host("myservice.amazonaws.com") + .encodedPath("/operation"); + executionAttributes.putAttribute(SdkInternalExecutionAttribute.ENDPOINT_RESOLVER, (req, attrs) -> endpoint); + executionAttributes.putAttribute(SdkInternalExecutionAttribute.CLIENT_ENDPOINT_PROVIDER, + ClientEndpointProvider.forEndpointOverride(CLIENT_ENDPOINT)); + RequestExecutionContext context = createContext(); + + SdkHttpFullRequest.Builder result = stage.execute(request, context); + + assertThat(result.host()).isEqualTo("resolved.amazonaws.com"); + assertThat(result.port()).isEqualTo(8443); + assertThat(result.protocol()).isEqualTo("https"); + } + + @Test + void execute_resolvedPathWithEncodedCharacters_preservesEncoding() throws Exception { + URI clientEndpoint = URI.create("https://s3.amazonaws.com"); + Endpoint endpoint = Endpoint.builder() + .url(URI.create("https://s3.amazonaws.com/bucket%20name")) + .build(); + SdkHttpFullRequest.Builder request = SdkHttpFullRequest.builder() + .method(SdkHttpMethod.GET) + .protocol("https") + .host("s3.amazonaws.com") + .encodedPath("/key%2Fwith%2Fslashes"); + executionAttributes.putAttribute(SdkInternalExecutionAttribute.ENDPOINT_RESOLVER, (req, attrs) -> endpoint); + executionAttributes.putAttribute(SdkInternalExecutionAttribute.CLIENT_ENDPOINT_PROVIDER, + ClientEndpointProvider.forEndpointOverride(clientEndpoint)); + RequestExecutionContext context = createContext(); + + SdkHttpFullRequest.Builder result = stage.execute(request, context); + + assertThat(result.encodedPath()).isEqualTo("/bucket%20name/key%2Fwith%2Fslashes"); + } } diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java index e5f4d07df7e3..98cbcd2b004d 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java @@ -518,7 +518,7 @@ private SdkHttpRequest resolveEndpointAndApply(GetObjectRequest request, SdkHttp ClientEndpointProvider clientEndpointProvider = executionAttributes.getAttribute(SdkInternalExecutionAttribute.CLIENT_ENDPOINT_PROVIDER); SdkHttpRequest result = AwsEndpointProviderUtils.setUri(httpRequest, - clientEndpointProvider.clientEndpoint(), endpoint.url()); + clientEndpointProvider.clientEndpoint(), endpoint.endpointUrl()); if (!endpoint.headers().isEmpty()) { SdkHttpRequest.Builder builder = result.toBuilder(); diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/signing/DefaultS3Presigner.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/signing/DefaultS3Presigner.java index d40ad32c01c2..16dc52a01411 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/signing/DefaultS3Presigner.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/signing/DefaultS3Presigner.java @@ -677,7 +677,7 @@ private void resolveEndpointAndUpdateContext(ExecutionContext execCtx, String op ClientEndpointProvider clientEndpointProvider = executionAttributes.getAttribute(SdkInternalExecutionAttribute.CLIENT_ENDPOINT_PROVIDER); SdkHttpRequest updatedRequest = AwsEndpointProviderUtils.setUri(httpRequest, - clientEndpointProvider.clientEndpoint(), endpoint.url()); + clientEndpointProvider.clientEndpoint(), endpoint.endpointUrl()); if (!endpoint.headers().isEmpty()) { SdkHttpRequest.Builder requestBuilder = updatedRequest.toBuilder(); From b67390b540cee7ef5cfb6f2f7f7496a5a0d95a9c Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Mon, 20 Jul 2026 09:23:34 -0700 Subject: [PATCH 3/5] [EndpointUri PR 3/4] Update codegen to use the EndpointUrl (#7160) --- .../poet/rules/EndpointProviderSpec.java | 4 +- .../poet/rules2/CodeGeneratorVisitor.java | 12 +- .../poet/rules2/EndpointProviderSpec2.java | 2 - .../poet/rules/endpoint-provider-class.java | 604 +++++++++--------- ...int-provider-know-prop-override-class.java | 582 ++++++++--------- .../poet/rules2/endpoint-provider-class.java | 16 +- ...int-provider-know-prop-override-class.java | 16 +- ...endpoint-provider-metric-values-class.java | 14 +- ...point-provider-unknown-property-class.java | 4 +- .../endpoint-provider-uri-cache-class.java | 20 +- 10 files changed, 631 insertions(+), 643 deletions(-) diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointProviderSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointProviderSpec.java index 0577934790b5..eb7d17b4e57f 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointProviderSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointProviderSpec.java @@ -25,7 +25,6 @@ import com.squareup.javapoet.TypeSpec; import com.squareup.javapoet.TypeVariableName; import com.squareup.javapoet.WildcardTypeName; -import java.net.URI; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -42,6 +41,7 @@ import software.amazon.awssdk.codegen.poet.PoetUtils; import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.endpoints.Endpoint; +import software.amazon.awssdk.endpoints.EndpointUrl; import software.amazon.awssdk.utils.CompletableFutureUtils; import software.amazon.awssdk.utils.Logger; import software.amazon.awssdk.utils.Validate; @@ -127,7 +127,7 @@ private MethodSpec valueAsEndpointOrThrowMethodSpec() { .addStatement("$T endpoint = $N.expectEndpoint()", endpointRulesSpecUtils.rulesRuntimeClassName("Value.Endpoint"), valueParamName) .addStatement("$T builder = Endpoint.builder()", Endpoint.Builder.class) - .addStatement("builder.url($T.create(endpoint.getUrl()))", URI.class) + .addStatement("builder.endpointUrl($T.fromString(endpoint.getUrl()))", EndpointUrl.class) .addStatement("$T headers = endpoint.getHeaders()", ParameterizedTypeName.get(ClassName.get(Map.class), TypeName.get(String.class), 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 daf0d5ff7f39..624173bde3a1 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 @@ -17,7 +17,6 @@ import com.squareup.javapoet.ClassName; import com.squareup.javapoet.CodeBlock; -import java.net.URI; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -29,7 +28,7 @@ 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; +import software.amazon.awssdk.endpoints.EndpointUrl; public class CodeGeneratorVisitor extends WalkRuleExpressionVisitor { private static final Logger log = LoggerFactory.getLogger(CodeGeneratorVisitor.class); @@ -39,20 +38,17 @@ public class CodeGeneratorVisitor extends WalkRuleExpressionVisitor { private final SymbolTable symbolTable; private final Map knownEndpointAttributes; private final Map ruleIdToScope; - private final boolean endpointCaching; public CodeGeneratorVisitor(RuleRuntimeTypeMirror typeMirror, SymbolTable symbolTable, Map knownEndpointAttributes, Map ruleIdToScope, - boolean endpointCaching, CodeBlock.Builder builder) { this.builder = builder; this.symbolTable = symbolTable; this.knownEndpointAttributes = knownEndpointAttributes; this.ruleIdToScope = ruleIdToScope; this.typeMirror = typeMirror; - this.endpointCaching = endpointCaching; } @Override @@ -333,11 +329,7 @@ private String callParams(String ruleId) { @Override public Void visitEndpointExpression(EndpointExpression e) { builder.add("return $T.endpoint(", typeMirror.rulesResult().type()); - 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); - } + builder.add("$T.builder().endpointUrl($T.fromString(", Endpoint.class, EndpointUrl.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 55dbf5358842..831b8d88af83 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 @@ -229,12 +229,10 @@ private MethodSpec.Builder methodBuilderForRule(RuleSetExpression expr) { } private void codegenExpr(RuleSetExpression expr, CodeBlock.Builder builder) { - boolean useEndpointCaching = intermediateModel.getCustomizationConfig().getEnableEndpointProviderUriCaching(); CodeGeneratorVisitor visitor = new CodeGeneratorVisitor(typeMirror, utils.symbolTable(), knownEndpointAttributes, utils.scopesByName(), - useEndpointCaching, builder); visitor.visitRuleSetExpression(expr); } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-provider-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-provider-class.java index 7c7e0170f7e5..6aeec5df170a 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-provider-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-provider-class.java @@ -1,6 +1,5 @@ package software.amazon.awssdk.services.query.endpoints.internal; -import java.net.URI; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -12,6 +11,7 @@ import software.amazon.awssdk.awscore.endpoints.AwsEndpointAttribute; import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.endpoints.Endpoint; +import software.amazon.awssdk.endpoints.EndpointUrl; import software.amazon.awssdk.services.query.endpoints.QueryEndpointParams; import software.amazon.awssdk.services.query.endpoints.QueryEndpointProvider; import software.amazon.awssdk.utils.CompletableFutureUtils; @@ -63,11 +63,11 @@ private static Map toIdentifierValueMap(QueryEndpointParams p } if (params.listOfStrings() != null) { paramsMap.put(Identifier.of("listOfStrings"), - Value.fromArray(params.listOfStrings().stream().map(Value::fromStr).collect(Collectors.toList()))); + Value.fromArray(params.listOfStrings().stream().map(Value::fromStr).collect(Collectors.toList()))); } if (params.defaultListOfStrings() != null) { paramsMap.put(Identifier.of("defaultListOfStrings"), - Value.fromArray(params.defaultListOfStrings().stream().map(Value::fromStr).collect(Collectors.toList()))); + Value.fromArray(params.defaultListOfStrings().stream().map(Value::fromStr).collect(Collectors.toList()))); } if (params.endpointId() != null) { paramsMap.put(Identifier.of("endpointId"), Value.fromStr(params.endpointId())); @@ -92,11 +92,11 @@ private static Map toIdentifierValueMap(QueryEndpointParams p } if (params.customEndpointArray() != null) { paramsMap.put(Identifier.of("CustomEndpointArray"), - Value.fromArray(params.customEndpointArray().stream().map(Value::fromStr).collect(Collectors.toList()))); + Value.fromArray(params.customEndpointArray().stream().map(Value::fromStr).collect(Collectors.toList()))); } if (params.arnList() != null) { paramsMap.put(Identifier.of("ArnList"), - Value.fromArray(params.arnList().stream().map(Value::fromStr).collect(Collectors.toList()))); + Value.fromArray(params.arnList().stream().map(Value::fromStr).collect(Collectors.toList()))); } return paramsMap; } @@ -105,7 +105,7 @@ Endpoint valueAsEndpointOrThrow(Value value) { if (value instanceof Value.Endpoint) { Value.Endpoint endpoint = value.expectEndpoint(); Endpoint.Builder builder = Endpoint.builder(); - builder.url(URI.create(endpoint.getUrl())); + builder.endpointUrl(EndpointUrl.fromString(endpoint.getUrl())); Map> headers = endpoint.getHeaders(); if (headers != null) { headers.forEach((name, values) -> values.forEach(v -> builder.putHeader(name, v))); @@ -120,214 +120,214 @@ Endpoint valueAsEndpointOrThrow(Value value) { throw SdkClientException.create(errorMsg); } else { throw SdkClientException.create("Rule engine return neither an endpoint result or error value. Returned value was: " - + value); + + value); } } private static Rule endpointRule_2() { return Rule - .builder() - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("isSet").argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))) - .build().validate()).build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("booleanEquals") - .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")), Expr.of(true))).build() - .validate()).build()).error("FIPS endpoints not supported with multi-region endpoints"); + .builder() + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("isSet").argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))) + .build().validate()).build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("booleanEquals") + .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")), Expr.of(true))).build() + .validate()).build()).error("FIPS endpoints not supported with multi-region endpoints"); } private static Rule endpointRule_3() { return Rule - .builder() - .addCondition( - Condition - .builder() - .fn(FnNode - .builder() - .fn("not") - .argv(Arrays.asList(FnNode.builder().fn("isSet") - .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))).build() - .validate())).build().validate()).build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("isSet") - .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")))).build().validate()) - .build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("booleanEquals") - .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")), Expr.of(true))) - .build().validate()).build()) - .endpoint( - EndpointResult - .builder() - .url(Expr.of("https://{endpointId}.query.{partitionResult#dualStackDnsSuffix}")) - .addProperty( - Identifier.of("authSchemes"), - Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), - Literal.fromStr("sigv4a"), Identifier.of("signingName"), - Literal.fromStr("query"), Identifier.of("signingRegionSet"), - Literal.fromTuple(Arrays.asList(Literal.fromStr("*")))))))).build()); + .builder() + .addCondition( + Condition + .builder() + .fn(FnNode + .builder() + .fn("not") + .argv(Arrays.asList(FnNode.builder().fn("isSet") + .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))).build() + .validate())).build().validate()).build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("isSet") + .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")))).build().validate()) + .build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("booleanEquals") + .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")), Expr.of(true))) + .build().validate()).build()) + .endpoint( + EndpointResult + .builder() + .url(Expr.of("https://{endpointId}.query.{partitionResult#dualStackDnsSuffix}")) + .addProperty( + Identifier.of("authSchemes"), + Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), + Literal.fromStr("sigv4a"), Identifier.of("signingName"), + Literal.fromStr("query"), Identifier.of("signingRegionSet"), + Literal.fromTuple(Arrays.asList(Literal.fromStr("*")))))))).build()); } private static Rule endpointRule_4() { return Rule.builder() - .endpoint( - EndpointResult - .builder() - .url(Expr.of("https://{endpointId}.query.{partitionResult#dnsSuffix}")) - .addProperty( - Identifier.of("authSchemes"), - Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), - Literal.fromStr("sigv4a"), Identifier.of("signingName"), - Literal.fromStr("query"), Identifier.of("signingRegionSet"), - Literal.fromTuple(Arrays.asList(Literal.fromStr("*")))))))).build()); + .endpoint( + EndpointResult + .builder() + .url(Expr.of("https://{endpointId}.query.{partitionResult#dnsSuffix}")) + .addProperty( + Identifier.of("authSchemes"), + Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), + Literal.fromStr("sigv4a"), Identifier.of("signingName"), + Literal.fromStr("query"), Identifier.of("signingRegionSet"), + Literal.fromTuple(Arrays.asList(Literal.fromStr("*")))))))).build()); } private static Rule endpointRule_1() { return Rule - .builder() - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("isSet").argv(Arrays.asList(Expr.ref(Identifier.of("endpointId")))) - .build().validate()).build()) - .treeRule(Arrays.asList(endpointRule_2(), endpointRule_3(), endpointRule_4())); + .builder() + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("isSet").argv(Arrays.asList(Expr.ref(Identifier.of("endpointId")))) + .build().validate()).build()) + .treeRule(Arrays.asList(endpointRule_2(), endpointRule_3(), endpointRule_4())); } private static Rule endpointRule_6() { return Rule - .builder() - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("isSet").argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))) - .build().validate()).build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("booleanEquals") - .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")), Expr.of(true))).build() - .validate()).build()) - .addCondition( - Condition - .builder() - .fn(FnNode - .builder() - .fn("not") - .argv(Arrays.asList(FnNode.builder().fn("isSet") - .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")))).build() - .validate())).build().validate()).build()) - .endpoint( - EndpointResult - .builder() - .url(Expr.of("https://query-fips.{region}.{partitionResult#dnsSuffix}")) - .addProperty( - Identifier.of("authSchemes"), - Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), - Literal.fromStr("sigv4a"), Identifier.of("signingName"), - Literal.fromStr("query"), Identifier.of("signingRegionSet"), - Literal.fromTuple(Arrays.asList(Literal.fromStr("*")))))))).build()); + .builder() + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("isSet").argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))) + .build().validate()).build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("booleanEquals") + .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")), Expr.of(true))).build() + .validate()).build()) + .addCondition( + Condition + .builder() + .fn(FnNode + .builder() + .fn("not") + .argv(Arrays.asList(FnNode.builder().fn("isSet") + .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")))).build() + .validate())).build().validate()).build()) + .endpoint( + EndpointResult + .builder() + .url(Expr.of("https://query-fips.{region}.{partitionResult#dnsSuffix}")) + .addProperty( + Identifier.of("authSchemes"), + Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), + Literal.fromStr("sigv4a"), Identifier.of("signingName"), + Literal.fromStr("query"), Identifier.of("signingRegionSet"), + Literal.fromTuple(Arrays.asList(Literal.fromStr("*")))))))).build()); } private static Rule endpointRule_7() { return Rule - .builder() - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("isSet") - .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")))).build().validate()) - .build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("booleanEquals") - .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")), Expr.of(true))) - .build().validate()).build()) - .addCondition( - Condition - .builder() - .fn(FnNode - .builder() - .fn("not") - .argv(Arrays.asList(FnNode.builder().fn("isSet") - .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))).build() - .validate())).build().validate()).build()) - .endpoint( - EndpointResult - .builder() - .url(Expr.of("https://query.{region}.{partitionResult#dualStackDnsSuffix}")) - .addProperty( - Identifier.of("authSchemes"), - Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), - Literal.fromStr("sigv4a"), Identifier.of("signingName"), - Literal.fromStr("query"), Identifier.of("signingRegionSet"), - Literal.fromTuple(Arrays.asList(Literal.fromStr("*"))))), Literal - .fromRecord(MapUtils.of(Identifier.of("name"), Literal.fromStr("sigv4"), - Identifier.of("signingName"), Literal.fromStr("query"), - Identifier.of("signingRegion"), Literal.fromStr("{region}")))))).build()); + .builder() + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("isSet") + .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")))).build().validate()) + .build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("booleanEquals") + .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")), Expr.of(true))) + .build().validate()).build()) + .addCondition( + Condition + .builder() + .fn(FnNode + .builder() + .fn("not") + .argv(Arrays.asList(FnNode.builder().fn("isSet") + .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))).build() + .validate())).build().validate()).build()) + .endpoint( + EndpointResult + .builder() + .url(Expr.of("https://query.{region}.{partitionResult#dualStackDnsSuffix}")) + .addProperty( + Identifier.of("authSchemes"), + Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), + Literal.fromStr("sigv4a"), Identifier.of("signingName"), + Literal.fromStr("query"), Identifier.of("signingRegionSet"), + Literal.fromTuple(Arrays.asList(Literal.fromStr("*"))))), Literal + .fromRecord(MapUtils.of(Identifier.of("name"), Literal.fromStr("sigv4"), + Identifier.of("signingName"), Literal.fromStr("query"), + Identifier.of("signingRegion"), Literal.fromStr("{region}")))))).build()); } private static Rule endpointRule_8() { return Rule - .builder() - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("isSet") - .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")))).build().validate()) - .build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("isSet").argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))) - .build().validate()).build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("booleanEquals") - .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")), Expr.of(true))) - .build().validate()).build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("booleanEquals") - .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")), Expr.of(true))).build() - .validate()).build()) - .endpoint( - EndpointResult - .builder() - .url(Expr.of("https://query-fips.{region}.{partitionResult#dualStackDnsSuffix}")) - .addProperty( - Identifier.of("authSchemes"), - Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), - Literal.fromStr("sigv4a"), Identifier.of("signingName"), - Literal.fromStr("query"), Identifier.of("signingRegionSet"), - Literal.fromTuple(Arrays.asList(Literal.fromStr("*")))))))).build()); + .builder() + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("isSet") + .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")))).build().validate()) + .build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("isSet").argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))) + .build().validate()).build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("booleanEquals") + .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")), Expr.of(true))) + .build().validate()).build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("booleanEquals") + .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")), Expr.of(true))).build() + .validate()).build()) + .endpoint( + EndpointResult + .builder() + .url(Expr.of("https://query-fips.{region}.{partitionResult#dualStackDnsSuffix}")) + .addProperty( + Identifier.of("authSchemes"), + Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), + Literal.fromStr("sigv4a"), Identifier.of("signingName"), + Literal.fromStr("query"), Identifier.of("signingRegionSet"), + Literal.fromTuple(Arrays.asList(Literal.fromStr("*")))))))).build()); } private static Rule endpointRule_9() { return Rule.builder().endpoint( - EndpointResult.builder().url(Expr.of("https://query.{region}.{partitionResult#dnsSuffix}")).build()); + EndpointResult.builder().url(Expr.of("https://query.{region}.{partitionResult#dnsSuffix}")).build()); } private static Rule endpointRule_5() { return Rule - .builder() - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("isValidHostLabel") - .argv(Arrays.asList(Expr.ref(Identifier.of("region")), Expr.of(false))).build() - .validate()).build()) - .treeRule(Arrays.asList(endpointRule_6(), endpointRule_7(), endpointRule_8(), endpointRule_9())); + .builder() + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("isValidHostLabel") + .argv(Arrays.asList(Expr.ref(Identifier.of("region")), Expr.of(false))).build() + .validate()).build()) + .treeRule(Arrays.asList(endpointRule_6(), endpointRule_7(), endpointRule_8(), endpointRule_9())); } private static Rule endpointRule_10() { @@ -336,133 +336,133 @@ private static Rule endpointRule_10() { private static Rule endpointRule_11() { return Rule - .builder() - .addCondition( - Condition - .builder() - .fn(FnNode - .builder() - .fn("not") - .argv(Arrays.asList(FnNode.builder().fn("isSet") - .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))).build() - .validate())).build().validate()).build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("isSet") - .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")))).build().validate()) - .build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("booleanEquals") - .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")), Expr.of(true))) - .build().validate()).build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("isSet").argv(Arrays.asList(Expr.ref(Identifier.of("ArnList")))).build() - .validate()).build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("getAttr") - .argv(Arrays.asList(Expr.ref(Identifier.of("ArnList")), Expr.of("[0]"))).build() - .validate()).result("FirstArn").build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("aws.parseArn").argv(Arrays.asList(Expr.ref(Identifier.of("FirstArn")))) - .build().validate()).result("ParsedArn").build()) - .endpoint( - EndpointResult - .builder() - .url(Expr.of("https://{endpointId}.query.{partitionResult#dualStackDnsSuffix}")) - .addProperty( - Identifier.of("authSchemes"), - Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), - Literal.fromStr("sigv4a"), Identifier.of("signingName"), - Literal.fromStr("query"), Identifier.of("signingRegionSet"), - Literal.fromTuple(Arrays.asList(Literal.fromStr("*")))))))).build()); + .builder() + .addCondition( + Condition + .builder() + .fn(FnNode + .builder() + .fn("not") + .argv(Arrays.asList(FnNode.builder().fn("isSet") + .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))).build() + .validate())).build().validate()).build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("isSet") + .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")))).build().validate()) + .build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("booleanEquals") + .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")), Expr.of(true))) + .build().validate()).build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("isSet").argv(Arrays.asList(Expr.ref(Identifier.of("ArnList")))).build() + .validate()).build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("getAttr") + .argv(Arrays.asList(Expr.ref(Identifier.of("ArnList")), Expr.of("[0]"))).build() + .validate()).result("FirstArn").build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("aws.parseArn").argv(Arrays.asList(Expr.ref(Identifier.of("FirstArn")))) + .build().validate()).result("ParsedArn").build()) + .endpoint( + EndpointResult + .builder() + .url(Expr.of("https://{endpointId}.query.{partitionResult#dualStackDnsSuffix}")) + .addProperty( + Identifier.of("authSchemes"), + Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), + Literal.fromStr("sigv4a"), Identifier.of("signingName"), + Literal.fromStr("query"), Identifier.of("signingRegionSet"), + Literal.fromTuple(Arrays.asList(Literal.fromStr("*")))))))).build()); } private static Rule endpointRule_0() { return Rule - .builder() - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("aws.partition").argv(Arrays.asList(Expr.ref(Identifier.of("region")))) - .build().validate()).result("partitionResult").build()) - .treeRule(Arrays.asList(endpointRule_1(), endpointRule_5(), endpointRule_10(), endpointRule_11())); + .builder() + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("aws.partition").argv(Arrays.asList(Expr.ref(Identifier.of("region")))) + .build().validate()).result("partitionResult").build()) + .treeRule(Arrays.asList(endpointRule_1(), endpointRule_5(), endpointRule_10(), endpointRule_11())); } private static EndpointRuleset ruleSet() { return EndpointRuleset - .builder() - .version("1.2") - .serviceId("query") - .parameters( - Parameters - .builder() - .addParameter( - Parameter.builder().name("region").type(ParameterType.fromValue("string")).required(true) - .builtIn("AWS::Region").documentation("The region to send requests to").build()) - .addParameter( - Parameter.builder().name("useDualStackEndpoint").type(ParameterType.fromValue("boolean")) - .required(false).builtIn("AWS::UseDualStack").build()) - .addParameter( - Parameter.builder().name("useFIPSEndpoint").type(ParameterType.fromValue("boolean")) - .required(false).builtIn("AWS::UseFIPS").build()) - .addParameter( - Parameter.builder().name("AccountId").type(ParameterType.fromValue("String")) - .required(false).builtIn("AWS::Auth::AccountId").build()) - .addParameter( - Parameter.builder().name("AccountIdEndpointMode").type(ParameterType.fromValue("String")) - .required(false).builtIn("AWS::Auth::AccountIdEndpointMode").build()) - .addParameter( - Parameter.builder().name("listOfStrings").type(ParameterType.fromValue("StringArray")) - .required(false).build()) - .addParameter( - Parameter - .builder() - .name("defaultListOfStrings") - .type(ParameterType.fromValue("stringarray")) - .required(false) - .defaultValue( - Value.fromArray(Arrays.asList("item1", "item2", "item3").stream() - .map(Value::fromStr).collect(Collectors.toList()))).build()) - .addParameter( - Parameter.builder().name("endpointId").type(ParameterType.fromValue("string")) - .required(false).build()) - .addParameter( - Parameter.builder().name("defaultTrueParam").type(ParameterType.fromValue("boolean")) - .required(false).documentation("A param that defauls to true") - .defaultValue(Value.fromBool(true)).build()) - .addParameter( - Parameter.builder().name("defaultStringParam").type(ParameterType.fromValue("string")) - .required(false).defaultValue(Value.fromStr("hello endpoints")).build()) - .addParameter( - Parameter.builder().name("deprecatedParam").type(ParameterType.fromValue("string")) - .required(false).deprecated(new Parameter.Deprecated("Don't use!", "2021-01-01")) - .build()) - .addParameter( - Parameter.builder().name("booleanContextParam").type(ParameterType.fromValue("boolean")) - .required(false).build()) - .addParameter( - Parameter.builder().name("stringContextParam").type(ParameterType.fromValue("string")) - .required(false).build()) - .addParameter( - Parameter.builder().name("operationContextParam").type(ParameterType.fromValue("string")) - .required(false).build()) - .addParameter( - Parameter.builder().name("CustomEndpointArray") - .type(ParameterType.fromValue("StringArray")).required(false) - .documentation("Parameter from the customization config").build()) - .addParameter( - Parameter.builder().name("ArnList").type(ParameterType.fromValue("StringArray")) - .required(false).documentation("Parameter from the customization config").build()) - .build()).addRule(endpointRule_0()).build(); + .builder() + .version("1.2") + .serviceId("query") + .parameters( + Parameters + .builder() + .addParameter( + Parameter.builder().name("region").type(ParameterType.fromValue("string")).required(true) + .builtIn("AWS::Region").documentation("The region to send requests to").build()) + .addParameter( + Parameter.builder().name("useDualStackEndpoint").type(ParameterType.fromValue("boolean")) + .required(false).builtIn("AWS::UseDualStack").build()) + .addParameter( + Parameter.builder().name("useFIPSEndpoint").type(ParameterType.fromValue("boolean")) + .required(false).builtIn("AWS::UseFIPS").build()) + .addParameter( + Parameter.builder().name("AccountId").type(ParameterType.fromValue("String")) + .required(false).builtIn("AWS::Auth::AccountId").build()) + .addParameter( + Parameter.builder().name("AccountIdEndpointMode").type(ParameterType.fromValue("String")) + .required(false).builtIn("AWS::Auth::AccountIdEndpointMode").build()) + .addParameter( + Parameter.builder().name("listOfStrings").type(ParameterType.fromValue("StringArray")) + .required(false).build()) + .addParameter( + Parameter + .builder() + .name("defaultListOfStrings") + .type(ParameterType.fromValue("stringarray")) + .required(false) + .defaultValue( + Value.fromArray(Arrays.asList("item1", "item2", "item3").stream() + .map(Value::fromStr).collect(Collectors.toList()))).build()) + .addParameter( + Parameter.builder().name("endpointId").type(ParameterType.fromValue("string")) + .required(false).build()) + .addParameter( + Parameter.builder().name("defaultTrueParam").type(ParameterType.fromValue("boolean")) + .required(false).documentation("A param that defauls to true") + .defaultValue(Value.fromBool(true)).build()) + .addParameter( + Parameter.builder().name("defaultStringParam").type(ParameterType.fromValue("string")) + .required(false).defaultValue(Value.fromStr("hello endpoints")).build()) + .addParameter( + Parameter.builder().name("deprecatedParam").type(ParameterType.fromValue("string")) + .required(false).deprecated(new Parameter.Deprecated("Don't use!", "2021-01-01")) + .build()) + .addParameter( + Parameter.builder().name("booleanContextParam").type(ParameterType.fromValue("boolean")) + .required(false).build()) + .addParameter( + Parameter.builder().name("stringContextParam").type(ParameterType.fromValue("string")) + .required(false).build()) + .addParameter( + Parameter.builder().name("operationContextParam").type(ParameterType.fromValue("string")) + .required(false).build()) + .addParameter( + Parameter.builder().name("CustomEndpointArray") + .type(ParameterType.fromValue("StringArray")).required(false) + .documentation("Parameter from the customization config").build()) + .addParameter( + Parameter.builder().name("ArnList").type(ParameterType.fromValue("StringArray")) + .required(false).documentation("Parameter from the customization config").build()) + .build()).addRule(endpointRule_0()).build(); } @Override @@ -478,12 +478,12 @@ public int hashCode() { private void addKnownProperties(Endpoint.Builder builder, Map properties) { properties.forEach((n, v) -> { switch (n) { - case "authSchemes": - builder.putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, endpointAuthSchemeStrategy.createAuthSchemes(v)); - break; - default: - LOG.debug(() -> "Ignoring unknown endpoint property: " + n); - break; + case "authSchemes": + builder.putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, endpointAuthSchemeStrategy.createAuthSchemes(v)); + break; + default: + LOG.debug(() -> "Ignoring unknown endpoint property: " + n); + break; } }); } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-provider-know-prop-override-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-provider-know-prop-override-class.java index 98af0f39dfe4..bc9850b0ec83 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-provider-know-prop-override-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-provider-know-prop-override-class.java @@ -1,6 +1,5 @@ package software.amazon.awssdk.services.query.endpoints.internal; -import java.net.URI; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -11,6 +10,7 @@ import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.endpoints.Endpoint; +import software.amazon.awssdk.endpoints.EndpointUrl; import software.amazon.awssdk.services.query.endpoints.QueryEndpointParams; import software.amazon.awssdk.services.query.endpoints.QueryEndpointProvider; import software.amazon.awssdk.utils.CompletableFutureUtils; @@ -62,11 +62,11 @@ private static Map toIdentifierValueMap(QueryEndpointParams p } if (params.listOfStrings() != null) { paramsMap.put(Identifier.of("listOfStrings"), - Value.fromArray(params.listOfStrings().stream().map(Value::fromStr).collect(Collectors.toList()))); + Value.fromArray(params.listOfStrings().stream().map(Value::fromStr).collect(Collectors.toList()))); } if (params.defaultListOfStrings() != null) { paramsMap.put(Identifier.of("defaultListOfStrings"), - Value.fromArray(params.defaultListOfStrings().stream().map(Value::fromStr).collect(Collectors.toList()))); + Value.fromArray(params.defaultListOfStrings().stream().map(Value::fromStr).collect(Collectors.toList()))); } if (params.endpointId() != null) { paramsMap.put(Identifier.of("endpointId"), Value.fromStr(params.endpointId())); @@ -91,7 +91,7 @@ private static Map toIdentifierValueMap(QueryEndpointParams p } if (params.arnList() != null) { paramsMap.put(Identifier.of("ArnList"), - Value.fromArray(params.arnList().stream().map(Value::fromStr).collect(Collectors.toList()))); + Value.fromArray(params.arnList().stream().map(Value::fromStr).collect(Collectors.toList()))); } return paramsMap; } @@ -100,7 +100,7 @@ Endpoint valueAsEndpointOrThrow(Value value) { if (value instanceof Value.Endpoint) { Value.Endpoint endpoint = value.expectEndpoint(); Endpoint.Builder builder = Endpoint.builder(); - builder.url(URI.create(endpoint.getUrl())); + builder.endpointUrl(EndpointUrl.fromString(endpoint.getUrl())); Map> headers = endpoint.getHeaders(); if (headers != null) { headers.forEach((name, values) -> values.forEach(v -> builder.putHeader(name, v))); @@ -115,214 +115,214 @@ Endpoint valueAsEndpointOrThrow(Value value) { throw SdkClientException.create(errorMsg); } else { throw SdkClientException.create("Rule engine return neither an endpoint result or error value. Returned value was: " - + value); + + value); } } private static Rule endpointRule_2() { return Rule - .builder() - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("isSet").argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))) - .build().validate()).build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("booleanEquals") - .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")), Expr.of(true))).build() - .validate()).build()).error("FIPS endpoints not supported with multi-region endpoints"); + .builder() + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("isSet").argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))) + .build().validate()).build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("booleanEquals") + .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")), Expr.of(true))).build() + .validate()).build()).error("FIPS endpoints not supported with multi-region endpoints"); } private static Rule endpointRule_3() { return Rule - .builder() - .addCondition( - Condition - .builder() - .fn(FnNode - .builder() - .fn("not") - .argv(Arrays.asList(FnNode.builder().fn("isSet") - .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))).build() - .validate())).build().validate()).build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("isSet") - .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")))).build().validate()) - .build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("booleanEquals") - .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")), Expr.of(true))) - .build().validate()).build()) - .endpoint( - EndpointResult - .builder() - .url(Expr.of("https://{endpointId}.query.{partitionResult#dualStackDnsSuffix}")) - .addProperty( - Identifier.of("authSchemes"), - Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), - Literal.fromStr("sigv4a"), Identifier.of("signingName"), - Literal.fromStr("query"), Identifier.of("signingRegionSet"), - Literal.fromTuple(Arrays.asList(Literal.fromStr("*")))))))).build()); + .builder() + .addCondition( + Condition + .builder() + .fn(FnNode + .builder() + .fn("not") + .argv(Arrays.asList(FnNode.builder().fn("isSet") + .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))).build() + .validate())).build().validate()).build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("isSet") + .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")))).build().validate()) + .build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("booleanEquals") + .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")), Expr.of(true))) + .build().validate()).build()) + .endpoint( + EndpointResult + .builder() + .url(Expr.of("https://{endpointId}.query.{partitionResult#dualStackDnsSuffix}")) + .addProperty( + Identifier.of("authSchemes"), + Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), + Literal.fromStr("sigv4a"), Identifier.of("signingName"), + Literal.fromStr("query"), Identifier.of("signingRegionSet"), + Literal.fromTuple(Arrays.asList(Literal.fromStr("*")))))))).build()); } private static Rule endpointRule_4() { return Rule.builder() - .endpoint( - EndpointResult - .builder() - .url(Expr.of("https://{endpointId}.query.{partitionResult#dnsSuffix}")) - .addProperty( - Identifier.of("authSchemes"), - Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), - Literal.fromStr("sigv4a"), Identifier.of("signingName"), - Literal.fromStr("query"), Identifier.of("signingRegionSet"), - Literal.fromTuple(Arrays.asList(Literal.fromStr("*")))))))).build()); + .endpoint( + EndpointResult + .builder() + .url(Expr.of("https://{endpointId}.query.{partitionResult#dnsSuffix}")) + .addProperty( + Identifier.of("authSchemes"), + Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), + Literal.fromStr("sigv4a"), Identifier.of("signingName"), + Literal.fromStr("query"), Identifier.of("signingRegionSet"), + Literal.fromTuple(Arrays.asList(Literal.fromStr("*")))))))).build()); } private static Rule endpointRule_1() { return Rule - .builder() - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("isSet").argv(Arrays.asList(Expr.ref(Identifier.of("endpointId")))) - .build().validate()).build()) - .treeRule(Arrays.asList(endpointRule_2(), endpointRule_3(), endpointRule_4())); + .builder() + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("isSet").argv(Arrays.asList(Expr.ref(Identifier.of("endpointId")))) + .build().validate()).build()) + .treeRule(Arrays.asList(endpointRule_2(), endpointRule_3(), endpointRule_4())); } private static Rule endpointRule_6() { return Rule - .builder() - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("isSet").argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))) - .build().validate()).build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("booleanEquals") - .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")), Expr.of(true))).build() - .validate()).build()) - .addCondition( - Condition - .builder() - .fn(FnNode - .builder() - .fn("not") - .argv(Arrays.asList(FnNode.builder().fn("isSet") - .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")))).build() - .validate())).build().validate()).build()) - .endpoint( - EndpointResult - .builder() - .url(Expr.of("https://query-fips.{region}.{partitionResult#dnsSuffix}")) - .addProperty( - Identifier.of("authSchemes"), - Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), - Literal.fromStr("sigv4a"), Identifier.of("signingName"), - Literal.fromStr("query"), Identifier.of("signingRegionSet"), - Literal.fromTuple(Arrays.asList(Literal.fromStr("*")))))))).build()); + .builder() + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("isSet").argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))) + .build().validate()).build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("booleanEquals") + .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")), Expr.of(true))).build() + .validate()).build()) + .addCondition( + Condition + .builder() + .fn(FnNode + .builder() + .fn("not") + .argv(Arrays.asList(FnNode.builder().fn("isSet") + .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")))).build() + .validate())).build().validate()).build()) + .endpoint( + EndpointResult + .builder() + .url(Expr.of("https://query-fips.{region}.{partitionResult#dnsSuffix}")) + .addProperty( + Identifier.of("authSchemes"), + Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), + Literal.fromStr("sigv4a"), Identifier.of("signingName"), + Literal.fromStr("query"), Identifier.of("signingRegionSet"), + Literal.fromTuple(Arrays.asList(Literal.fromStr("*")))))))).build()); } private static Rule endpointRule_7() { return Rule - .builder() - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("isSet") - .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")))).build().validate()) - .build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("booleanEquals") - .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")), Expr.of(true))) - .build().validate()).build()) - .addCondition( - Condition - .builder() - .fn(FnNode - .builder() - .fn("not") - .argv(Arrays.asList(FnNode.builder().fn("isSet") - .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))).build() - .validate())).build().validate()).build()) - .endpoint( - EndpointResult - .builder() - .url(Expr.of("https://query.{region}.{partitionResult#dualStackDnsSuffix}")) - .addProperty( - Identifier.of("authSchemes"), - Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), - Literal.fromStr("sigv4a"), Identifier.of("signingName"), - Literal.fromStr("query"), Identifier.of("signingRegionSet"), - Literal.fromTuple(Arrays.asList(Literal.fromStr("*"))))), Literal - .fromRecord(MapUtils.of(Identifier.of("name"), Literal.fromStr("sigv4"), - Identifier.of("signingName"), Literal.fromStr("query"), - Identifier.of("signingRegion"), Literal.fromStr("{region}")))))).build()); + .builder() + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("isSet") + .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")))).build().validate()) + .build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("booleanEquals") + .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")), Expr.of(true))) + .build().validate()).build()) + .addCondition( + Condition + .builder() + .fn(FnNode + .builder() + .fn("not") + .argv(Arrays.asList(FnNode.builder().fn("isSet") + .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))).build() + .validate())).build().validate()).build()) + .endpoint( + EndpointResult + .builder() + .url(Expr.of("https://query.{region}.{partitionResult#dualStackDnsSuffix}")) + .addProperty( + Identifier.of("authSchemes"), + Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), + Literal.fromStr("sigv4a"), Identifier.of("signingName"), + Literal.fromStr("query"), Identifier.of("signingRegionSet"), + Literal.fromTuple(Arrays.asList(Literal.fromStr("*"))))), Literal + .fromRecord(MapUtils.of(Identifier.of("name"), Literal.fromStr("sigv4"), + Identifier.of("signingName"), Literal.fromStr("query"), + Identifier.of("signingRegion"), Literal.fromStr("{region}")))))).build()); } private static Rule endpointRule_8() { return Rule - .builder() - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("isSet") - .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")))).build().validate()) - .build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("isSet").argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))) - .build().validate()).build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("booleanEquals") - .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")), Expr.of(true))) - .build().validate()).build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("booleanEquals") - .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")), Expr.of(true))).build() - .validate()).build()) - .endpoint( - EndpointResult - .builder() - .url(Expr.of("https://query-fips.{region}.{partitionResult#dualStackDnsSuffix}")) - .addProperty( - Identifier.of("authSchemes"), - Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), - Literal.fromStr("sigv4a"), Identifier.of("signingName"), - Literal.fromStr("query"), Identifier.of("signingRegionSet"), - Literal.fromTuple(Arrays.asList(Literal.fromStr("*")))))))).build()); + .builder() + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("isSet") + .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")))).build().validate()) + .build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("isSet").argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))) + .build().validate()).build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("booleanEquals") + .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")), Expr.of(true))) + .build().validate()).build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("booleanEquals") + .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")), Expr.of(true))).build() + .validate()).build()) + .endpoint( + EndpointResult + .builder() + .url(Expr.of("https://query-fips.{region}.{partitionResult#dualStackDnsSuffix}")) + .addProperty( + Identifier.of("authSchemes"), + Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), + Literal.fromStr("sigv4a"), Identifier.of("signingName"), + Literal.fromStr("query"), Identifier.of("signingRegionSet"), + Literal.fromTuple(Arrays.asList(Literal.fromStr("*")))))))).build()); } private static Rule endpointRule_9() { return Rule.builder().endpoint( - EndpointResult.builder().url(Expr.of("https://query.{region}.{partitionResult#dnsSuffix}")).build()); + EndpointResult.builder().url(Expr.of("https://query.{region}.{partitionResult#dnsSuffix}")).build()); } private static Rule endpointRule_5() { return Rule - .builder() - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("isValidHostLabel") - .argv(Arrays.asList(Expr.ref(Identifier.of("region")), Expr.of(false))).build() - .validate()).build()) - .treeRule(Arrays.asList(endpointRule_6(), endpointRule_7(), endpointRule_8(), endpointRule_9())); + .builder() + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("isValidHostLabel") + .argv(Arrays.asList(Expr.ref(Identifier.of("region")), Expr.of(false))).build() + .validate()).build()) + .treeRule(Arrays.asList(endpointRule_6(), endpointRule_7(), endpointRule_8(), endpointRule_9())); } private static Rule endpointRule_10() { @@ -331,129 +331,129 @@ private static Rule endpointRule_10() { private static Rule endpointRule_11() { return Rule - .builder() - .addCondition( - Condition - .builder() - .fn(FnNode - .builder() - .fn("not") - .argv(Arrays.asList(FnNode.builder().fn("isSet") - .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))).build() - .validate())).build().validate()).build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("isSet") - .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")))).build().validate()) - .build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("booleanEquals") - .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")), Expr.of(true))) - .build().validate()).build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("isSet").argv(Arrays.asList(Expr.ref(Identifier.of("ArnList")))).build() - .validate()).build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("getAttr") - .argv(Arrays.asList(Expr.ref(Identifier.of("ArnList")), Expr.of("[0]"))).build() - .validate()).result("FirstArn").build()) - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("aws.parseArn").argv(Arrays.asList(Expr.ref(Identifier.of("FirstArn")))) - .build().validate()).result("ParsedArn").build()) - .endpoint( - EndpointResult - .builder() - .url(Expr.of("https://{endpointId}.query.{partitionResult#dualStackDnsSuffix}")) - .addProperty( - Identifier.of("authSchemes"), - Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), - Literal.fromStr("sigv4a"), Identifier.of("signingName"), - Literal.fromStr("query"), Identifier.of("signingRegionSet"), - Literal.fromTuple(Arrays.asList(Literal.fromStr("*")))))))).build()); + .builder() + .addCondition( + Condition + .builder() + .fn(FnNode + .builder() + .fn("not") + .argv(Arrays.asList(FnNode.builder().fn("isSet") + .argv(Arrays.asList(Expr.ref(Identifier.of("useFIPSEndpoint")))).build() + .validate())).build().validate()).build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("isSet") + .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")))).build().validate()) + .build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("booleanEquals") + .argv(Arrays.asList(Expr.ref(Identifier.of("useDualStackEndpoint")), Expr.of(true))) + .build().validate()).build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("isSet").argv(Arrays.asList(Expr.ref(Identifier.of("ArnList")))).build() + .validate()).build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("getAttr") + .argv(Arrays.asList(Expr.ref(Identifier.of("ArnList")), Expr.of("[0]"))).build() + .validate()).result("FirstArn").build()) + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("aws.parseArn").argv(Arrays.asList(Expr.ref(Identifier.of("FirstArn")))) + .build().validate()).result("ParsedArn").build()) + .endpoint( + EndpointResult + .builder() + .url(Expr.of("https://{endpointId}.query.{partitionResult#dualStackDnsSuffix}")) + .addProperty( + Identifier.of("authSchemes"), + Literal.fromTuple(Arrays.asList(Literal.fromRecord(MapUtils.of(Identifier.of("name"), + Literal.fromStr("sigv4a"), Identifier.of("signingName"), + Literal.fromStr("query"), Identifier.of("signingRegionSet"), + Literal.fromTuple(Arrays.asList(Literal.fromStr("*")))))))).build()); } private static Rule endpointRule_0() { return Rule - .builder() - .addCondition( - Condition - .builder() - .fn(FnNode.builder().fn("aws.partition").argv(Arrays.asList(Expr.ref(Identifier.of("region")))) - .build().validate()).result("partitionResult").build()) - .treeRule(Arrays.asList(endpointRule_1(), endpointRule_5(), endpointRule_10(), endpointRule_11())); + .builder() + .addCondition( + Condition + .builder() + .fn(FnNode.builder().fn("aws.partition").argv(Arrays.asList(Expr.ref(Identifier.of("region")))) + .build().validate()).result("partitionResult").build()) + .treeRule(Arrays.asList(endpointRule_1(), endpointRule_5(), endpointRule_10(), endpointRule_11())); } private static EndpointRuleset ruleSet() { return EndpointRuleset - .builder() - .version("1.2") - .serviceId("query") - .parameters( - Parameters - .builder() - .addParameter( - Parameter.builder().name("region").type(ParameterType.fromValue("string")).required(true) - .builtIn("AWS::Region").documentation("The region to send requests to").build()) - .addParameter( - Parameter.builder().name("useDualStackEndpoint").type(ParameterType.fromValue("boolean")) - .required(false).builtIn("AWS::UseDualStack").build()) - .addParameter( - Parameter.builder().name("useFIPSEndpoint").type(ParameterType.fromValue("boolean")) - .required(false).builtIn("AWS::UseFIPS").build()) - .addParameter( - Parameter.builder().name("AccountId").type(ParameterType.fromValue("String")) - .required(false).builtIn("AWS::Auth::AccountId").build()) - .addParameter( - Parameter.builder().name("AccountIdEndpointMode").type(ParameterType.fromValue("String")) - .required(false).builtIn("AWS::Auth::AccountIdEndpointMode").build()) - .addParameter( - Parameter.builder().name("listOfStrings").type(ParameterType.fromValue("StringArray")) - .required(false).build()) - .addParameter( - Parameter - .builder() - .name("defaultListOfStrings") - .type(ParameterType.fromValue("stringarray")) - .required(false) - .defaultValue( - Value.fromArray(Arrays.asList("item1", "item2", "item3").stream() - .map(Value::fromStr).collect(Collectors.toList()))).build()) - .addParameter( - Parameter.builder().name("endpointId").type(ParameterType.fromValue("string")) - .required(false).build()) - .addParameter( - Parameter.builder().name("defaultTrueParam").type(ParameterType.fromValue("boolean")) - .required(false).documentation("A param that defauls to true") - .defaultValue(Value.fromBool(true)).build()) - .addParameter( - Parameter.builder().name("defaultStringParam").type(ParameterType.fromValue("string")) - .required(false).defaultValue(Value.fromStr("hello endpoints")).build()) - .addParameter( - Parameter.builder().name("deprecatedParam").type(ParameterType.fromValue("string")) - .required(false).deprecated(new Parameter.Deprecated("Don't use!", "2021-01-01")) - .build()) - .addParameter( - Parameter.builder().name("booleanContextParam").type(ParameterType.fromValue("boolean")) - .required(false).build()) - .addParameter( - Parameter.builder().name("stringContextParam").type(ParameterType.fromValue("string")) - .required(false).build()) - .addParameter( - Parameter.builder().name("operationContextParam").type(ParameterType.fromValue("string")) - .required(false).build()) - .addParameter( - Parameter.builder().name("ArnList").type(ParameterType.fromValue("StringArray")) - .required(false).documentation("Parameter from the customization config").build()) - .build()).addRule(endpointRule_0()).build(); + .builder() + .version("1.2") + .serviceId("query") + .parameters( + Parameters + .builder() + .addParameter( + Parameter.builder().name("region").type(ParameterType.fromValue("string")).required(true) + .builtIn("AWS::Region").documentation("The region to send requests to").build()) + .addParameter( + Parameter.builder().name("useDualStackEndpoint").type(ParameterType.fromValue("boolean")) + .required(false).builtIn("AWS::UseDualStack").build()) + .addParameter( + Parameter.builder().name("useFIPSEndpoint").type(ParameterType.fromValue("boolean")) + .required(false).builtIn("AWS::UseFIPS").build()) + .addParameter( + Parameter.builder().name("AccountId").type(ParameterType.fromValue("String")) + .required(false).builtIn("AWS::Auth::AccountId").build()) + .addParameter( + Parameter.builder().name("AccountIdEndpointMode").type(ParameterType.fromValue("String")) + .required(false).builtIn("AWS::Auth::AccountIdEndpointMode").build()) + .addParameter( + Parameter.builder().name("listOfStrings").type(ParameterType.fromValue("StringArray")) + .required(false).build()) + .addParameter( + Parameter + .builder() + .name("defaultListOfStrings") + .type(ParameterType.fromValue("stringarray")) + .required(false) + .defaultValue( + Value.fromArray(Arrays.asList("item1", "item2", "item3").stream() + .map(Value::fromStr).collect(Collectors.toList()))).build()) + .addParameter( + Parameter.builder().name("endpointId").type(ParameterType.fromValue("string")) + .required(false).build()) + .addParameter( + Parameter.builder().name("defaultTrueParam").type(ParameterType.fromValue("boolean")) + .required(false).documentation("A param that defauls to true") + .defaultValue(Value.fromBool(true)).build()) + .addParameter( + Parameter.builder().name("defaultStringParam").type(ParameterType.fromValue("string")) + .required(false).defaultValue(Value.fromStr("hello endpoints")).build()) + .addParameter( + Parameter.builder().name("deprecatedParam").type(ParameterType.fromValue("string")) + .required(false).deprecated(new Parameter.Deprecated("Don't use!", "2021-01-01")) + .build()) + .addParameter( + Parameter.builder().name("booleanContextParam").type(ParameterType.fromValue("boolean")) + .required(false).build()) + .addParameter( + Parameter.builder().name("stringContextParam").type(ParameterType.fromValue("string")) + .required(false).build()) + .addParameter( + Parameter.builder().name("operationContextParam").type(ParameterType.fromValue("string")) + .required(false).build()) + .addParameter( + Parameter.builder().name("ArnList").type(ParameterType.fromValue("StringArray")) + .required(false).documentation("Parameter from the customization config").build()) + .build()).addRule(endpointRule_0()).build(); } @Override diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-class.java index 684823698760..2edf6b89329c 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-class.java @@ -1,6 +1,5 @@ package software.amazon.awssdk.services.query.endpoints.internal; -import java.net.URI; import java.util.Arrays; import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.annotations.Generated; @@ -10,6 +9,7 @@ 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.endpoints.EndpointUrl; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.query.endpoints.QueryEndpointParams; import software.amazon.awssdk.services.query.endpoints.QueryEndpointProvider; @@ -66,7 +66,7 @@ private static RuleResult endpointRule1(QueryEndpointParams params, String regio if (parsedArn != null) { return RuleResult.endpoint(Endpoint .builder() - .url(URI.create("https://" + params.endpointId() + ".query." + .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, @@ -87,7 +87,7 @@ private static RuleResult endpointRule2(QueryEndpointParams params, RulePartitio if (params.useFipsEndpoint() == null && params.useDualStackEndpoint() != null && params.useDualStackEndpoint()) { return RuleResult.endpoint(Endpoint .builder() - .url(URI.create("https://" + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix())) + .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -95,7 +95,7 @@ private static RuleResult endpointRule2(QueryEndpointParams params, RulePartitio } return RuleResult.endpoint(Endpoint .builder() - .url(URI.create("https://" + params.endpointId() + ".query." + partitionResult.dnsSuffix())) + .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query").signingRegionSet(Arrays.asList("*")) @@ -109,7 +109,7 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio if (params.useFipsEndpoint() != null && params.useFipsEndpoint() && params.useDualStackEndpoint() == null) { return RuleResult.endpoint(Endpoint .builder() - .url(URI.create("https://query-fips." + region + "." + partitionResult.dnsSuffix())) + .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -118,7 +118,7 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio if (params.useDualStackEndpoint() != null && params.useDualStackEndpoint() && params.useFipsEndpoint() == null) { return RuleResult.endpoint(Endpoint .builder() - .url(URI.create("https://query." + region + "." + partitionResult.dualStackDnsSuffix())) + .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dualStackDnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -129,14 +129,14 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio && params.useFipsEndpoint()) { return RuleResult.endpoint(Endpoint .builder() - .url(URI.create("https://query-fips." + region + "." + partitionResult.dualStackDnsSuffix())) + .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dualStackDnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") .signingRegionSet(Arrays.asList("*")).build())).build()); } return RuleResult.endpoint(Endpoint.builder() - .url(URI.create("https://query." + region + "." + partitionResult.dnsSuffix())).build()); + .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dnsSuffix())).build()); } return RuleResult.carryOn(); } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-know-prop-override-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-know-prop-override-class.java index 684823698760..2edf6b89329c 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-know-prop-override-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-know-prop-override-class.java @@ -1,6 +1,5 @@ package software.amazon.awssdk.services.query.endpoints.internal; -import java.net.URI; import java.util.Arrays; import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.annotations.Generated; @@ -10,6 +9,7 @@ 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.endpoints.EndpointUrl; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.query.endpoints.QueryEndpointParams; import software.amazon.awssdk.services.query.endpoints.QueryEndpointProvider; @@ -66,7 +66,7 @@ private static RuleResult endpointRule1(QueryEndpointParams params, String regio if (parsedArn != null) { return RuleResult.endpoint(Endpoint .builder() - .url(URI.create("https://" + params.endpointId() + ".query." + .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, @@ -87,7 +87,7 @@ private static RuleResult endpointRule2(QueryEndpointParams params, RulePartitio if (params.useFipsEndpoint() == null && params.useDualStackEndpoint() != null && params.useDualStackEndpoint()) { return RuleResult.endpoint(Endpoint .builder() - .url(URI.create("https://" + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix())) + .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -95,7 +95,7 @@ private static RuleResult endpointRule2(QueryEndpointParams params, RulePartitio } return RuleResult.endpoint(Endpoint .builder() - .url(URI.create("https://" + params.endpointId() + ".query." + partitionResult.dnsSuffix())) + .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query").signingRegionSet(Arrays.asList("*")) @@ -109,7 +109,7 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio if (params.useFipsEndpoint() != null && params.useFipsEndpoint() && params.useDualStackEndpoint() == null) { return RuleResult.endpoint(Endpoint .builder() - .url(URI.create("https://query-fips." + region + "." + partitionResult.dnsSuffix())) + .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -118,7 +118,7 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio if (params.useDualStackEndpoint() != null && params.useDualStackEndpoint() && params.useFipsEndpoint() == null) { return RuleResult.endpoint(Endpoint .builder() - .url(URI.create("https://query." + region + "." + partitionResult.dualStackDnsSuffix())) + .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dualStackDnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -129,14 +129,14 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio && params.useFipsEndpoint()) { return RuleResult.endpoint(Endpoint .builder() - .url(URI.create("https://query-fips." + region + "." + partitionResult.dualStackDnsSuffix())) + .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dualStackDnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") .signingRegionSet(Arrays.asList("*")).build())).build()); } return RuleResult.endpoint(Endpoint.builder() - .url(URI.create("https://query." + region + "." + partitionResult.dnsSuffix())).build()); + .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dnsSuffix())).build()); } return RuleResult.carryOn(); } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-metric-values-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-metric-values-class.java index 6c854826e953..a086d3466d4e 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-metric-values-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-metric-values-class.java @@ -1,6 +1,5 @@ package software.amazon.awssdk.services.query.endpoints.internal; -import java.net.URI; import java.util.Arrays; import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.annotations.Generated; @@ -10,6 +9,7 @@ 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.endpoints.EndpointUrl; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.query.endpoints.QueryEndpointParams; import software.amazon.awssdk.services.query.endpoints.QueryEndpointProvider; @@ -70,7 +70,7 @@ private static RuleResult endpointRule2(QueryEndpointParams params, RulePartitio if (params.useFipsEndpoint() == null && params.useDualStackEndpoint() != null && params.useDualStackEndpoint()) { return RuleResult.endpoint(Endpoint .builder() - .url(URI.create("https://" + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix())) + .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -78,7 +78,7 @@ private static RuleResult endpointRule2(QueryEndpointParams params, RulePartitio } return RuleResult.endpoint(Endpoint .builder() - .url(URI.create("https://" + params.endpointId() + ".query." + partitionResult.dnsSuffix())) + .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query").signingRegionSet(Arrays.asList("*")) @@ -92,7 +92,7 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio if (params.useFipsEndpoint() != null && params.useFipsEndpoint() && params.useDualStackEndpoint() == null) { return RuleResult.endpoint(Endpoint .builder() - .url(URI.create("https://query-fips." + region + "." + partitionResult.dnsSuffix())) + .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -101,7 +101,7 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio if (params.useDualStackEndpoint() != null && params.useDualStackEndpoint() && params.useFipsEndpoint() == null) { return RuleResult.endpoint(Endpoint .builder() - .url(URI.create("https://query." + region + "." + partitionResult.dualStackDnsSuffix())) + .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dualStackDnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -112,14 +112,14 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio && params.useFipsEndpoint()) { return RuleResult.endpoint(Endpoint .builder() - .url(URI.create("https://query-fips." + region + "." + partitionResult.dualStackDnsSuffix())) + .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dualStackDnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") .signingRegionSet(Arrays.asList("*")).build())).build()); } return RuleResult.endpoint(Endpoint.builder() - .url(URI.create("https://query." + region + "." + partitionResult.dnsSuffix())).build()); + .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dnsSuffix())).build()); } return RuleResult.carryOn(); } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-unknown-property-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-unknown-property-class.java index 18f0c5cb6e42..908313b824e5 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-unknown-property-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-unknown-property-class.java @@ -1,11 +1,11 @@ package software.amazon.awssdk.services.query.endpoints.internal; -import java.net.URI; import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.annotations.Generated; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.endpoints.Endpoint; +import software.amazon.awssdk.endpoints.EndpointUrl; import software.amazon.awssdk.services.query.endpoints.QueryEndpointParams; import software.amazon.awssdk.services.query.endpoints.QueryEndpointProvider; import software.amazon.awssdk.utils.CompletableFutureUtils; @@ -43,7 +43,7 @@ private static RuleResult endpointRule0(QueryEndpointParams params) { private static RuleResult endpointRule1(QueryEndpointParams params) { if (params.endpoint() != null) { - return RuleResult.endpoint(Endpoint.builder().url(URI.create(params.endpoint())).build()); + return RuleResult.endpoint(Endpoint.builder().endpointUrl(EndpointUrl.fromString(params.endpoint())).build()); } return RuleResult.carryOn(); } 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 index 53eb66b3c147..2edf6b89329c 100644 --- 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 @@ -9,12 +9,12 @@ 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.endpoints.EndpointUrl; 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 @@ -66,8 +66,8 @@ private static RuleResult endpointRule1(QueryEndpointParams params, String regio if (parsedArn != null) { return RuleResult.endpoint(Endpoint .builder() - .url(SdkUri.getInstance().create( - "https://" + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix())) + .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + + partitionResult.dualStackDnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -87,8 +87,7 @@ private static RuleResult endpointRule2(QueryEndpointParams params, RulePartitio if (params.useFipsEndpoint() == null && params.useDualStackEndpoint() != null && params.useDualStackEndpoint()) { return RuleResult.endpoint(Endpoint .builder() - .url(SdkUri.getInstance().create( - "https://" + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix())) + .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -96,7 +95,7 @@ private static RuleResult endpointRule2(QueryEndpointParams params, RulePartitio } return RuleResult.endpoint(Endpoint .builder() - .url(SdkUri.getInstance().create("https://" + params.endpointId() + ".query." + partitionResult.dnsSuffix())) + .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query").signingRegionSet(Arrays.asList("*")) @@ -110,7 +109,7 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio if (params.useFipsEndpoint() != null && params.useFipsEndpoint() && params.useDualStackEndpoint() == null) { return RuleResult.endpoint(Endpoint .builder() - .url(SdkUri.getInstance().create("https://query-fips." + region + "." + partitionResult.dnsSuffix())) + .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -119,7 +118,7 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio if (params.useDualStackEndpoint() != null && params.useDualStackEndpoint() && params.useFipsEndpoint() == null) { return RuleResult.endpoint(Endpoint .builder() - .url(SdkUri.getInstance().create("https://query." + region + "." + partitionResult.dualStackDnsSuffix())) + .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dualStackDnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -130,15 +129,14 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio && params.useFipsEndpoint()) { return RuleResult.endpoint(Endpoint .builder() - .url(SdkUri.getInstance().create( - "https://query-fips." + region + "." + partitionResult.dualStackDnsSuffix())) + .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dualStackDnsSuffix())) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") .signingRegionSet(Arrays.asList("*")).build())).build()); } return RuleResult.endpoint(Endpoint.builder() - .url(SdkUri.getInstance().create("https://query." + region + "." + partitionResult.dnsSuffix())).build()); + .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dnsSuffix())).build()); } return RuleResult.carryOn(); } From 35b3eb90a64e6e725b86f9a796765f2cbd286fdf Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Tue, 21 Jul 2026 13:30:01 -0700 Subject: [PATCH 4/5] [EndpointUri PR 4/4] Optimize EndpointUri creation by pre-parsing urls in codegen (#7165) --- .../poet/rules2/CodeGeneratorVisitor.java | 7 +- .../poet/rules2/EndpointUrlCodeEmitter.java | 366 ++++++++++++++++++ .../rules2/EndpointUrlCodeEmitterTest.java | 334 ++++++++++++++++ .../poet/rules2/endpoint-provider-class.java | 22 +- ...int-provider-know-prop-override-class.java | 22 +- ...endpoint-provider-metric-values-class.java | 71 ++-- .../endpoint-provider-uri-cache-class.java | 22 +- .../amazon/awssdk/endpoints/EndpointUrl.java | 5 + .../internal/EndpointUrlConformanceTest.java | 136 +++++++ 9 files changed, 924 insertions(+), 61 deletions(-) create mode 100644 codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/EndpointUrlCodeEmitter.java create mode 100644 codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/EndpointUrlCodeEmitterTest.java create mode 100644 test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/compiledendpointrules/endpoints/internal/EndpointUrlConformanceTest.java 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 624173bde3a1..34d6a64a3154 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 @@ -28,7 +28,6 @@ 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.endpoints.EndpointUrl; public class CodeGeneratorVisitor extends WalkRuleExpressionVisitor { private static final Logger log = LoggerFactory.getLogger(CodeGeneratorVisitor.class); @@ -329,9 +328,9 @@ private String callParams(String ruleId) { @Override public Void visitEndpointExpression(EndpointExpression e) { builder.add("return $T.endpoint(", typeMirror.rulesResult().type()); - builder.add("$T.builder().endpointUrl($T.fromString(", Endpoint.class, EndpointUrl.class); - e.url().accept(this); - builder.add("))"); + builder.add("$T.builder().endpointUrl(", Endpoint.class); + EndpointUrlCodeEmitter.emit(e.url(), builder, this); + builder.add(")"); e.headers().accept(this); e.properties().accept(this); builder.add(".build()"); diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/EndpointUrlCodeEmitter.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/EndpointUrlCodeEmitter.java new file mode 100644 index 000000000000..957c0a72e933 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/EndpointUrlCodeEmitter.java @@ -0,0 +1,366 @@ +/* + * 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.codegen.poet.rules2; + +import com.squareup.javapoet.CodeBlock; +import java.util.ArrayList; +import java.util.List; +import software.amazon.awssdk.endpoints.EndpointUrl; + +/** + * Emits the optimal {@code EndpointUrl} construction code for a URL expression. + * + *

This class encapsulates both the analysis of the URL expression structure (determining whether + * it can be statically decomposed) and the code emission. When the URL starts with a literal scheme + * prefix ({@code https://} or {@code http://}) and the components (scheme, host, port, path) can be + * identified at codegen time then we eliminate runtime parsing and use {@code EndpointUrl.fromComponents()}. + * Otherwise, falls back to {@code EndpointUrl.fromString()} for runtime parsing. + * + *

The endpoint URL spec guarantees that URLs contain only scheme, host, optional port, and optional + * base path (no query or fragment). Dynamic (template based) resolution are only supported in the host + * and path segments in pre-parsing. Otherwise, we fall back to runtime parsing. + */ +final class EndpointUrlCodeEmitter { + + private static final String HTTPS_SCHEME_PREFIX = "https://"; + private static final String HTTP_SCHEME_PREFIX = "http://"; + + private EndpointUrlCodeEmitter() { + } + + /** + * Emit the optimal EndpointUrl construction code for the given URL expression. + * + *

Writes to {@code builder} either: + *

    + *
  • {@code EndpointUrl.fromComponents(scheme, hostExpr, port, pathExpr)} when the URL + * can be statically decomposed, or
  • + *
  • {@code EndpointUrl.fromString(urlExpr)} as a fallback.
  • + *
+ * + * @param urlExpr the URL expression from the endpoint rule + * @param builder the CodeBlock builder to emit into + * @param codegenVisitor the parent code generator, used to emit sub-expressions + */ + static void emit(RuleExpression urlExpr, CodeBlock.Builder builder, CodeGeneratorVisitor codegenVisitor) { + if (urlExpr instanceof LiteralStringExpression) { + emitFromLiteralString(((LiteralStringExpression) urlExpr).value(), builder); + return; + } + if (urlExpr instanceof StringConcatExpression) { + emitFromStringConcat((StringConcatExpression) urlExpr, builder, codegenVisitor); + return; + } + // Expression type we can't decompose (e.g. variable reference, function call) + emitRuntimeParse(urlExpr, builder, codegenVisitor); + } + + /** + * Emit a fully static URL as EndpointUrl.fromComponents() with all literal arguments. + */ + private static void emitFromLiteralString(String url, CodeBlock.Builder builder) { + int schemeEnd = url.indexOf("://"); + if (schemeEnd < 0) { + // No scheme found — shouldn't happen per spec, but fall back safely + builder.add("$T.fromString($S)", EndpointUrl.class, url); + return; + } + + String scheme = url.substring(0, schemeEnd); + int authorityStart = schemeEnd + 3; + int pathStart = url.indexOf('/', authorityStart); + + String authority; + String encodedPath; + if (pathStart < 0) { + authority = url.substring(authorityStart); + encodedPath = ""; + } else { + authority = url.substring(authorityStart, pathStart); + encodedPath = url.substring(pathStart); + } + + int port = -1; + String host; + int colonPos = authority.lastIndexOf(':'); + if (colonPos >= 0) { + try { + port = Integer.parseInt(authority.substring(colonPos + 1)); + host = authority.substring(0, colonPos); + } catch (NumberFormatException e) { + // Not a valid port — can't reliably decompose, fall back to runtime parsing + builder.add("$T.fromString($S)", EndpointUrl.class, url); + return; + } + } else { + host = authority; + } + + builder.add("$T.fromComponents($S, $S, $L, $S)", EndpointUrl.class, scheme, host, port, encodedPath); + } + + /** + * Emit code for a StringConcatExpression URL template. If the URL can be statically decomposed + * (starts with a literal scheme prefix), emits {@code EndpointUrl.fromComponents()}. Otherwise, + * falls back to {@code EndpointUrl.fromString()} for runtime parsing. + * + *

StringConcatExpressions are created by our codegen pre-parser: {@link ExpressionParser}. + * It splits on "{...}" boundaries, producing alternating literal strings and variable/member-access references. The + * result is a StringConcatExpression whose expressions() list interleaves: + *

    + *
  • LiteralStringExpression — static text fragments
  • + *
  • VariableReferenceExpression — "{Region}" eg a variable reference to "Region"
  • + *
  • MemberAccessExpression — "{PartitionResult#dnsSuffix}" → member access on variable "PartitionResult"
  • + *
+ * + *

Typical service endpoints follow something like: "https://sts.{Region}.{PartitionResult#dnsSuffix}" which + * parses to something like: + * {@snippet : + * StringConcatExpression([ + * LiteralStringExpression("https://sts."), + * VariableReferenceExpression("Region"), + * LiteralStringExpression("."), + * MemberAccessExpression(source=VarRef("PartitionResult"), name="dnsSuffix") + * ]) + * } + * + */ + private static void emitFromStringConcat(StringConcatExpression concatExpr, + CodeBlock.Builder builder, + CodeGeneratorVisitor codegenVisitor) { + List expressions = concatExpr.expressions(); + if (expressions.isEmpty()) { + emitRuntimeParse(concatExpr, builder, codegenVisitor); + return; + } + + // The first expression must be a literal starting with a scheme to preparse + RuleExpression firstExpr = expressions.get(0); + if (!(firstExpr instanceof LiteralStringExpression)) { + emitRuntimeParse(concatExpr, builder, codegenVisitor); + return; + } + + String firstLiteral = ((LiteralStringExpression) firstExpr).value(); + String scheme; + String hostStartStr; + if (firstLiteral.startsWith(HTTPS_SCHEME_PREFIX)) { + scheme = "https"; + hostStartStr = firstLiteral.substring(HTTPS_SCHEME_PREFIX.length()); + } else if (firstLiteral.startsWith(HTTP_SCHEME_PREFIX)) { + scheme = "http"; + hostStartStr = firstLiteral.substring(HTTP_SCHEME_PREFIX.length()); + } else { + emitRuntimeParse(concatExpr, builder, codegenVisitor); + return; + } + + // Scan expressions to find port and path boundaries, collecting host and path parts + int port = -1; + List hostParts = new ArrayList<>(); + List pathParts = new ArrayList<>(); + boolean foundPath = false; + + // Process the remainder of the first literal (after "scheme://") + if (!hostStartStr.isEmpty()) { + ScanResult scanResult = scanLiteral(hostStartStr, hostParts, pathParts); + if (!scanResult.parseable) { + emitRuntimeParse(concatExpr, builder, codegenVisitor); + return; + } + if (scanResult.foundPort) { + port = scanResult.port; + } + foundPath = scanResult.foundPath; + } + + // Process remaining expressions + for (int i = 1; i < expressions.size(); i++) { + RuleExpression expr = expressions.get(i); + + if (foundPath) { + // Everything after the path boundary is path — literals and expressions alike + pathParts.add(expr); + continue; + } + + if (expr instanceof LiteralStringExpression) { + String literal = ((LiteralStringExpression) expr).value(); + ScanResult scanResult = scanLiteral(literal, hostParts, pathParts); + if (!scanResult.parseable) { + emitRuntimeParse(concatExpr, builder, codegenVisitor); + return; + } + if (scanResult.foundPort) { + port = scanResult.port; + } + foundPath = scanResult.foundPath; + } else { + // Non-literal expression — part of the host since we haven't hit a path + hostParts.add(expr); + } + } + + // Emit: EndpointUrl.fromComponents(scheme, hostExpr, port, pathExpr) + builder.add("$T.fromComponents($S, ", EndpointUrl.class, scheme); + emitConcatExpression(hostParts, builder, codegenVisitor); + builder.add(", $L, ", port); + emitConcatExpression(pathParts, builder, codegenVisitor); + builder.add(")"); + } + + /** + * Emit EndpointUrl.fromString(urlExpr) for runtime parsing when static decomposition isn't possible. + */ + private static void emitRuntimeParse(RuleExpression urlExpr, CodeBlock.Builder builder, + CodeGeneratorVisitor codegenVisitor) { + builder.add("$T.fromString(", EndpointUrl.class); + urlExpr.accept(codegenVisitor); + builder.add(")"); + } + + /** + * Emit a list of expression parts as a concatenated expression. + * + *

If the list is empty, emits an empty string literal. Otherwise, wraps them in a + * {@link StringConcatExpression} and delegates to the code generator's existing concat + * emission logic. + */ + private static void emitConcatExpression(List parts, CodeBlock.Builder builder, + CodeGeneratorVisitor codegenVisitor) { + if (parts.isEmpty()) { + builder.add("$S", ""); + return; + } + StringConcatExpression.Builder concatBuilder = StringConcatExpression.builder(); + for (RuleExpression part : parts) { + concatBuilder.addExpression(part); + } + concatBuilder.build().accept(codegenVisitor); + } + + /** + * Scan a literal string segment for port (:{digits}) and path (/) boundaries. + * + *

This method mutates {@code hostParts} and {@code pathParts} as a side effect, appending + * the relevant portions of the literal to each list. On a {@code NOT_PARSEABLE} result, the + * lists may have been partially modified — callers must not reuse the lists after a failure. + */ + private static ScanResult scanLiteral(String literal, + List hostParts, + List pathParts) { + int slashIdx = literal.indexOf('/'); + int portColonIdx = findPortColon(literal); + + if (slashIdx >= 0 && (portColonIdx < 0 || slashIdx < portColonIdx)) { + // Path found before any port in this literal + String hostPart = literal.substring(0, slashIdx); + if (!hostPart.isEmpty()) { + hostParts.add(new LiteralStringExpression(hostPart)); + } + pathParts.add(new LiteralStringExpression(literal.substring(slashIdx))); + return ScanResult.pathFound(); + } + if (portColonIdx >= 0) { + // Port found + String hostPart = literal.substring(0, portColonIdx); + if (!hostPart.isEmpty()) { + hostParts.add(new LiteralStringExpression(hostPart)); + } + String portAndRest = literal.substring(portColonIdx + 1); + int restSlashIdx = portAndRest.indexOf('/'); + int port; + if (restSlashIdx >= 0) { + try { + port = Integer.parseInt(portAndRest.substring(0, restSlashIdx)); + } catch (NumberFormatException e) { + return ScanResult.NOT_PARSEABLE; + } + pathParts.add(new LiteralStringExpression(portAndRest.substring(restSlashIdx))); + return ScanResult.portAndPathFound(port); + } else { + try { + port = Integer.parseInt(portAndRest); + } catch (NumberFormatException e) { + return ScanResult.NOT_PARSEABLE; + } + return ScanResult.portFound(port); + } + } else { + // No port or path — this literal is part of the host + hostParts.add(new LiteralStringExpression(literal)); + return ScanResult.HOST_CONTINUE; + } + } + + /** + * Find the index of a port colon in a literal string. A port colon is a ':' followed by + * one or more digits (optionally followed by '/' or end of string). + */ + private static int findPortColon(String literal) { + int idx = literal.indexOf(':'); + while (idx >= 0) { + int digitStart = idx + 1; + if (digitStart < literal.length() && isDigit(literal.charAt(digitStart))) { + int digitEnd = digitStart; + while (digitEnd < literal.length() && isDigit(literal.charAt(digitEnd))) { + digitEnd++; + } + if (digitEnd == literal.length() || literal.charAt(digitEnd) == '/') { + return idx; + } + } + idx = literal.indexOf(':', idx + 1); + } + return -1; + } + + private static boolean isDigit(char c) { + return c >= '0' && c <= '9'; + } + + /** + * Internal result type for the literal scanning step. + */ + private static final class ScanResult { + private static final ScanResult NOT_PARSEABLE = new ScanResult(false, false, false, -1); + private static final ScanResult HOST_CONTINUE = new ScanResult(true, false, false, -1); + + private final boolean parseable; + private final boolean foundPort; + private final boolean foundPath; + private final int port; + + private ScanResult(boolean parseable, boolean foundPort, boolean foundPath, int port) { + this.parseable = parseable; + this.foundPort = foundPort; + this.foundPath = foundPath; + this.port = port; + } + + static ScanResult pathFound() { + return new ScanResult(true, false, true, -1); + } + + static ScanResult portFound(int port) { + return new ScanResult(true, true, false, port); + } + + static ScanResult portAndPathFound(int port) { + return new ScanResult(true, true, true, port); + } + } +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/EndpointUrlCodeEmitterTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/EndpointUrlCodeEmitterTest.java new file mode 100644 index 000000000000..c95c81ac3416 --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/EndpointUrlCodeEmitterTest.java @@ -0,0 +1,334 @@ +/* + * 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.codegen.poet.rules2; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.squareup.javapoet.CodeBlock; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link EndpointUrlCodeEmitter}. + * + *

Verifies that URL expressions are correctly decomposed into {@code EndpointUrl.fromComponents()} + * calls when possible, and fall back to {@code EndpointUrl.fromString()} otherwise.

+ */ +class EndpointUrlCodeEmitterTest { + + private static VariableReferenceExpression varRef(String name) { + return new VariableReferenceExpression(name); + } + + private static LiteralStringExpression literal(String value) { + return new LiteralStringExpression(value); + } + + private static MemberAccessExpression memberAccess(String source, String name) { + return MemberAccessExpression.builder() + .source(new VariableReferenceExpression(source)) + .name(name) + .build(); + } + + /** + * Creates a minimal CodeGeneratorVisitor suitable for testing expression emission. + * Only the expression visiting methods (literal, varRef, memberAccess, stringConcat) are used. + */ + private static String emitUrl(RuleExpression urlExpr) { + CodeBlock.Builder builder = CodeBlock.builder(); + CodeGeneratorVisitor visitor = new CodeGeneratorVisitor( + new RuleRuntimeTypeMirror("software.amazon.awssdk.test"), + SymbolTable.builder().build(), + Collections.emptyMap(), + Collections.emptyMap(), + builder + ); + EndpointUrlCodeEmitter.emit(urlExpr, builder, visitor); + return builder.build().toString(); + } + + // --- Pre-parseable cases: emit fromComponents --- + + /** + * "https://sts.{Region}.{PartitionResult#dnsSuffix}" — typical service URL. + */ + @Test + void emit_simpleHostConcat_emitsFromComponents() { + StringConcatExpression urlExpr = StringConcatExpression.builder() + .addExpression(literal("https://sts.")) + .addExpression(varRef("region")) + .addExpression(literal(".")) + .addExpression(memberAccess("partitionResult", "dnsSuffix")) + .build(); + + String code = emitUrl(urlExpr); + + assertThat(code).contains("EndpointUrl.fromComponents("); + assertThat(code).contains("\"https\""); + assertThat(code).contains("\"sts.\""); + assertThat(code).contains("region"); + assertThat(code).contains("partitionResult.dnsSuffix()"); + assertThat(code).contains("-1"); + assertThat(code).contains("\"\""); // empty path + assertThat(code).doesNotContain("fromString"); + } + + /** + * "https://runtime.sagemaker.{Region}.{dnsSuffix}:8443" — URL with port. + */ + @Test + void emit_withPort_emitsFromComponentsWithPort() { + StringConcatExpression urlExpr = StringConcatExpression.builder() + .addExpression(literal("https://runtime.sagemaker.")) + .addExpression(varRef("region")) + .addExpression(literal(".")) + .addExpression(varRef("dnsSuffix")) + .addExpression(literal(":8443")) + .build(); + + String code = emitUrl(urlExpr); + + assertThat(code).contains("EndpointUrl.fromComponents("); + assertThat(code).contains("\"https\""); + assertThat(code).contains("8443"); + assertThat(code).contains("\"\""); // empty path + assertThat(code).doesNotContain("fromString"); + } + + /** + * "https://places.geo.{Region}.{dnsSuffix}/v2" — URL with static path. + */ + @Test + void emit_withStaticPath_emitsFromComponentsWithPath() { + StringConcatExpression urlExpr = StringConcatExpression.builder() + .addExpression(literal("https://places.geo.")) + .addExpression(varRef("region")) + .addExpression(literal(".")) + .addExpression(varRef("dnsSuffix")) + .addExpression(literal("/v2")) + .build(); + + String code = emitUrl(urlExpr); + + assertThat(code).contains("EndpointUrl.fromComponents("); + assertThat(code).contains("\"https\""); + assertThat(code).contains("-1"); + assertThat(code).contains("\"/v2\""); // static path emitted as literal + assertThat(code).doesNotContain("fromString"); + } + + /** + * "https://service.{Region}:8443/v2" — URL with port and path in same literal. + */ + @Test + void emit_withPortAndPath_emitsFromComponentsWithBoth() { + StringConcatExpression urlExpr = StringConcatExpression.builder() + .addExpression(literal("https://service.")) + .addExpression(varRef("region")) + .addExpression(literal(":8443/v2")) + .build(); + + String code = emitUrl(urlExpr); + + assertThat(code).contains("EndpointUrl.fromComponents("); + assertThat(code).contains("\"https\""); + assertThat(code).contains("8443"); + assertThat(code).contains("\"/v2\""); + assertThat(code).doesNotContain("fromString"); + } + + /** + * "https://s3.{Region}.{dnsSuffix}/{uriEncodedBucket}" — URL with dynamic path variable. + * This should now be pre-parseable (path contains dynamic expression). + */ + @Test + void emit_withDynamicPath_emitsFromComponentsWithPathExpression() { + StringConcatExpression urlExpr = StringConcatExpression.builder() + .addExpression(literal("https://s3.")) + .addExpression(varRef("region")) + .addExpression(literal(".")) + .addExpression(varRef("dnsSuffix")) + .addExpression(literal("/")) + .addExpression(varRef("uriEncodedBucket")) + .build(); + + String code = emitUrl(urlExpr); + + assertThat(code).contains("EndpointUrl.fromComponents("); + assertThat(code).contains("\"https\""); + assertThat(code).contains("region"); + assertThat(code).contains("dnsSuffix"); + // Path is dynamic: "/" + uriEncodedBucket + assertThat(code).contains("\"/\""); + assertThat(code).contains("uriEncodedBucket"); + assertThat(code).doesNotContain("fromString"); + } + + /** + * "https://sts.amazonaws.com" — fully static literal URL. + */ + @Test + void emit_fullyStaticLiteral_emitsFromComponentsAllLiterals() { + LiteralStringExpression urlExpr = literal("https://sts.amazonaws.com"); + + String code = emitUrl(urlExpr); + + assertThat(code).contains("EndpointUrl.fromComponents("); + assertThat(code).contains("\"https\""); + assertThat(code).contains("\"sts.amazonaws.com\""); + assertThat(code).contains("-1"); + assertThat(code).contains("\"\""); + assertThat(code).doesNotContain("fromString"); + } + + /** + * "https://example.com:443/api/v1" — static URL with port and path. + */ + @Test + void emit_fullyStaticWithPortAndPath_emitsFromComponentsAllLiterals() { + LiteralStringExpression urlExpr = literal("https://example.com:443/api/v1"); + + String code = emitUrl(urlExpr); + + assertThat(code).contains("EndpointUrl.fromComponents("); + assertThat(code).contains("\"https\""); + assertThat(code).contains("\"example.com\""); + assertThat(code).contains("443"); + assertThat(code).contains("\"/api/v1\""); + assertThat(code).doesNotContain("fromString"); + } + + // --- Fallback cases: emit fromString --- + + /** + * "{url#scheme}://{Bucket}.{url#authority}{url#path}" — scheme is dynamic. + */ + @Test + void emit_dynamicScheme_emitsFromString() { + StringConcatExpression urlExpr = StringConcatExpression.builder() + .addExpression(memberAccess("url", "scheme")) + .addExpression(literal("://")) + .addExpression(varRef("Bucket")) + .addExpression(literal(".")) + .addExpression(memberAccess("url", "authority")) + .addExpression(memberAccess("url", "path")) + .build(); + + String code = emitUrl(urlExpr); + + assertThat(code).contains("EndpointUrl.fromString("); + assertThat(code).doesNotContain("fromComponents"); + } + + /** + * VariableReferenceExpression (e.g. just a variable) — not a string template or literal. + */ + @Test + void emit_variableReferenceExpression_emitsFromString() { + VariableReferenceExpression urlExpr = varRef("computedUrl"); + + String code = emitUrl(urlExpr); + + assertThat(code).contains("EndpointUrl.fromString("); + assertThat(code).contains("computedUrl"); + assertThat(code).doesNotContain("fromComponents"); + } + + /** + * region + ".amazonaws.com" — first expression is not a scheme literal. + */ + @Test + void emit_firstExpressionNotSchemeLiteral_emitsFromString() { + StringConcatExpression urlExpr = StringConcatExpression.builder() + .addExpression(varRef("region")) + .addExpression(literal(".amazonaws.com")) + .build(); + + String code = emitUrl(urlExpr); + + assertThat(code).contains("EndpointUrl.fromString("); + assertThat(code).doesNotContain("fromComponents"); + } + + // --- Edge cases --- + + /** + * "http://localhost:8080/test" — http scheme (not https). + */ + @Test + void emit_httpScheme_emitsFromComponents() { + LiteralStringExpression urlExpr = literal("http://localhost:8080/test"); + + String code = emitUrl(urlExpr); + + assertThat(code).contains("EndpointUrl.fromComponents("); + assertThat(code).contains("\"http\""); + assertThat(code).contains("\"localhost\""); + assertThat(code).contains("8080"); + assertThat(code).contains("\"/test\""); + } + + /** + * "https://{Bucket}.s3.{Region}.{dnsSuffix}" — host starts with a dynamic expression + * after the scheme prefix (the literal "https://" is followed by nothing before the first varRef). + */ + @Test + void emit_hostStartsWithVariable_emitsFromComponents() { + StringConcatExpression urlExpr = StringConcatExpression.builder() + .addExpression(literal("https://")) + .addExpression(varRef("Bucket")) + .addExpression(literal(".s3.")) + .addExpression(varRef("region")) + .addExpression(literal(".")) + .addExpression(varRef("dnsSuffix")) + .build(); + + String code = emitUrl(urlExpr); + + assertThat(code).contains("EndpointUrl.fromComponents("); + assertThat(code).contains("\"https\""); + assertThat(code).contains("Bucket"); + assertThat(code).doesNotContain("fromString"); + } + + /** + * Custom endpoint override: "{url#scheme}://{url#authority}{url#path}" + * + *

This is the standard representation when a user provides a custom endpoint URL via + * client configuration. The endpoint rule decomposes the user-provided URL into its parts + * (scheme, authority, path) and reassembles them with possible modifications (e.g., host prefix). + * Since the scheme is a dynamic expression (not a literal "https://" or "http://"), this + * cannot be pre-parsed and must fall back to runtime parsing. + */ + @Test + void emit_customEndpointOverride_emitsFromString() { + StringConcatExpression urlExpr = StringConcatExpression.builder() + .addExpression(memberAccess("url", "scheme")) + .addExpression(literal("://")) + .addExpression(memberAccess("url", "authority")) + .addExpression(memberAccess("url", "path")) + .build(); + + String code = emitUrl(urlExpr); + + assertThat(code).contains("EndpointUrl.fromString("); + assertThat(code).contains("url.scheme()"); + assertThat(code).contains("url.authority()"); + assertThat(code).contains("url.path()"); + assertThat(code).doesNotContain("fromComponents"); + } +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-class.java index 2edf6b89329c..d01b466c69f1 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-class.java @@ -66,8 +66,9 @@ private static RuleResult endpointRule1(QueryEndpointParams params, String regio if (parsedArn != null) { return RuleResult.endpoint(Endpoint .builder() - .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." - + partitionResult.dualStackDnsSuffix())) + .endpointUrl( + EndpointUrl.fromComponents("https", + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix(), -1, "")) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -87,7 +88,9 @@ private static RuleResult endpointRule2(QueryEndpointParams params, RulePartitio if (params.useFipsEndpoint() == null && params.useDualStackEndpoint() != null && params.useDualStackEndpoint()) { return RuleResult.endpoint(Endpoint .builder() - .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix())) + .endpointUrl( + EndpointUrl.fromComponents("https", params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix(), + -1, "")) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -95,7 +98,7 @@ private static RuleResult endpointRule2(QueryEndpointParams params, RulePartitio } return RuleResult.endpoint(Endpoint .builder() - .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dnsSuffix())) + .endpointUrl(EndpointUrl.fromComponents("https", params.endpointId() + ".query." + partitionResult.dnsSuffix(), -1, "")) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query").signingRegionSet(Arrays.asList("*")) @@ -109,7 +112,7 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio if (params.useFipsEndpoint() != null && params.useFipsEndpoint() && params.useDualStackEndpoint() == null) { return RuleResult.endpoint(Endpoint .builder() - .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dnsSuffix())) + .endpointUrl(EndpointUrl.fromComponents("https", "query-fips." + region + "." + partitionResult.dnsSuffix(), -1, "")) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -118,7 +121,8 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio if (params.useDualStackEndpoint() != null && params.useDualStackEndpoint() && params.useFipsEndpoint() == null) { return RuleResult.endpoint(Endpoint .builder() - .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dualStackDnsSuffix())) + .endpointUrl( + EndpointUrl.fromComponents("https", "query." + region + "." + partitionResult.dualStackDnsSuffix(), -1, "")) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -129,14 +133,16 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio && params.useFipsEndpoint()) { return RuleResult.endpoint(Endpoint .builder() - .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dualStackDnsSuffix())) + .endpointUrl( + EndpointUrl.fromComponents("https", "query-fips." + region + "." + partitionResult.dualStackDnsSuffix(), -1, + "")) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") .signingRegionSet(Arrays.asList("*")).build())).build()); } return RuleResult.endpoint(Endpoint.builder() - .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dnsSuffix())).build()); + .endpointUrl(EndpointUrl.fromComponents("https", "query." + region + "." + partitionResult.dnsSuffix(), -1, "")).build()); } return RuleResult.carryOn(); } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-know-prop-override-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-know-prop-override-class.java index 2edf6b89329c..d01b466c69f1 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-know-prop-override-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-know-prop-override-class.java @@ -66,8 +66,9 @@ private static RuleResult endpointRule1(QueryEndpointParams params, String regio if (parsedArn != null) { return RuleResult.endpoint(Endpoint .builder() - .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." - + partitionResult.dualStackDnsSuffix())) + .endpointUrl( + EndpointUrl.fromComponents("https", + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix(), -1, "")) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -87,7 +88,9 @@ private static RuleResult endpointRule2(QueryEndpointParams params, RulePartitio if (params.useFipsEndpoint() == null && params.useDualStackEndpoint() != null && params.useDualStackEndpoint()) { return RuleResult.endpoint(Endpoint .builder() - .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix())) + .endpointUrl( + EndpointUrl.fromComponents("https", params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix(), + -1, "")) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -95,7 +98,7 @@ private static RuleResult endpointRule2(QueryEndpointParams params, RulePartitio } return RuleResult.endpoint(Endpoint .builder() - .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dnsSuffix())) + .endpointUrl(EndpointUrl.fromComponents("https", params.endpointId() + ".query." + partitionResult.dnsSuffix(), -1, "")) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query").signingRegionSet(Arrays.asList("*")) @@ -109,7 +112,7 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio if (params.useFipsEndpoint() != null && params.useFipsEndpoint() && params.useDualStackEndpoint() == null) { return RuleResult.endpoint(Endpoint .builder() - .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dnsSuffix())) + .endpointUrl(EndpointUrl.fromComponents("https", "query-fips." + region + "." + partitionResult.dnsSuffix(), -1, "")) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -118,7 +121,8 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio if (params.useDualStackEndpoint() != null && params.useDualStackEndpoint() && params.useFipsEndpoint() == null) { return RuleResult.endpoint(Endpoint .builder() - .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dualStackDnsSuffix())) + .endpointUrl( + EndpointUrl.fromComponents("https", "query." + region + "." + partitionResult.dualStackDnsSuffix(), -1, "")) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -129,14 +133,16 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio && params.useFipsEndpoint()) { return RuleResult.endpoint(Endpoint .builder() - .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dualStackDnsSuffix())) + .endpointUrl( + EndpointUrl.fromComponents("https", "query-fips." + region + "." + partitionResult.dualStackDnsSuffix(), -1, + "")) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") .signingRegionSet(Arrays.asList("*")).build())).build()); } return RuleResult.endpoint(Endpoint.builder() - .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dnsSuffix())).build()); + .endpointUrl(EndpointUrl.fromComponents("https", "query." + region + "." + partitionResult.dnsSuffix(), -1, "")).build()); } return RuleResult.carryOn(); } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-metric-values-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-metric-values-class.java index a086d3466d4e..cadeb0b86890 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-metric-values-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-metric-values-class.java @@ -69,20 +69,22 @@ private static RuleResult endpointRule2(QueryEndpointParams params, RulePartitio } if (params.useFipsEndpoint() == null && params.useDualStackEndpoint() != null && params.useDualStackEndpoint()) { return RuleResult.endpoint(Endpoint - .builder() - .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix())) - .putAttribute( - AwsEndpointAttribute.AUTH_SCHEMES, - Arrays.asList(SigV4aAuthScheme.builder().signingName("query") - .signingRegionSet(Arrays.asList("*")).build())).build()); + .builder() + .endpointUrl( + EndpointUrl.fromComponents("https", params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix(), + -1, "")) + .putAttribute( + AwsEndpointAttribute.AUTH_SCHEMES, + Arrays.asList(SigV4aAuthScheme.builder().signingName("query") + .signingRegionSet(Arrays.asList("*")).build())).build()); } return RuleResult.endpoint(Endpoint - .builder() - .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dnsSuffix())) - .putAttribute( - AwsEndpointAttribute.AUTH_SCHEMES, - Arrays.asList(SigV4aAuthScheme.builder().signingName("query").signingRegionSet(Arrays.asList("*")) - .build())).putAttribute(AwsEndpointAttribute.METRIC_VALUES, Arrays.asList("1", "2")).build()); + .builder() + .endpointUrl(EndpointUrl.fromComponents("https", params.endpointId() + ".query." + partitionResult.dnsSuffix(), -1, "")) + .putAttribute( + AwsEndpointAttribute.AUTH_SCHEMES, + Arrays.asList(SigV4aAuthScheme.builder().signingName("query").signingRegionSet(Arrays.asList("*")) + .build())).putAttribute(AwsEndpointAttribute.METRIC_VALUES, Arrays.asList("1", "2")).build()); } return RuleResult.carryOn(); } @@ -91,35 +93,38 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio if (RulesFunctions.isValidHostLabel(region, false)) { if (params.useFipsEndpoint() != null && params.useFipsEndpoint() && params.useDualStackEndpoint() == null) { return RuleResult.endpoint(Endpoint - .builder() - .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dnsSuffix())) - .putAttribute( - AwsEndpointAttribute.AUTH_SCHEMES, - Arrays.asList(SigV4aAuthScheme.builder().signingName("query") - .signingRegionSet(Arrays.asList("*")).build())).build()); + .builder() + .endpointUrl(EndpointUrl.fromComponents("https", "query-fips." + region + "." + partitionResult.dnsSuffix(), -1, "")) + .putAttribute( + AwsEndpointAttribute.AUTH_SCHEMES, + Arrays.asList(SigV4aAuthScheme.builder().signingName("query") + .signingRegionSet(Arrays.asList("*")).build())).build()); } if (params.useDualStackEndpoint() != null && params.useDualStackEndpoint() && params.useFipsEndpoint() == null) { return RuleResult.endpoint(Endpoint - .builder() - .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dualStackDnsSuffix())) - .putAttribute( - AwsEndpointAttribute.AUTH_SCHEMES, - Arrays.asList(SigV4aAuthScheme.builder().signingName("query") - .signingRegionSet(Arrays.asList("*")).build(), - SigV4AuthScheme.builder().signingName("query").signingRegion(region).build())).build()); + .builder() + .endpointUrl( + EndpointUrl.fromComponents("https", "query." + region + "." + partitionResult.dualStackDnsSuffix(), -1, "")) + .putAttribute( + AwsEndpointAttribute.AUTH_SCHEMES, + Arrays.asList(SigV4aAuthScheme.builder().signingName("query") + .signingRegionSet(Arrays.asList("*")).build(), + SigV4AuthScheme.builder().signingName("query").signingRegion(region).build())).build()); } if (params.useDualStackEndpoint() != null && params.useFipsEndpoint() != null && params.useDualStackEndpoint() - && params.useFipsEndpoint()) { + && params.useFipsEndpoint()) { return RuleResult.endpoint(Endpoint - .builder() - .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dualStackDnsSuffix())) - .putAttribute( - AwsEndpointAttribute.AUTH_SCHEMES, - Arrays.asList(SigV4aAuthScheme.builder().signingName("query") - .signingRegionSet(Arrays.asList("*")).build())).build()); + .builder() + .endpointUrl( + EndpointUrl.fromComponents("https", "query-fips." + region + "." + partitionResult.dualStackDnsSuffix(), -1, + "")) + .putAttribute( + AwsEndpointAttribute.AUTH_SCHEMES, + Arrays.asList(SigV4aAuthScheme.builder().signingName("query") + .signingRegionSet(Arrays.asList("*")).build())).build()); } return RuleResult.endpoint(Endpoint.builder() - .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dnsSuffix())).build()); + .endpointUrl(EndpointUrl.fromComponents("https", "query." + region + "." + partitionResult.dnsSuffix(), -1, "")).build()); } return RuleResult.carryOn(); } 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 index 2edf6b89329c..d01b466c69f1 100644 --- 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 @@ -66,8 +66,9 @@ private static RuleResult endpointRule1(QueryEndpointParams params, String regio if (parsedArn != null) { return RuleResult.endpoint(Endpoint .builder() - .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." - + partitionResult.dualStackDnsSuffix())) + .endpointUrl( + EndpointUrl.fromComponents("https", + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix(), -1, "")) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -87,7 +88,9 @@ private static RuleResult endpointRule2(QueryEndpointParams params, RulePartitio if (params.useFipsEndpoint() == null && params.useDualStackEndpoint() != null && params.useDualStackEndpoint()) { return RuleResult.endpoint(Endpoint .builder() - .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix())) + .endpointUrl( + EndpointUrl.fromComponents("https", params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix(), + -1, "")) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -95,7 +98,7 @@ private static RuleResult endpointRule2(QueryEndpointParams params, RulePartitio } return RuleResult.endpoint(Endpoint .builder() - .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dnsSuffix())) + .endpointUrl(EndpointUrl.fromComponents("https", params.endpointId() + ".query." + partitionResult.dnsSuffix(), -1, "")) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query").signingRegionSet(Arrays.asList("*")) @@ -109,7 +112,7 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio if (params.useFipsEndpoint() != null && params.useFipsEndpoint() && params.useDualStackEndpoint() == null) { return RuleResult.endpoint(Endpoint .builder() - .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dnsSuffix())) + .endpointUrl(EndpointUrl.fromComponents("https", "query-fips." + region + "." + partitionResult.dnsSuffix(), -1, "")) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -118,7 +121,8 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio if (params.useDualStackEndpoint() != null && params.useDualStackEndpoint() && params.useFipsEndpoint() == null) { return RuleResult.endpoint(Endpoint .builder() - .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dualStackDnsSuffix())) + .endpointUrl( + EndpointUrl.fromComponents("https", "query." + region + "." + partitionResult.dualStackDnsSuffix(), -1, "")) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") @@ -129,14 +133,16 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio && params.useFipsEndpoint()) { return RuleResult.endpoint(Endpoint .builder() - .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dualStackDnsSuffix())) + .endpointUrl( + EndpointUrl.fromComponents("https", "query-fips." + region + "." + partitionResult.dualStackDnsSuffix(), -1, + "")) .putAttribute( AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().signingName("query") .signingRegionSet(Arrays.asList("*")).build())).build()); } return RuleResult.endpoint(Endpoint.builder() - .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dnsSuffix())).build()); + .endpointUrl(EndpointUrl.fromComponents("https", "query." + region + "." + partitionResult.dnsSuffix(), -1, "")).build()); } return RuleResult.carryOn(); } diff --git a/core/endpoints-spi/src/main/java/software/amazon/awssdk/endpoints/EndpointUrl.java b/core/endpoints-spi/src/main/java/software/amazon/awssdk/endpoints/EndpointUrl.java index b6b9594beb45..43a8ac97ea00 100644 --- a/core/endpoints-spi/src/main/java/software/amazon/awssdk/endpoints/EndpointUrl.java +++ b/core/endpoints-spi/src/main/java/software/amazon/awssdk/endpoints/EndpointUrl.java @@ -154,6 +154,11 @@ public static EndpointUrl fromString(String url) { * The {@code rawUrl} field is {@code null} in this case, so {@link #toUri()} reconstructs * the URI from components. No query or fragment is included. * + *

Equivalence contract: For any valid endpoint URL string {@code s} with no query or fragment, + * {@code fromComponents(scheme, host, port, path)} MUST produce an {@code EndpointUrl} that equals + * {@code fromString(scheme + "://" + host + (port >= 0 ? ":" + port : "") + path)}. + * The codegen relies on this equivalence — see {@code EndpointUrlCodeEmitter}. + * * @param scheme the URL scheme (e.g., "https") * @param host the hostname (e.g., "s3.us-east-1.amazonaws.com") * @param port the port number, or -1 if not specified diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/compiledendpointrules/endpoints/internal/EndpointUrlConformanceTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/compiledendpointrules/endpoints/internal/EndpointUrlConformanceTest.java new file mode 100644 index 000000000000..2c4212607a29 --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/compiledendpointrules/endpoints/internal/EndpointUrlConformanceTest.java @@ -0,0 +1,136 @@ +/* + * 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.compiledendpointrules.endpoints.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.endpoints.Endpoint; +import software.amazon.awssdk.endpoints.EndpointUrl; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.compiledendpointrules.endpoints.CompiledEndpointRulesEndpointParams; +import software.amazon.awssdk.services.compiledendpointrules.endpoints.CompiledEndpointRulesEndpointProvider; + +/** + * Conformance test verifying that the build-time decomposed EndpointUrl (via {@code EndpointUrl.fromComponents()}) + * produces results equivalent to runtime parsing (via {@code EndpointUrl.fromString()}). + + *

This guards against drift between the build-time ({@code EndpointUrlCodeEmitter}) and runtime + * ({@code EndpointUrl.fromString()}) URL decomposition logic. + */ +class EndpointUrlConformanceTest { + + private static final CompiledEndpointRulesEndpointProvider PROVIDER = + CompiledEndpointRulesEndpointProvider.defaultProvider(); + + /** + * Test cases covering the standard region-based resolution path (uses fromComponents in generated code) + * and the custom endpoint override path (uses fromString in generated code). + */ + static List testCases() { + return Arrays.asList( + // Standard region resolution — exercises fromComponents code path + TestCase.ofRegion("us-east-1", "https://compiledendpointrules.us-east-1.amazonaws.com"), + TestCase.ofRegion("us-west-2", "https://compiledendpointrules.us-west-2.amazonaws.com"), + TestCase.ofRegion("eu-west-1", "https://compiledendpointrules.eu-west-1.amazonaws.com"), + TestCase.ofRegion("ap-southeast-1", "https://compiledendpointrules.ap-southeast-1.amazonaws.com"), + TestCase.ofRegion("cn-north-1", "https://compiledendpointrules.cn-north-1.amazonaws.com.cn"), + TestCase.ofRegion("us-gov-west-1", "https://compiledendpointrules.us-gov-west-1.amazonaws.com"), + + // Custom endpoint override — exercises fromString code path + TestCase.ofEndpoint("us-east-1", "https://custom.example.com", + "https://custom.example.com"), + TestCase.ofEndpoint("us-east-1", "https://custom.example.com:8443", + "https://custom.example.com:8443"), + TestCase.ofEndpoint("us-east-1", "https://custom.example.com/base/path", + "https://custom.example.com/base/path"), + TestCase.ofEndpoint("us-east-1", "http://localhost:4566", + "http://localhost:4566") + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("testCases") + void resolvedEndpointUrl_componentsAreConsistentWithUri(TestCase testCase) throws Exception { + CompletableFuture future = PROVIDER.resolveEndpoint(testCase.params()); + Endpoint endpoint = future.get(); + EndpointUrl endpointUrl = endpoint.endpointUrl(); + + // 1. Verify the resolved URL matches what we expect + assertThat(endpointUrl.toUri()).isEqualTo(URI.create(testCase.expectedUrl)); + + // 2. Verify component getters are consistent with toUri() + URI uri = endpointUrl.toUri(); + assertThat(endpointUrl.scheme()).isEqualTo(uri.getScheme()); + assertThat(endpointUrl.host()).isEqualTo(uri.getHost()); + assertThat(endpointUrl.port()).isEqualTo(uri.getPort()); + String expectedPath = uri.getRawPath() != null ? uri.getRawPath() : ""; + assertThat(endpointUrl.encodedPath()).isEqualTo(expectedPath); + + // 3. Verify round-trip: reconstruct URL string from components, re-parse, and compare + String reconstructed = endpointUrl.scheme() + "://" + endpointUrl.host() + + (endpointUrl.port() >= 0 ? ":" + endpointUrl.port() : "") + + endpointUrl.encodedPath(); + EndpointUrl reparsed = EndpointUrl.fromString(reconstructed); + + assertThat(reparsed.scheme()).isEqualTo(endpointUrl.scheme()); + assertThat(reparsed.host()).isEqualTo(endpointUrl.host()); + assertThat(reparsed.port()).isEqualTo(endpointUrl.port()); + assertThat(reparsed.encodedPath()).isEqualTo(endpointUrl.encodedPath()); + assertThat(reparsed).isEqualTo(endpointUrl); + } + + static class TestCase { + private final String description; + private final Region region; + private final String customEndpoint; + private final String expectedUrl; + + TestCase(String description, Region region, String customEndpoint, String expectedUrl) { + this.description = description; + this.region = region; + this.customEndpoint = customEndpoint; + this.expectedUrl = expectedUrl; + } + + static TestCase ofRegion(String regionId, String expectedUrl) { + return new TestCase("region=" + regionId, Region.of(regionId), null, expectedUrl); + } + + static TestCase ofEndpoint(String regionId, String endpoint, String expectedUrl) { + return new TestCase("endpoint=" + endpoint, Region.of(regionId), endpoint, expectedUrl); + } + + CompiledEndpointRulesEndpointParams params() { + CompiledEndpointRulesEndpointParams.Builder builder = CompiledEndpointRulesEndpointParams.builder() + .region(region); + if (customEndpoint != null) { + builder.endpoint(customEndpoint); + } + return builder.build(); + } + + @Override + public String toString() { + return description; + } + } +} From efb4194327c606b290013319bef8338a9f90ea1f Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Tue, 21 Jul 2026 13:33:03 -0700 Subject: [PATCH 5/5] Add changelog --- .changes/next-release/bugfix-AWSSDKforJavav2-e78ae1f.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changes/next-release/bugfix-AWSSDKforJavav2-e78ae1f.json diff --git a/.changes/next-release/bugfix-AWSSDKforJavav2-e78ae1f.json b/.changes/next-release/bugfix-AWSSDKforJavav2-e78ae1f.json new file mode 100644 index 000000000000..210227afd6eb --- /dev/null +++ b/.changes/next-release/bugfix-AWSSDKforJavav2-e78ae1f.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Improve endpoint resolution performance by replacing URI.create with a lightweight EndpointUrl." +}