Add initial RequestHeaderArgumentResolver implementation and tests.

This commit is contained in:
Olga Maciaszek-Sharma 2022-04-28 19:07:10 +02:00 committed by rstoyanchev
parent 0a9271e721
commit 38bf0776a1
5 changed files with 355 additions and 5 deletions

View File

@ -189,6 +189,7 @@ public final class HttpServiceProxyFactory {
List<HttpServiceArgumentResolver> resolvers = new ArrayList<>(this.customResolvers);
resolvers.add(new HttpMethodArgumentResolver());
resolvers.add(new PathVariableArgumentResolver(conversionService));
resolvers.add(new RequestHeaderArgumentResolver(conversionService));
return resolvers;
}

View File

@ -66,7 +66,7 @@ public class PathVariableArgumentResolver implements HttpServiceArgumentResolver
if (Map.class.isAssignableFrom(parameter.getParameterType())) {
if (argument != null) {
Assert.isInstanceOf(Map.class, argument);
((Map<String, ?>) argument).forEach((key, value) ->
((Map<Object, ?>) argument).forEach((key, value) ->
addUriParameter(key, value, annotation.required(), requestValues));
}
}
@ -81,7 +81,10 @@ public class PathVariableArgumentResolver implements HttpServiceArgumentResolver
}
private void addUriParameter(
String name, @Nullable Object value, boolean required, HttpRequestValues.Builder requestValues) {
Object name, @Nullable Object value, boolean required, HttpRequestValues.Builder requestValues) {
String stringName = this.conversionService.convert(name, String.class);
Assert.notNull(stringName, "Missing path variable name");
if (value instanceof Optional) {
value = ((Optional<?>) value).orElse(null);
@ -92,15 +95,15 @@ public class PathVariableArgumentResolver implements HttpServiceArgumentResolver
}
if (value == null) {
Assert.isTrue(!required, "Missing required path variable '" + name + "'");
Assert.isTrue(!required, "Missing required path variable '" + stringName + "'");
return;
}
if (logger.isTraceEnabled()) {
logger.trace("Resolved path variable '" + name + "' to " + value);
logger.trace("Resolved path variable '" + stringName + "' to " + value);
}
requestValues.setUriVariable(name, (String) value);
requestValues.setUriVariable(stringName, (String) value);
}
}

View File

@ -0,0 +1,139 @@
/*
* 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.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.ValueConstants;
/**
* An implementation of {@link HttpServiceArgumentResolver} that resolves
* request headers based on method arguments annotated
* with {@link RequestHeader}. {@code null} values are allowed only
* if {@link RequestHeader#required()} is {@code true}. {@code null}
* values are replaced with {@link RequestHeader#defaultValue()} if it
* is not equal to {@link ValueConstants#DEFAULT_NONE}.
*
* @author Olga Maciaszek-Sharma
* @since 6.0
*/
public class RequestHeaderArgumentResolver implements HttpServiceArgumentResolver {
private static final Log logger = LogFactory.getLog(RequestHeaderArgumentResolver.class);
private final ConversionService conversionService;
public RequestHeaderArgumentResolver(ConversionService conversionService) {
Assert.notNull(conversionService, "ConversionService is required");
this.conversionService = conversionService;
}
@Override
public boolean resolve(@Nullable Object argument, MethodParameter parameter,
HttpRequestValues.Builder requestValues) {
RequestHeader annotation = parameter.getParameterAnnotation(RequestHeader.class);
if (annotation == null) {
return false;
}
if (Map.class.isAssignableFrom(parameter.getParameterType())) {
if (argument != null) {
Assert.isInstanceOf(Map.class, argument);
((Map<?, ?>) argument).forEach((key, value) ->
addRequestHeader(key, value, annotation.required(), annotation.defaultValue(),
requestValues));
}
}
else {
String name = StringUtils.hasText(annotation.value()) ?
annotation.value() : annotation.name();
name = StringUtils.hasText(name) ? name : parameter.getParameterName();
Assert.notNull(name, "Failed to determine request header name for parameter: " + parameter);
addRequestHeader(name, argument, annotation.required(), annotation.defaultValue(),
requestValues);
}
return true;
}
private void addRequestHeader(
Object name, @Nullable Object value, boolean required, String defaultValue,
HttpRequestValues.Builder requestValues) {
String stringName = this.conversionService.convert(name, String.class);
Assert.notNull(stringName, "Failed to convert request header name '" +
name + "' to String");
if (value instanceof Optional) {
value = ((Optional<?>) value).orElse(null);
}
if (value == null) {
if (!ValueConstants.DEFAULT_NONE.equals(defaultValue)) {
value = defaultValue;
}
else {
Assert.isTrue(!required, "Missing required request header '" + stringName + "'");
return;
}
}
String[] headerValues = toStringArray(value);
if (logger.isTraceEnabled()) {
logger.trace("Resolved request header '" + stringName + "' to list of values: " +
String.join(", ", headerValues));
}
requestValues.addHeader(stringName, headerValues);
}
private String[] toStringArray(Object value) {
return toValueStream(value)
.filter(Objects::nonNull)
.map(headerElement -> headerElement instanceof String
? (String) headerElement :
this.conversionService.convert(headerElement, String.class))
.filter(Objects::nonNull)
.toArray(String[]::new);
}
private Stream<?> toValueStream(Object value) {
if (value instanceof Object[]) {
return Arrays.stream((Object[]) value);
}
if (value instanceof Collection<?>) {
return ((Collection<?>) value).stream();
}
return Stream.of(value);
}
}

View File

@ -135,6 +135,12 @@ class PathVariableArgumentResolverTests {
.isThrownBy(() -> this.service.executeOptionalValueMap(Map.of("id", Optional.empty())));
}
@Test
void shouldResolvePathVariableNameFromObjectMapKey() {
this.service.executeValueMapWithObjectKey(Map.of(Boolean.TRUE, "true"));
assertPathVariable("true", "true");
}
@SuppressWarnings("SameParameterValue")
private void assertPathVariable(String name, @Nullable String expectedValue) {
assertThat(getActualUriVariables().get(name)).isEqualTo(expectedValue);
@ -178,6 +184,9 @@ class PathVariableArgumentResolverTests {
@GetExchange
void executeValueMap(@Nullable @PathVariable Map<String, String> map);
@GetExchange
void executeValueMapWithObjectKey(@Nullable @PathVariable Map<Object, String> map);
@GetExchange
void executeOptionalValueMap(@PathVariable Map<String, Optional<String>> map);
}

View File

@ -0,0 +1,198 @@
/*
* 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.http.HttpHeaders;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.service.annotation.GetExchange;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link RequestHeaderArgumentResolver}.
*
* @author Olga Maciaszek-Sharma
*/
class RequestHeaderArgumentResolverTests {
private final TestHttpClientAdapter clientAdapter = new TestHttpClientAdapter();
private final Service service = this.clientAdapter.createService(Service.class);
@Test
void shouldResolveSingleValueRequestHeader() {
this.service.executeString("test");
assertRequestHeaders("id", "test");
}
@Test
void shouldResolveRequestHeaderWithNameFromAnnotationName() {
this.service.executeNamed("test");
assertRequestHeaders("id", "test");
}
@Test
void shouldResolveRequestHeaderNameFromValue() {
this.service.executeNamedWithValue("test");
assertRequestHeaders("test", "test");
}
@Test
void shouldResolveObjectValueRequestHeader() {
this.service.execute(Boolean.TRUE);
assertRequestHeaders("id", "true");
}
@Test
void shouldResolveListRequestHeader() {
this.service.execute(List.of("test1", Boolean.TRUE, "test3"));
assertRequestHeaders("id", "test1", "true", "test3");
}
@Test
void shouldResolveArrayRequestHeader() {
this.service.execute("test1", Boolean.FALSE, "test3");
assertRequestHeaders("id", "test1", "false", "test3");
}
@Test
void shouldResolveRequestHeadersFromMap() {
this.service.executeMap(Maps.of(Boolean.TRUE, "true", Boolean.FALSE, "false"));
assertRequestHeaders("true", "true");
assertRequestHeaders("false", "false");
}
@Test
void shouldThrowExceptionWhenRequiredHeaderNull() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.service.executeString(null));
}
@Test
void shouldIgnoreNullWhenHeaderNotRequired() {
this.service.executeNotRequired(null);
assertThat(getActualHeaders().get("id")).isNull();
}
@Test
void shouldIgnoreNullMapValue() {
this.service.executeMap(null);
assertThat(getActualHeaders()).isEmpty();
}
@Test
void shouldResolveRequestHeaderFromOptionalArgumentWithConversion() {
this.service.executeOptional(Optional.of(Boolean.TRUE));
assertRequestHeaders("id", "true");
}
@Test
void shouldResolveRequestHeaderFromOptionalArgument() {
this.service.executeOptional(Optional.of("test"));
assertRequestHeaders("id", "test");
}
@Test
void shouldThrowExceptionForEmptyOptional() {
assertThatIllegalArgumentException().isThrownBy(() -> this.service.execute(Optional.empty()));
}
@Test
void shouldIgnoreEmptyOptionalWhenNotRequired() {
this.service.executeOptionalNotRequired(Optional.empty());
assertThat(getActualHeaders().get("id")).isNull();
}
@Test
void shouldResolveRequestHeaderFromOptionalMapValue() {
this.service.executeOptionalMapValue(Map.of("id", Optional.of("test")));
assertRequestHeaders("id", "test");
}
@Test
void shouldReplaceNullValueWithDefaultWhenAvailable() {
this.service.executeWithDefaultValue(null);
assertRequestHeaders("id", "default");
}
@Test
void shouldReplaceEmptyOptionalValueWithDefaultWhenAvailable() {
this.service.executeOptionalWithDefaultValue(Optional.empty());
assertRequestHeaders("id", "default");
}
private void assertRequestHeaders(String key, String... values) {
assertThat(getActualHeaders().get(key)).containsOnly(values);
}
private HttpHeaders getActualHeaders() {
return this.clientAdapter.getRequestValues().getHeaders();
}
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private interface Service {
@GetExchange
void executeString(@Nullable @RequestHeader String id);
@GetExchange
void executeNotRequired(@Nullable @RequestHeader(required = false) String id);
@GetExchange
void execute(@RequestHeader Object id);
@GetExchange
void execute(@RequestHeader List<Object> id);
@GetExchange
void execute(@RequestHeader Object... id);
@GetExchange
void executeMap(@Nullable @RequestHeader Map<Object, String> id);
@GetExchange
void executeOptionalMapValue(@RequestHeader Map<Object, Optional<String>> id);
@GetExchange
void executeOptional(@RequestHeader Optional<Object> id);
@GetExchange
void executeOptionalNotRequired(@RequestHeader(required = false) Optional<String> id);
@GetExchange
void executeNamedWithValue(@Nullable @RequestHeader(name = "id", value = "test") String employeeId);
@GetExchange
void executeNamed(@RequestHeader(name = "id") String employeeId);
@GetExchange
void executeWithDefaultValue(@Nullable @RequestHeader(defaultValue = "default") String id);
@GetExchange
void executeOptionalWithDefaultValue(@Nullable @RequestHeader(defaultValue = "default") Optional<Object> id);
}
}