Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -28,14 +29,21 @@
*/
@SdkPublicApi
public final class Endpoint {
/**
* Initial capacity for the attribute and header maps. Endpoints carry very few of either, so the
* default capacity of 16 is extra overhead.
*/
private static final int ATTRIBUTE_MAP_CAPACITY = 4;
private static final int HEADER_MAP_CAPACITY = 4;

private final EndpointUrl endpointUrl;
private final Map<String, List<String>> headers;
private final Map<EndpointAttributeKey<?>, Object> attributes;

private Endpoint(BuilderImpl b) {
this.endpointUrl = b.endpointUrl;
this.headers = b.headers;
this.attributes = b.attributes;
this.headers = b.headers == null ? Collections.emptyMap() : Collections.unmodifiableMap(b.headers);
this.attributes = b.buildAttributes();
}

/**
Expand Down Expand Up @@ -137,20 +145,67 @@ default Builder endpointUrl(EndpointUrl endpointUrl) {

private static class BuilderImpl implements Builder {
private EndpointUrl endpointUrl;
private final Map<String, List<String>> headers = new HashMap<>();
private final Map<EndpointAttributeKey<?>, Object> attributes = new HashMap<>();

/**
* Most endpoints declare no headers, so the map is allocated only once a header is added.
*/
private Map<String, List<String>> headers;

/**
* Endpoints almost always carry zero or one attribute (typically {@code AUTH_SCHEMES}), so the first
* entry is held in these two fields and {@link #attributes} is allocated only if a second distinct
* key arrives. This keeps the common cases free of a {@code HashMap} and its backing table.
*/
private EndpointAttributeKey<?> firstAttributeKey;
private Object firstAttributeValue;
private Map<EndpointAttributeKey<?>, Object> attributes;

private BuilderImpl() {
}

private BuilderImpl(Endpoint e) {
this.endpointUrl = e.endpointUrl;
if (e.headers != null) {
if (!e.headers.isEmpty()) {
this.headers = new HashMap<>(Math.max(HEADER_MAP_CAPACITY, e.headers.size()));
e.headers.forEach((n, v) -> {
this.headers.put(n, new ArrayList<>(v));
});
}
this.attributes.putAll(e.attributes);
e.attributes.forEach(this::putAttributeUnchecked);
}

/**
* Collapses the staged attributes into the smallest immutable map that can hold them.
*/
private Map<EndpointAttributeKey<?>, Object> buildAttributes() {
if (attributes != null) {
return Collections.unmodifiableMap(attributes);
}
if (firstAttributeKey != null) {
return Collections.singletonMap(firstAttributeKey, firstAttributeValue);
}
return Collections.emptyMap();
}

/**
* Stores an attribute without the generic key/value pairing, for use by callers that have already
* had that relationship checked (the {@link #putAttribute} overload and the copy constructor).
*/
private void putAttributeUnchecked(EndpointAttributeKey<?> key, Object value) {
if (attributes != null) {
attributes.put(key, value);
} else if (firstAttributeKey == null || firstAttributeKey.equals(key)) {
firstAttributeKey = key;
firstAttributeValue = value;
} else {
// Sized for the realistic maximum rather than the default 16, whose backing table alone
// costs more than every other allocation on this path combined.
attributes = new HashMap<>(ATTRIBUTE_MAP_CAPACITY);
attributes.put(firstAttributeKey, firstAttributeValue);
attributes.put(key, value);
firstAttributeKey = null;
firstAttributeValue = null;
}
}

@SuppressWarnings("deprecation")
Expand All @@ -168,14 +223,17 @@ public Builder endpointUrl(EndpointUrl endpointUrl) {

@Override
public Builder putHeader(String name, String value) {
if (this.headers == null) {
this.headers = new HashMap<>(HEADER_MAP_CAPACITY);
}
List<String> values = this.headers.computeIfAbsent(name, (n) -> new ArrayList<>());
values.add(value);
return this;
}

@Override
public <T> Builder putAttribute(EndpointAttributeKey<T> key, T value) {
this.attributes.put(key, value);
putAttributeUnchecked(key, value);
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
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;
Expand Down Expand Up @@ -133,4 +134,105 @@ public void endpointUrlAccessor_returnsCorrectComponents() {
assertThat(endpointUrl.encodedPath()).isEqualTo("/bucket");
assertThat(endpointUrl.queryAndFragment()).isEmpty();
}

@Test
public void build_noHeadersOrAttributes_returnsEmptyMaps() {
Endpoint endpoint = Endpoint.builder()
.endpointUrl(EndpointUrl.fromString("https://example.com"))
.build();

assertThat(endpoint.headers()).isEmpty();
assertThat(endpoint.attribute(TEST_STRING_ATTR)).isNull();
}

@Test
public void headers_isUnmodifiable() {
Endpoint noHeaders = Endpoint.builder()
.endpointUrl(EndpointUrl.fromString("https://example.com"))
.build();
Endpoint withHeaders = Endpoint.builder()
.endpointUrl(EndpointUrl.fromString("https://example.com"))
.putHeader("foo", "bar")
.build();

assertThatThrownBy(() -> noHeaders.headers().put("a", Arrays.asList("b")))
.isInstanceOf(UnsupportedOperationException.class);
assertThatThrownBy(() -> withHeaders.headers().put("a", Arrays.asList("b")))
.isInstanceOf(UnsupportedOperationException.class);
}

/**
* A single attribute is staged inline rather than in a map, so exercise the read path for one, two and
* three attributes as well as overwriting the staged entry.
*/
@Test
public void putAttribute_variousArities_allReadable() {
EndpointAttributeKey<String> second = new EndpointAttributeKey<>("Second", String.class);
EndpointAttributeKey<String> third = new EndpointAttributeKey<>("Third", String.class);

Endpoint one = Endpoint.builder()
.endpointUrl(EndpointUrl.fromString("https://example.com"))
.putAttribute(TEST_STRING_ATTR, "a")
.build();
assertThat(one.attribute(TEST_STRING_ATTR)).isEqualTo("a");
assertThat(one.attribute(second)).isNull();

Endpoint two = Endpoint.builder()
.endpointUrl(EndpointUrl.fromString("https://example.com"))
.putAttribute(TEST_STRING_ATTR, "a")
.putAttribute(second, "b")
.build();
assertThat(two.attribute(TEST_STRING_ATTR)).isEqualTo("a");
assertThat(two.attribute(second)).isEqualTo("b");

Endpoint three = Endpoint.builder()
.endpointUrl(EndpointUrl.fromString("https://example.com"))
.putAttribute(TEST_STRING_ATTR, "a")
.putAttribute(second, "b")
.putAttribute(third, "c")
.build();
assertThat(three.attribute(TEST_STRING_ATTR)).isEqualTo("a");
assertThat(three.attribute(second)).isEqualTo("b");
assertThat(three.attribute(third)).isEqualTo("c");
}

@Test
public void putAttribute_sameKeyTwice_lastValueWins() {
EndpointAttributeKey<String> second = new EndpointAttributeKey<>("Second", String.class);

Endpoint staged = Endpoint.builder()
.endpointUrl(EndpointUrl.fromString("https://example.com"))
.putAttribute(TEST_STRING_ATTR, "first")
.putAttribute(TEST_STRING_ATTR, "second")
.build();
assertThat(staged.attribute(TEST_STRING_ATTR)).isEqualTo("second");

// Same key overwritten after the builder has been promoted to a map.
Endpoint promoted = Endpoint.builder()
.endpointUrl(EndpointUrl.fromString("https://example.com"))
.putAttribute(TEST_STRING_ATTR, "first")
.putAttribute(second, "other")
.putAttribute(TEST_STRING_ATTR, "second")
.build();
assertThat(promoted.attribute(TEST_STRING_ATTR)).isEqualTo("second");
assertThat(promoted.attribute(second)).isEqualTo("other");
}

@Test
public void toBuilder_roundTripsAllAttributeArities() {
EndpointAttributeKey<String> second = new EndpointAttributeKey<>("Second", String.class);

Endpoint none = Endpoint.builder()
.endpointUrl(EndpointUrl.fromString("https://example.com"))
.build();
assertThat(none.toBuilder().build()).isEqualTo(none);

Endpoint one = none.toBuilder().putAttribute(TEST_STRING_ATTR, "a").build();
assertThat(one.toBuilder().build()).isEqualTo(one);

Endpoint two = one.toBuilder().putAttribute(second, "b").build();
assertThat(two.toBuilder().build()).isEqualTo(two);
assertThat(two.attribute(TEST_STRING_ATTR)).isEqualTo("a");
assertThat(two.attribute(second)).isEqualTo("b");
}
}
Loading