Skip to content

Commit b0a6be7

Browse files
committed
Add @PathVariable to disambiguate binding. Issue micronaut-projects#1085
1 parent ffdf7cd commit b0a6be7

File tree

9 files changed

+292
-11
lines changed

9 files changed

+292
-11
lines changed

http-client/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -262,13 +262,20 @@ public Object intercept(MethodInvocationContext<Object, Object> context) {
262262
} else if (annotationMetadata.isAnnotationPresent(QueryValue.class)) {
263263
String parameterName = annotationMetadata.getValue(QueryValue.class, String.class).orElse(null);
264264
conversionService.convert(definedValue, ConversionContext.of(String.class).with(annotationMetadata)).ifPresent(o -> {
265-
if (!StringUtils.isEmpty(parameterName)) {
265+
if (!StringUtils.isEmpty(o)) {
266266
paramMap.put(parameterName, o);
267267
queryParams.put(parameterName, o);
268268
} else {
269269
queryParams.put(argumentName, o);
270270
}
271271
});
272+
} else if (annotationMetadata.isAnnotationPresent(PathVariable.class)) {
273+
String parameterName = annotationMetadata.getValue(PathVariable.class, String.class).orElse(null);
274+
conversionService.convert(definedValue, ConversionContext.of(String.class).with(annotationMetadata)).ifPresent(o -> {
275+
if (!StringUtils.isEmpty(o)) {
276+
paramMap.put(parameterName, o);
277+
}
278+
});
272279
} else if (!uriVariables.contains(argumentName)) {
273280
bodyArguments.add(argument);
274281
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package io.micronaut.http.client.aop
2+
3+
import io.micronaut.context.ApplicationContext
4+
import io.micronaut.http.annotation.Controller
5+
import io.micronaut.http.annotation.Get
6+
import io.micronaut.http.annotation.PathVariable
7+
import io.micronaut.http.annotation.QueryValue
8+
import io.micronaut.http.client.annotation.Client
9+
import io.micronaut.runtime.server.EmbeddedServer
10+
import spock.lang.AutoCleanup
11+
import spock.lang.Shared
12+
import spock.lang.Specification
13+
14+
class PathVariableSpec extends Specification {
15+
16+
@Shared
17+
@AutoCleanup
18+
ApplicationContext context = ApplicationContext.run()
19+
20+
@Shared
21+
@AutoCleanup
22+
EmbeddedServer embeddedServer = context.getBean(EmbeddedServer).start()
23+
24+
void "test send and receive with path variable"() {
25+
given:
26+
UserClient userClient = context.getBean(UserClient)
27+
User user = userClient.get("Fred")
28+
29+
expect:
30+
user.username == "Fred"
31+
user.age == 10
32+
33+
when:
34+
user = userClient.findByAge(18)
35+
36+
then:
37+
user.username == "John"
38+
user.age == 18
39+
40+
}
41+
42+
@Client('/path-variables')
43+
static interface UserClient extends MyApi {
44+
45+
}
46+
47+
@Controller('/path-variables')
48+
static class UserController implements MyApi {
49+
50+
@Override
51+
User get(String username) {
52+
return new User(username:username, age: 10)
53+
}
54+
55+
@Override
56+
User findByAge(Integer age) {
57+
return new User(username:"John", age: 18)
58+
}
59+
}
60+
61+
static interface MyApi {
62+
63+
@Get('/user/{X-username}')
64+
User get(@PathVariable('X-username') String username)
65+
66+
@Get('/user/age/{userAge}')
67+
User findByAge(@PathVariable('userAge') Integer age)
68+
}
69+
70+
static class User {
71+
String username
72+
Integer age
73+
}
74+
}

http/src/main/java/io/micronaut/http/annotation/CookieValue.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
*/
3636
@Documented
3737
@Retention(RUNTIME)
38-
@Target({ElementType.PARAMETER})
38+
@Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
3939
@Bindable
4040
public @interface CookieValue {
4141

http/src/main/java/io/micronaut/http/annotation/Header.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
*/
5555
@Documented
5656
@Retention(RUNTIME)
57-
@Target({ElementType.PARAMETER, ElementType.TYPE, ElementType.METHOD}) // this can be either type or param
57+
@Target({ElementType.PARAMETER, ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) // this can be either type or param
5858
@Repeatable(value = Headers.class)
5959
@Bindable
6060
public @interface Header {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2017-2019 original 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+
* http://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 io.micronaut.http.annotation;
18+
19+
import io.micronaut.context.annotation.AliasFor;
20+
import io.micronaut.core.bind.annotation.Bindable;
21+
22+
import java.lang.annotation.Documented;
23+
import java.lang.annotation.ElementType;
24+
import java.lang.annotation.Retention;
25+
import java.lang.annotation.Target;
26+
27+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
28+
29+
/**
30+
* Used to bind a parameter exclusively from a path variable.
31+
*
32+
* @author graemerocher
33+
* @since 1.0.3
34+
*/
35+
@Documented
36+
@Retention(RUNTIME)
37+
@Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
38+
@Bindable
39+
public @interface PathVariable {
40+
/**
41+
* @return The name of the parameter
42+
*/
43+
@AliasFor(annotation = Bindable.class, member = "value")
44+
@AliasFor(member = "name")
45+
String value() default "";
46+
47+
/**
48+
* @return The name of the parameter
49+
*/
50+
@AliasFor(annotation = Bindable.class, member = "value")
51+
@AliasFor(member = "value")
52+
String name() default "";
53+
54+
/**
55+
* @see Bindable#defaultValue()
56+
* @return The default value
57+
*/
58+
@AliasFor(annotation = Bindable.class, member = "defaultValue")
59+
String defaultValue() default "";
60+
}

http/src/main/java/io/micronaut/http/annotation/QueryValue.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
*/
3737
@Documented
3838
@Retention(RUNTIME)
39-
@Target({ElementType.PARAMETER})
39+
@Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
4040
@Bindable
4141
public @interface QueryValue {
4242

http/src/main/java/io/micronaut/http/bind/DefaultRequestBinderRegistry.java

+4-7
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,9 @@
3030
import io.micronaut.http.HttpRequest;
3131
import io.micronaut.http.MediaType;
3232
import io.micronaut.http.annotation.Body;
33+
import io.micronaut.http.bind.binders.*;
3334
import io.micronaut.http.cookie.Cookie;
3435
import io.micronaut.http.cookie.Cookies;
35-
import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder;
36-
import io.micronaut.http.bind.binders.CookieAnnotationBinder;
37-
import io.micronaut.http.bind.binders.DefaultBodyAnnotationBinder;
38-
import io.micronaut.http.bind.binders.HeaderAnnotationBinder;
39-
import io.micronaut.http.bind.binders.ParameterAnnotationBinder;
40-
import io.micronaut.http.bind.binders.RequestArgumentBinder;
41-
import io.micronaut.http.bind.binders.TypedRequestArgumentBinder;
4236

4337
import javax.inject.Inject;
4438
import javax.inject.Singleton;
@@ -204,6 +198,9 @@ protected void registerDefaultAnnotationBinders(Map<Class<? extends Annotation>,
204198

205199
ParameterAnnotationBinder<Object> parameterAnnotationBinder = new ParameterAnnotationBinder<>(conversionService);
206200
byAnnotation.put(parameterAnnotationBinder.getAnnotationType(), parameterAnnotationBinder);
201+
202+
PathVariableAnnotationBinder<Object> pathVariableAnnotationBinder = new PathVariableAnnotationBinder<>(conversionService);
203+
byAnnotation.put(pathVariableAnnotationBinder.getAnnotationType(), pathVariableAnnotationBinder);
207204
}
208205

209206
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright 2017-2019 original 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+
* http://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 io.micronaut.http.bind.binders;
18+
19+
import io.micronaut.core.annotation.AnnotationMetadata;
20+
import io.micronaut.core.bind.annotation.AbstractAnnotatedArgumentBinder;
21+
import io.micronaut.core.convert.ArgumentConversionContext;
22+
import io.micronaut.core.convert.ConversionService;
23+
import io.micronaut.core.convert.value.ConvertibleMultiValues;
24+
import io.micronaut.core.convert.value.ConvertibleValues;
25+
import io.micronaut.core.type.Argument;
26+
import io.micronaut.http.HttpAttributes;
27+
import io.micronaut.http.HttpMethod;
28+
import io.micronaut.http.HttpRequest;
29+
import io.micronaut.http.annotation.PathVariable;
30+
import io.micronaut.http.uri.UriMatchInfo;
31+
import io.micronaut.http.uri.UriMatchVariable;
32+
33+
import java.util.Collections;
34+
import java.util.Optional;
35+
36+
/**
37+
* Used for binding a parameter exclusively from a path variable.
38+
*
39+
* @author graemerocher
40+
* @since 1.0.3
41+
* @see PathVariable
42+
* @param <T>
43+
*/
44+
public class PathVariableAnnotationBinder<T> extends AbstractAnnotatedArgumentBinder<PathVariable, T, HttpRequest<?>> implements AnnotatedRequestArgumentBinder<PathVariable, T> {
45+
46+
/**
47+
* @param conversionService The conversion service
48+
*/
49+
public PathVariableAnnotationBinder(ConversionService<?> conversionService) {
50+
super(conversionService);
51+
}
52+
53+
@Override
54+
public Class<PathVariable> getAnnotationType() {
55+
return PathVariable.class;
56+
}
57+
58+
@Override
59+
public BindingResult<T> bind(ArgumentConversionContext<T> context, HttpRequest<?> source) {
60+
ConvertibleMultiValues<String> parameters = source.getParameters();
61+
Argument<T> argument = context.getArgument();
62+
HttpMethod httpMethod = source.getMethod();
63+
64+
AnnotationMetadata annotationMetadata = argument.getAnnotationMetadata();
65+
boolean hasAnnotation = annotationMetadata.hasAnnotation(PathVariable.class);
66+
String parameterName = annotationMetadata.getValue(PathVariable.class, String.class).orElse(argument.getName());
67+
// If we need to bind all request params to command object
68+
// checks if the variable is defined with modifier char *
69+
// eg. ?pojo*
70+
final Optional<UriMatchInfo> matchInfo = source.getAttribute(HttpAttributes.ROUTE_MATCH, UriMatchInfo.class);
71+
boolean bindAll = matchInfo
72+
.flatMap(umi -> umi.getVariables()
73+
.stream()
74+
.filter(v -> v.getName().equals(parameterName))
75+
.findFirst()
76+
.map(UriMatchVariable::isExploded)).orElse(false);
77+
78+
79+
BindingResult<T> result;
80+
// if the annotation is present or the HTTP method doesn't allow a request body
81+
// attempt to bind from request parameters. This avoids allowing the request URI to
82+
// be manipulated to override POST or JSON variables
83+
if (hasAnnotation && matchInfo.isPresent()) {
84+
final ConvertibleValues<Object> variableValues = ConvertibleValues.of(matchInfo.get().getVariableValues());
85+
if (bindAll) {
86+
Object value;
87+
// Only maps and POJOs will "bindAll", lists work like normal
88+
if (Iterable.class.isAssignableFrom(argument.getType())) {
89+
value = doResolve(context, variableValues, parameterName);
90+
if (value == null) {
91+
value = Collections.emptyList();
92+
}
93+
} else {
94+
value = parameters.asMap();
95+
}
96+
result = doConvert(value, context);
97+
} else {
98+
result = doBind(context, variableValues, parameterName);
99+
}
100+
} else {
101+
//noinspection unchecked
102+
result = BindingResult.EMPTY;
103+
}
104+
105+
return result;
106+
}
107+
}

validation/src/test/groovy/io/micronaut/validation/routes/MissingParameterRuleSpec.groovy

+36
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,42 @@ class Book {
177177
}
178178
}
179179
180+
""")
181+
182+
then:
183+
noExceptionThrown()
184+
}
185+
186+
void "test map name to different header"() {
187+
when:
188+
buildTypeElement("""
189+
190+
package test;
191+
192+
import io.micronaut.http.annotation.*;
193+
194+
@Controller("/foo")
195+
class Foo {
196+
197+
@Get("/{name}")
198+
String abc(@Header("pet-name") String name, @QueryValue("name") String pathName) {
199+
return "abc";
200+
}
201+
}
202+
203+
class Book {
204+
205+
private String abc;
206+
207+
public String getAbc() {
208+
return this.abc;
209+
}
210+
211+
public void setAbc(String abc) {
212+
this.abc = abc;
213+
}
214+
}
215+
180216
""")
181217

182218
then:

0 commit comments

Comments
 (0)