Skip to content

Commit 895a263

Browse files
committed
Add DecodeUrlEncodedResponseInterceptor
This interceptor adds support for automatically decoding URL encoded portions of a response. Currently, it support decoding parts of the ListObjects and ListObjectsV2 responses.
1 parent 81a7f2b commit 895a263

File tree

5 files changed

+257
-34
lines changed

5 files changed

+257
-34
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"category": "Amazon S3",
3+
"type": "feature",
4+
"description": "Add support for automatically decoding URL-encoded parts of the ListObjects and ListObjectsV2 responses. See https://docs.aws.amazon.com/AmazonS3/latest/API/v2-RESTBucketGET.html and https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketGET.html."
5+
}

services/s3/src/main/java/software/amazon/awssdk/services/s3/handlers/DecodeUrlEncodedResponseInterceptor.java

Lines changed: 73 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,43 +15,82 @@
1515

1616
package software.amazon.awssdk.services.s3.handlers;
1717

18-
//import java.lang.reflect.Method;
19-
//import java.util.Arrays;
20-
//import java.util.List;
21-
//import software.amazon.awssdk.core.Request;
22-
//import software.amazon.awssdk.Response;
18+
import static software.amazon.awssdk.utils.http.SdkHttpUtils.urlDecode;
2319

24-
import software.amazon.awssdk.annotations.ReviewBeforeRelease;
20+
import java.util.List;
21+
import java.util.stream.Collectors;
2522
import software.amazon.awssdk.annotations.SdkProtectedApi;
23+
import software.amazon.awssdk.core.SdkResponse;
24+
import software.amazon.awssdk.core.interceptor.Context;
25+
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
2626
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
27-
//import software.amazon.awssdk.services.s3.model.ListObjectVersionsResponse;
28-
//import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
29-
//import software.amazon.awssdk.services.s3.model.ListObjectsV2Response;
27+
import software.amazon.awssdk.services.s3.model.EncodingType;
28+
import software.amazon.awssdk.services.s3.model.ListObjectsResponse;
29+
import software.amazon.awssdk.services.s3.model.ListObjectsV2Response;
30+
import software.amazon.awssdk.services.s3.model.S3Object;
3031

32+
/**
33+
* Encoding type affects the following values in the response:
34+
* <ul>
35+
* <li>V1: Delimiter, Marker, Prefix, NextMarker, Key</li>
36+
* <li>V2: Delimiter, Prefix, Key, and StartAfter</li>
37+
* </ul>
38+
* <p>
39+
* See <a
40+
* href="https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketGET.html">https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketGET.html</a>
41+
* and <a
42+
* href="https://docs.aws.amazon.com/AmazonS3/latest/API/v2-RESTBucketGET.html">https://docs.aws.amazon.com/AmazonS3/latest/API/v2-RESTBucketGET.html</a>
43+
*/
3144
@SdkProtectedApi
32-
@ReviewBeforeRelease("Finish this and hook it up")
33-
public class DecodeUrlEncodedResponseInterceptor implements ExecutionInterceptor {
34-
35-
// @Override
36-
// public void afterResponse(Request<?> request, Response<?> response) {
37-
//
38-
// if (response.getAwsResponse() instanceof ListObjectsV2Response) {
39-
// decodeListObjectsV2ResponseIfRequired(request, response);
40-
// }
41-
//
42-
// if (response.getAwsResponse() instanceof ListObjectVersionsResponse) {
43-
// decodeListObjectVersionsResponseIfRequired(request, response);
44-
// }
45-
// }
46-
//
47-
// public void decodeListObjectsV2ResponseIfRequired(Request<?> request, Response<?> response) {
48-
// ListObjectsV2Request listObjectsV2Request = (ListObjectsV2Request) request.getOriginalRequest();
49-
// ListObjectsV2Response listObjectsV2Response = (ListObjectsV2Response) response.getAwsResponse();
50-
//
51-
// if (listObjectsV2Request.encodingType() != null) {
52-
// listObjectsV2Response.toBuilder().
53-
// }
54-
//
55-
// response.
56-
// }
45+
public final class DecodeUrlEncodedResponseInterceptor implements ExecutionInterceptor {
46+
47+
@Override
48+
public SdkResponse modifyResponse(Context.ModifyResponse context,
49+
ExecutionAttributes executionAttributes) {
50+
SdkResponse response = context.response();
51+
if (shouldHandle(response)) {
52+
if (response instanceof ListObjectsResponse) {
53+
response = modifyListObjectsResponse((ListObjectsResponse) response);
54+
} else if (response instanceof ListObjectsV2Response) {
55+
response = modifyListObjectsV2Response((ListObjectsV2Response) response);
56+
}
57+
}
58+
return response;
59+
}
60+
61+
private static boolean shouldHandle(SdkResponse sdkResponse) {
62+
return sdkResponse.getValueForField("EncodingType", String.class)
63+
.map(et -> EncodingType.URL.toString().equals(et))
64+
.orElse(false);
65+
}
66+
67+
// Elements to decode: Delimiter, Marker, Prefix, NextMarker, Key
68+
private static SdkResponse modifyListObjectsResponse(ListObjectsResponse response) {
69+
return response.toBuilder()
70+
.delimiter(urlDecode(response.delimiter()))
71+
.marker(urlDecode(response.delimiter()))
72+
.prefix(urlDecode(response.prefix()))
73+
.nextMarker(urlDecode(response.nextMarker()))
74+
.contents(decodeContents(response.contents()))
75+
.build();
76+
}
77+
78+
// Elements to decode: Delimiter, Prefix, Key, and StartAfter
79+
private static SdkResponse modifyListObjectsV2Response(ListObjectsV2Response response) {
80+
return response.toBuilder()
81+
.delimiter(urlDecode(response.delimiter()))
82+
.prefix(urlDecode(response.prefix()))
83+
.startAfter(urlDecode(response.startAfter()))
84+
.contents(decodeContents(response.contents()))
85+
.build();
86+
}
87+
88+
private static List<S3Object> decodeContents(List<S3Object> contents) {
89+
if (contents == null) {
90+
return null;
91+
}
92+
return contents.stream()
93+
.map(o -> o.toBuilder().key(urlDecode(o.key())).build())
94+
.collect(Collectors.toList());
95+
}
5796
}

services/s3/src/main/resources/software/amazon/awssdk/services/s3/execution.interceptors

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ software.amazon.awssdk.services.s3.handlers.CreateBucketInterceptor
33
software.amazon.awssdk.services.s3.handlers.PutObjectInterceptor
44
software.amazon.awssdk.services.s3.handlers.EnableChunkedEncodingInterceptor
55
software.amazon.awssdk.services.s3.handlers.DisableDoubleUrlEncodingInterceptor
6+
software.amazon.awssdk.services.s3.handlers.DecodeUrlEncodedResponseInterceptor
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
* Copyright 2010-2018 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.handlers;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.util.Arrays;
21+
import java.util.List;
22+
import java.util.function.Supplier;
23+
import org.junit.Test;
24+
import software.amazon.awssdk.core.SdkRequest;
25+
import software.amazon.awssdk.core.SdkResponse;
26+
import software.amazon.awssdk.core.interceptor.Context;
27+
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
28+
import software.amazon.awssdk.http.SdkHttpFullRequest;
29+
import software.amazon.awssdk.http.SdkHttpFullResponse;
30+
import software.amazon.awssdk.services.s3.model.EncodingType;
31+
import software.amazon.awssdk.services.s3.model.ListObjectsResponse;
32+
import software.amazon.awssdk.services.s3.model.ListObjectsV2Response;
33+
import software.amazon.awssdk.services.s3.model.S3Object;
34+
35+
/**
36+
* Unit tests for {@link DecodeUrlEncodedResponseInterceptor}.
37+
* <p>
38+
* See <a
39+
* href="https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketGET.html">https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketGET.html</a>
40+
* and <a
41+
* href="https://docs.aws.amazon.com/AmazonS3/latest/API/v2-RESTBucketGET.html">https://docs.aws.amazon.com/AmazonS3/latest/API/v2-RESTBucketGET.html</a>
42+
* for information on which parts of the response must are affected by the EncodingType member.
43+
*/
44+
public class DecodeUrlEncodedResponseInterceptorTest {
45+
private static final String TEST_URL_ENCODED = "foo+%3D+bar+baz+%CE%B1+%CE%B2+%F0%9F%98%8A";
46+
47+
// foo = bar baz α β 😊
48+
private static final String TEST_URL_DECODED = "foo = bar baz α β \uD83D\uDE0A";
49+
50+
private static final DecodeUrlEncodedResponseInterceptor INTERCEPTOR = new DecodeUrlEncodedResponseInterceptor();
51+
52+
private static final List<S3Object> TEST_CONTENTS = Arrays.asList(
53+
S3Object.builder().key(TEST_URL_ENCODED).build(),
54+
S3Object.builder().key(TEST_URL_ENCODED).build(),
55+
S3Object.builder().key(TEST_URL_ENCODED).build()
56+
);
57+
58+
private static final ListObjectsResponse V1_TEST_ENCODED_RESPONSE = ListObjectsResponse.builder()
59+
.encodingType(EncodingType.URL)
60+
.delimiter(TEST_URL_ENCODED)
61+
.nextMarker(TEST_URL_ENCODED)
62+
.prefix(TEST_URL_ENCODED)
63+
.marker(TEST_URL_ENCODED)
64+
.contents(TEST_CONTENTS)
65+
.build();
66+
67+
private static final ListObjectsV2Response V2_TEST_ENCODED_RESPONSE = ListObjectsV2Response.builder()
68+
.encodingType(EncodingType.URL)
69+
.delimiter(TEST_URL_ENCODED)
70+
.prefix(TEST_URL_ENCODED)
71+
.startAfter(TEST_URL_ENCODED)
72+
.contents(TEST_CONTENTS)
73+
.build();
74+
75+
@Test
76+
public void encodingTypeSet_decodesListObjectsResponseParts() {
77+
Context.ModifyResponse ctx = newContext(V1_TEST_ENCODED_RESPONSE);
78+
79+
ListObjectsResponse decoded = (ListObjectsResponse) INTERCEPTOR.modifyResponse(ctx, new ExecutionAttributes());
80+
81+
assertDecoded(decoded::delimiter);
82+
assertDecoded(decoded::nextMarker);
83+
assertDecoded(decoded::prefix);
84+
assertDecoded(decoded::marker);
85+
assertKeysAreDecoded(decoded.contents());
86+
}
87+
88+
@Test
89+
public void encodingTypeSet_decodesListObjectsV2ResponseParts() {
90+
Context.ModifyResponse ctx = newContext(V2_TEST_ENCODED_RESPONSE);
91+
92+
ListObjectsV2Response decoded = (ListObjectsV2Response) INTERCEPTOR.modifyResponse(ctx, new ExecutionAttributes());
93+
94+
assertDecoded(decoded::delimiter);
95+
assertDecoded(decoded::prefix);
96+
assertDecoded(decoded::startAfter);
97+
assertKeysAreDecoded(decoded.contents());
98+
}
99+
100+
@Test
101+
public void encodingTypeNotSet_doesNotDecodeListObjectsResponseParts() {
102+
ListObjectsResponse original = V1_TEST_ENCODED_RESPONSE.toBuilder()
103+
.encodingType((String) null)
104+
.build();
105+
106+
Context.ModifyResponse ctx = newContext(original);
107+
108+
ListObjectsResponse fromInterceptor = (ListObjectsResponse) INTERCEPTOR.modifyResponse(ctx, new ExecutionAttributes());
109+
110+
assertThat(fromInterceptor).isEqualTo(original);
111+
}
112+
113+
@Test
114+
public void encodingTypeNotSet_doesNotDecodeListObjectsV2ResponseParts() {
115+
ListObjectsV2Response original = V2_TEST_ENCODED_RESPONSE.toBuilder()
116+
.encodingType((String) null)
117+
.build();
118+
119+
Context.ModifyResponse ctx = newContext(original);
120+
121+
ListObjectsV2Response fromInterceptor = (ListObjectsV2Response) INTERCEPTOR.modifyResponse(ctx, new ExecutionAttributes());
122+
123+
assertThat(fromInterceptor).isEqualTo(original);
124+
}
125+
126+
private void assertKeysAreDecoded(List<S3Object> objects) {
127+
objects.forEach(o -> assertDecoded(o::key));
128+
}
129+
130+
private void assertDecoded(Supplier<String> supplier) {
131+
assertThat(supplier.get()).isEqualTo(TEST_URL_DECODED);
132+
}
133+
134+
private static Context.ModifyResponse newContext(SdkResponse response) {
135+
return new Context.ModifyResponse() {
136+
@Override
137+
public SdkResponse response() {
138+
return response;
139+
}
140+
141+
@Override
142+
public SdkHttpFullResponse httpResponse() {
143+
return null;
144+
}
145+
146+
@Override
147+
public SdkHttpFullRequest httpRequest() {
148+
return null;
149+
}
150+
151+
@Override
152+
public SdkRequest request() {
153+
return null;
154+
}
155+
};
156+
}
157+
}

utils/src/main/java/software/amazon/awssdk/utils/http/SdkHttpUtils.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717

1818
import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;
1919

20+
import java.io.UnsupportedEncodingException;
2021
import java.net.URI;
22+
import java.net.URLDecoder;
2123
import java.net.URLEncoder;
2224
import java.util.Collections;
2325
import java.util.LinkedHashMap;
@@ -78,6 +80,25 @@ public static String formDataEncode(String value) {
7880
return value == null ? null : invokeSafely(() -> URLEncoder.encode(value, DEFAULT_ENCODING));
7981
}
8082

83+
/**
84+
* Decode the string according to RFC 3986: encoding for URI paths, query strings, etc.
85+
* <p>
86+
* Assumes the decoded string is UTF-8 encoded.
87+
*
88+
* @param value The string to decode.
89+
* @return The decoded string.
90+
*/
91+
public static String urlDecode(String value) {
92+
if (value == null) {
93+
return null;
94+
}
95+
try {
96+
return URLDecoder.decode(value, DEFAULT_ENCODING);
97+
} catch (UnsupportedEncodingException e) {
98+
throw new RuntimeException("Unable to decode value", e);
99+
}
100+
}
101+
81102
/**
82103
* Encode each of the keys and values in the provided query parameters using {@link #urlEncode(String)}.
83104
*/

0 commit comments

Comments
 (0)