diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpMethodArgumentResolver.java index 1f0a336fa83..6f3e1c2f764 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpMethodArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpMethodArgumentResolver.java @@ -23,29 +23,32 @@ import org.springframework.core.MethodParameter; import org.springframework.http.HttpMethod; import org.springframework.lang.Nullable; + /** - * An implementation of {@link HttpServiceMethodArgumentResolver} that resolves - * request HTTP method based on argument type. Arguments of type - * {@link HttpMethod} will be used to determine the method. + * {@link HttpServiceMethodArgumentResolver} that resolves the target + * request's HTTP method from an {@link HttpMethod} argument. * * @author Olga Maciaszek-Sharma * @since 6.0 */ public class HttpMethodArgumentResolver implements HttpServiceMethodArgumentResolver { - private static final Log LOG = LogFactory.getLog(HttpMethodArgumentResolver.class); + private static final Log logger = LogFactory.getLog(HttpMethodArgumentResolver.class); + @Override - public void resolve(@Nullable Object argument, MethodParameter parameter, - HttpRequestDefinition requestDefinition) { + public void resolve( + @Nullable Object argument, MethodParameter parameter, HttpRequestDefinition requestDefinition) { + if (argument == null) { return; } if (argument instanceof HttpMethod httpMethod) { - if (LOG.isTraceEnabled()) { - LOG.trace("Resolved HTTP method to: " + httpMethod.name()); + if (logger.isTraceEnabled()) { + logger.trace("Resolved HTTP method to: " + httpMethod.name()); } requestDefinition.setHttpMethod(httpMethod); } } + } 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 399e068d7de..34332d31ae5 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 @@ -23,14 +23,13 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.core.MethodParameter; -import org.springframework.core.ReactiveAdapter; -import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.TypeDescriptor; 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 HttpServiceMethodArgumentResolver} that resolves * request path variables based on method arguments annotated @@ -42,111 +41,64 @@ import org.springframework.web.bind.annotation.PathVariable; */ public class PathVariableArgumentResolver implements HttpServiceMethodArgumentResolver { - private static final Log LOG = LogFactory.getLog(PathVariableArgumentResolver.class); - private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class); - @Nullable + private static final Log logger = LogFactory.getLog(PathVariableArgumentResolver.class); + + private final ConversionService conversionService; - public PathVariableArgumentResolver(@Nullable ConversionService conversionService) { + + public PathVariableArgumentResolver(ConversionService conversionService) { + Assert.notNull(conversionService, "ConversionService is required"); this.conversionService = conversionService; } + + @SuppressWarnings("unchecked") @Override - public void resolve(@Nullable Object argument, MethodParameter parameter, - HttpRequestDefinition requestDefinition) { + public void resolve( + @Nullable Object argument, MethodParameter parameter, HttpRequestDefinition requestDefinition) { + PathVariable annotation = parameter.getParameterAnnotation(PathVariable.class); if (annotation == null) { return; } - String resolvedAnnotationName = StringUtils.hasText(annotation.value()) - ? annotation.value() : annotation.name(); - boolean required = annotation.required(); - Object resolvedArgument = resolveFromOptional(argument); - if (resolvedArgument instanceof Map valueMap) { - if (StringUtils.hasText(resolvedAnnotationName)) { - Object value = valueMap.get(resolvedAnnotationName); - Object resolvedValue = resolveFromOptional(value); - addUriParameter(requestDefinition, resolvedAnnotationName, resolvedValue, required); - return; + + if (Map.class.isAssignableFrom(parameter.getParameterType())) { + if (argument != null) { + Assert.isInstanceOf(Map.class, argument); + ((Map) argument).forEach((key, value) -> + addUriParameter(key, value, annotation.required(), requestDefinition)); } - valueMap.entrySet() - .forEach(entry -> addUriParameter(requestDefinition, entry, required)); + } + 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, annotation.required(), requestDefinition); + } + } + + private void addUriParameter( + String name, @Nullable Object value, boolean required, HttpRequestDefinition requestDefinition) { + + if (value instanceof Optional) { + value = ((Optional) value).orElse(null); + } + + if (value == null) { + Assert.isTrue(!required, "Missing required path variable '" + name + "'"); return; } - String name = StringUtils.hasText(resolvedAnnotationName) - ? resolvedAnnotationName : parameter.getParameterName(); - addUriParameter(requestDefinition, name, resolvedArgument, required); - } - private void addUriParameter(HttpRequestDefinition requestDefinition, @Nullable String name, - @Nullable Object value, boolean required) { - if (name == null) { - throw new IllegalStateException("Path variable name cannot be null"); - } - String stringValue = getStringValue(value, required); - if (LOG.isTraceEnabled()) { - LOG.trace("Path variable " + name + " resolved to " + stringValue); - } - requestDefinition.getUriVariables().put(name, stringValue); - } - - @Nullable - private String getStringValue(@Nullable Object value, boolean required) { - validateForNull(value, required); - validateForReactiveWrapper(value); - return value != null - ? convertToString(TypeDescriptor.valueOf(value.getClass()), value) : null; - } - - private void addUriParameter(HttpRequestDefinition requestDefinition, - Map.Entry entry, boolean required) { - Object resolvedName = resolveFromOptional(entry.getKey()); - String stringName = getStringValue(resolvedName, true); - Object resolvedValue = resolveFromOptional(entry.getValue()); - addUriParameter(requestDefinition, stringName, resolvedValue, required); - } - - private void validateForNull(@Nullable Object argument, boolean required) { - if (argument == null) { - if (required) { - throw new IllegalStateException("Required variable cannot be null"); - } - } - } - - private void validateForReactiveWrapper(@Nullable Object object) { - if (object != null) { - Class type = object.getClass(); - ReactiveAdapterRegistry adapterRegistry = ReactiveAdapterRegistry.getSharedInstance(); - ReactiveAdapter adapter = adapterRegistry.getAdapter(type); - if (adapter != null) { - throw new IllegalStateException(getClass().getSimpleName() + - " does not support reactive type wrapper: " + type); - } + if (!(value instanceof String)) { + value = this.conversionService.convert(value, String.class); } - } + if (logger.isTraceEnabled()) { + logger.trace("Resolved path variable '" + name + "' to " + value); + } - @Nullable - private Object resolveFromOptional(@Nullable Object argument) { - if (argument instanceof Optional) { - return ((Optional) argument).orElse(null); - } - return argument; - } - - @Nullable - private String convertToString(TypeDescriptor typeDescriptor, @Nullable Object value) { - if (value == null) { - return null; - } - if (value instanceof String) { - return (String) value; - } - if (this.conversionService != null) { - return (String) this.conversionService.convert(value, typeDescriptor, STRING_TYPE_DESCRIPTOR); - } - return String.valueOf(value); + requestDefinition.getUriVariables().put(name, (String) value); } } diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpMethodArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpMethodArgumentResolverTests.java index 9c28aba1bfd..2ff88877409 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpMethodArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpMethodArgumentResolverTests.java @@ -16,11 +16,7 @@ package org.springframework.web.service.invoker; -import java.util.Collections; - import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; import org.springframework.http.HttpMethod; import org.springframework.lang.Nullable; @@ -29,65 +25,65 @@ import org.springframework.web.service.annotation.HttpRequest; import static org.assertj.core.api.Assertions.assertThat; + /** * Tests for {@link HttpMethodArgumentResolver}. * * @author Olga Maciaszek-Sharma */ -class HttpMethodArgumentResolverTests extends HttpServiceMethodTestSupport { +public class HttpMethodArgumentResolverTests { - private final Service service = createService(Service.class, - Collections.singletonList(new HttpMethodArgumentResolver())); + private final TestHttpClientAdapter clientAdapter = new TestHttpClientAdapter(); + private final Service service = this.clientAdapter.createService(Service.class, new HttpMethodArgumentResolver()); + + @Test void shouldResolveRequestMethodFromArgument() { - Mono execution = this.service.execute(HttpMethod.GET); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getHttpMethod()).isEqualTo(HttpMethod.GET); + this.service.execute(HttpMethod.GET); + assertThat(getActualMethod()).isEqualTo(HttpMethod.GET); } - + @Test void shouldIgnoreArgumentsNotMatchingType() { - Mono execution = this.service.execute("test"); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getHttpMethod()).isNull(); + this.service.execute("test"); + assertThat(getActualMethod()).isNull(); } @Test - void shouldOverrideMethodAnnotationWithMethodArgument() { - Mono execution = this.service.executeGet(HttpMethod.POST); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getHttpMethod()).isEqualTo(HttpMethod.POST); + void shouldOverrideMethodAnnotation() { + this.service.executeGet(HttpMethod.POST); + assertThat(getActualMethod()).isEqualTo(HttpMethod.POST); } @Test void shouldIgnoreNullValue() { - Mono execution = this.service.executeForNull(null); + this.service.executeForNull(null); + assertThat(getActualMethod()).isNull(); + } - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getHttpMethod()).isNull(); + @Nullable + private HttpMethod getActualMethod() { + return this.clientAdapter.getRequestDefinition().getHttpMethod(); } private interface Service { @HttpRequest - Mono execute(HttpMethod method); + void execute(HttpMethod method); @GetRequest - Mono executeGet(HttpMethod method); + void executeGet(HttpMethod method); @HttpRequest - Mono execute(String test); + void execute(String test); @HttpRequest - Mono execute(HttpMethod firstMethod, HttpMethod secondMethod); + void execute(HttpMethod firstMethod, HttpMethod secondMethod); @HttpRequest - Mono executeForNull(@Nullable HttpMethod method); + void executeForNull(@Nullable HttpMethod method); } } diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTestSupport.java b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTestSupport.java deleted file mode 100644 index bccacd01ca8..00000000000 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTestSupport.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * 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.time.Duration; -import java.util.Collections; -import java.util.List; - -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.ReactiveAdapterRegistry; -import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseEntity; -import org.springframework.lang.Nullable; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Contains utility methods for {@link HttpServiceMethod} tests. - * - * @author Rossen Stoyanchev - * @author Olga Maciaszek-Sharma - */ -public class HttpServiceMethodTestSupport { - - private final TestHttpClientAdapter clientAdapter; - - protected HttpServiceMethodTestSupport() { - clientAdapter = new TestHttpClientAdapter(); - } - - protected S createService(Class serviceType) { - return createService(serviceType, Collections.emptyList()); - } - - protected S createService(Class serviceType, - List argumentResolvers) { - HttpServiceProxyFactory factory = new HttpServiceProxyFactory( - argumentResolvers, this.clientAdapter, ReactiveAdapterRegistry.getSharedInstance(), - Duration.ofSeconds(5)); - - return factory.createService(serviceType); - } - - protected HttpRequestDefinition getRequestDefinition() { - return this.clientAdapter.getRequestDefinition(); - } - - - protected TestHttpClientAdapter getClientAdapter() { - return this.clientAdapter; - } - - protected static class TestHttpClientAdapter implements HttpClientAdapter { - - @Nullable - private String methodName; - - @Nullable - private HttpRequestDefinition requestDefinition; - - @Nullable - private ParameterizedTypeReference bodyType; - - - String getMethodName() { - assertThat(this.methodName).isNotNull(); - return this.methodName; - } - - HttpRequestDefinition getRequestDefinition() { - assertThat(this.requestDefinition).isNotNull(); - return this.requestDefinition; - } - - @Nullable - public ParameterizedTypeReference getBodyType() { - return this.bodyType; - } - - - @Override - public Mono requestToVoid(HttpRequestDefinition def) { - saveInput("requestToVoid", def, null); - return Mono.empty(); - } - - @Override - public Mono requestToHeaders(HttpRequestDefinition def) { - saveInput("requestToHeaders", def, null); - return Mono.just(new HttpHeaders()); - } - - @Override - public Mono requestToBody(HttpRequestDefinition def, - ParameterizedTypeReference bodyType) { - saveInput("requestToBody", def, bodyType); - return (Mono) Mono.just(getMethodName()); - } - - @Override - public Flux requestToBodyFlux(HttpRequestDefinition def, - ParameterizedTypeReference bodyType) { - saveInput("requestToBodyFlux", def, bodyType); - return (Flux) Flux.just("request", "To", "Body", "Flux"); - } - - @Override - public Mono> requestToBodilessEntity(HttpRequestDefinition def) { - saveInput("requestToBodilessEntity", def, null); - return Mono.just(ResponseEntity.ok().build()); - } - - @Override - public Mono> requestToEntity(HttpRequestDefinition def, - ParameterizedTypeReference bodyType) { - saveInput("requestToEntity", def, bodyType); - return Mono.just((ResponseEntity) ResponseEntity.ok("requestToEntity")); - } - - @Override - public Mono>> requestToEntityFlux(HttpRequestDefinition def, - ParameterizedTypeReference bodyType) { - saveInput("requestToEntityFlux", def, bodyType); - return Mono.just(ResponseEntity.ok((Flux) Flux.just("request", "To", "Entity", "Flux"))); - } - - private void saveInput( - String methodName, HttpRequestDefinition definition, - @Nullable ParameterizedTypeReference bodyType) { - - this.methodName = methodName; - this.requestDefinition = definition; - this.bodyType = bodyType; - } - - } - -} diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java index 5c085876aea..791b450f26d 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java @@ -51,13 +51,17 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; * * @author Rossen Stoyanchev */ -public class HttpServiceMethodTests extends HttpServiceMethodTestSupport { +public class HttpServiceMethodTests { private static final ParameterizedTypeReference BODY_TYPE = new ParameterizedTypeReference<>() {}; + + private final TestHttpClientAdapter clientAdapter = new TestHttpClientAdapter(); + + @Test void reactorService() { - ReactorService service = createService(ReactorService.class); + ReactorService service = this.clientAdapter.createService(ReactorService.class); Mono voidMono = service.execute(); StepVerifier.create(voidMono).verifyComplete(); @@ -90,7 +94,7 @@ public class HttpServiceMethodTests extends HttpServiceMethodTestSupport { @Test void rxJavaService() { - RxJavaService service = createService(RxJavaService.class); + RxJavaService service = this.clientAdapter.createService(RxJavaService.class); Completable completable = service.execute(); assertThat(completable).isNotNull(); @@ -117,7 +121,7 @@ public class HttpServiceMethodTests extends HttpServiceMethodTestSupport { @Test void blockingService() { - BlockingService service = createService(BlockingService.class); + BlockingService service = this.clientAdapter.createService(BlockingService.class); service.execute(); @@ -137,11 +141,11 @@ public class HttpServiceMethodTests extends HttpServiceMethodTestSupport { @Test void methodAnnotatedService() { - MethodAnnotatedService service = createService(MethodAnnotatedService.class); + MethodAnnotatedService service = this.clientAdapter.createService(MethodAnnotatedService.class); service.performGet(); - HttpRequestDefinition request = getRequestDefinition(); + HttpRequestDefinition request = this.clientAdapter.getRequestDefinition(); assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.GET); assertThat(request.getUriTemplate()).isNull(); assertThat(request.getHeaders().getContentType()).isNull(); @@ -149,7 +153,7 @@ public class HttpServiceMethodTests extends HttpServiceMethodTestSupport { service.performPost(); - request = getRequestDefinition(); + request = this.clientAdapter.getRequestDefinition(); assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.POST); assertThat(request.getUriTemplate()).isEqualTo("/url"); assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON); @@ -159,11 +163,11 @@ public class HttpServiceMethodTests extends HttpServiceMethodTestSupport { @Test void typeAndMethodAnnotatedService() { - MethodAnnotatedService service = createService(TypeAndMethodAnnotatedService.class); + MethodAnnotatedService service = this.clientAdapter.createService(TypeAndMethodAnnotatedService.class); service.performGet(); - HttpRequestDefinition request = getRequestDefinition(); + HttpRequestDefinition request = this.clientAdapter.getRequestDefinition(); assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.GET); assertThat(request.getUriTemplate()).isEqualTo("/base"); assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_CBOR); @@ -171,7 +175,7 @@ public class HttpServiceMethodTests extends HttpServiceMethodTestSupport { service.performPost(); - request = getRequestDefinition(); + request = this.clientAdapter.getRequestDefinition(); assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.POST); assertThat(request.getUriTemplate()).isEqualTo("/base/url"); assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON); @@ -179,9 +183,8 @@ public class HttpServiceMethodTests extends HttpServiceMethodTestSupport { } private void verifyClientInvocation(String methodName, @Nullable ParameterizedTypeReference expectedBodyType) { - TestHttpClientAdapter clientAdapter = getClientAdapter(); - assertThat((clientAdapter.getMethodName())).isEqualTo(methodName); - assertThat(clientAdapter.getBodyType()).isEqualTo(expectedBodyType); + assertThat((this.clientAdapter.getInvokedMethodName())).isEqualTo(methodName); + assertThat(this.clientAdapter.getBodyType()).isEqualTo(expectedBodyType); } @@ -273,4 +276,5 @@ public class HttpServiceMethodTests extends HttpServiceMethodTestSupport { @HttpRequest(url = "/base", contentType = APPLICATION_CBOR_VALUE, accept = APPLICATION_CBOR_VALUE) private interface TypeAndMethodAnnotatedService extends MethodAnnotatedService { } + } 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 edb400961f7..68040657e18 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 @@ -16,16 +16,11 @@ package org.springframework.web.service.invoker; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; import java.util.Optional; -import io.reactivex.rxjava3.core.Observable; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.lang.Nullable; @@ -33,483 +28,162 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.service.annotation.HttpRequest; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + /** * Tests for {@link PathVariableArgumentResolver}. * * @author Olga Maciaszek-Sharma */ -class PathVariableArgumentResolverTests extends HttpServiceMethodTestSupport { +class PathVariableArgumentResolverTests { + + private final TestHttpClientAdapter clientAdapter = new TestHttpClientAdapter(); + + private final Service service = this.clientAdapter.createService( + Service.class, new PathVariableArgumentResolver(new DefaultConversionService())); - private final Service service = createService(Service.class, - Collections.singletonList(new PathVariableArgumentResolver(null))); @Test void shouldResolvePathVariableWithNameFromParameter() { - Mono execution = this.service.execute("test"); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo("test"); + this.service.execute("test"); + assertPathVariable("id", "test"); } @Test void shouldResolvePathVariableWithNameFromAnnotationName() { - Mono execution = this.service.executeNamed("test"); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo("test"); + this.service.executeNamed("test"); + assertPathVariable("id", "test"); } @Test void shouldResolvePathVariableNameFromValue() { - Mono execution = this.service.executeNamedWithValue("test"); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo("test"); + this.service.executeNamedWithValue("test"); + assertPathVariable("id", "test"); } @Test void shouldOverrideNameIfValuePresentInAnnotation() { - Mono execution = this.service.executeValueNamed("test"); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo("test"); + this.service.executeValueNamed("test"); + assertPathVariable("id", "test"); } @Test - void shouldResolvePathVariableWithNameFromObject() { - Mono execution = this.service.execute(new TestObject("test")); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo("test"); + void shouldResolvePathVariableWithConversion() { + this.service.execute(Boolean.TRUE); + assertPathVariable("id", "true"); } @Test - void shouldResolvePathVariableWithConversionService() { - Service service = createService(Service.class, - Collections.singletonList(new PathVariableArgumentResolver( - new DefaultConversionService()))); - Mono execution = service.execute(Boolean.TRUE); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo("true"); - } - - @Test - void shouldResolvePathVariableFromOptionalArgumentWithConversionService() { - Service service = createService(Service.class, - Collections.singletonList(new PathVariableArgumentResolver( - new DefaultConversionService()))); - Mono execution = service.executeOptional(Optional.of(Boolean.TRUE)); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo("true"); + void shouldResolvePathVariableFromOptionalArgumentWithConversion() { + this.service.executeOptional(Optional.of(Boolean.TRUE)); + assertPathVariable("id", "true"); } @Test void shouldResolvePathVariableFromOptionalArgument() { - Mono execution = this.service.execute(Optional.of("test")); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo("test"); - } - - @Test - void shouldThrowExceptionForNullWithConversionService() { - assertThatIllegalStateException().isThrownBy(() -> { - Service service = createService(Service.class, - Collections.singletonList(new PathVariableArgumentResolver( - new DefaultConversionService()))); - Mono execution = service.executeNamedWithValue(null); - - StepVerifier.create(execution).verifyComplete(); - }); + this.service.execute(Optional.of("test")); + assertPathVariable("id", "test"); } @Test void shouldThrowExceptionForNull() { - assertThatIllegalStateException().isThrownBy(() -> { - Mono execution = this.service.executeNamedWithValue(null); - - StepVerifier.create(execution).verifyComplete(); - }); + assertThatIllegalArgumentException().isThrownBy(() -> this.service.executeNamedWithValue(null)); } @Test void shouldThrowExceptionForEmptyOptional() { - assertThatIllegalStateException().isThrownBy(() -> { - Service service = createService(Service.class, Collections.singletonList(new PathVariableArgumentResolver(new DefaultConversionService()))); - Mono execution = service.execute(Optional.empty()); - - StepVerifier.create(execution).verifyComplete(); - }); + assertThatIllegalArgumentException().isThrownBy(() -> this.service.execute(Optional.empty())); } @Test - void shouldThrowExceptionForEmptyOptionalWithoutConversionService() { - assertThatIllegalStateException().isThrownBy(() -> { - Mono execution = this.service.execute(Optional.empty()); - - StepVerifier.create(execution).verifyComplete(); - }); + void shouldIgnoreNullWithConversionServiceWhenNotRequired() { + this.service.executeNotRequired(null); + assertThat(getActualUriVariables().get("id")).isNull(); } @Test - void shouldNotThrowExceptionForNullWithConversionServiceWhenNotRequired() { - Service service = createService(Service.class, Collections.singletonList(new PathVariableArgumentResolver(new DefaultConversionService()))); - Mono execution = service.executeNotRequired(null); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getUriVariables().get("id")).isNull(); + void shouldIgnoreNullWhenNotRequired() { + this.service.executeNotRequired(null); + assertPathVariable("id", null); } @Test - void shouldNotThrowExceptionForNullWhenNotRequired() { - Mono execution = this.service.executeNotRequired(null); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo(null); - } - - @Test - void shouldNotThrowExceptionForEmptyOptionalWhenNotRequired() { - Service service = createService(Service.class, Collections.singletonList(new PathVariableArgumentResolver(new DefaultConversionService()))); - Mono execution = service.executeOptionalNotRequired(Optional.empty()); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo(null); - } - - @Test - void shouldNotThrowExceptionForEmptyOptionalWithoutConversionServiceWhenNotRequired() { - Mono execution = this.service.executeOptionalNotRequired(Optional.empty()); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo(null); - } - - @Test - void shouldThrowExceptionForReactorWrapper() { - assertThatIllegalStateException().isThrownBy(() -> { - Mono execution = this.service.executeMono(Mono.just("test")); - - StepVerifier.create(execution).verifyComplete(); - }); - } - - @Test - void shouldThrowExceptionForRXWrapper() { - assertThatIllegalStateException().isThrownBy(() -> { - Mono execution = this.service.executeObservable(Observable.just("test")); - - StepVerifier.create(execution).verifyComplete(); - }); - } - - @Test - void shouldThrowExceptionForOptionalReactorWrapper() { - assertThatIllegalStateException().isThrownBy(() -> { - Mono execution = this.service.executeOptionalMono(Optional.of(Mono.just("test"))); - - StepVerifier.create(execution).verifyComplete(); - }); - } - - @Test - void shouldThrowExceptionForOptionalRXWrapper() { - assertThatIllegalStateException().isThrownBy(() -> { - Mono execution = this.service.executeOptionalObservable(Optional.of(Observable.just("test"))); - - StepVerifier.create(execution).verifyComplete(); - }); - } - - @Test - void shouldResolvePathVariableFromNamedMap() { - Mono execution = this.service.executeNamedMap(Map.of("id", "test")); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo("test"); - } - - @Test - void shouldResolvePathVariableFromMapWithAnnotationValue() { - Mono execution = this.service.executeNamedValueMap(Map.of("id", "test")); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo("test"); - } - - @Test - void shouldThrowExceptionForReactorWrapperInNamedMap() { - assertThatIllegalStateException().isThrownBy(() -> { - Mono execution = this.service.executeNamedReactorMap(Map.of("id", Flux.just("test"))); - - StepVerifier.create(execution).verifyComplete(); - }); - } - - @Test - void shouldResolveOptionalPathVariableFromNamedMap() { - Mono execution = this.service.executeOptionalValueNamedMap(Map.of("id", Optional.of("test"))); - - StepVerifier.create(execution).verifyComplete(); - - assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo("test"); - } - - @Test - void shouldResolveNamedPathVariableFromNamedMapWithConversionService() { - Service service = createService(Service.class, Collections.singletonList(new PathVariableArgumentResolver(new DefaultConversionService()))); - Mono execution = service.executeNamedBooleanMap(Map.of("id", Boolean.TRUE)); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo("true"); - } - - @Test - void shouldThrowExceptionForNullNamedMap() { - assertThatIllegalStateException().isThrownBy(() -> { - Mono execution = this.service.executeNamedValueMap(null); - - StepVerifier.create(execution).verifyComplete(); - }); - } - - @Test - void shouldThrowExceptionForNullNamedMapValue() { - assertThatIllegalStateException().isThrownBy(() -> { - Mono execution = this.service.executeNamedValueMap(new HashMap<>()); - - StepVerifier.create(execution).verifyComplete(); - }); - } - - @Test - void shouldThrowExceptionForEmptyOptionalNamedMapValue() { - assertThatIllegalStateException().isThrownBy(() -> { - Service service = createService(Service.class, Collections.singletonList(new PathVariableArgumentResolver(new DefaultConversionService()))); - Mono execution = service.executeOptionalValueNamedMap(Map.of("id", Optional.empty())); - - StepVerifier.create(execution).verifyComplete(); - }); - } - - @Test - void shouldNotThrowExceptionForNullNamedMapValueWhenNotRequired() { - Mono execution = this.service.executeNamedValueMapNotRequired(null); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo(null); - } - - @Test - void shouldNotThrowExceptionForEmptyOptionalNamedMapValueWhenNotRequired() { - Service service = createService(Service.class, Collections.singletonList(new PathVariableArgumentResolver(new DefaultConversionService()))); - Mono execution = service.executeOptionalValueMapNotRequired(Map.of("id", Optional.empty())); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo(null); + void shouldIgnoreEmptyOptionalWhenNotRequired() { + this.service.executeOptionalNotRequired(Optional.empty()); + assertPathVariable("id", null); } @Test void shouldResolvePathVariablesFromMap() { - Mono execution = this.service.executeValueMap(Map.of("id", "test")); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo("test"); - } - - @Test - void shouldThrowExceptionForReactorWrapperValueInMap() { - assertThatIllegalStateException().isThrownBy(() -> { - Mono execution = this.service.executeReactorValueMap(Map.of("id", Flux.just("test"))); - - StepVerifier.create(execution).verifyComplete(); - }); - } - - @Test - void shouldThrowExceptionForReactorWrapperKeyInMap() { - assertThatIllegalStateException().isThrownBy(() -> { - Mono execution = this.service.executeReactorKeyMap(Map.of(Flux.just("id"), "test")); - - StepVerifier.create(execution).verifyComplete(); - }); + this.service.executeValueMap(Map.of("id", "test")); + assertPathVariable("id", "test"); } @Test void shouldResolvePathVariableFromOptionalMapValue() { - Mono execution = this.service.executeOptionalValueMap(Map.of("id", Optional.of("test"))); - - StepVerifier.create(execution).verifyComplete(); - - assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo("test"); + this.service.executeOptionalValueMap(Map.of("id", Optional.of("test"))); + assertPathVariable("id", "test"); } @Test - void shouldResolvePathVariableFromOptionalMapKey() { - Mono execution = this.service.executeOptionalKeyMap(Map.of(Optional.of("id"), "test")); - - StepVerifier.create(execution).verifyComplete(); - - assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo("test"); - } - - @Test - void shouldResolvePathVariableFromMapWithConversionService() { - Service service = createService(Service.class, Collections.singletonList(new PathVariableArgumentResolver(new DefaultConversionService()))); - Mono execution = service.executeBooleanMap(Map.of(Boolean.TRUE, Boolean.TRUE)); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getUriVariables() - .get("true")).isEqualTo("true"); - } - - @Test - void shouldThrowExceptionForNullMapValue() { - assertThatIllegalStateException().isThrownBy(() -> { - Mono execution = this.service.executeValueMap(null); - - StepVerifier.create(execution).verifyComplete(); - }); + void shouldIgnoreNullMapValue() { + this.service.executeValueMap(null); + assertThat(getActualUriVariables()).isEmpty(); } @Test void shouldThrowExceptionForEmptyOptionalMapValue() { - assertThatIllegalStateException().isThrownBy(() -> { - Service service = createService(Service.class, Collections.singletonList(new PathVariableArgumentResolver(new DefaultConversionService()))); - Mono execution = service.executeOptionalValueMap(Map.of("id", Optional.empty())); - - StepVerifier.create(execution).verifyComplete(); - }); + assertThatIllegalArgumentException() + .isThrownBy(() -> this.service.executeOptionalValueMap(Map.of("id", Optional.empty()))); } - @Test - void shouldThrowExceptionForEmptyOptionalMapKey() { - assertThatIllegalStateException().isThrownBy(() -> { - Service service = createService(Service.class, Collections.singletonList(new PathVariableArgumentResolver(new DefaultConversionService()))); - Mono execution = service.executeOptionalKeyMap(Map.of(Optional.empty(), "test")); - - StepVerifier.create(execution).verifyComplete(); - }); + @SuppressWarnings("SameParameterValue") + private void assertPathVariable(String name, @Nullable String expectedValue) { + assertThat(getActualUriVariables().get(name)).isEqualTo(expectedValue); } - @Test - void shouldNotThrowExceptionForNullMapValueWhenNotRequired() { - Mono execution = this.service.executeValueMapNotRequired(null); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo(null); - } - - @Test - void shouldNotThrowExceptionForEmptyOptionalMapValueWhenNotRequired() { - Service service = createService(Service.class, Collections.singletonList(new PathVariableArgumentResolver(new DefaultConversionService()))); - Mono execution = service.executeOptionalValueMapNotRequired(Map.of("id", Optional.empty())); - - StepVerifier.create(execution).verifyComplete(); - assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo(null); + @NotNull + private Map getActualUriVariables() { + return this.clientAdapter.getRequestDefinition().getUriVariables(); } + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") private interface Service { @HttpRequest - Mono execute(@PathVariable String id); + void execute(@PathVariable String id); @HttpRequest - Mono executeNotRequired(@Nullable @PathVariable(required = false) String id); + void executeNotRequired(@Nullable @PathVariable(required = false) String id); @HttpRequest - Mono executeOptional(@PathVariable Optional id); + void executeOptional(@PathVariable Optional id); @HttpRequest - Mono executeOptionalNotRequired(@PathVariable(required = false) Optional id); + void executeOptionalNotRequired(@PathVariable(required = false) Optional id); @HttpRequest - Mono executeNamedWithValue(@Nullable @PathVariable(name = "test", value = "id") String employeeId); + void executeNamedWithValue(@Nullable @PathVariable(name = "test", value = "id") String employeeId); @HttpRequest - Mono executeNamed(@PathVariable(name = "id") String employeeId); + void executeNamed(@PathVariable(name = "id") String employeeId); @HttpRequest - Mono executeValueNamed(@PathVariable("id") String employeeId); + void executeValueNamed(@PathVariable("id") String employeeId); @HttpRequest - Mono execute(@PathVariable Object id); + void execute(@PathVariable Object id); @HttpRequest - Mono execute(@PathVariable Boolean id); + void execute(@PathVariable Boolean id); @HttpRequest - Mono executeMono(@PathVariable Mono id); + void executeValueMap(@Nullable @PathVariable Map map); @HttpRequest - Mono executeObservable(@PathVariable Observable id); - - @HttpRequest - Mono executeOptionalMono(@PathVariable Optional> id); - - @HttpRequest - Mono executeOptionalObservable(@PathVariable Optional> id); - - @HttpRequest - Mono executeNamedMap(@PathVariable(name = "id") Map map); - - @HttpRequest - Mono executeNamedValueMap(@Nullable @PathVariable("id") Map map); - - @HttpRequest - Mono executeNamedBooleanMap(@PathVariable("id") Map map); - - @HttpRequest - Mono executeNamedReactorMap(@PathVariable(name = "id") Map> map); - - @HttpRequest - Mono executeOptionalValueNamedMap(@PathVariable("id") Map> map); - - @HttpRequest - Mono executeNamedValueMapNotRequired(@Nullable @PathVariable(name = "id", required = false) Map map); - - @HttpRequest - Mono executeOptionalValueMapNotRequired(@PathVariable(name = "id", required = false) Map> map); - - @HttpRequest - Mono executeValueMap(@Nullable @PathVariable Map map); - - @HttpRequest - Mono executeReactorValueMap(@PathVariable Map> map); - - @HttpRequest - Mono executeReactorKeyMap(@PathVariable Map, String> map); - - @HttpRequest - Mono executeOptionalValueMap(@PathVariable Map> map); - - @HttpRequest - Mono executeOptionalKeyMap(@PathVariable Map, String> map); - - @HttpRequest - Mono executeBooleanMap(@PathVariable Map map); - - @HttpRequest - Mono executeValueMapNotRequired(@Nullable @PathVariable(required = false) Map map); - } - - static class TestObject { - - TestObject(String value) { - this.value = value; - } - - String value; - - @Override - public String toString() { - return value; - } + void executeOptionalValueMap(@PathVariable Map> map); } } diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/TestHttpClientAdapter.java b/spring-web/src/test/java/org/springframework/web/service/invoker/TestHttpClientAdapter.java new file mode 100644 index 00000000000..06abeb90c3b --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/TestHttpClientAdapter.java @@ -0,0 +1,137 @@ +/* + * 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.time.Duration; +import java.util.Arrays; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * {@link HttpClientAdapter} with stubbed responses. + * + * @author Rossen Stoyanchev + * @author Olga Maciaszek-Sharma + */ +@SuppressWarnings("unchecked") +class TestHttpClientAdapter implements HttpClientAdapter { + + @Nullable + private String invokedMethodName; + + @Nullable + private HttpRequestDefinition requestDefinition; + + @Nullable + private ParameterizedTypeReference bodyType; + + + /** + * Create the proxy for the give service type. + */ + public S createService(Class serviceType, HttpServiceMethodArgumentResolver... resolvers) { + + HttpServiceProxyFactory factory = new HttpServiceProxyFactory( + Arrays.asList(resolvers), this, ReactiveAdapterRegistry.getSharedInstance(), Duration.ofSeconds(5)); + + return factory.createService(serviceType); + } + + + public String getInvokedMethodName() { + assertThat(this.invokedMethodName).isNotNull(); + return this.invokedMethodName; + } + + public HttpRequestDefinition getRequestDefinition() { + assertThat(this.requestDefinition).isNotNull(); + return this.requestDefinition; + } + + @Nullable + public ParameterizedTypeReference getBodyType() { + return this.bodyType; + } + + + // HttpClientAdapter implementation + + @Override + public Mono requestToVoid(HttpRequestDefinition definition) { + saveInput("requestToVoid", definition, null); + return Mono.empty(); + } + + @Override + public Mono requestToHeaders(HttpRequestDefinition definition) { + saveInput("requestToHeaders", definition, null); + return Mono.just(new HttpHeaders()); + } + + @Override + public Mono requestToBody(HttpRequestDefinition definition, ParameterizedTypeReference bodyType) { + saveInput("requestToBody", definition, bodyType); + return (Mono) Mono.just(getInvokedMethodName()); + } + + @Override + public Flux requestToBodyFlux(HttpRequestDefinition definition, ParameterizedTypeReference bodyType) { + saveInput("requestToBodyFlux", definition, bodyType); + return (Flux) Flux.just("request", "To", "Body", "Flux"); + } + + @Override + public Mono> requestToBodilessEntity(HttpRequestDefinition definition) { + saveInput("requestToBodilessEntity", definition, null); + return Mono.just(ResponseEntity.ok().build()); + } + + @Override + public Mono> requestToEntity( + HttpRequestDefinition definition, ParameterizedTypeReference type) { + + saveInput("requestToEntity", definition, type); + return Mono.just((ResponseEntity) ResponseEntity.ok("requestToEntity")); + } + + @Override + public Mono>> requestToEntityFlux( + HttpRequestDefinition definition, ParameterizedTypeReference bodyType) { + + saveInput("requestToEntityFlux", definition, bodyType); + return Mono.just(ResponseEntity.ok((Flux) Flux.just("request", "To", "Entity", "Flux"))); + } + + private void saveInput( + String methodName, HttpRequestDefinition definition, @Nullable ParameterizedTypeReference bodyType) { + + this.invokedMethodName = methodName; + this.requestDefinition = definition; + this.bodyType = bodyType; + } + +}