Skip to content

Commit 3db06bb

Browse files
Feature/Support for QueryValue as a object (#11787)
Support binding objects from query parameters --------- Co-authored-by: Graeme Rocher <graeme.rocher@oracle.com>
1 parent a0e0dd8 commit 3db06bb

File tree

3 files changed

+90
-3
lines changed

3 files changed

+90
-3
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package io.micronaut.http.server.netty.binding;
2+
3+
import io.micronaut.core.annotation.Introspected;
4+
5+
@Introspected
6+
public record PaginationRequest(Integer page, Integer size) {}

http-server-netty/src/test/groovy/io/micronaut/http/server/netty/binding/ParameterBindingSpec.groovy

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ class ParameterBindingSpec extends AbstractMicronautSpec {
9090
HttpMethod.GET | '/parameter/queryName/Fr%20ed' | "Parameter Value: Fr ed" | HttpStatus.OK
9191
HttpMethod.POST | '/parameter/query?name=Fr%20ed' | "Parameter Value: Fr ed" | HttpStatus.OK
9292
HttpMethod.GET | '/parameter/arrayStyle?param[]=a&param[]=b&param[]=c' | "Parameter Value: [a, b, c]" | HttpStatus.OK
93+
94+
HttpMethod.GET | '/parameter/query-object?age=30&title=JavaBook&author=JavaAuthor' | "Parameter Value: 30 JavaBook" | HttpStatus.OK
95+
HttpMethod.GET | '/parameter/query-record?page=1&size=123' | "Parameter Value: 1 123" | HttpStatus.OK
9396
}
9497

9598
void "test list to single error"() {
@@ -105,7 +108,6 @@ class ParameterBindingSpec extends AbstractMicronautSpec {
105108

106109
expect:
107110
response.status() == HttpStatus.BAD_REQUEST
108-
response.body().contains('Unexpected token (VALUE_STRING), expected END_ARRAY')
109111
}
110112

111113
@Requires(property = 'spec.name', value = 'ParameterBindingSpec')
@@ -229,6 +231,16 @@ class ParameterBindingSpec extends AbstractMicronautSpec {
229231
"Parameter Value: $params"
230232
}
231233

234+
@Get('/query-object')
235+
String queryObject(@QueryValue Book book) {
236+
"Parameter Value: $book.age $book.title"
237+
}
238+
239+
@Get('/query-record')
240+
String queryRecord(@QueryValue PaginationRequest paginationRequest) {
241+
"Parameter Value: $paginationRequest.page $paginationRequest.size"
242+
}
243+
232244

233245
@Introspected
234246
static class Book {
@@ -250,7 +262,7 @@ class ParameterBindingSpec extends AbstractMicronautSpec {
250262
int getAge() {
251263
return age
252264
}
253-
254265
}
266+
255267
}
256268
}

http/src/main/java/io/micronaut/http/bind/binders/QueryValueArgumentBinder.java

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,13 @@
1616
package io.micronaut.http.bind.binders;
1717

1818
import io.micronaut.core.annotation.AnnotationMetadata;
19+
import io.micronaut.core.annotation.Nullable;
20+
import io.micronaut.core.beans.BeanIntrospection;
21+
import io.micronaut.core.beans.BeanIntrospector;
1922
import io.micronaut.core.bind.annotation.AbstractArgumentBinder;
23+
import io.micronaut.core.bind.annotation.Bindable;
2024
import io.micronaut.core.convert.ArgumentConversionContext;
25+
import io.micronaut.core.convert.ConversionError;
2126
import io.micronaut.core.convert.ConversionService;
2227
import io.micronaut.core.convert.format.Format;
2328
import io.micronaut.core.convert.value.ConvertibleMultiValues;
@@ -28,6 +33,7 @@
2833
import io.micronaut.http.uri.UriMatchVariable;
2934

3035
import java.util.Collections;
36+
import java.util.List;
3137
import java.util.Optional;
3238

3339
/**
@@ -53,7 +59,7 @@ public QueryValueArgumentBinder(ConversionService conversionService) {
5359
* Constructor.
5460
*
5561
* @param conversionService conversion service
56-
* @param argument The argument
62+
* @param argument The argument
5763
*/
5864
public QueryValueArgumentBinder(ConversionService conversionService, Argument<T> argument) {
5965
super(conversionService, argument);
@@ -91,6 +97,18 @@ public BindingResult<T> bind(ArgumentConversionContext<T> context, HttpRequest<?
9197
return BindingResult.unsatisfied();
9298
}
9399

100+
BindingResult<T> bindSimpleResult = bindSimple(context, source, annotationMetadata, parameters, argument);
101+
if (bindSimpleResult.isSatisfied()) {
102+
return bindSimpleResult;
103+
}
104+
return bindPojo(context, parameters, argument);
105+
}
106+
107+
private BindingResult<T> bindSimple(ArgumentConversionContext<T> context,
108+
HttpRequest<?> source,
109+
AnnotationMetadata annotationMetadata,
110+
ConvertibleMultiValues<String> parameters,
111+
Argument<T> argument) {
94112
// First try converting from the ConvertibleMultiValues type and if conversion is successful, return it.
95113
// Otherwise, use the given uri template to deduce what to do with the variable
96114
Optional<T> multiValueConversion;
@@ -130,6 +148,57 @@ public BindingResult<T> bind(ArgumentConversionContext<T> context, HttpRequest<?
130148
return doBind(context, parameters, BindingResult.unsatisfied());
131149
}
132150

151+
private BindingResult<T> bindPojo(ArgumentConversionContext<T> context,
152+
ConvertibleMultiValues<String> parameters,
153+
Argument<T> argument) {
154+
Optional<BeanIntrospection<T>> introspectionOpt = BeanIntrospector.SHARED.findIntrospection(argument.getType());
155+
if (introspectionOpt.isEmpty()) {
156+
return BindingResult.unsatisfied();
157+
}
158+
159+
BeanIntrospection<T> introspection = introspectionOpt.get();
160+
BeanIntrospection.Builder<T> introspectionBuilder = introspection.builder();
161+
Argument<?>[] builderArguments = introspectionBuilder.getBuilderArguments();
162+
163+
for (int index = 0; index < builderArguments.length; index++) {
164+
Argument<?> builderArg = builderArguments[index];
165+
String propertyName = builderArg.getName();
166+
List<String> values = parameters.getAll(propertyName);
167+
boolean hasNoValue = values.isEmpty();
168+
@Nullable String defaultValue = hasNoValue ? builderArg
169+
.getAnnotationMetadata()
170+
.stringValue(Bindable.class, "defaultValue").orElse(null) : null;
171+
172+
ArgumentConversionContext<?> conversionContext = context.with(builderArg);
173+
Optional<?> converted = hasNoValue ? conversionService.convert(defaultValue, conversionContext) : conversionService.convert(values, conversionContext);
174+
if (converted.isPresent()) {
175+
try {
176+
@SuppressWarnings({"unchecked"})
177+
Argument<Object> rawArg = (Argument<Object>) builderArg;
178+
introspectionBuilder.with(index, rawArg, converted.get());
179+
} catch (Exception e) {
180+
context.reject(builderArg, e);
181+
return BindingResult.unsatisfied();
182+
}
183+
} else if (conversionContext.hasErrors()) {
184+
ConversionError conversionError = conversionContext.getLastError().orElse(null);
185+
if (conversionError != null) {
186+
Exception cause = conversionError.getCause();
187+
context.reject(builderArg, cause);
188+
return BindingResult.unsatisfied();
189+
}
190+
}
191+
}
192+
193+
try {
194+
T instance = introspectionBuilder.build();
195+
return () -> Optional.of(instance);
196+
} catch (Exception e) {
197+
context.reject(argument, e);
198+
return BindingResult.unsatisfied();
199+
}
200+
}
201+
133202
@Override
134203
protected String getParameterName(Argument<T> argument) {
135204
return argument.getAnnotationMetadata().stringValue(QueryValue.class).orElse(argument.getName());

0 commit comments

Comments
 (0)