Skip to content

Commit 3d03932

Browse files
committed
Fix HeaderUnmarshaller to compare headers ignoring cases
1 parent 5dce825 commit 3d03932

File tree

8 files changed

+122
-11
lines changed

8 files changed

+122
-11
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"category": "AWS SDK for Java v2",
3+
"type": "bugfix",
4+
"description": "Fix HeaderUnmarshaller to compare header ignoring cases."
5+
}

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515

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

18+
import static software.amazon.awssdk.utils.StringUtils.replacePrefixIgnoreCase;
19+
import static software.amazon.awssdk.utils.StringUtils.startsWithIgnoreCase;
20+
1821
import java.time.Instant;
1922
import java.util.HashMap;
2023
import java.util.List;
2124
import java.util.Map;
22-
import java.util.stream.Collectors;
2325
import software.amazon.awssdk.annotations.SdkInternalApi;
2426
import software.amazon.awssdk.core.SdkField;
2527
import software.amazon.awssdk.protocols.core.StringToValueConverter;
@@ -39,9 +41,9 @@ public final class HeaderUnmarshaller {
3941
public static final XmlUnmarshaller<Map<String, ?>> MAP = ((context, content, field) -> {
4042
Map<String, String> result = new HashMap<>();
4143
context.response().headers().entrySet().stream()
42-
.filter(e -> e.getKey().startsWith(field.locationName()))
43-
.forEach(e -> result.put(e.getKey().replace(field.locationName(), ""),
44-
e.getValue().stream().collect(Collectors.joining(","))));
44+
.filter(e -> startsWithIgnoreCase(e.getKey(), field.locationName()))
45+
.forEach(e -> result.put(replacePrefixIgnoreCase(e.getKey(), field.locationName(), ""),
46+
String.join(",", e.getValue())));
4547
return result;
4648
});
4749

services/s3/src/it/java/software/amazon/awssdk/services/s3/UserMetadataIntegrationTest.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public class UserMetadataIntegrationTest extends S3IntegrationTestBase {
3636
* The S3 bucket created and used by these tests.
3737
*/
3838
private static final String BUCKET_NAME = temporaryBucketName("user-metadata-integ-test");
39+
40+
private static final String KEY = "UserMetadataIntegrationTest";
3941
/**
4042
* Length of the data uploaded to S3.
4143
*/
@@ -54,6 +56,10 @@ public static void initializeTestData() throws Exception {
5456
createBucket(BUCKET_NAME);
5557

5658
file = new RandomTempFile("user-metadata-integ-test-" + new Date().getTime(), CONTENT_LENGTH);
59+
s3.putObject(PutObjectRequest.builder()
60+
.bucket(BUCKET_NAME)
61+
.key(KEY)
62+
.build(), file.toPath());
5763
}
5864

5965
@AfterClass
@@ -67,11 +73,15 @@ public void putObject_PutsUserMetadata() throws Exception {
6773
userMetadata.put("thing1", "IAmThing1");
6874
userMetadata.put("thing2", "IAmThing2");
6975

76+
String mixedCasePrefix = "x-AmZ-mEtA-";
77+
String metadataKey = "test";
78+
7079
final String key = "user-metadata-key";
7180
s3.putObject(PutObjectRequest.builder()
7281
.bucket(BUCKET_NAME)
7382
.key(key)
7483
.metadata(userMetadata)
84+
.overrideConfiguration(b -> b.putHeader(mixedCasePrefix + metadataKey, "test"))
7585
.build(),
7686
RequestBody.fromFile(file));
7787

@@ -83,5 +93,6 @@ public void putObject_PutsUserMetadata() throws Exception {
8393
Map<String, String> returnedMetadata = response.metadata();
8494

8595
assertThat(returnedMetadata).containsAllEntriesOf(userMetadata);
96+
assertThat(returnedMetadata).containsKey(metadataKey);
8697
}
8798
}

services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/PutObjectHeaderTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
2424
import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
2525
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
26+
import static org.assertj.core.api.Assertions.assertThat;
2627
import static software.amazon.awssdk.http.Header.CONTENT_LENGTH;
2728
import static software.amazon.awssdk.http.Header.CONTENT_TYPE;
2829

@@ -41,6 +42,7 @@
4142
import software.amazon.awssdk.regions.Region;
4243
import software.amazon.awssdk.services.s3.S3Client;
4344
import software.amazon.awssdk.services.s3.S3Configuration;
45+
import software.amazon.awssdk.services.s3.model.HeadObjectResponse;
4446
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
4547
import software.amazon.awssdk.testutils.RandomTempFile;
4648

@@ -135,6 +137,22 @@ public void putObjectWithContentTypeHeader_shouldNotOverrideContentTypeInRawConf
135137
verify(putRequestedFor(anyUrl()).withHeader(CONTENT_TYPE, equalTo(contentType)));
136138
}
137139

140+
@Test
141+
public void headObject_userMetadataReturnMixedCaseMetadata() {
142+
String lowerCaseMetadataPrefix = "x-amz-meta-";
143+
String mixedCaseMetadataPrefix = "X-AmZ-MEta-";
144+
String metadataKey = "foo";
145+
String mixedCaseMetadataKey = "bAr";
146+
147+
stubFor(any(urlMatching(".*"))
148+
.willReturn(response().withHeader(lowerCaseMetadataPrefix + metadataKey, "test")
149+
.withHeader(mixedCaseMetadataPrefix + mixedCaseMetadataKey, "test")));
150+
HeadObjectResponse headObjectResponse = s3Client.headObject(b -> b.key("key").bucket("bucket"));
151+
152+
assertThat(headObjectResponse.metadata()).containsKey(metadataKey);
153+
assertThat(headObjectResponse.metadata()).containsKey(mixedCaseMetadataKey);
154+
}
155+
138156
private ResponseDefinitionBuilder response() {
139157
return aResponse().withStatus(200).withHeader(CONTENT_LENGTH, "0").withBody("");
140158
}

test/protocol-tests-core/src/main/resources/software/amazon/awssdk/protocol/suites/cases/rest-xml-output.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,5 +310,29 @@
310310
"stringMember": ""
311311
}
312312
}
313+
},
314+
{
315+
"description": "Map headers are unmarshalled correctly",
316+
"given": {
317+
"response": {
318+
"status_code": 200,
319+
"headers": {
320+
"x-amz-meta-FoO": "foo",
321+
"X-aMZ-mEtA-bAr": "bar"
322+
}
323+
}
324+
},
325+
"when": {
326+
"action": "unmarshall",
327+
"operation": "MembersInHeaders"
328+
},
329+
"then": {
330+
"deserializedAs": {
331+
"MetadataMember": {
332+
"FoO": "foo",
333+
"bAr": "bar"
334+
}
335+
}
336+
}
313337
}
314338
]

test/protocol-tests/src/main/resources/codegen-resources/restxml/service-2.json

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,11 @@
295295
"shape":"Timestamp",
296296
"location":"header",
297297
"locationName":"x-amz-timestamp"
298+
},
299+
"MetadataMember":{
300+
"shape":"Metadata",
301+
"location":"header",
302+
"locationName":"x-amz-meta-"
298303
}
299304
}
300305
},
@@ -535,12 +540,6 @@
535540
},
536541
"exception":true
537542
},
538-
"EmptyModeledException":{
539-
"type":"structure",
540-
"members":{
541-
},
542-
"exception":true
543-
},
544543
"ExplicitPayloadAndHeadersException":{
545544
"type":"structure",
546545
"members":{
@@ -584,6 +583,13 @@
584583
"exception":true,
585584
"payload":"PayloadMember"
586585
},
587-
"Timestamp":{"type":"timestamp"}
586+
"Timestamp":{"type":"timestamp"},
587+
"Metadata":{
588+
"type":"map",
589+
"key":{"shape":"MetadataKey"},
590+
"value":{"shape":"MetadataValue"}
591+
},
592+
"MetadataKey":{"type":"string"},
593+
"MetadataValue":{"type":"string"}
588594
}
589595
}

utils/src/main/java/software/amazon/awssdk/utils/StringUtils.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,4 +578,31 @@ public static String fromBytes(byte[] bytes, Charset charset) throws UncheckedIO
578578
throw new UncheckedIOException("Cannot encode string.", e);
579579
}
580580
}
581+
582+
/**
583+
* Tests if this string starts with the specified prefix ignoring case considerations.
584+
*
585+
* @param str the string to be tested
586+
* @param prefix the prefix
587+
* @return true if the string starts with the prefix ignoring case
588+
*/
589+
public static boolean startsWithIgnoreCase(String str, String prefix) {
590+
return str.regionMatches(true, 0, prefix, 0, prefix.length());
591+
}
592+
593+
/**
594+
* Replace the prefix of the string provided ignoring case considerations.
595+
*
596+
* <p>
597+
* The unmatched part is unchanged.
598+
*
599+
*
600+
* @param str the string to replace
601+
* @param prefix the prefix to find
602+
* @param replacement the replacement
603+
* @return the replaced string
604+
*/
605+
public static String replacePrefixIgnoreCase(String str, String prefix, String replacement) {
606+
return str.replaceFirst("(?i)" + prefix, replacement);
607+
}
581608
}

utils/src/test/java/software/amazon/awssdk/utils/StringUtilsTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
package software.amazon.awssdk.utils;
1717

1818
import static org.junit.Assert.assertEquals;
19+
import static org.junit.Assert.assertFalse;
1920
import static org.junit.Assert.assertNull;
21+
import static org.junit.Assert.assertThat;
22+
import static org.junit.Assert.assertTrue;
23+
import static software.amazon.awssdk.utils.StringUtils.replacePrefixIgnoreCase;
2024

2125
import org.junit.Test;
2226

@@ -107,4 +111,18 @@ public void testReCapitalize() {
107111
assertEquals("capitalize(uncapitalize(String)) failed",
108112
FOO_CAP, StringUtils.capitalize(StringUtils.uncapitalize(FOO_CAP)));
109113
}
114+
115+
@Test
116+
public void testStartsWithIgnoreCase() {
117+
assertTrue(StringUtils.startsWithIgnoreCase("helloworld", "hello"));
118+
assertTrue(StringUtils.startsWithIgnoreCase("hELlOwOrlD", "hello"));
119+
assertFalse(StringUtils.startsWithIgnoreCase("hello", "world"));
120+
}
121+
122+
@Test
123+
public void testReplacePrefixIgnoreCase() {
124+
assertEquals("lloWorld" ,replacePrefixIgnoreCase("helloWorld", "he", ""));
125+
assertEquals("lloWORld" ,replacePrefixIgnoreCase("helloWORld", "He", ""));
126+
assertEquals("llOwOrld" ,replacePrefixIgnoreCase("HEllOwOrld", "he", ""));
127+
}
110128
}

0 commit comments

Comments
 (0)