Skip to content

Commit 3d38a08

Browse files
Add support for marshalling lists of strings in HTTP headers... (aws#2588)
* Add support for marshalling lists of strings in HTTP headers... ...for JSON/XML protocols. ## Motivation and Context We currently lack support for marshalling lists of strings (and enums) in HTTP headers. Other IDLs and modeling languages, e.g., Smithy, do support such bindings and services may come to expect such support: https://awslabs.github.io/smithy/1.0/spec/core/http-traits.html#httpheader-trait ## Description * Add support for marshalling a list of strings to relevant JSON and XML marshalling classes * Consistent with the existing XML HeaderUnmarshaller map implementation, unmarshalling only supports string value types (this can be extended in the future if needed) * Marshalling implementations are changed to use appendHeader rather than putHeader * Add convenience method SdkField#getRequiredTrait(..) to support the common use case of ensuring a trait is present; update other existing use cases to utilize this method * Update relevant test classes to correctly recognize headers as a Map<String, List<String>> * Only wrap marshalling assertion exceptions as runtime when needed, minimizing stack trace noise ## Testing New tests added to: * rest-json-input.json * rest-json-output.json * rest-xml-input.json * rest-xml-output.json
1 parent ef15b89 commit 3d38a08

File tree

22 files changed

+414
-20
lines changed

22 files changed

+414
-20
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "AWS SDK for Java v2",
3+
"contributor": "",
4+
"type": "feature",
5+
"description": "Add support for marshalling lists of strings in HTTP headers"
6+
}

core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/HeaderMarshaller.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,16 @@
1515

1616
package software.amazon.awssdk.protocols.json.internal.marshall;
1717

18+
import static software.amazon.awssdk.utils.CollectionUtils.isNullOrEmpty;
19+
1820
import java.nio.charset.StandardCharsets;
1921
import java.time.Instant;
22+
import java.util.List;
2023
import software.amazon.awssdk.annotations.SdkInternalApi;
2124
import software.amazon.awssdk.core.SdkField;
25+
import software.amazon.awssdk.core.protocol.MarshallLocation;
2226
import software.amazon.awssdk.core.traits.JsonValueTrait;
27+
import software.amazon.awssdk.core.traits.ListTrait;
2328
import software.amazon.awssdk.protocols.core.ValueToStringConverter;
2429
import software.amazon.awssdk.utils.BinaryUtils;
2530

@@ -45,6 +50,19 @@ public final class HeaderMarshaller {
4550
public static final JsonMarshaller<Instant> INSTANT
4651
= new SimpleHeaderMarshaller<>(JsonProtocolMarshaller.INSTANT_VALUE_TO_STRING);
4752

53+
public static final JsonMarshaller<List<?>> LIST = (list, context, paramName, sdkField) -> {
54+
// Null or empty lists cannot be meaningfully (or safely) represented in an HTTP header message since header-fields must
55+
// typically have a non-empty field-value. https://datatracker.ietf.org/doc/html/rfc7230#section-3.2
56+
if (isNullOrEmpty(list)) {
57+
return;
58+
}
59+
SdkField memberFieldInfo = sdkField.getRequiredTrait(ListTrait.class).memberFieldInfo();
60+
for (Object listValue : list) {
61+
JsonMarshaller marshaller = context.marshallerRegistry().getMarshaller(MarshallLocation.HEADER, listValue);
62+
marshaller.marshall(listValue, context, paramName, memberFieldInfo);
63+
}
64+
};
65+
4866
private HeaderMarshaller() {
4967
}
5068

@@ -58,8 +76,7 @@ private SimpleHeaderMarshaller(ValueToStringConverter.ValueToString<T> converter
5876

5977
@Override
6078
public void marshall(T val, JsonMarshallerContext context, String paramName, SdkField<T> sdkField) {
61-
context.request().putHeader(paramName, converter.convert(val, sdkField));
79+
context.request().appendHeader(paramName, converter.convert(val, sdkField));
6280
}
6381
}
64-
6582
}

core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/JsonProtocolMarshaller.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ private static JsonMarshallerRegistry createMarshallerRegistry() {
110110
.headerMarshaller(MarshallingType.FLOAT, HeaderMarshaller.FLOAT)
111111
.headerMarshaller(MarshallingType.BOOLEAN, HeaderMarshaller.BOOLEAN)
112112
.headerMarshaller(MarshallingType.INSTANT, HeaderMarshaller.INSTANT)
113+
.headerMarshaller(MarshallingType.LIST, HeaderMarshaller.LIST)
113114
.headerMarshaller(MarshallingType.NULL, JsonMarshaller.NULL)
114115

115116
.queryParamMarshaller(MarshallingType.STRING, QueryParamMarshaller.STRING)

core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/unmarshall/HeaderUnmarshaller.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,18 @@
1515

1616
package software.amazon.awssdk.protocols.json.internal.unmarshall;
1717

18+
import static java.util.stream.Collectors.toList;
19+
1820
import java.nio.charset.StandardCharsets;
1921
import java.time.Instant;
22+
import java.util.List;
2023
import software.amazon.awssdk.annotations.SdkInternalApi;
2124
import software.amazon.awssdk.core.SdkField;
2225
import software.amazon.awssdk.core.traits.JsonValueTrait;
2326
import software.amazon.awssdk.protocols.core.StringToValueConverter;
2427
import software.amazon.awssdk.protocols.json.internal.dom.SdkJsonNode;
2528
import software.amazon.awssdk.utils.BinaryUtils;
29+
import software.amazon.awssdk.utils.http.SdkHttpUtils;
2630

2731
/**
2832
* Header unmarshallers for all the simple types we support.
@@ -39,6 +43,11 @@ final class HeaderUnmarshaller {
3943
public static final JsonUnmarshaller<Boolean> BOOLEAN = new SimpleHeaderUnmarshaller<>(StringToValueConverter.TO_BOOLEAN);
4044
public static final JsonUnmarshaller<Float> FLOAT = new SimpleHeaderUnmarshaller<>(StringToValueConverter.TO_FLOAT);
4145

46+
// Only supports string value type
47+
public static final JsonUnmarshaller<List<?>> LIST = (context, jsonContent, field) -> {
48+
return SdkHttpUtils.allMatchingHeaders(context.response().headers(), field.locationName()).collect(toList());
49+
};
50+
4251
private HeaderUnmarshaller() {
4352
}
4453

core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/unmarshall/JsonProtocolUnmarshaller.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ private static JsonUnmarshallerRegistry createUnmarshallerRegistry(
8282
.headerUnmarshaller(MarshallingType.BOOLEAN, HeaderUnmarshaller.BOOLEAN)
8383
.headerUnmarshaller(MarshallingType.INSTANT, HeaderUnmarshaller.createInstantHeaderUnmarshaller(instantStringToValue))
8484
.headerUnmarshaller(MarshallingType.FLOAT, HeaderUnmarshaller.FLOAT)
85+
.headerUnmarshaller(MarshallingType.LIST, HeaderUnmarshaller.LIST)
8586

8687
.payloadUnmarshaller(MarshallingType.STRING, new SimpleTypeJsonUnmarshaller<>(StringToValueConverter.TO_STRING))
8788
.payloadUnmarshaller(MarshallingType.INTEGER, new SimpleTypeJsonUnmarshaller<>(StringToValueConverter.TO_INTEGER))

core/protocols/aws-xml-protocol/src/main/java/software/amazon/awssdk/protocols/xml/internal/marshall/HeaderMarshaller.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,15 @@
1515

1616
package software.amazon.awssdk.protocols.xml.internal.marshall;
1717

18+
import static software.amazon.awssdk.utils.CollectionUtils.isNullOrEmpty;
19+
1820
import java.time.Instant;
21+
import java.util.List;
1922
import java.util.Map;
2023
import software.amazon.awssdk.annotations.SdkInternalApi;
2124
import software.amazon.awssdk.core.SdkField;
2225
import software.amazon.awssdk.core.protocol.MarshallLocation;
26+
import software.amazon.awssdk.core.traits.ListTrait;
2327
import software.amazon.awssdk.protocols.core.ValueToStringConverter;
2428

2529
@SdkInternalApi
@@ -62,10 +66,30 @@ public void marshall(Map<String, ?> map, XmlMarshallerContext context, String pa
6266

6367
@Override
6468
protected boolean shouldEmit(Map map) {
65-
return map != null && !map.isEmpty();
69+
return !isNullOrEmpty(map);
6670
}
6771
};
6872

73+
public static final XmlMarshaller<List<?>> LIST = new SimpleHeaderMarshaller<List<?>>(null) {
74+
@Override
75+
public void marshall(List<?> list, XmlMarshallerContext context, String paramName, SdkField<List<?>> sdkField) {
76+
if (!shouldEmit(list)) {
77+
return;
78+
}
79+
SdkField memberFieldInfo = sdkField.getRequiredTrait(ListTrait.class).memberFieldInfo();
80+
for (Object listValue : list) {
81+
XmlMarshaller marshaller = context.marshallerRegistry().getMarshaller(MarshallLocation.HEADER, listValue);
82+
marshaller.marshall(listValue, context, paramName, memberFieldInfo);
83+
}
84+
}
85+
86+
@Override
87+
protected boolean shouldEmit(List list) {
88+
// Null or empty lists cannot be meaningfully (or safely) represented in an HTTP header message since header-fields
89+
// must typically have a non-empty field-value. https://datatracker.ietf.org/doc/html/rfc7230#section-3.2
90+
return !isNullOrEmpty(list);
91+
}
92+
};
6993

7094
private HeaderMarshaller() {
7195
}
@@ -83,7 +107,7 @@ public void marshall(T val, XmlMarshallerContext context, String paramName, SdkF
83107
return;
84108
}
85109

86-
context.request().putHeader(paramName, converter.convert(val, sdkField));
110+
context.request().appendHeader(paramName, converter.convert(val, sdkField));
87111
}
88112

89113
protected boolean shouldEmit(T val) {

core/protocols/aws-xml-protocol/src/main/java/software/amazon/awssdk/protocols/xml/internal/marshall/QueryParamMarshaller.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,7 @@ public final class QueryParamMarshaller {
6060
return;
6161
}
6262

63-
MapTrait mapTrait = sdkField.getOptionalTrait(MapTrait.class)
64-
.orElseThrow(() -> new IllegalStateException("SdkField of list type is missing List trait"));
63+
MapTrait mapTrait = sdkField.getRequiredTrait(MapTrait.class);
6564
SdkField valueField = mapTrait.valueFieldInfo();
6665

6766
for (Map.Entry<String, ?> entry : map.entrySet()) {

core/protocols/aws-xml-protocol/src/main/java/software/amazon/awssdk/protocols/xml/internal/marshall/XmlPayloadMarshaller.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,7 @@ public void marshall(List<?> val, XmlMarshallerContext context, String paramName
8181
@Override
8282
public void marshall(List<?> list, XmlMarshallerContext context, String paramName,
8383
SdkField<List<?>> sdkField, ValueToStringConverter.ValueToString<List<?>> converter) {
84-
ListTrait listTrait = sdkField
85-
.getOptionalTrait(ListTrait.class)
86-
.orElseThrow(() -> new IllegalStateException(paramName + " member is missing ListTrait"));
84+
ListTrait listTrait = sdkField.getRequiredTrait(ListTrait.class);
8785

8886
if (!listTrait.isFlattened()) {
8987
context.xmlGenerator().startElement(paramName);
@@ -125,8 +123,7 @@ protected boolean shouldEmit(List list, String paramName) {
125123
public void marshall(Map<String, ?> map, XmlMarshallerContext context, String paramName,
126124
SdkField<Map<String, ?>> sdkField, ValueToStringConverter.ValueToString<Map<String, ?>> converter) {
127125

128-
MapTrait mapTrait = sdkField.getOptionalTrait(MapTrait.class)
129-
.orElseThrow(() -> new IllegalStateException(paramName + " member is missing MapTrait"));
126+
MapTrait mapTrait = sdkField.getRequiredTrait(MapTrait.class);
130127

131128
for (Map.Entry<String, ?> entry : map.entrySet()) {
132129
context.xmlGenerator().startElement("entry");

core/protocols/aws-xml-protocol/src/main/java/software/amazon/awssdk/protocols/xml/internal/marshall/XmlProtocolMarshaller.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ private static XmlMarshallerRegistry createMarshallerRegistry() {
176176
.headerMarshaller(MarshallingType.BOOLEAN, HeaderMarshaller.BOOLEAN)
177177
.headerMarshaller(MarshallingType.INSTANT, HeaderMarshaller.INSTANT)
178178
.headerMarshaller(MarshallingType.MAP, HeaderMarshaller.MAP)
179+
.headerMarshaller(MarshallingType.LIST, HeaderMarshaller.LIST)
179180
.headerMarshaller(MarshallingType.NULL, XmlMarshaller.NULL)
180181

181182
.queryParamMarshaller(MarshallingType.STRING, QueryParamMarshaller.STRING)

core/protocols/aws-xml-protocol/src/main/java/software/amazon/awssdk/protocols/xml/internal/unmarshall/HeaderUnmarshaller.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
package software.amazon.awssdk.protocols.xml.internal.unmarshall;
1717

18+
import static java.util.stream.Collectors.toList;
1819
import static software.amazon.awssdk.utils.StringUtils.replacePrefixIgnoreCase;
1920
import static software.amazon.awssdk.utils.StringUtils.startsWithIgnoreCase;
2021

@@ -26,6 +27,7 @@
2627
import software.amazon.awssdk.core.SdkField;
2728
import software.amazon.awssdk.protocols.core.StringToValueConverter;
2829
import software.amazon.awssdk.protocols.query.unmarshall.XmlElement;
30+
import software.amazon.awssdk.utils.http.SdkHttpUtils;
2931

3032
@SdkInternalApi
3133
public final class HeaderUnmarshaller {
@@ -39,6 +41,7 @@ public final class HeaderUnmarshaller {
3941
public static final XmlUnmarshaller<Instant> INSTANT =
4042
new SimpleHeaderUnmarshaller<>(XmlProtocolUnmarshaller.INSTANT_STRING_TO_VALUE);
4143

44+
// Only supports string value type
4245
public static final XmlUnmarshaller<Map<String, ?>> MAP = ((context, content, field) -> {
4346
Map<String, String> result = new HashMap<>();
4447
context.response().headers().entrySet().stream()
@@ -48,6 +51,11 @@ public final class HeaderUnmarshaller {
4851
return result;
4952
});
5053

54+
// Only supports string value type
55+
public static final XmlUnmarshaller<List<?>> LIST = (context, content, field) -> {
56+
return SdkHttpUtils.allMatchingHeaders(context.response().headers(), field.locationName()).collect(toList());
57+
};
58+
5159
private HeaderUnmarshaller() {
5260
}
5361

0 commit comments

Comments
 (0)