Skip to content

feature 2034: SdkHttpFullRequest builder.URI conditionally accept que… #2082

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 17, 2020
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
5 changes: 5 additions & 0 deletions .changes/next-release/feature-HTTPclientspi-25d1ed9.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"category": "HTTP Client SPI",
"type": "feature",
"description": "Calling the SdkHttpFullRequest uri() builder method, query parameters of the provided URI will be kept.\nThis can be useful in case you want to provide an already fully formed URI like a callback URI."
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,24 @@ static SdkHttpFullRequest.Builder builder() {
*/
interface Builder extends SdkHttpRequest.Builder {
/**
* Convenience method to set the {@link #protocol()}, {@link #host()}, {@link #port()}, and
* {@link #encodedPath()} from a {@link URI} object.
* Convenience method to set the {@link #protocol()}, {@link #host()}, {@link #port()},
* {@link #encodedPath()} and extracts query parameters from a {@link URI} object.
*
* @param uri URI containing protocol, host, port and path.
* @return This builder for method chaining.
*/
@Override
default Builder uri(URI uri) {
return this.protocol(uri.getScheme())
Builder builder = this.protocol(uri.getScheme())
.host(uri.getHost())
.port(uri.getPort())
.encodedPath(SdkHttpUtils.appendUri(uri.getRawPath(), encodedPath()));
if (uri.getRawQuery() != null) {
builder.clearQueryParameters();
SdkHttpUtils.uriParams(uri)
.forEach(this::putRawQueryParameter);
}
return builder;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,17 +135,23 @@ default URI getUri() {
*/
interface Builder extends CopyableBuilder<Builder, SdkHttpRequest> {
/**
* Convenience method to set the {@link #protocol()}, {@link #host()}, {@link #port()}, and
* {@link #encodedPath()} from a {@link URI} object.
* Convenience method to set the {@link #protocol()}, {@link #host()}, {@link #port()},
* {@link #encodedPath()} and extracts query parameters from a {@link URI} object.
*
* @param uri URI containing protocol, host, port and path.
* @return This builder for method chaining.
*/
default Builder uri(URI uri) {
return this.protocol(uri.getScheme())
Builder builder = this.protocol(uri.getScheme())
.host(uri.getHost())
.port(uri.getPort())
.encodedPath(SdkHttpUtils.appendUri(uri.getRawPath(), encodedPath()));
if (uri.getRawQuery() != null) {
builder.clearQueryParameters();
SdkHttpUtils.uriParams(uri)
.forEach(this::putRawQueryParameter);
}
return builder;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -401,13 +402,74 @@ public Map<String, List<String>> getMap() {
});
}

@Test
public void testSdkHttpFullRequestBuilderNoQueryParams() {
URI uri = URI.create("https://github.com/aws/aws-sdk-java-v2/issues/2034");
final SdkHttpFullRequest sdkHttpFullRequest = SdkHttpFullRequest.builder().method(SdkHttpMethod.POST).uri(uri).build();
assertThat(sdkHttpFullRequest.getUri().getQuery()).isNullOrEmpty();
}

@Test
public void testSdkHttpFullRequestBuilderUriWithQueryParams() {
URI uri = URI.create("https://github.com/aws/aws-sdk-java-v2/issues/2034?reqParam=1234&oParam=3456%26reqParam%3D5678");
final SdkHttpFullRequest sdkHttpFullRequest =
SdkHttpFullRequest.builder().method(SdkHttpMethod.POST).uri(uri).build();
assertThat(sdkHttpFullRequest.getUri().getQuery()).contains("reqParam=1234", "oParam=3456&reqParam=5678");
}

@Test
public void testSdkHttpFullRequestBuilderUriWithQueryParamWithoutValue() {
final String expected = "https://github.com/aws/aws-sdk-for-java-v2?foo";
URI myUri = URI.create(expected);
SdkHttpFullRequest actual = SdkHttpFullRequest.builder().method(SdkHttpMethod.POST).uri(myUri).build();
assertThat(actual.getUri()).hasToString(expected);
}

@Test
public void testSdkHttpRequestBuilderNoQueryParams() {
URI uri = URI.create("https://github.com/aws/aws-sdk-java-v2/issues/2034");
final SdkHttpRequest sdkHttpRequest = SdkHttpRequest.builder().method(SdkHttpMethod.POST).uri(uri).build();
assertThat(sdkHttpRequest.getUri().getQuery()).isNullOrEmpty();
}

@Test
public void testSdkHttpRequestBuilderUriWithQueryParams() {
URI uri = URI.create("https://github.com/aws/aws-sdk-java-v2/issues/2034?reqParam=1234&oParam=3456%26reqParam%3D5678");
final SdkHttpRequest sdkHttpRequest =
SdkHttpRequest.builder().method(SdkHttpMethod.POST).uri(uri).build();
assertThat(sdkHttpRequest.getUri().getQuery()).contains("reqParam=1234", "oParam=3456&reqParam=5678");
}

@Test
public void testSdkHttpRequestBuilderUriWithQueryParamsIgnoreOtherQueryParams() {
URI uri = URI.create("https://github.com/aws/aws-sdk-java-v2/issues/2034?reqParam=1234&oParam=3456%26reqParam%3D5678");
final SdkHttpRequest sdkHttpRequest =
SdkHttpRequest.builder().method(SdkHttpMethod.POST).appendRawQueryParameter("clean", "up").uri(uri).build();
assertThat(sdkHttpRequest.getUri().getQuery()).contains("reqParam=1234", "oParam=3456&reqParam=5678")
.doesNotContain("clean");
}

@Test
public void testSdkHttpRequestBuilderUriWithQueryParamWithoutValue() {
final String expected = "https://github.com/aws/aws-sdk-for-java-v2?foo";
URI myUri = URI.create(expected);
SdkHttpRequest actual = SdkHttpRequest.builder().method(SdkHttpMethod.POST).uri(myUri).build();
assertThat(actual.getUri()).hasToString(expected);
}

private interface BuilderProxy {
BuilderProxy setValue(String key, String value);

BuilderProxy appendValue(String key, String value);

BuilderProxy setValues(String key, List<String> values);

BuilderProxy removeValue(String key);

BuilderProxy clearValues();

BuilderProxy setMap(Map<String, List<String>> map);

Map<String, List<String>> getMap();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@

package software.amazon.awssdk.utils.http;

import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;

import java.io.UnsupportedEncodingException;
Expand All @@ -30,6 +33,7 @@
import java.util.Optional;
import java.util.Set;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import software.amazon.awssdk.annotations.SdkProtectedApi;
Expand All @@ -52,6 +56,9 @@ public final class SdkHttpUtils {
private static final String[] ENCODED_CHARACTERS_WITHOUT_SLASHES = new String[] {"+", "*", "%7E"};
private static final String[] ENCODED_CHARACTERS_WITHOUT_SLASHES_REPLACEMENTS = new String[] {"%20", "%2A", "~"};

private static final String QUERY_PARAM_DELIMITER_REGEX = "\\s*&\\s*";
private static final Pattern QUERY_PARAM_DELIMITER_PATTERN = Pattern.compile(QUERY_PARAM_DELIMITER_REGEX);

// List of headers that may appear only once in a request; i.e. is not a list of values.
// Taken from https://github.com/apache/httpcomponents-client/blob/81c1bc4dc3ca5a3134c5c60e8beff08be2fd8792/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpTestUtils.java#L69-L85 with modifications:
// removed: accept-ranges, if-match, if-none-match, vary since it looks like they're defined as lists
Expand All @@ -62,6 +69,7 @@ public final class SdkHttpUtils {
"proxy-authorization", "range", "referer", "retry-after", "server", "user-agent")
.collect(Collectors.toSet());


private SdkHttpUtils() {
}

Expand Down Expand Up @@ -322,4 +330,15 @@ public static Optional<String> firstMatchingHeaderFromCollection(Map<String, Lis
public static boolean isSingleHeader(String h) {
return SINGLE_HEADERS.contains(StringUtils.lowerCase(h));
}

/**
* Extracts query parameters from the given URI
*/
public static Map<String, List<String>> uriParams(URI uri) {
return QUERY_PARAM_DELIMITER_PATTERN
.splitAsStream(uri.getRawQuery().trim())
.map(s -> s.contains("=") ? s.split("=", 2) : new String[] {s, null})
.collect(groupingBy(a -> urlDecode(a[0]), mapping(a -> urlDecode(a[1]), toList())));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.entry;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
Expand Down Expand Up @@ -173,4 +178,46 @@ public void headersFromCollectionWorksCorrectly() {
assertThat(SdkHttpUtils.firstMatchingHeaderFromCollection(headers, asList("foo", "nothing"))).hasValue("bar");
assertThat(SdkHttpUtils.firstMatchingHeaderFromCollection(headers, asList("foo", "other"))).hasValue("foo");
}

@Test
public void isSingleHeader() {
assertThat(SdkHttpUtils.isSingleHeader("age")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("authorization")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("content-length")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("content-location")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("content-md5")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("content-range")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("content-type")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("date")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("etag")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("expires")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("from")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("host")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("if-modified-since")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("if-range")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("if-unmodified-since")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("last-modified")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("location")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("max-forwards")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("proxy-authorization")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("range")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("referer")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("retry-after")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("server")).isTrue();
assertThat(SdkHttpUtils.isSingleHeader("user-agent")).isTrue();

assertThat(SdkHttpUtils.isSingleHeader("custom")).isFalse();
}

@Test
public void uriParams() throws URISyntaxException {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have tests for the two requests classes that we changed as well.

URI uri = URI.create("https://github.com/aws/aws-sdk-java-v2/issues/2034?reqParam=1234&oParam=3456&reqParam=5678&noval"
+ "&decoded%26Part=equals%3Dval");
Map<String, List<String>> uriParams = SdkHttpUtils.uriParams(uri);
assertThat(uriParams).contains(entry("reqParam", Arrays.asList("1234", "5678")),
entry("oParam", Collections.singletonList("3456")),
entry("noval", Arrays.asList((String)null)),
entry("decoded&Part", Arrays.asList("equals=val")));
}

}