Add resolvers for URI, cookies, and request params

See gh-28386
This commit is contained in:
rstoyanchev
2022-05-02 21:36:09 +01:00
parent f8ac5985bd
commit 2794553d2e
9 changed files with 659 additions and 18 deletions

View File

@@ -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.
*
* <p>The argument may be:
* <ul>
* <li>{@code Map<String, ?>} or
* {@link org.springframework.util.MultiValueMap MultiValueMap&lt;String, ?&gt;} with
* multiple cookies and value(s).
* <li>{@code Collection} or an array of cookie values.
* <li>An individual cookie value.
* </ul>
*
* <p>Individual cookie values may be Strings or Objects to be converted to
* String values through the configured {@link ConversionService}.
*
* <p>If the value is required but {@code null}, {@link IllegalArgumentException}
* is raised. The value is not required if:
* <ul>
* <li>{@link CookieValue#required()} is set to {@code false}
* <li>{@link CookieValue#defaultValue()} provides a fallback value
* <li>The argument is declared as {@link java.util.Optional}
* </ul>
*
* @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);
}
}

View File

@@ -18,10 +18,14 @@ package org.springframework.web.service.invoker;
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.core.ParameterizedTypeReference;
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<String, String> uriVariables,
@Nullable HttpHeaders headers, @Nullable MultiValueMap<String, String> cookies,
private HttpRequestValues(HttpMethod httpMethod,
@Nullable URI uri, @Nullable String uriTemplate, Map<String, String> uriVariables,
HttpHeaders headers, MultiValueMap<String, String> 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 final class HttpRequestValues {
*/
public final static class Builder {
private static final Function<MultiValueMap<String, String>, byte[]> FORM_DATA_SERIALIZER = new FormDataSerializer();
private HttpMethod httpMethod;
@Nullable
@@ -192,7 +200,7 @@ public final class HttpRequestValues {
private String uriTemplate;
@Nullable
private Map<String, String> uriVariables;
private Map<String, String> uriVars;
@Nullable
private HttpHeaders headers;
@@ -200,6 +208,9 @@ public final class HttpRequestValues {
@Nullable
private MultiValueMap<String, String> cookies;
@Nullable
private MultiValueMap<String, String> requestParams;
@Nullable
private Object bodyValue;
@@ -231,6 +242,7 @@ public final class HttpRequestValues {
public Builder setUri(URI uri) {
this.uri = uri;
this.uriTemplate = null;
this.uriVars = null;
return this;
}
@@ -251,8 +263,8 @@ public final class HttpRequestValues {
* {@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 final class HttpRequestValues {
return this;
}
/**
* Add the given request parameter name and values.
* <p>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.
* <p>This is mutually exclusive with, and resets any previously set
@@ -326,10 +353,76 @@ public final class HttpRequestValues {
* Builder the {@link HttpRequestValues} instance.
*/
public HttpRequestValues build() {
URI uri = this.uri;
String uriTemplate = (this.uriTemplate != null || uri != null ? this.uriTemplate : "");
Map<String, String> 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<String, String> 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<String, String> uriVars, MultiValueMap<String, String> requestParams) {
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(uriTemplate);
int i = 0;
for (Map.Entry<String, List<String>> 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<MultiValueMap<String, String>, byte[]> {
@Override
public byte[] apply(MultiValueMap<String, String> requestParams) {
Charset charset = StandardCharsets.UTF_8;
return serializeForm(requestParams, charset).getBytes(charset);
}
}

View File

@@ -189,6 +189,9 @@ public final class HttpServiceProxyFactory {
List<HttpServiceArgumentResolver> 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;
}

View File

@@ -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;
}
}

View File

@@ -27,8 +27,9 @@ import org.springframework.web.bind.annotation.RequestHeader;
*
* <p>The argument may be:
* <ul>
* <li>{@code Map} or {@link org.springframework.util.MultiValueMap} with
* multiple headers and value(s).
* <li>{@code Map<String, ?>} or
* {@link org.springframework.util.MultiValueMap MultiValueMap&lt;String, ?&gt;}
* with multiple headers and value(s).
* <li>{@code Collection} or an array of header values.
* <li>An individual header value.
* </ul>

View File

@@ -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.
*
* <p>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.
*
* <p>The argument may be:
* <ul>
* <li>{@code Map<String, ?>} or
* {@link org.springframework.util.MultiValueMap MultiValueMap&lt;String, ?&gt;} with
* multiple request parameter and value(s).
* <li>{@code Collection} or an array of request parameters.
* <li>An individual request parameter.
* </ul>
*
* <p>Individual request parameters may be Strings or Objects to be converted to
* String values through the configured {@link ConversionService}.
*
* <p>If the value is required but {@code null}, {@link IllegalArgumentException}
* is raised. The value is not required if:
* <ul>
* <li>{@link RequestParam#required()} is set to {@code false}
* <li>{@link RequestParam#defaultValue()} provides a fallback value
* <li>The argument is declared as {@link java.util.Optional}
* </ul>
*
* @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);
}
}