Skip to content

Commit d0ef89e

Browse files
authored
Trim object metadata keys for PUT and MPU (#4902)
This brings the V2's behavior in line with 1.x
1 parent a3fe036 commit d0ef89e

File tree

4 files changed

+206
-1
lines changed

4 files changed

+206
-1
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "Amazon S3",
4+
"contributor": "",
5+
"description": "Automatically trim object metadata keys of whitespace for `PutObject` and `CreateMultipartUpload`."
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.services.s3.internal.handlers;
17+
18+
import java.util.Map;
19+
import java.util.stream.Collectors;
20+
import software.amazon.awssdk.annotations.SdkInternalApi;
21+
import software.amazon.awssdk.core.SdkRequest;
22+
import software.amazon.awssdk.core.interceptor.Context;
23+
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
24+
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
25+
import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute;
26+
import software.amazon.awssdk.services.s3.model.CreateMultipartUploadRequest;
27+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
28+
import software.amazon.awssdk.utils.StringUtils;
29+
30+
/**
31+
* Interceptor that trims object metadata keys of any leading or trailing whitespace for {@code PutObject} and {@code
32+
* CreateMultipartUpload}. This behavior is intended to provide the same functionality as in 1.x.
33+
*/
34+
@SdkInternalApi
35+
public final class ObjectMetadataInterceptor implements ExecutionInterceptor {
36+
@Override
37+
public SdkRequest modifyRequest(Context.ModifyRequest context, ExecutionAttributes executionAttributes) {
38+
SdkRequest request = context.request();
39+
40+
switch (executionAttributes.getAttribute(SdkExecutionAttribute.OPERATION_NAME)) {
41+
case "PutObject":
42+
return trimMetadataNames((PutObjectRequest) request);
43+
case "CreateMultipartUpload":
44+
return trimMetadataNames((CreateMultipartUploadRequest) request);
45+
default:
46+
return request;
47+
}
48+
}
49+
50+
private PutObjectRequest trimMetadataNames(PutObjectRequest putObjectRequest) {
51+
if (!putObjectRequest.hasMetadata()) {
52+
return putObjectRequest;
53+
}
54+
55+
return putObjectRequest.toBuilder()
56+
.metadata(trimKeys(putObjectRequest.metadata()))
57+
.build();
58+
}
59+
60+
private CreateMultipartUploadRequest trimMetadataNames(CreateMultipartUploadRequest createMultipartUploadRequest) {
61+
if (!createMultipartUploadRequest.hasMetadata()) {
62+
return createMultipartUploadRequest;
63+
}
64+
65+
return createMultipartUploadRequest.toBuilder()
66+
.metadata(trimKeys(createMultipartUploadRequest.metadata()))
67+
.build();
68+
}
69+
70+
private Map<String, String> trimKeys(Map<String, String> map) {
71+
return map.entrySet().stream()
72+
.collect(Collectors.toMap(e -> StringUtils.trim(e.getKey()), Map.Entry::getValue));
73+
}
74+
}

services/s3/src/main/resources/codegen-resources/customization.config

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,8 @@
256256
"software.amazon.awssdk.services.s3.internal.handlers.EnableTrailingChecksumInterceptor",
257257
"software.amazon.awssdk.services.s3.internal.handlers.ExceptionTranslationInterceptor",
258258
"software.amazon.awssdk.services.s3.internal.handlers.GetObjectInterceptor",
259-
"software.amazon.awssdk.services.s3.internal.handlers.CopySourceInterceptor"
259+
"software.amazon.awssdk.services.s3.internal.handlers.CopySourceInterceptor",
260+
"software.amazon.awssdk.services.s3.internal.handlers.ObjectMetadataInterceptor"
260261
],
261262
"internalPlugins": [
262263
"software.amazon.awssdk.services.s3.internal.plugins.S3DisableChunkEncodingIfConfiguredPlugin(config)",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.services.s3.internal.handlers;
17+
18+
import static java.util.Arrays.asList;
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.mockito.Mockito.mock;
21+
import static org.mockito.Mockito.when;
22+
23+
import java.util.List;
24+
import java.util.Map;
25+
import java.util.stream.Collectors;
26+
import org.junit.Test;
27+
import org.junit.jupiter.params.ParameterizedTest;
28+
import org.junit.jupiter.params.provider.MethodSource;
29+
import software.amazon.awssdk.core.SdkRequest;
30+
import software.amazon.awssdk.core.interceptor.Context;
31+
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
32+
import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute;
33+
import software.amazon.awssdk.services.s3.model.CreateMultipartUploadRequest;
34+
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
35+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
36+
37+
public class ObjectMetadataInterceptorTest {
38+
private static final ObjectMetadataInterceptor INTERCEPTOR = new ObjectMetadataInterceptor();
39+
40+
41+
42+
public static List<TestCase> testCases() {
43+
return asList(
44+
tc(asList("a", "b", "c"), asList("a", "b", "c")),
45+
tc(asList(" a ", "b", "c"), asList("a", "b", "c")),
46+
tc(asList(" a", "\tb", "\tc"), asList("a", "b", "c")),
47+
tc(asList("a\n", "\tb", "\tc\r\n"), asList("a", "b", "c"))
48+
49+
);
50+
}
51+
52+
@ParameterizedTest
53+
@MethodSource("testCases")
54+
public void modifyRequest_putObject_metadataKeysAreTrimmed(TestCase tc) {
55+
Map<String, String> metadata = tc.inputKeys.stream()
56+
.collect(Collectors.toMap(k -> k, k -> "value"));
57+
58+
Context.ModifyHttpRequest ctx = mock(Context.ModifyHttpRequest.class);
59+
60+
PutObjectRequest put = PutObjectRequest.builder()
61+
.metadata(metadata)
62+
.build();
63+
64+
when(ctx.request()).thenReturn(put);
65+
66+
ExecutionAttributes attrs = new ExecutionAttributes();
67+
attrs.putAttribute(SdkExecutionAttribute.OPERATION_NAME, "PutObject");
68+
69+
PutObjectRequest modified = (PutObjectRequest) INTERCEPTOR.modifyRequest(ctx, attrs);
70+
71+
assertThat(modified.metadata().keySet()).containsExactlyElementsOf(tc.expectedKeys);
72+
}
73+
74+
@ParameterizedTest
75+
@MethodSource("testCases")
76+
public void modifyRequest_creatMultipartUpload_metadataKeysAreTrimmed(TestCase tc) {
77+
Map<String, String> metadata = tc.inputKeys.stream()
78+
.collect(Collectors.toMap(k -> k, k -> "value"));
79+
80+
Context.ModifyHttpRequest ctx = mock(Context.ModifyHttpRequest.class);
81+
82+
CreateMultipartUploadRequest mpu = CreateMultipartUploadRequest.builder()
83+
.metadata(metadata)
84+
.build();
85+
86+
when(ctx.request()).thenReturn(mpu);
87+
88+
ExecutionAttributes attrs = new ExecutionAttributes();
89+
attrs.putAttribute(SdkExecutionAttribute.OPERATION_NAME, "CreateMultipartUpload");
90+
91+
CreateMultipartUploadRequest modified = (CreateMultipartUploadRequest) INTERCEPTOR.modifyRequest(ctx, attrs);
92+
93+
assertThat(modified.metadata().keySet()).containsExactlyElementsOf(tc.expectedKeys);
94+
}
95+
96+
@Test
97+
public void modifyRequest_unknownOperation_ignores() {
98+
Context.ModifyHttpRequest ctx = mock(Context.ModifyHttpRequest.class);
99+
100+
GetObjectRequest get = GetObjectRequest.builder().build();
101+
102+
when(ctx.request()).thenReturn(get);
103+
104+
ExecutionAttributes attrs = new ExecutionAttributes();
105+
attrs.putAttribute(SdkExecutionAttribute.OPERATION_NAME, "GetObject");
106+
107+
SdkRequest sdkRequest = INTERCEPTOR.modifyRequest(ctx, attrs);
108+
109+
assertThat(sdkRequest).isEqualTo(get);
110+
}
111+
112+
private static TestCase tc(List<String> input, List<String> expected) {
113+
return new TestCase(input, expected);
114+
}
115+
private static class TestCase {
116+
private List<String> inputKeys;
117+
private List<String> expectedKeys;
118+
119+
public TestCase(List<String> inputKeys, List<String> expectedKeys) {
120+
this.inputKeys = inputKeys;
121+
this.expectedKeys = expectedKeys;
122+
}
123+
}
124+
}

0 commit comments

Comments
 (0)