Skip to content

Commit c2a008f

Browse files
OlgaMaciaszekrstoyanchev
authored andcommitted
Add HttpMethod and PathVariable argument resolvers
See gh-28386
1 parent c418768 commit c2a008f

File tree

8 files changed

+979
-112
lines changed

8 files changed

+979
-112
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.service.invoker;
18+
19+
import org.apache.commons.logging.Log;
20+
import org.apache.commons.logging.LogFactory;
21+
22+
import org.springframework.core.MethodParameter;
23+
import org.springframework.http.HttpMethod;
24+
import org.springframework.lang.Nullable;
25+
26+
/**
27+
* An implementation of {@link HttpServiceMethodArgumentResolver} that resolves
28+
* request HTTP method based on argument type. Arguments of type
29+
* {@link HttpMethod} will be used to determine the method.
30+
*
31+
* @author Olga Maciaszek-Sharma
32+
* @since 6.0
33+
*/
34+
public class HttpMethodArgumentResolver implements HttpServiceMethodArgumentResolver {
35+
36+
private static final Log LOG = LogFactory.getLog(HttpMethodArgumentResolver.class);
37+
38+
@Override
39+
public void resolve(@Nullable Object argument, MethodParameter parameter,
40+
HttpRequestDefinition requestDefinition) {
41+
if (argument == null) {
42+
return;
43+
}
44+
if (argument instanceof HttpMethod httpMethod) {
45+
if (LOG.isTraceEnabled()) {
46+
LOG.trace("Resolved HTTP method to: " + httpMethod.name());
47+
}
48+
requestDefinition.setHttpMethod(httpMethod);
49+
}
50+
}
51+
}

spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java

+4
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828
import reactor.core.publisher.Flux;
2929
import reactor.core.publisher.Mono;
3030

31+
import org.springframework.core.DefaultParameterNameDiscoverer;
3132
import org.springframework.core.MethodParameter;
33+
import org.springframework.core.ParameterNameDiscoverer;
3234
import org.springframework.core.ParameterizedTypeReference;
3335
import org.springframework.core.ReactiveAdapter;
3436
import org.springframework.core.ReactiveAdapterRegistry;
@@ -104,6 +106,8 @@ private void applyArguments(HttpRequestDefinition requestDefinition, Object[] ar
104106
Assert.isTrue(arguments.length == this.parameters.length, "Method argument mismatch");
105107
for (int i = 0; i < this.parameters.length; i++) {
106108
Object argumentValue = arguments[i];
109+
ParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
110+
this.parameters[i].initParameterNameDiscovery(nameDiscoverer);
107111
for (HttpServiceMethodArgumentResolver resolver : this.argumentResolvers) {
108112
resolver.resolve(argumentValue, this.parameters[i], requestDefinition);
109113
}

spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java

-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package org.springframework.web.service.invoker;
1818

19-
2019
import java.lang.reflect.Method;
2120
import java.time.Duration;
2221
import java.util.HashMap;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.service.invoker;
18+
19+
import java.util.Map;
20+
import java.util.Optional;
21+
22+
import org.apache.commons.logging.Log;
23+
import org.apache.commons.logging.LogFactory;
24+
25+
import org.springframework.core.MethodParameter;
26+
import org.springframework.core.ReactiveAdapter;
27+
import org.springframework.core.ReactiveAdapterRegistry;
28+
import org.springframework.core.convert.ConversionService;
29+
import org.springframework.core.convert.TypeDescriptor;
30+
import org.springframework.lang.Nullable;
31+
import org.springframework.util.StringUtils;
32+
import org.springframework.web.bind.annotation.PathVariable;
33+
34+
/**
35+
* An implementation of {@link HttpServiceMethodArgumentResolver} that resolves
36+
* request path variables based on method arguments annotated
37+
* with {@link PathVariable}. {@code null} values are allowed only
38+
* if {@link PathVariable#required()} is {@code true}.
39+
*
40+
* @author Olga Maciaszek-Sharma
41+
* @since 6.0
42+
*/
43+
public class PathVariableArgumentResolver implements HttpServiceMethodArgumentResolver {
44+
45+
private static final Log LOG = LogFactory.getLog(PathVariableArgumentResolver.class);
46+
private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
47+
@Nullable
48+
private final ConversionService conversionService;
49+
50+
public PathVariableArgumentResolver(@Nullable ConversionService conversionService) {
51+
this.conversionService = conversionService;
52+
}
53+
54+
@Override
55+
public void resolve(@Nullable Object argument, MethodParameter parameter,
56+
HttpRequestDefinition requestDefinition) {
57+
PathVariable annotation = parameter.getParameterAnnotation(PathVariable.class);
58+
if (annotation == null) {
59+
return;
60+
}
61+
String resolvedAnnotationName = StringUtils.hasText(annotation.value())
62+
? annotation.value() : annotation.name();
63+
boolean required = annotation.required();
64+
Object resolvedArgument = resolveFromOptional(argument);
65+
if (resolvedArgument instanceof Map<?, ?> valueMap) {
66+
if (StringUtils.hasText(resolvedAnnotationName)) {
67+
Object value = valueMap.get(resolvedAnnotationName);
68+
Object resolvedValue = resolveFromOptional(value);
69+
addUriParameter(requestDefinition, resolvedAnnotationName, resolvedValue, required);
70+
return;
71+
}
72+
valueMap.entrySet()
73+
.forEach(entry -> addUriParameter(requestDefinition, entry, required));
74+
return;
75+
}
76+
String name = StringUtils.hasText(resolvedAnnotationName)
77+
? resolvedAnnotationName : parameter.getParameterName();
78+
addUriParameter(requestDefinition, name, resolvedArgument, required);
79+
}
80+
81+
private void addUriParameter(HttpRequestDefinition requestDefinition, @Nullable String name,
82+
@Nullable Object value, boolean required) {
83+
if (name == null) {
84+
throw new IllegalStateException("Path variable name cannot be null");
85+
}
86+
String stringValue = getStringValue(value, required);
87+
if (LOG.isTraceEnabled()) {
88+
LOG.trace("Path variable " + name + " resolved to " + stringValue);
89+
}
90+
requestDefinition.getUriVariables().put(name, stringValue);
91+
}
92+
93+
@Nullable
94+
private String getStringValue(@Nullable Object value, boolean required) {
95+
validateForNull(value, required);
96+
validateForReactiveWrapper(value);
97+
return value != null
98+
? convertToString(TypeDescriptor.valueOf(value.getClass()), value) : null;
99+
}
100+
101+
private void addUriParameter(HttpRequestDefinition requestDefinition,
102+
Map.Entry<?, ?> entry, boolean required) {
103+
Object resolvedName = resolveFromOptional(entry.getKey());
104+
String stringName = getStringValue(resolvedName, true);
105+
Object resolvedValue = resolveFromOptional(entry.getValue());
106+
addUriParameter(requestDefinition, stringName, resolvedValue, required);
107+
}
108+
109+
private void validateForNull(@Nullable Object argument, boolean required) {
110+
if (argument == null) {
111+
if (required) {
112+
throw new IllegalStateException("Required variable cannot be null");
113+
}
114+
}
115+
}
116+
117+
private void validateForReactiveWrapper(@Nullable Object object) {
118+
if (object != null) {
119+
Class<?> type = object.getClass();
120+
ReactiveAdapterRegistry adapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
121+
ReactiveAdapter adapter = adapterRegistry.getAdapter(type);
122+
if (adapter != null) {
123+
throw new IllegalStateException(getClass().getSimpleName() +
124+
" does not support reactive type wrapper: " + type);
125+
}
126+
}
127+
128+
}
129+
130+
@Nullable
131+
private Object resolveFromOptional(@Nullable Object argument) {
132+
if (argument instanceof Optional) {
133+
return ((Optional<?>) argument).orElse(null);
134+
}
135+
return argument;
136+
}
137+
138+
@Nullable
139+
private String convertToString(TypeDescriptor typeDescriptor, @Nullable Object value) {
140+
if (value == null) {
141+
return null;
142+
}
143+
if (value instanceof String) {
144+
return (String) value;
145+
}
146+
if (this.conversionService != null) {
147+
return (String) this.conversionService.convert(value, typeDescriptor, STRING_TYPE_DESCRIPTOR);
148+
}
149+
return String.valueOf(value);
150+
}
151+
152+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.service.invoker;
18+
19+
import java.util.Collections;
20+
21+
import org.junit.jupiter.api.Test;
22+
import reactor.core.publisher.Mono;
23+
import reactor.test.StepVerifier;
24+
25+
import org.springframework.http.HttpMethod;
26+
import org.springframework.lang.Nullable;
27+
import org.springframework.web.service.annotation.GetRequest;
28+
import org.springframework.web.service.annotation.HttpRequest;
29+
30+
import static org.assertj.core.api.Assertions.assertThat;
31+
32+
/**
33+
* Tests for {@link HttpMethodArgumentResolver}.
34+
*
35+
* @author Olga Maciaszek-Sharma
36+
*/
37+
class HttpMethodArgumentResolverTests extends HttpServiceMethodTestSupport {
38+
39+
private final Service service = createService(Service.class,
40+
Collections.singletonList(new HttpMethodArgumentResolver()));
41+
42+
@Test
43+
void shouldResolveRequestMethodFromArgument() {
44+
Mono<Void> execution = this.service.execute(HttpMethod.GET);
45+
46+
StepVerifier.create(execution).verifyComplete();
47+
assertThat(getRequestDefinition().getHttpMethod()).isEqualTo(HttpMethod.GET);
48+
}
49+
50+
@Test
51+
void shouldIgnoreArgumentsNotMatchingType() {
52+
Mono<Void> execution = this.service.execute("test");
53+
54+
StepVerifier.create(execution).verifyComplete();
55+
assertThat(getRequestDefinition().getHttpMethod()).isNull();
56+
}
57+
58+
@Test
59+
void shouldOverrideMethodAnnotationWithMethodArgument() {
60+
Mono<Void> execution = this.service.executeGet(HttpMethod.POST);
61+
62+
StepVerifier.create(execution).verifyComplete();
63+
assertThat(getRequestDefinition().getHttpMethod()).isEqualTo(HttpMethod.POST);
64+
}
65+
66+
@Test
67+
void shouldIgnoreNullValue() {
68+
Mono<Void> execution = this.service.executeForNull(null);
69+
70+
StepVerifier.create(execution).verifyComplete();
71+
assertThat(getRequestDefinition().getHttpMethod()).isNull();
72+
}
73+
74+
75+
private interface Service {
76+
77+
@HttpRequest
78+
Mono<Void> execute(HttpMethod method);
79+
80+
@GetRequest
81+
Mono<Void> executeGet(HttpMethod method);
82+
83+
@HttpRequest
84+
Mono<Void> execute(String test);
85+
86+
@HttpRequest
87+
Mono<Void> execute(HttpMethod firstMethod, HttpMethod secondMethod);
88+
89+
@HttpRequest
90+
Mono<Void> executeForNull(@Nullable HttpMethod method);
91+
}
92+
93+
}

0 commit comments

Comments
 (0)