Skip to content

Commit e823fec

Browse files
authored
Add support for client binding API (micronaut-projects#3994)
* Add support for client binding API. Closes micronaut-projects#3992
1 parent dcdf6df commit e823fec

36 files changed

+1100
-162
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2017-2020 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+
* 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+
package io.micronaut.http.client.bind;
17+
18+
import edu.umd.cs.findbugs.annotations.NonNull;
19+
import io.micronaut.core.annotation.Experimental;
20+
21+
import java.lang.annotation.Annotation;
22+
23+
/**
24+
* An interface for classes that bind an {@link io.micronaut.core.type.Argument} to an
25+
* {@link io.micronaut.http.MutableHttpRequest} driven by an annotation.
26+
*
27+
* @param <A> An annotation
28+
* @author James Kleeh
29+
* @since 2.1.0
30+
*/
31+
@Experimental
32+
public interface AnnotatedClientArgumentRequestBinder<A extends Annotation> extends ClientArgumentRequestBinder<Object> {
33+
34+
/**
35+
* @return The annotation type.
36+
*/
37+
@NonNull
38+
Class<A> getAnnotationType();
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2017-2020 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+
* 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+
package io.micronaut.http.client.bind;
17+
18+
import edu.umd.cs.findbugs.annotations.NonNull;
19+
import io.micronaut.core.annotation.Experimental;
20+
import io.micronaut.core.annotation.Indexed;
21+
import io.micronaut.core.convert.ArgumentConversionContext;
22+
import io.micronaut.http.MutableHttpRequest;
23+
24+
/**
25+
* A binder that binds to a {@link MutableHttpRequest}. Argument binders
26+
* are not able to modify the URI of the request.
27+
*
28+
* @param <T> A type
29+
* @author James Kleeh
30+
* @since 2.1.0
31+
*/
32+
@Experimental
33+
@Indexed(ClientArgumentRequestBinder.class)
34+
public interface ClientArgumentRequestBinder<T> {
35+
36+
/**
37+
* Bind the given argument to the request. Argument binders
38+
* are not able to modify the URI of the request.
39+
*
40+
* @param context The {@link ArgumentConversionContext}
41+
* @param value The argument value
42+
* @param request The request
43+
*/
44+
void bind(@NonNull ArgumentConversionContext<T> context, @NonNull T value, @NonNull MutableHttpRequest<?> request);
45+
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2017-2020 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+
* 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+
package io.micronaut.http.client.bind;
17+
18+
import edu.umd.cs.findbugs.annotations.NonNull;
19+
import io.micronaut.core.annotation.AnnotationMetadata;
20+
import io.micronaut.core.annotation.Internal;
21+
import io.micronaut.core.convert.ArgumentConversionContext;
22+
import io.micronaut.core.convert.ConversionContext;
23+
import io.micronaut.core.convert.ConversionService;
24+
import io.micronaut.core.convert.format.Format;
25+
import io.micronaut.core.type.Argument;
26+
import io.micronaut.core.util.StringUtils;
27+
import io.micronaut.http.MutableHttpRequest;
28+
import io.micronaut.http.annotation.PathVariable;
29+
import io.micronaut.http.annotation.QueryValue;
30+
import io.micronaut.http.uri.UriMatchTemplate;
31+
32+
import java.util.List;
33+
import java.util.Map;
34+
35+
/**
36+
* Handles arguments relating to the URI or partial body.
37+
*
38+
* @author James Kleeh
39+
* @since 2.1.0
40+
*/
41+
@Internal
42+
public class DefaultClientBinder implements ClientArgumentRequestBinder<Object> {
43+
44+
private final Map<String, Object> paramMap;
45+
private final Map<String, String> queryParams;
46+
private final List<Argument> bodyArguments;
47+
private final UriMatchTemplate uriTemplate;
48+
private final ConversionService<?> conversionService;
49+
50+
public DefaultClientBinder(Map<String, Object> paramMap,
51+
Map<String, String> queryParams,
52+
List<Argument> bodyArguments,
53+
UriMatchTemplate uriTemplate,
54+
ConversionService<?> conversionService) {
55+
this.paramMap = paramMap;
56+
this.queryParams = queryParams;
57+
this.bodyArguments = bodyArguments;
58+
this.uriTemplate = uriTemplate;
59+
this.conversionService = conversionService;
60+
}
61+
62+
@Override
63+
public void bind(@NonNull ArgumentConversionContext<Object> context, @NonNull Object value, @NonNull MutableHttpRequest<?> request) {
64+
AnnotationMetadata metadata = context.getAnnotationMetadata();
65+
Argument argument = context.getArgument();
66+
String argumentName = argument.getName();
67+
ArgumentConversionContext<String> stringConversion = ConversionContext.of(String.class).with(metadata);
68+
if (metadata.isAnnotationPresent(QueryValue.class)) {
69+
String parameterName = metadata.stringValue(QueryValue.class).orElse(argumentName);
70+
71+
conversionService.convert(value, stringConversion)
72+
.filter(StringUtils::isNotEmpty)
73+
.ifPresent(o -> {
74+
queryParams.put(parameterName, o);
75+
});
76+
} else if (metadata.isAnnotationPresent(PathVariable.class)) {
77+
String parameterName = metadata.stringValue(PathVariable.class).orElse(argumentName);
78+
if (!(value instanceof String)) {
79+
conversionService.convert(value, stringConversion)
80+
.filter(StringUtils::isNotEmpty)
81+
.ifPresent(param -> paramMap.put(parameterName, param));
82+
}
83+
} else if (!uriTemplate.getVariableNames().contains(context.getArgument().getName())) {
84+
bodyArguments.add(context.getArgument());
85+
}
86+
}
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Copyright 2017-2020 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+
* 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+
package io.micronaut.http.client.bind;
17+
18+
import io.micronaut.core.annotation.AnnotationMetadata;
19+
import io.micronaut.core.annotation.Internal;
20+
import io.micronaut.core.bind.annotation.Bindable;
21+
import io.micronaut.core.convert.ConversionService;
22+
import io.micronaut.core.naming.NameUtils;
23+
import io.micronaut.core.type.Argument;
24+
import io.micronaut.core.util.CollectionUtils;
25+
import io.micronaut.core.util.StringUtils;
26+
import io.micronaut.http.*;
27+
import io.micronaut.http.annotation.Body;
28+
import io.micronaut.http.annotation.CookieValue;
29+
import io.micronaut.http.annotation.Header;
30+
import io.micronaut.http.annotation.RequestAttribute;
31+
import io.micronaut.http.cookie.Cookie;
32+
import io.micronaut.http.cookie.Cookies;
33+
34+
import javax.inject.Inject;
35+
import javax.inject.Singleton;
36+
import java.lang.annotation.Annotation;
37+
import java.util.*;
38+
39+
/**
40+
* Default implementation of {@link HttpClientBinderRegistry} that searches by
41+
* annotation then by type.
42+
*
43+
* @author James Kleeh
44+
* @since 2.1.0
45+
*/
46+
@Singleton
47+
@Internal
48+
public class DefaultHttpClientBinderRegistry implements HttpClientBinderRegistry {
49+
50+
private final Map<Class<? extends Annotation>, ClientArgumentRequestBinder> byAnnotation = new LinkedHashMap<>();
51+
private final Map<Integer, ClientArgumentRequestBinder> byType = new LinkedHashMap<>();
52+
53+
/**
54+
* @param conversionService The conversion service
55+
* @param binders The request argument binders
56+
*/
57+
@Inject
58+
protected DefaultHttpClientBinderRegistry(ConversionService<?> conversionService,
59+
List<ClientArgumentRequestBinder> binders) {
60+
byType.put(Argument.of(HttpHeaders.class).typeHashCode(), (ClientArgumentRequestBinder<HttpHeaders>) (context, value, request) -> {
61+
value.forEachValue(request::header);
62+
});
63+
byType.put(Argument.of(Cookies.class).typeHashCode(), (ClientArgumentRequestBinder<Cookies>) (context, value, request) -> {
64+
request.cookies(value.getAll());
65+
});
66+
byType.put(Argument.of(Cookie.class).typeHashCode(), (ClientArgumentRequestBinder<Cookie>) (context, value, request) -> {
67+
request.cookie(value);
68+
});
69+
byType.put(Argument.of(BasicAuth.class).typeHashCode(), (ClientArgumentRequestBinder<BasicAuth>) (context, value, request) -> {
70+
request.basicAuth(value.getUsername(), value.getPassword());
71+
});
72+
byAnnotation.put(CookieValue.class, (context, value, request) -> {
73+
String cookieName = context.getAnnotationMetadata().stringValue(CookieValue.class)
74+
.filter(StringUtils::isNotEmpty)
75+
.orElse(context.getArgument().getName());
76+
77+
conversionService.convert(value, String.class)
78+
.ifPresent(o -> request.cookie(Cookie.of(cookieName, o)));
79+
});
80+
byAnnotation.put(Header.class, (context, value, request) -> {
81+
AnnotationMetadata annotationMetadata = context.getAnnotationMetadata();
82+
String headerName = annotationMetadata
83+
.stringValue(Header.class)
84+
.filter(StringUtils::isNotEmpty)
85+
.orElse(NameUtils.hyphenate(context.getArgument().getName()));
86+
87+
conversionService.convert(value, String.class)
88+
.ifPresent(header -> request.getHeaders().set(headerName, header));
89+
});
90+
byAnnotation.put(RequestAttribute.class, (context, value, request) -> {
91+
AnnotationMetadata annotationMetadata = context.getAnnotationMetadata();
92+
String attributeName = annotationMetadata
93+
.stringValue(RequestAttribute.class)
94+
.filter(StringUtils::isNotEmpty)
95+
.orElse(NameUtils.hyphenate(context.getArgument().getName()));
96+
request.getAttributes().put(attributeName, value);
97+
});
98+
byAnnotation.put(Body.class, (ClientArgumentRequestBinder<Object>) (context, value, request) -> {
99+
request.body(value);
100+
});
101+
102+
if (CollectionUtils.isNotEmpty(binders)) {
103+
for (ClientArgumentRequestBinder binder : binders) {
104+
addBinder(binder);
105+
}
106+
}
107+
}
108+
109+
@Override
110+
public <T> Optional<ClientArgumentRequestBinder<T>> findArgumentBinder(Argument<T> argument) {
111+
Optional<Class<? extends Annotation>> opt = argument.getAnnotationMetadata().getAnnotationTypeByStereotype(Bindable.class);
112+
if (opt.isPresent()) {
113+
Class<? extends Annotation> annotationType = opt.get();
114+
ClientArgumentRequestBinder<T> binder = byAnnotation.get(annotationType);
115+
return Optional.ofNullable(binder);
116+
} else {
117+
ClientArgumentRequestBinder<T> binder = byType.get(argument.typeHashCode());
118+
if (binder != null) {
119+
return Optional.of(binder);
120+
} else {
121+
binder = byType.get(Argument.of(argument.getType()).typeHashCode());
122+
return Optional.ofNullable(binder);
123+
}
124+
}
125+
}
126+
127+
/**
128+
* Adds a binder to the registry.
129+
*
130+
* @param binder The binder
131+
* @param <T> The type
132+
*/
133+
public <T> void addBinder(ClientArgumentRequestBinder<T> binder) {
134+
if (binder instanceof AnnotatedClientArgumentRequestBinder) {
135+
AnnotatedClientArgumentRequestBinder<?> annotatedRequestArgumentBinder = (AnnotatedClientArgumentRequestBinder) binder;
136+
Class<? extends Annotation> annotationType = annotatedRequestArgumentBinder.getAnnotationType();
137+
byAnnotation.put(annotationType, annotatedRequestArgumentBinder);
138+
} else if (binder instanceof TypedClientArgumentRequestBinder) {
139+
TypedClientArgumentRequestBinder<?> typedRequestArgumentBinder = (TypedClientArgumentRequestBinder) binder;
140+
byType.put(typedRequestArgumentBinder.argumentType().typeHashCode(), typedRequestArgumentBinder);
141+
List<Class<?>> superTypes = typedRequestArgumentBinder.superTypes();
142+
if (CollectionUtils.isNotEmpty(superTypes)) {
143+
for (Class<?> superType : superTypes) {
144+
byType.put(Argument.of(superType).typeHashCode(), typedRequestArgumentBinder);
145+
}
146+
}
147+
}
148+
}
149+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2017-2020 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+
* 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+
package io.micronaut.http.client.bind;
17+
18+
import edu.umd.cs.findbugs.annotations.NonNull;
19+
import io.micronaut.core.annotation.Experimental;
20+
import io.micronaut.core.type.Argument;
21+
22+
import java.util.Optional;
23+
24+
/**
25+
* A registry of {@link ClientArgumentRequestBinder} instances.
26+
*
27+
* @author James Kleeh
28+
* @since 2.1.0
29+
*/
30+
@Experimental
31+
public interface HttpClientBinderRegistry {
32+
33+
/**
34+
* Locate an {@link ClientArgumentRequestBinder} for the given argument.
35+
*
36+
* @param argument The argument
37+
* @param <T> The argument type
38+
* @return An {@link Optional} of {@link ClientArgumentRequestBinder}
39+
*/
40+
<T> Optional<ClientArgumentRequestBinder<T>> findArgumentBinder(@NonNull Argument<T> argument);
41+
42+
}

0 commit comments

Comments
 (0)