From c2a008fc22df64cc6ee5e1f69c1a8069dedcfa5e Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Thu, 21 Apr 2022 16:06:38 +0000 Subject: [PATCH] Add HttpMethod and PathVariable argument resolvers See gh-28386 --- .../invoker/HttpMethodArgumentResolver.java | 51 ++ .../service/invoker/HttpServiceMethod.java | 4 + .../invoker/HttpServiceProxyFactory.java | 1 - .../invoker/PathVariableArgumentResolver.java | 152 ++++++ .../HttpMethodArgumentResolverTests.java | 93 ++++ .../invoker/HttpServiceMethodTestSupport.java | 155 ++++++ .../invoker/HttpServiceMethodTests.java | 120 +--- .../PathVariableArgumentResolverTests.java | 515 ++++++++++++++++++ 8 files changed, 979 insertions(+), 112 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/service/invoker/HttpMethodArgumentResolver.java create mode 100644 spring-web/src/main/java/org/springframework/web/service/invoker/PathVariableArgumentResolver.java create mode 100644 spring-web/src/test/java/org/springframework/web/service/invoker/HttpMethodArgumentResolverTests.java create mode 100644 spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTestSupport.java create mode 100644 spring-web/src/test/java/org/springframework/web/service/invoker/PathVariableArgumentResolverTests.java 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 new file mode 100644 index 00000000000..1f0a336fa83 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpMethodArgumentResolver.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.service.invoker; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +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. + * + * @author Olga Maciaszek-Sharma + * @since 6.0 + */ +public class HttpMethodArgumentResolver implements HttpServiceMethodArgumentResolver { + + private static final Log LOG = LogFactory.getLog(HttpMethodArgumentResolver.class); + + @Override + 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()); + } + requestDefinition.setHttpMethod(httpMethod); + } + } +} 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 c6eea95b5e2..96133b697af 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 @@ -28,7 +28,9 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; @@ -104,6 +106,8 @@ final class HttpServiceMethod { Assert.isTrue(arguments.length == this.parameters.length, "Method argument mismatch"); for (int i = 0; i < this.parameters.length; i++) { Object argumentValue = arguments[i]; + ParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer(); + this.parameters[i].initParameterNameDiscovery(nameDiscoverer); for (HttpServiceMethodArgumentResolver resolver : this.argumentResolvers) { resolver.resolve(argumentValue, this.parameters[i], requestDefinition); } diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java index 02b2aaf48b4..686d8745146 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java @@ -16,7 +16,6 @@ package org.springframework.web.service.invoker; - import java.lang.reflect.Method; import java.time.Duration; import java.util.HashMap; 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 new file mode 100644 index 00000000000..399e068d7de --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/PathVariableArgumentResolver.java @@ -0,0 +1,152 @@ +/* + * 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.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.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.StringUtils; +import org.springframework.web.bind.annotation.PathVariable; + +/** + * An implementation of {@link HttpServiceMethodArgumentResolver} 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}. + * + * @author Olga Maciaszek-Sharma + * @since 6.0 + */ +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 final ConversionService conversionService; + + public PathVariableArgumentResolver(@Nullable ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Override + 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; + } + valueMap.entrySet() + .forEach(entry -> addUriParameter(requestDefinition, entry, required)); + 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); + } + } + + } + + @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); + } + +} 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 new file mode 100644 index 00000000000..9c28aba1bfd --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpMethodArgumentResolverTests.java @@ -0,0 +1,93 @@ +/* + * 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.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; +import org.springframework.web.service.annotation.GetRequest; +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 { + + private final Service service = createService(Service.class, + Collections.singletonList(new HttpMethodArgumentResolver())); + + @Test + void shouldResolveRequestMethodFromArgument() { + Mono execution = this.service.execute(HttpMethod.GET); + + StepVerifier.create(execution).verifyComplete(); + assertThat(getRequestDefinition().getHttpMethod()).isEqualTo(HttpMethod.GET); + } + + @Test + void shouldIgnoreArgumentsNotMatchingType() { + Mono execution = this.service.execute("test"); + + StepVerifier.create(execution).verifyComplete(); + assertThat(getRequestDefinition().getHttpMethod()).isNull(); + } + + @Test + void shouldOverrideMethodAnnotationWithMethodArgument() { + Mono execution = this.service.executeGet(HttpMethod.POST); + + StepVerifier.create(execution).verifyComplete(); + assertThat(getRequestDefinition().getHttpMethod()).isEqualTo(HttpMethod.POST); + } + + @Test + void shouldIgnoreNullValue() { + Mono execution = this.service.executeForNull(null); + + StepVerifier.create(execution).verifyComplete(); + assertThat(getRequestDefinition().getHttpMethod()).isNull(); + } + + + private interface Service { + + @HttpRequest + Mono execute(HttpMethod method); + + @GetRequest + Mono executeGet(HttpMethod method); + + @HttpRequest + Mono execute(String test); + + @HttpRequest + Mono execute(HttpMethod firstMethod, HttpMethod secondMethod); + + @HttpRequest + Mono 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 new file mode 100644 index 00000000000..bccacd01ca8 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTestSupport.java @@ -0,0 +1,155 @@ +/* + * 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 98fd65aff67..5c085876aea 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 @@ -17,9 +17,6 @@ package org.springframework.web.service.invoker; -import java.time.Duration; -import java.util.Collections; - import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Single; @@ -29,7 +26,6 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -55,19 +51,14 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; * * @author Rossen Stoyanchev */ -public class HttpServiceMethodTests { +public class HttpServiceMethodTests extends HttpServiceMethodTestSupport { private static final ParameterizedTypeReference BODY_TYPE = new ParameterizedTypeReference<>() {}; - - private final TestHttpClientAdapter clientAdapter = new TestHttpClientAdapter(); - - @Test void reactorService() { - ReactorService service = createService(ReactorService.class); - + Mono voidMono = service.execute(); StepVerifier.create(voidMono).verifyComplete(); verifyClientInvocation("requestToVoid", null); @@ -99,9 +90,7 @@ public class HttpServiceMethodTests { @Test void rxJavaService() { - RxJavaService service = createService(RxJavaService.class); - Completable completable = service.execute(); assertThat(completable).isNotNull(); @@ -152,7 +141,7 @@ public class HttpServiceMethodTests { service.performGet(); - HttpRequestDefinition request = this.clientAdapter.getRequestDefinition(); + HttpRequestDefinition request = getRequestDefinition(); assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.GET); assertThat(request.getUriTemplate()).isNull(); assertThat(request.getHeaders().getContentType()).isNull(); @@ -160,7 +149,7 @@ public class HttpServiceMethodTests { service.performPost(); - request = this.clientAdapter.getRequestDefinition(); + request = getRequestDefinition(); assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.POST); assertThat(request.getUriTemplate()).isEqualTo("/url"); assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON); @@ -174,7 +163,7 @@ public class HttpServiceMethodTests { service.performGet(); - HttpRequestDefinition request = this.clientAdapter.getRequestDefinition(); + HttpRequestDefinition request = getRequestDefinition(); assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.GET); assertThat(request.getUriTemplate()).isEqualTo("/base"); assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_CBOR); @@ -182,25 +171,17 @@ public class HttpServiceMethodTests { service.performPost(); - request = this.clientAdapter.getRequestDefinition(); + request = getRequestDefinition(); assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.POST); assertThat(request.getUriTemplate()).isEqualTo("/base/url"); assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON); assertThat(request.getHeaders().getAccept()).containsExactly(MediaType.APPLICATION_JSON); } - private S createService(Class serviceType) { - - HttpServiceProxyFactory factory = new HttpServiceProxyFactory( - Collections.emptyList(), this.clientAdapter, ReactiveAdapterRegistry.getSharedInstance(), - Duration.ofSeconds(5)); - - return factory.createService(serviceType); - } - private void verifyClientInvocation(String methodName, @Nullable ParameterizedTypeReference expectedBodyType) { - assertThat((this.clientAdapter.getMethodName())).isEqualTo(methodName); - assertThat(this.clientAdapter.getBodyType()).isEqualTo(expectedBodyType); + TestHttpClientAdapter clientAdapter = getClientAdapter(); + assertThat((clientAdapter.getMethodName())).isEqualTo(methodName); + assertThat(clientAdapter.getBodyType()).isEqualTo(expectedBodyType); } @@ -292,87 +273,4 @@ public class HttpServiceMethodTests { @HttpRequest(url = "/base", contentType = APPLICATION_CBOR_VALUE, accept = APPLICATION_CBOR_VALUE) private interface TypeAndMethodAnnotatedService extends MethodAnnotatedService { } - - - @SuppressWarnings("unchecked") - private static class TestHttpClientAdapter implements HttpClientAdapter { - - @Nullable - private String methodName; - - @Nullable - private HttpRequestDefinition requestDefinition; - - @Nullable - private ParameterizedTypeReference bodyType; - - - public String getMethodName() { - assertThat(this.methodName).isNotNull(); - return this.methodName; - } - - public 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/PathVariableArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/PathVariableArgumentResolverTests.java new file mode 100644 index 00000000000..edb400961f7 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/PathVariableArgumentResolverTests.java @@ -0,0 +1,515 @@ +/* + * 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.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import io.reactivex.rxjava3.core.Observable; +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; +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; + +/** + * Tests for {@link PathVariableArgumentResolver}. + * + * @author Olga Maciaszek-Sharma + */ +class PathVariableArgumentResolverTests extends HttpServiceMethodTestSupport { + + 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"); + } + + @Test + void shouldResolvePathVariableWithNameFromAnnotationName() { + Mono execution = this.service.executeNamed("test"); + + StepVerifier.create(execution).verifyComplete(); + assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo("test"); + } + + @Test + void shouldResolvePathVariableNameFromValue() { + Mono execution = this.service.executeNamedWithValue("test"); + + StepVerifier.create(execution).verifyComplete(); + assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo("test"); + } + + @Test + void shouldOverrideNameIfValuePresentInAnnotation() { + Mono execution = this.service.executeValueNamed("test"); + + StepVerifier.create(execution).verifyComplete(); + assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo("test"); + } + + @Test + void shouldResolvePathVariableWithNameFromObject() { + Mono execution = this.service.execute(new TestObject("test")); + + StepVerifier.create(execution).verifyComplete(); + assertThat(getRequestDefinition().getUriVariables().get("id")).isEqualTo("test"); + } + + @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"); + } + + @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(); + }); + } + + @Test + void shouldThrowExceptionForNull() { + assertThatIllegalStateException().isThrownBy(() -> { + Mono execution = this.service.executeNamedWithValue(null); + + StepVerifier.create(execution).verifyComplete(); + }); + } + + @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(); + }); + } + + @Test + void shouldThrowExceptionForEmptyOptionalWithoutConversionService() { + assertThatIllegalStateException().isThrownBy(() -> { + Mono execution = this.service.execute(Optional.empty()); + + StepVerifier.create(execution).verifyComplete(); + }); + } + + @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(); + } + + @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); + } + + @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(); + }); + } + + @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"); + } + + @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(); + }); + } + + @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(); + }); + } + + @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(); + }); + } + + @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); + } + + + private interface Service { + + @HttpRequest + Mono execute(@PathVariable String id); + + @HttpRequest + Mono executeNotRequired(@Nullable @PathVariable(required = false) String id); + + @HttpRequest + Mono executeOptional(@PathVariable Optional id); + + @HttpRequest + Mono executeOptionalNotRequired(@PathVariable(required = false) Optional id); + + @HttpRequest + Mono executeNamedWithValue(@Nullable @PathVariable(name = "test", value = "id") String employeeId); + + @HttpRequest + Mono executeNamed(@PathVariable(name = "id") String employeeId); + + @HttpRequest + Mono executeValueNamed(@PathVariable("id") String employeeId); + + @HttpRequest + Mono execute(@PathVariable Object id); + + @HttpRequest + Mono execute(@PathVariable Boolean id); + + @HttpRequest + Mono executeMono(@PathVariable Mono id); + + @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; + } + } + +}