diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/AbstractNamedValueArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/AbstractNamedValueArgumentResolver.java new file mode 100644 index 00000000000..baf50d06901 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/AbstractNamedValueArgumentResolver.java @@ -0,0 +1,235 @@ +/* + * 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.Optional; +import java.util.concurrent.ConcurrentHashMap; + +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.ObjectUtils; +import org.springframework.web.bind.annotation.ValueConstants; + + +/** + * Base class for arguments that resolve to a named request value such as a + * request header, path variable, cookie, and others. + * + * @author Rossen Stoyanchev + * @since 6.0 + */ +public abstract class AbstractNamedValueArgumentResolver implements HttpServiceArgumentResolver { + + protected final Log logger = LogFactory.getLog(getClass()); + + private final ConversionService conversionService; + + private final Map namedValueInfoCache = new ConcurrentHashMap<>(256); + + + /** + * Create an instance. + * @param conversionService the {@link ConversionService} to use to format + * Object to String values. + */ + protected AbstractNamedValueArgumentResolver(ConversionService conversionService) { + Assert.notNull(conversionService, "ConversionService is required"); + this.conversionService = conversionService; + } + + + /** + * Return the configured {@link ConversionService}. + */ + public ConversionService getConversionService() { + return this.conversionService; + } + + + @SuppressWarnings("unchecked") + @Override + public boolean resolve( + @Nullable Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) { + + NamedValueInfo info = getNamedValueInfo(parameter); + if (info == null) { + return false; + } + + if (Map.class.isAssignableFrom(parameter.getParameterType())) { + Assert.isInstanceOf(Map.class, argument); + for (Map.Entry entry : ((Map) argument).entrySet()) { + addSingleOrMultipleValues( + entry.getKey(), entry.getValue(), false, null, info.label, info.multiValued, + requestValues); + } + } + else { + addSingleOrMultipleValues( + info.name, argument, info.required, info.defaultValue, info.label, info.multiValued, + requestValues); + } + + return true; + } + + @Nullable + private NamedValueInfo getNamedValueInfo(MethodParameter parameter) { + NamedValueInfo info = this.namedValueInfoCache.get(parameter); + if (info == null) { + info = createNamedValueInfo(parameter); + if (info == null) { + return null; + } + info = updateNamedValueInfo(parameter, info); + this.namedValueInfoCache.put(parameter, info); + } + return info; + } + + /** + * Return information about the request value, or {@code null} if the + * parameter does not represent a request value of interest. + */ + @Nullable + protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter); + + private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) { + String name = info.name; + if (info.name.isEmpty()) { + name = parameter.getParameterName(); + if (name == null) { + throw new IllegalArgumentException( + "Name for argument of type [" + parameter.getParameterType().getName() + "] " + + "not specified, and parameter name information not found in class file either."); + } + } + boolean required = (info.required && !parameter.getParameterType().equals(Optional.class)); + String defaultValue = (ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue); + return info.update(name, required, defaultValue); + } + + private void addSingleOrMultipleValues( + String name, @Nullable Object value, boolean required, @Nullable Object defaultValue, + String valueLabel, boolean supportsMultiValues, HttpRequestValues.Builder requestValues) { + + if (supportsMultiValues) { + value = (ObjectUtils.isArray(value) ? Arrays.asList((Object[]) value) : value); + if (value instanceof Collection elements) { + boolean hasValues = false; + for (Object element : elements) { + if (element != null) { + hasValues = true; + addSingleValue(name, element, false, null, valueLabel, requestValues); + } + } + if (hasValues) { + return; + } + value = null; + } + } + + addSingleValue(name, value, required, defaultValue, valueLabel, requestValues); + } + + private void addSingleValue( + String name, @Nullable Object value, boolean required, @Nullable Object defaultValue, String valueLabel, + HttpRequestValues.Builder requestValues) { + + if (value instanceof Optional optionalValue) { + value = optionalValue.orElse(null); + } + + if (value == null && defaultValue != null) { + value = defaultValue; + } + + if (!(value instanceof String)) { + value = getConversionService().convert(value, String.class); + } + + if (value == null) { + Assert.isTrue(!required, "Missing " + valueLabel + " value '" + name + "'"); + return; + } + + if (logger.isTraceEnabled()) { + logger.trace("Resolved " + valueLabel + " value '" + name + ":" + value + "'"); + } + + addRequestValue(name, (String) value, requestValues); + } + + /** + * Add the given, single request value. This may be called multiples times + * if the request value is multivalued. + * @param name the request value name + * @param value the value + * @param requestValues builder to add the request value to + */ + protected abstract void addRequestValue(String name, String value, HttpRequestValues.Builder requestValues); + + + /** + * Info about a request value, typically extracted from a method parameter annotation. + */ + protected static class NamedValueInfo { + + private final String name; + + private final boolean required; + + @Nullable + private final String defaultValue; + + private final String label; + + private final boolean multiValued; + + /** + * Create an instance. + * @param name the name to use, possibly empty if not specified + * @param required whether it is marked as required + * @param defaultValue fallback value, possibly {@link ValueConstants#DEFAULT_NONE} + * @param label how it should appear in error messages, e.g. "path variable", "request header" + */ + public NamedValueInfo( + String name, boolean required, @Nullable String defaultValue, String label, boolean multiValued) { + + this.name = name; + this.required = required; + this.defaultValue = defaultValue; + this.label = label; + this.multiValued = multiValued; + } + + public NamedValueInfo update(String name, boolean required, @Nullable String defaultValue) { + return new NamedValueInfo(name, required, defaultValue, this.label, this.multiValued); + } + + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java index 764cbc68779..01ac5ff233f 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java @@ -34,6 +34,7 @@ import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; @@ -86,7 +87,7 @@ final class HttpServiceMethod { DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer(); MethodParameter[] parameters = new MethodParameter[count]; for (int i = 0; i < count; i++) { - parameters[i] = new MethodParameter(method, i); + parameters[i] = new SynthesizingMethodParameter(method, i); parameters[i].initParameterNameDiscovery(nameDiscoverer); } return parameters; diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/PathVariableArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/PathVariableArgumentResolver.java index a9e6b5f79b6..af87e2418f9 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/PathVariableArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/PathVariableArgumentResolver.java @@ -16,94 +16,48 @@ package org.springframework.web.service.invoker; -import java.util.Map; -import java.util.Optional; - -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.PathVariable; /** - * An implementation of {@link HttpServiceArgumentResolver} that resolves - * request path variables based on method arguments annotated - * with {@link PathVariable}. {@code null} values are allowed only - * if {@link PathVariable#required()} is {@code true}. + * {@link HttpServiceArgumentResolver} for {@link PathVariable @PathVariable} + * annotated arguments. + * + *

The argument may be a single variable value or a {@code Map} with multiple + * variable and values. Each value may be a String or an Object to be converted + * to a String through the configured {@link ConversionService}. + * + *

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

    + *
  • {@link PathVariable#required()} is set to {@code false} + *
  • The argument is declared as {@link java.util.Optional} + *
* * @author Olga Maciaszek-Sharma + * @author Rossen Stoyanchev * @since 6.0 */ -public class PathVariableArgumentResolver implements HttpServiceArgumentResolver { - - private static final Log logger = LogFactory.getLog(PathVariableArgumentResolver.class); - - - private final ConversionService conversionService; +public class PathVariableArgumentResolver extends AbstractNamedValueArgumentResolver { public PathVariableArgumentResolver(ConversionService conversionService) { - Assert.notNull(conversionService, "ConversionService is required"); - this.conversionService = conversionService; + super(conversionService); } - @SuppressWarnings("unchecked") @Override - public boolean resolve( - @Nullable Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) { - - PathVariable annotation = parameter.getParameterAnnotation(PathVariable.class); - if (annotation == null) { - return false; - } - - Class parameterType = parameter.getParameterType(); - boolean required = (annotation.required() && !Optional.class.isAssignableFrom(parameterType)); - - if (Map.class.isAssignableFrom(parameterType)) { - if (argument != null) { - Assert.isInstanceOf(Map.class, argument); - ((Map) argument).forEach((key, value) -> - addUriParameter(key, value, required, 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 path variable name for parameter: " + parameter); - addUriParameter(name, argument, required, requestValues); - } - - return true; + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + PathVariable annot = parameter.getParameterAnnotation(PathVariable.class); + return (annot == null ? null : + new NamedValueInfo(annot.name(), annot.required(), null, "path variable", false)); } - private void addUriParameter( - String name, @Nullable Object value, boolean required, HttpRequestValues.Builder requestValues) { - - if (value instanceof Optional) { - value = ((Optional) value).orElse(null); - } - - if (!(value instanceof String)) { - value = this.conversionService.convert(value, String.class); - } - - if (value == null) { - Assert.isTrue(!required, "Missing required path variable '" + name + "'"); - return; - } - - if (logger.isTraceEnabled()) { - logger.trace("Resolved path variable '" + name + "' to " + value); - } - - requestValues.setUriVariable(name, (String) value); + @Override + protected void addRequestValue(String name, String value, HttpRequestValues.Builder requestValues) { + requestValues.setUriVariable(name, value); } } 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 a00955c344e..58928329bf9 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 @@ -16,22 +16,9 @@ package org.springframework.web.service.invoker; -import java.util.Arrays; -import java.util.Collection; -import java.util.Map; -import java.util.Optional; - -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.ObjectUtils; -import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.ValueConstants; /** @@ -49,100 +36,36 @@ import org.springframework.web.bind.annotation.ValueConstants; *

Individual header 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: + *

    + *
  • {@link RequestHeader#required()} is set to {@code false} + *
  • {@link RequestHeader#defaultValue()} provides a fallback value + *
  • The argument is declared as {@link java.util.Optional} + *
+ * * @author Olga Maciaszek-Sharma + * @author Rossen Stoyanchev * @since 6.0 */ -public class RequestHeaderArgumentResolver implements HttpServiceArgumentResolver { - - private static final Log logger = LogFactory.getLog(RequestHeaderArgumentResolver.class); - - private final ConversionService conversionService; +public class RequestHeaderArgumentResolver extends AbstractNamedValueArgumentResolver { public RequestHeaderArgumentResolver(ConversionService conversionService) { - Assert.notNull(conversionService, "ConversionService is required"); - this.conversionService = conversionService; + super(conversionService); } - @SuppressWarnings("unchecked") @Override - public boolean resolve( - @Nullable Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) { - + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { RequestHeader annot = parameter.getParameterAnnotation(RequestHeader.class); - if (annot == null) { - return false; - } - - Class parameterType = parameter.getParameterType(); - boolean required = (annot.required() && !Optional.class.isAssignableFrom(parameterType)); - Object defaultValue = (ValueConstants.DEFAULT_NONE.equals(annot.defaultValue()) ? null : annot.defaultValue()); - - if (Map.class.isAssignableFrom(parameterType)) { - if (argument != null) { - Assert.isInstanceOf(Map.class, argument); - ((Map) argument).forEach((key, value) -> - addHeader(key, value, false, defaultValue, requestValues)); - } - } - else { - String name = StringUtils.hasText(annot.value()) ? annot.value() : annot.name(); - name = StringUtils.hasText(name) ? name : parameter.getParameterName(); - Assert.notNull(name, "Failed to determine request header name for parameter: " + parameter); - addHeader(name, argument, required, defaultValue, requestValues); - } - - return true; + return (annot == null ? null : + new NamedValueInfo(annot.name(), annot.required(), annot.defaultValue(), "request header", true)); } - private void addHeader( - String name, @Nullable Object value, boolean required, @Nullable Object defaultValue, - HttpRequestValues.Builder requestValues) { - - value = (ObjectUtils.isArray(value) ? Arrays.asList((Object[]) value) : value); - if (value instanceof Collection elements) { - boolean hasValue = false; - for (Object element : elements) { - if (element != null) { - hasValue = true; - addHeaderValue(name, element, false, requestValues); - } - } - if (hasValue) { - return; - } - value = null; - } - - if (value instanceof Optional optionalValue) { - value = optionalValue.orElse(null); - } - - if (value == null && defaultValue != null) { - value = defaultValue; - } - - addHeaderValue(name, value, required, requestValues); - } - - private void addHeaderValue( - String name, @Nullable Object value, boolean required, HttpRequestValues.Builder requestValues) { - - if (!(value instanceof String)) { - value = this.conversionService.convert(value, String.class); - } - - if (value == null) { - Assert.isTrue(!required, "Missing required header '" + name + "'"); - return; - } - - if (logger.isTraceEnabled()) { - logger.trace("Resolved header '" + name + ":" + value + "'"); - } - - requestValues.addHeader(name, (String) value); + @Override + protected void addRequestValue(String name, String value, HttpRequestValues.Builder requestValues) { + requestValues.addHeader(name, value); } } diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/PathVariableArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/PathVariableArgumentResolverTests.java index 8db7873a553..bfd97595c8f 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/PathVariableArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/PathVariableArgumentResolverTests.java @@ -42,106 +42,86 @@ class PathVariableArgumentResolverTests { @Test - void shouldResolvePathVariableWithNameFromParameter() { + void stringVariable() { this.service.execute("test"); assertPathVariable("id", "test"); } @Test - void shouldResolvePathVariableWithNameFromAnnotationName() { - this.service.executeNamed("test"); - assertPathVariable("id", "test"); - } - - @Test - void shouldResolvePathVariableNameFromValue() { - this.service.executeNamedWithValue("test"); - assertPathVariable("id", "test"); - } - - @Test - void shouldOverrideNameIfValuePresentInAnnotation() { - this.service.executeValueNamed("test"); - assertPathVariable("id", "test"); - } - - @Test - void shouldResolvePathVariableWithConversion() { + void objectVariable() { this.service.execute(Boolean.TRUE); assertPathVariable("id", "true"); } @Test - void shouldResolvePathVariableFromOptionalArgumentWithConversion() { - this.service.executeOptional(Optional.of(Boolean.TRUE)); - assertPathVariable("id", "true"); + void namedVariable() { + this.service.executeNamed("test"); + assertPathVariable("id", "test"); + } + + @SuppressWarnings("ConstantConditions") + @Test + void nullVariableRequired() { + assertThatIllegalArgumentException().isThrownBy(() -> this.service.execute(null)); } @Test - void shouldResolvePathVariableFromOptionalArgument() { + void nullVariableNotRequired() { + this.service.executeNotRequired(null); + assertPathVariable("id", null); + } + + @Test + void optionalStringVariable() { this.service.execute(Optional.of("test")); assertPathVariable("id", "test"); } @Test - void shouldThrowExceptionForNull() { - assertThatIllegalArgumentException().isThrownBy(() -> this.service.executeNamedWithValue(null)); + void optionalObjectVariable() { + this.service.executeOptional(Optional.of(Boolean.TRUE)); + assertPathVariable("id", "true"); } @Test - void shouldThrowExceptionForEmptyOptional() { + void optionalEmpty() { + this.service.executeOptional(Optional.empty()); + assertPathVariable("id", null); + } + + @Test + void optionalEmptyOnObjectArgument() { assertThatIllegalArgumentException().isThrownBy(() -> this.service.execute(Optional.empty())); } @Test - void shouldIgnoreNullWithConversionServiceWhenNotRequired() { - this.service.executeNotRequired(null); - assertThat(getActualUriVariables().get("id")).isNull(); - } - - @Test - void shouldIgnoreNullWhenNotRequired() { - this.service.executeNotRequired(null); - assertPathVariable("id", null); - } - - @Test - void shouldIgnoreEmptyOptionalWhenNotRequired() { - this.service.executeOptionalNotRequired(Optional.empty()); - assertPathVariable("id", null); - } - - @Test - void shouldResolvePathVariablesFromMap() { - this.service.executeValueMap(Map.of("id", "test")); + void mapOfVariables() { + this.service.executeMap(Map.of("id", "test")); assertPathVariable("id", "test"); } @Test - void shouldResolvePathVariableFromOptionalMapValue() { - this.service.executeOptionalValueMap(Map.of("id", Optional.of("test"))); + void mapOfVariablesIsNull() { + assertThatIllegalArgumentException().isThrownBy(() -> this.service.executeMap(null)); + } + + @Test + void mapOfVariablesHasOptionalValue() { + this.service.executeMapWithOptionalValue(Map.of("id", Optional.of("test"))); assertPathVariable("id", "test"); } - @Test - void shouldIgnoreNullMapValue() { - this.service.executeValueMap(null); - assertThat(getActualUriVariables()).isEmpty(); - } @Test - void shouldThrowExceptionForEmptyOptionalMapValue() { - assertThatIllegalArgumentException() - .isThrownBy(() -> this.service.executeOptionalValueMap(Map.of("id", Optional.empty()))); + void mapOfVariablesHasOptionalEmpty() { + this.service.executeMapWithOptionalValue(Map.of("id", Optional.empty())); + assertPathVariable("id", null); } @SuppressWarnings("SameParameterValue") private void assertPathVariable(String name, @Nullable String expectedValue) { - assertThat(getActualUriVariables().get(name)).isEqualTo(expectedValue); - } - - private Map getActualUriVariables() { - return this.clientAdapter.getRequestValues().getUriVariables(); + assertThat(this.clientAdapter.getRequestValues().getUriVariables().get(name)) + .isEqualTo(expectedValue); } @@ -151,6 +131,12 @@ class PathVariableArgumentResolverTests { @GetExchange void execute(@PathVariable String id); + @GetExchange + void execute(@PathVariable Object id); + + @GetExchange + void executeNamed(@PathVariable(name = "id") String employeeId); + @GetExchange void executeNotRequired(@Nullable @PathVariable(required = false) String id); @@ -158,28 +144,10 @@ class PathVariableArgumentResolverTests { void executeOptional(@PathVariable Optional id); @GetExchange - void executeOptionalNotRequired(@PathVariable(required = false) Optional id); + void executeMap(@Nullable @PathVariable Map map); @GetExchange - void executeNamedWithValue(@Nullable @PathVariable(name = "test", value = "id") String employeeId); - - @GetExchange - void executeNamed(@PathVariable(name = "id") String employeeId); - - @GetExchange - void executeValueNamed(@PathVariable("id") String employeeId); - - @GetExchange - void execute(@PathVariable Object id); - - @GetExchange - void execute(@PathVariable Boolean id); - - @GetExchange - void executeValueMap(@Nullable @PathVariable Map map); - - @GetExchange - void executeOptionalValueMap(@PathVariable Map> map); + void executeMapWithOptionalValue(@PathVariable Map> map); } } diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/RequestHeaderArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/RequestHeaderArgumentResolverTests.java index 167a717dae4..bf78d4cd723 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/RequestHeaderArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/RequestHeaderArgumentResolverTests.java @@ -23,8 +23,8 @@ 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.util.ObjectUtils; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.service.annotation.GetExchange; @@ -56,43 +56,25 @@ class RequestHeaderArgumentResolverTests { assertRequestHeaders("id", "true"); } - @Test - void namedHeader() { - this.service.executeNamed("test"); - assertRequestHeaders("id", "test"); - } - @Test void listHeader() { - this.service.execute(List.of("test1", Boolean.TRUE, "test3")); + this.service.executeList(List.of("test1", Boolean.TRUE, "test3")); assertRequestHeaders("multiValueHeader", "test1", "true", "test3"); } @Test void arrayHeader() { - this.service.execute("test1", Boolean.FALSE, "test3"); + this.service.executeArray("test1", Boolean.FALSE, "test3"); assertRequestHeaders("multiValueHeader", "test1", "false", "test3"); } @Test - void mapHeader() { - this.service.executeMap(Maps.of("header1", "true", "header2", "false")); - assertRequestHeaders("header1", "true"); - assertRequestHeaders("header2", "false"); - } - - @Test - void mapHeaderNull() { - this.service.executeMap(null); - assertThat(getActualHeaders()).isEmpty(); - } - - @Test - void mapWithOptional() { - this.service.executeOptionalMapValue(Map.of("id", Optional.of("test"))); + void namedHeader() { + this.service.executeNamed("test"); assertRequestHeaders("id", "test"); } + @SuppressWarnings("ConstantConditions") @Test void nullHeaderRequired() { assertThatIllegalArgumentException().isThrownBy(() -> this.service.executeString(null)); @@ -101,18 +83,23 @@ class RequestHeaderArgumentResolverTests { @Test void nullHeaderNotRequired() { this.service.executeNotRequired(null); - assertThat(getActualHeaders().get("id")).isNull(); + assertRequestHeaders("id"); } + @Test + void nullHeaderWithDefaultValue() { + this.service.executeWithDefaultValue(null); + assertRequestHeaders("id", "default"); + } @Test - void optional() { + void optionalStringHeader() { this.service.executeOptional(Optional.of("test")); assertRequestHeaders("id", "test"); } @Test - void optionalWithConversion() { + void optionalObjectHeader() { this.service.executeOptional(Optional.of(Boolean.TRUE)); assertRequestHeaders("id", "true"); } @@ -120,27 +107,41 @@ class RequestHeaderArgumentResolverTests { @Test void optionalEmpty() { this.service.executeOptional(Optional.empty()); - assertThat(getActualHeaders().get("id")).isNull(); + assertRequestHeaders("id"); } @Test - void defaultValueWithNull() { - this.service.executeWithDefaultValue(null); - assertRequestHeaders("id", "default"); - } - - @Test - void defaultValueWithOptional() { + void optionalEmpthyWithDefaultValue() { this.service.executeOptionalWithDefaultValue(Optional.empty()); assertRequestHeaders("id", "default"); } - private void assertRequestHeaders(String key, String... values) { - assertThat(getActualHeaders().get(key)).containsOnly(values); + @Test + void mapOfHeaders() { + this.service.executeMap(Maps.of("header1", "true", "header2", "false")); + assertRequestHeaders("header1", "true"); + assertRequestHeaders("header2", "false"); } - private HttpHeaders getActualHeaders() { - return this.clientAdapter.getRequestValues().getHeaders(); + @Test + void mapOfHeadersIsNull() { + assertThatIllegalArgumentException().isThrownBy(() -> this.service.executeMap(null)); + } + + @Test + void mapOfHeadersHasOptionalValue() { + this.service.executeMapWithOptionalValue(Map.of("id", Optional.of("test"))); + assertRequestHeaders("id", "test"); + } + + private void assertRequestHeaders(String key, String... values) { + List actualValues = this.clientAdapter.getRequestValues().getHeaders().get(key); + if (ObjectUtils.isEmpty(values)) { + assertThat(actualValues).isNull(); + } + else { + assertThat(actualValues).containsOnly(values); + } } @@ -148,38 +149,38 @@ class RequestHeaderArgumentResolverTests { private interface Service { @GetExchange - void executeString(@Nullable @RequestHeader String id); + void executeString(@RequestHeader String id); @GetExchange void execute(@RequestHeader Object id); + @GetExchange + void executeList(@RequestHeader List multiValueHeader); + + @GetExchange + void executeArray(@RequestHeader Object... multiValueHeader); + @GetExchange void executeNamed(@RequestHeader(name = "id") String employeeId); - @GetExchange - void execute(@RequestHeader List multiValueHeader); - - @GetExchange - void execute(@RequestHeader Object... multiValueHeader); - - @GetExchange - void executeMap(@Nullable @RequestHeader Map id); - - @GetExchange - void executeOptionalMapValue(@RequestHeader Map> headers); - @GetExchange void executeNotRequired(@Nullable @RequestHeader(required = false) String id); @GetExchange - void executeOptional(@RequestHeader Optional id); + void executeWithDefaultValue(@Nullable @RequestHeader(defaultValue = "default") String id); @GetExchange - void executeWithDefaultValue(@Nullable @RequestHeader(defaultValue = "default") String id); + void executeOptional(@RequestHeader Optional id); @GetExchange void executeOptionalWithDefaultValue(@RequestHeader(defaultValue = "default") Optional id); + @GetExchange + void executeMap(@Nullable @RequestHeader Map id); + + @GetExchange + void executeMapWithOptionalValue(@RequestHeader Map> headers); + } }