From 2794553d2e12a7c74f7654fc093bcfc93de1723d Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 2 May 2022 21:36:09 +0100 Subject: [PATCH] Add resolvers for URI, cookies, and request params See gh-28386 --- .../invoker/CookieValueArgumentResolver.java | 71 +++++++ .../service/invoker/HttpRequestValues.java | 125 ++++++++++-- .../invoker/HttpServiceProxyFactory.java | 3 + .../invoker/HttpUrlArgumentResolver.java | 47 +++++ .../RequestHeaderArgumentResolver.java | 5 +- .../invoker/RequestParamArgumentResolver.java | 75 +++++++ .../CookieValueArgumentResolverTests.java | 186 ++++++++++++++++++ .../invoker/HttpUrlArgumentResolverTests.java | 61 ++++++ .../RequestParamArgumentResolverTests.java | 104 ++++++++++ 9 files changed, 659 insertions(+), 18 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/service/invoker/CookieValueArgumentResolver.java create mode 100644 spring-web/src/main/java/org/springframework/web/service/invoker/HttpUrlArgumentResolver.java create mode 100644 spring-web/src/main/java/org/springframework/web/service/invoker/RequestParamArgumentResolver.java create mode 100644 spring-web/src/test/java/org/springframework/web/service/invoker/CookieValueArgumentResolverTests.java create mode 100644 spring-web/src/test/java/org/springframework/web/service/invoker/HttpUrlArgumentResolverTests.java create mode 100644 spring-web/src/test/java/org/springframework/web/service/invoker/RequestParamArgumentResolverTests.java diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/CookieValueArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/CookieValueArgumentResolver.java new file mode 100644 index 000000000000..d883765c6815 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/CookieValueArgumentResolver.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.service.invoker; + +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.web.bind.annotation.CookieValue; + + +/** + * {@link HttpServiceArgumentResolver} for {@link CookieValue @CookieValue} + * annotated arguments. + * + *

The argument may be: + *

+ * + *

Individual cookie values may be Strings or Objects to be converted to + * String values through the configured {@link ConversionService}. + * + *

If the value is required but {@code null}, {@link IllegalArgumentException} + * is raised. The value is not required if: + *

+ * + * @author Rossen Stoyanchev + * @since 6.0 + */ +public class CookieValueArgumentResolver extends AbstractNamedValueArgumentResolver { + + + public CookieValueArgumentResolver(ConversionService conversionService) { + super(conversionService); + } + + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + CookieValue annot = parameter.getParameterAnnotation(CookieValue.class); + return (annot == null ? null : + new NamedValueInfo(annot.name(), annot.required(), annot.defaultValue(), "cookie value", true)); + } + + @Override + protected void addRequestValue(String name, String value, HttpRequestValues.Builder requestValues) { + requestValues.addCookie(name, value); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java index 64f8f525344f..f022b1e76426 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java @@ -18,10 +18,14 @@ import java.net.URI; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; import org.reactivestreams.Publisher; @@ -29,11 +33,14 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.http.codec.FormHttpMessageWriter; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriUtils; /** @@ -75,21 +82,20 @@ public final class HttpRequestValues { private final ParameterizedTypeReference bodyElementType; - private HttpRequestValues(HttpMethod httpMethod, @Nullable URI uri, - @Nullable String uriTemplate, @Nullable Map uriVariables, - @Nullable HttpHeaders headers, @Nullable MultiValueMap cookies, + private HttpRequestValues(HttpMethod httpMethod, + @Nullable URI uri, @Nullable String uriTemplate, Map uriVariables, + HttpHeaders headers, MultiValueMap cookies, @Nullable Object bodyValue, - @Nullable Publisher body, - @Nullable ParameterizedTypeReference bodyElementType) { + @Nullable Publisher body, @Nullable ParameterizedTypeReference bodyElementType) { - Assert.isTrue(uri == null || uriTemplate == null, "Expected either URI or URI template, not both"); + Assert.isTrue(uri != null || uriTemplate != null, "Neither URI nor URI template"); this.httpMethod = httpMethod; this.uri = uri; - this.uriTemplate = (uri != null || uriTemplate != null ? uriTemplate : ""); - this.uriVariables = (uriVariables != null ? uriVariables : Collections.emptyMap()); - this.headers = (headers != null ? headers : HttpHeaders.EMPTY); - this.cookies = (cookies != null ? cookies : EMPTY_COOKIES_MAP); + this.uriTemplate = uriTemplate; + this.uriVariables = uriVariables; + this.headers = headers; + this.cookies = cookies; this.bodyValue = bodyValue; this.body = body; this.bodyElementType = bodyElementType; @@ -183,6 +189,8 @@ public static Builder builder(HttpMethod httpMethod) { */ public final static class Builder { + private static final Function, byte[]> FORM_DATA_SERIALIZER = new FormDataSerializer(); + private HttpMethod httpMethod; @Nullable @@ -192,7 +200,7 @@ public final static class Builder { private String uriTemplate; @Nullable - private Map uriVariables; + private Map uriVars; @Nullable private HttpHeaders headers; @@ -200,6 +208,9 @@ public final static class Builder { @Nullable private MultiValueMap cookies; + @Nullable + private MultiValueMap requestParams; + @Nullable private Object bodyValue; @@ -231,6 +242,7 @@ public Builder setHttpMethod(HttpMethod httpMethod) { public Builder setUri(URI uri) { this.uri = uri; this.uriTemplate = null; + this.uriVars = null; return this; } @@ -251,8 +263,8 @@ public Builder setUriTemplate(String uriTemplate) { * {@link #setUri(URI) full URI}. */ public Builder setUriVariable(String name, String value) { - this.uriVariables = (this.uriVariables != null ? this.uriVariables : new LinkedHashMap<>()); - this.uriVariables.put(name, value); + this.uriVars = (this.uriVars != null ? this.uriVars : new LinkedHashMap<>()); + this.uriVars.put(name, value); this.uri = null; return this; } @@ -300,6 +312,21 @@ public Builder addCookie(String name, String... values) { return this; } + /** + * Add the given request parameter name and values. + *

When {@code "content-type"} is set to + * {@code "application/x-www-form-urlencoded"}, request parameters are + * encoded in the request body. Otherwise, they are added as URL query + * parameters. + */ + public Builder addRequestParameter(String name, String... values) { + this.requestParams = (this.requestParams != null ? this.requestParams : new LinkedMultiValueMap<>()); + for (String value : values) { + this.requestParams.add(name, value); + } + return this; + } + /** * Set the request body as a concrete value to be serialized. *

This is mutually exclusive with, and resets any previously set @@ -326,10 +353,76 @@ public > void setBody(Publisher

body, Parameterized * Builder the {@link HttpRequestValues} instance. */ public HttpRequestValues build() { + + URI uri = this.uri; + String uriTemplate = (this.uriTemplate != null || uri != null ? this.uriTemplate : ""); + Map uriVars = (this.uriVars != null ? new HashMap<>(this.uriVars) : Collections.emptyMap()); + + Object bodyValue = this.bodyValue; + + if (!CollectionUtils.isEmpty(this.requestParams)) { + + boolean isFormData = (this.headers != null && + MediaType.APPLICATION_FORM_URLENCODED.equals(this.headers.getContentType())); + + if (isFormData) { + Assert.isTrue(bodyValue == null && this.body == null, "Expected body or request params, not both"); + bodyValue = FORM_DATA_SERIALIZER.apply(this.requestParams); + } + else if (uri != null) { + uri = UriComponentsBuilder.fromUri(uri) + .queryParams(UriUtils.encodeQueryParams(this.requestParams)) + .build(true) + .toUri(); + } + else { + uriVars = (uriVars.isEmpty() ? new HashMap<>() : uriVars); + uriTemplate = appendQueryParams(uriTemplate, uriVars, this.requestParams); + } + } + + HttpHeaders headers = HttpHeaders.EMPTY; + if (this.headers != null) { + headers = new HttpHeaders(); + headers.putAll(this.headers); + } + + MultiValueMap cookies = (this.cookies != null ? + new LinkedMultiValueMap<>(this.cookies) : EMPTY_COOKIES_MAP); + return new HttpRequestValues( - this.httpMethod, this.uri, this.uriTemplate, this.uriVariables, - this.headers, this.cookies, - this.bodyValue, this.body, this.bodyElementType); + this.httpMethod, uri, uriTemplate, uriVars, headers, cookies, bodyValue, + this.body, this.bodyElementType); + } + + private String appendQueryParams( + String uriTemplate, Map uriVars, MultiValueMap requestParams) { + + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(uriTemplate); + int i = 0; + for (Map.Entry> entry : requestParams.entrySet()) { + String nameVar = "queryParam" + i; + uriVars.put(nameVar, entry.getKey()); + for (int j = 0; j < entry.getValue().size(); j++) { + String valueVar = nameVar + "[" + j + "]"; + uriVars.put(valueVar, entry.getValue().get(j)); + uriComponentsBuilder.queryParam("{" + nameVar + "}", "{" + valueVar + "}"); + } + i++; + } + return uriComponentsBuilder.build().toUriString(); + } + + } + + + private static class FormDataSerializer + extends FormHttpMessageWriter implements Function, byte[]> { + + @Override + public byte[] apply(MultiValueMap requestParams) { + Charset charset = StandardCharsets.UTF_8; + return serializeForm(requestParams, charset).getBytes(charset); } } diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java index e4fcb5e4af8e..cebb149f8623 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java @@ -189,6 +189,9 @@ private List initArgumentResolvers(ConversionServic List resolvers = new ArrayList<>(this.customResolvers); resolvers.add(new RequestHeaderArgumentResolver(conversionService)); resolvers.add(new PathVariableArgumentResolver(conversionService)); + resolvers.add(new CookieValueArgumentResolver(conversionService)); + resolvers.add(new RequestParamArgumentResolver(conversionService)); + resolvers.add(new HttpUrlArgumentResolver()); resolvers.add(new HttpMethodArgumentResolver()); return resolvers; } diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpUrlArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpUrlArgumentResolver.java new file mode 100644 index 000000000000..7b65280b8232 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpUrlArgumentResolver.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.service.invoker; + +import java.net.URI; + +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpMethod; +import org.springframework.lang.Nullable; + + +/** + * {@link HttpServiceArgumentResolver} that resolves the target + * request's URL from an {@link HttpMethod} argument. + * + * @author Rossen Stoyanchev + * @since 6.0 + */ +public class HttpUrlArgumentResolver implements HttpServiceArgumentResolver { + + @Override + public boolean resolve( + @Nullable Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) { + + if (argument instanceof URI uri) { + requestValues.setUri(uri); + return true; + } + + return false; + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/RequestHeaderArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestHeaderArgumentResolver.java index 58928329bf96..92691c0a0f92 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/RequestHeaderArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestHeaderArgumentResolver.java @@ -27,8 +27,9 @@ * *

The argument may be: *

    - *
  • {@code Map} or {@link org.springframework.util.MultiValueMap} with - * multiple headers and value(s). + *
  • {@code Map} or + * {@link org.springframework.util.MultiValueMap MultiValueMap<String, ?>} + * with multiple headers and value(s). *
  • {@code Collection} or an array of header values. *
  • An individual header value. *
diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/RequestParamArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestParamArgumentResolver.java new file mode 100644 index 000000000000..ac414c6910ed --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestParamArgumentResolver.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.service.invoker; + +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.web.bind.annotation.RequestParam; + + +/** + * {@link HttpServiceArgumentResolver} for {@link RequestParam @RequestParam} + * annotated arguments. + * + *

When {@code "content-type"} is set to + * {@code "application/x-www-form-urlencoded"}, request parameters are encoded + * in the request body. Otherwise, they are added as URL query parameters. + * + *

The argument may be: + *

    + *
  • {@code Map} or + * {@link org.springframework.util.MultiValueMap MultiValueMap<String, ?>} with + * multiple request parameter and value(s). + *
  • {@code Collection} or an array of request parameters. + *
  • An individual request parameter. + *
+ * + *

Individual request parameters may be Strings or Objects to be converted to + * String values through the configured {@link ConversionService}. + * + *

If the value is required but {@code null}, {@link IllegalArgumentException} + * is raised. The value is not required if: + *

    + *
  • {@link RequestParam#required()} is set to {@code false} + *
  • {@link RequestParam#defaultValue()} provides a fallback value + *
  • The argument is declared as {@link java.util.Optional} + *
+ * + * @author Rossen Stoyanchev + * @since 6.0 + */ +public class RequestParamArgumentResolver extends AbstractNamedValueArgumentResolver { + + + public RequestParamArgumentResolver(ConversionService conversionService) { + super(conversionService); + } + + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + RequestParam annot = parameter.getParameterAnnotation(RequestParam.class); + return (annot == null ? null : + new NamedValueInfo(annot.name(), annot.required(), annot.defaultValue(), "request parameter", true)); + } + + @Override + protected void addRequestValue(String name, String value, HttpRequestValues.Builder requestValues) { + requestValues.addRequestParameter(name, value); + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/CookieValueArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/CookieValueArgumentResolverTests.java new file mode 100644 index 000000000000..0ff41756b0af --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/CookieValueArgumentResolverTests.java @@ -0,0 +1,186 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.service.invoker; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.apache.groovy.util.Maps; +import org.junit.jupiter.api.Test; + +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.service.annotation.GetExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + + +/** + * Unit tests for {@link RequestHeaderArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +class CookieValueArgumentResolverTests { + + private final TestHttpClientAdapter clientAdapter = new TestHttpClientAdapter(); + + private final Service service = this.clientAdapter.createService(Service.class); + + + @Test + void stringCookie() { + this.service.executeString("test"); + assertCookie("cookie", "test"); + } + + @Test + void objectCookie() { + this.service.execute(Boolean.TRUE); + assertCookie("cookie", "true"); + } + + @Test + void listCookie() { + this.service.executeList(List.of("test1", Boolean.TRUE, "test3")); + assertCookie("multiValueCookie", "test1", "true", "test3"); + } + + @Test + void arrayCookie() { + this.service.executeArray("test1", Boolean.FALSE, "test3"); + assertCookie("multiValueCookie", "test1", "false", "test3"); + } + + @Test + void namedCookie() { + this.service.executeNamed("test"); + assertCookie("cookieRenamed", "test"); + } + + @SuppressWarnings("ConstantConditions") + @Test + void nullCookieRequired() { + assertThatIllegalArgumentException().isThrownBy(() -> this.service.executeString(null)); + } + + @Test + void nullCookieNotRequired() { + this.service.executeNotRequired(null); + assertCookie("cookie"); + } + + @Test + void nullCookieWithDefaultValue() { + this.service.executeWithDefaultValue(null); + assertCookie("cookie", "default"); + } + + @Test + void optionalStringCookie() { + this.service.executeOptional(Optional.of("test")); + assertCookie("cookie", "test"); + } + + @Test + void optionalObjectCookie() { + this.service.executeOptional(Optional.of(Boolean.TRUE)); + assertCookie("cookie", "true"); + } + + @Test + void optionalEmpty() { + this.service.executeOptional(Optional.empty()); + assertCookie("cookie"); + } + + @Test + void optionalEmpthyWithDefaultValue() { + this.service.executeOptionalWithDefaultValue(Optional.empty()); + assertCookie("cookie", "default"); + } + + @Test + void mapOfCookies() { + this.service.executeMap(Maps.of("cookie1", "true", "cookie2", "false")); + assertCookie("cookie1", "true"); + assertCookie("cookie2", "false"); + } + + @Test + void mapOfCookiesIsNull() { + assertThatIllegalArgumentException().isThrownBy(() -> this.service.executeMap(null)); + } + + @Test + void mapOfCookiesHasOptionalValue() { + this.service.executeMapWithOptionalValue(Map.of("cookie", Optional.of("test"))); + assertCookie("cookie", "test"); + } + + private void assertCookie(String key, String... values) { + List actualValues = this.clientAdapter.getRequestValues().getCookies().get(key); + if (ObjectUtils.isEmpty(values)) { + assertThat(actualValues).isNull(); + } + else { + assertThat(actualValues).containsOnly(values); + } + } + + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private interface Service { + + @GetExchange + void executeString(@CookieValue String cookie); + + @GetExchange + void execute(@CookieValue Object cookie); + + @GetExchange + void executeList(@CookieValue List multiValueCookie); + + @GetExchange + void executeArray(@CookieValue Object... multiValueCookie); + + @GetExchange + void executeNamed(@CookieValue(name = "cookieRenamed") String cookie); + + @GetExchange + void executeNotRequired(@Nullable @CookieValue(required = false) String cookie); + + @GetExchange + void executeWithDefaultValue(@Nullable @CookieValue(defaultValue = "default") String cookie); + + @GetExchange + void executeOptional(@CookieValue Optional cookie); + + @GetExchange + void executeOptionalWithDefaultValue(@CookieValue(defaultValue = "default") Optional cookie); + + @GetExchange + void executeMap(@Nullable @CookieValue Map cookie); + + @GetExchange + void executeMapWithOptionalValue(@CookieValue Map> cookies); + + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpUrlArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpUrlArgumentResolverTests.java new file mode 100644 index 000000000000..805065e01e7f --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpUrlArgumentResolverTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.service.invoker; + +import java.net.URI; + +import org.junit.jupiter.api.Test; + +import org.springframework.web.service.annotation.GetExchange; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Unit tests for {@link HttpUrlArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +public class HttpUrlArgumentResolverTests { + + private final TestHttpClientAdapter clientAdapter = new TestHttpClientAdapter(); + + private final Service service = this.clientAdapter.createService(Service.class); + + + @Test + void url() { + URI dynamicUrl = URI.create("dynamic-path"); + this.service.execute(dynamicUrl); + + assertThat(getRequestValues().getUri()).isEqualTo(dynamicUrl); + assertThat(getRequestValues().getUriTemplate()).isNull(); + } + + private HttpRequestValues getRequestValues() { + return this.clientAdapter.getRequestValues(); + } + + + private interface Service { + + @GetExchange("/path") + void execute(URI uri); + + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/RequestParamArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/RequestParamArgumentResolverTests.java new file mode 100644 index 000000000000..4c3119547155 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/RequestParamArgumentResolverTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.service.invoker; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.service.annotation.GetExchange; +import org.springframework.web.service.annotation.PostExchange; +import org.springframework.web.util.UriComponentsBuilder; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Unit tests for {@link RequestParamArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +public class RequestParamArgumentResolverTests { + + private final TestHttpClientAdapter clientAdapter = new TestHttpClientAdapter(); + + private final Service service = this.clientAdapter.createService(Service.class); + + + @Test + void formData() { + this.service.postForm("value 1", "value 2"); + + Object body = this.clientAdapter.getRequestValues().getBodyValue(); + assertThat(body).isNotNull().isInstanceOf(byte[].class); + assertThat(new String((byte[]) body, UTF_8)).isEqualTo("param1=value+1¶m2=value+2"); + } + + @Test + void uriTemplate() { + this.service.search("1st value", Arrays.asList("2nd value A", "2nd value B")); + + HttpRequestValues requestValues = this.clientAdapter.getRequestValues(); + + assertThat(requestValues.getUriTemplate()) + .isEqualTo("/path?" + + "{queryParam0}={queryParam0[0]}&" + + "{queryParam1}={queryParam1[0]}&" + + "{queryParam1}={queryParam1[1]}"); + + assertThat(requestValues.getUriVariables()) + .containsOnlyKeys("queryParam0", "queryParam1", "queryParam0[0]", "queryParam1[0]", "queryParam1[1]") + .containsEntry("queryParam0", "param1") + .containsEntry("queryParam1", "param2") + .containsEntry("queryParam0[0]", "1st value") + .containsEntry("queryParam1[0]", "2nd value A") + .containsEntry("queryParam1[1]", "2nd value B"); + + URI uri = UriComponentsBuilder.fromUriString(requestValues.getUriTemplate()) + .encode().build(requestValues.getUriVariables()); + + assertThat(uri.toString()) + .isEqualTo("/path?param1=1st%20value¶m2=2nd%20value%20A¶m2=2nd%20value%20B"); + } + + @Test + void uri() { + URI baseUrl = URI.create("http://localhost:8080/path"); + this.service.searchWithDynamicUri(baseUrl, "1st value", Arrays.asList("2nd value A", "2nd value B")); + + assertThat(this.clientAdapter.getRequestValues().getUri().toString()) + .isEqualTo(baseUrl + "?param1=1st%20value¶m2=2nd%20value%20A¶m2=2nd%20value%20B"); + } + + + private interface Service { + + @PostExchange(contentType = "application/x-www-form-urlencoded") + void postForm(@RequestParam String param1, @RequestParam String param2); + + @GetExchange("/path") + void search(@RequestParam String param1, @RequestParam List param2); + + @GetExchange + void searchWithDynamicUri(URI uri, @RequestParam String param1, @RequestParam List param2); + } + +}