5 changed files with 355 additions and 5 deletions
@ -0,0 +1,139 @@
@@ -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); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,198 @@
@@ -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); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue