From f695369f071463e4c865a4e826c9269c576dbb6f Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 29 Nov 2022 09:26:14 +0200 Subject: [PATCH] Introduce @Separator annotation for query parameters This annotation allows to actually turn a String into a List automatically for each query param value Resolves: #29528 --- .../asciidoc/resteasy-reactive-migration.adoc | 4 ++ .../CustomResourceProducersGenerator.java | 5 +- .../test/simple/SeparatorQueryParamTest.java | 69 +++++++++++++++++++ .../scanning/ClientEndpointIndexer.java | 2 +- .../common/processor/EndpointIndexer.java | 9 +++ .../processor/ResteasyReactiveDotNames.java | 2 + .../jboss/resteasy/reactive/Separator.java | 18 +++++ .../common/model/MethodParameter.java | 4 +- .../processor/ServerEndpointIndexer.java | 3 +- .../core/parameters/QueryParamExtractor.java | 30 +++++++- .../startup/RuntimeResourceDeployment.java | 2 +- .../server/model/ServerMethodParameter.java | 4 +- 12 files changed, 142 insertions(+), 10 deletions(-) create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/SeparatorQueryParamTest.java create mode 100644 independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/Separator.java diff --git a/docs/src/main/asciidoc/resteasy-reactive-migration.adoc b/docs/src/main/asciidoc/resteasy-reactive-migration.adoc index f4e6674e015a9..96caaba04f23f 100644 --- a/docs/src/main/asciidoc/resteasy-reactive-migration.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive-migration.adoc @@ -87,6 +87,10 @@ The following table matches the legacy RESTEasy annotations with the new RESTEas |`org.jboss.resteasy.reactive.RestStreamElementType` | +|`org.jboss.resteasy.annotations.Separator` +|`org.jboss.resteasy.reactive.Separator` +| + |=== NOTE: The previous table does not include the `org.jboss.resteasy.annotations.Form` annotation because there is no RESTEasy Reactive specific replacement for it. diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomResourceProducersGenerator.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomResourceProducersGenerator.java index a0b95bbb21214..600dc62fc03da 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomResourceProducersGenerator.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomResourceProducersGenerator.java @@ -136,8 +136,9 @@ public static void generate(Map resourcesThatNeedCustomProd ResultHandle quarkusRestContextHandle = m.invokeVirtualMethod(getContextMethodCreator.getMethodDescriptor(), m.getThis()); ResultHandle extractorHandle = m.newInstance( - MethodDescriptor.ofConstructor(QueryParamExtractor.class, String.class, boolean.class, boolean.class), - m.getMethodParam(0), m.load(true), m.load(false)); + MethodDescriptor.ofConstructor(QueryParamExtractor.class, String.class, boolean.class, boolean.class, + String.class), + m.getMethodParam(0), m.load(true), m.load(false), m.loadNull()); ResultHandle resultHandle = m.invokeVirtualMethod(MethodDescriptor.ofMethod(QueryParamExtractor.class, "extractParameter", Object.class, ResteasyReactiveRequestContext.class), extractorHandle, quarkusRestContextHandle); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/SeparatorQueryParamTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/SeparatorQueryParamTest.java new file mode 100644 index 0000000000000..b8973cd1edce6 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/SeparatorQueryParamTest.java @@ -0,0 +1,69 @@ +package io.quarkus.resteasy.reactive.server.test.simple; + +import static io.restassured.RestAssured.*; + +import java.util.List; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.hamcrest.Matchers; +import org.jboss.resteasy.reactive.RestQuery; +import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.Separator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class SeparatorQueryParamTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(HelloResource.class)); + + @Test + public void noQueryParams() { + get("/hello") + .then() + .statusCode(200) + .body(Matchers.equalTo("hello world")) + .header("x-size", "0"); + } + + @Test + public void singleQueryParam() { + get("/hello?name=foo") + .then() + .statusCode(200) + .body(Matchers.equalTo("hello foo")) + .header("x-size", "1"); + } + + @Test + public void multipleQueryParams() { + get("/hello?name=foo,bar&name=one,two,three&name=yolo") + .then() + .statusCode(200) + .body(Matchers.equalTo("hello foo bar one two three yolo")) + .header("x-size", "6"); + } + + @Path("hello") + public static class HelloResource { + + @GET + public RestResponse hello(@RestQuery("name") @Separator(",") List names) { + int size = names.size(); + String body = ""; + if (names.isEmpty()) { + body = "hello world"; + } else { + body = "hello " + String.join(" ", names); + } + return RestResponse.ResponseBuilder.ok(body).header("x-size", size).build(); + } + + } +} diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java index ba58ddbc17f5b..7fb5137508642 100644 --- a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java @@ -143,7 +143,7 @@ protected MethodParameter createMethodParameter(ClassInfo currentClassInfo, Clas elementType, declaredTypes.getDeclaredType(), declaredTypes.getDeclaredUnresolvedType(), signature, type, single, defaultValue, parameterResult.isObtainedAsCollection(), parameterResult.isOptional(), encoded, - mimePart, partFileName); + mimePart, partFileName, null); } private String getPartFileName(Map annotations) { diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java index 9d30b7b60041e..f0c129876ac7e 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java @@ -1500,6 +1500,15 @@ protected String getPartMime(Map annotations) { return mimeType; } + protected String getSeparator(Map annotations) { + AnnotationInstance separator = annotations.get(ResteasyReactiveDotNames.SEPARATOR); + String result = null; + if (separator != null && separator.value() != null) { + result = separator.value().asString(); + } + return result; + } + @SuppressWarnings({ "unchecked", "rawtypes" }) public static abstract class Builder, B extends Builder, METHOD extends ResourceMethod> { private Function> factoryCreator; diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java index c86e07b6d04ac..8a7efd8526c8b 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java @@ -92,6 +92,7 @@ import org.jboss.resteasy.reactive.RestResponse; import org.jboss.resteasy.reactive.RestSseElementType; import org.jboss.resteasy.reactive.RestStreamElementType; +import org.jboss.resteasy.reactive.Separator; import org.reactivestreams.Publisher; import io.smallrye.common.annotation.Blocking; @@ -136,6 +137,7 @@ public final class ResteasyReactiveDotNames { public static final DotName MULTI_PART_FORM_PARAM = DotName.createSimple(MultipartForm.class.getName()); public static final DotName PART_TYPE_NAME = DotName.createSimple(PartType.class.getName()); public static final DotName PART_FILE_NAME = DotName.createSimple(PartFilename.class.getName()); + public static final DotName SEPARATOR = DotName.createSimple(Separator.class.getName()); public static final DotName REST_MATRIX_PARAM = DotName.createSimple(RestMatrix.class.getName()); public static final DotName REST_COOKIE_PARAM = DotName.createSimple(RestCookie.class.getName()); public static final DotName GET = DotName.createSimple(javax.ws.rs.GET.class.getName()); diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/Separator.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/Separator.java new file mode 100644 index 0000000000000..86e2629e80c46 --- /dev/null +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/Separator.java @@ -0,0 +1,18 @@ +package org.jboss.resteasy.reactive; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.List; + +/** + * When used on a {@link List} parameter annotated with {@link RestQuery}, RESTEasy Reactive will split the value of the + * parameter (using the value of the annotation) and populate the list with those values. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.PARAMETER }) +public @interface Separator { + + String value(); +} diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/MethodParameter.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/MethodParameter.java index 3490ee77bde90..cf86ba52d4261 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/MethodParameter.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/MethodParameter.java @@ -25,6 +25,7 @@ public class MethodParameter { private boolean isObtainedAsCollection; public String mimeType; public String partFileName; + public String separator; public MethodParameter() { } @@ -33,7 +34,7 @@ public MethodParameter(String name, String type, String declaredType, String dec ParameterType parameterType, boolean single, String defaultValue, boolean isObtainedAsCollection, boolean optional, boolean encoded, - String mimeType, String partFileName) { + String mimeType, String partFileName, String separator) { this.name = name; this.type = type; this.declaredType = declaredType; @@ -47,6 +48,7 @@ public MethodParameter(String name, String type, String declaredType, String dec this.encoded = encoded; this.mimeType = mimeType; this.partFileName = partFileName; + this.separator = separator; } public String getName() { diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java index 247375f997f6b..caf772ebb5e66 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java @@ -315,11 +315,12 @@ protected MethodParameter createMethodParameter(ClassInfo currentClassInfo, Clas ParameterConverterSupplier converter = parameterResult.getConverter(); DeclaredTypes declaredTypes = getDeclaredTypes(paramType, currentClassInfo, actualEndpointInfo); String mimeType = getPartMime(parameterResult.getAnns()); + String separator = getSeparator(parameterResult.getAnns()); return new ServerMethodParameter(name, elementType, declaredTypes.getDeclaredType(), declaredTypes.getDeclaredUnresolvedType(), type, single, signature, converter, defaultValue, parameterResult.isObtainedAsCollection(), parameterResult.isOptional(), encoded, - parameterResult.getCustomParameterExtractor(), mimeType); + parameterResult.getCustomParameterExtractor(), mimeType, separator); } protected void handleOtherParam(Map existingConverters, String errorLocation, boolean hasRuntimeConverters, diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/QueryParamExtractor.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/QueryParamExtractor.java index ba9efb34ea930..f6d31259de9bb 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/QueryParamExtractor.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/QueryParamExtractor.java @@ -1,5 +1,9 @@ package org.jboss.resteasy.reactive.server.core.parameters; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; public class QueryParamExtractor implements ParameterExtractor { @@ -7,15 +11,37 @@ public class QueryParamExtractor implements ParameterExtractor { private final String name; private final boolean single; private final boolean encoded; + private final String separator; - public QueryParamExtractor(String name, boolean single, boolean encoded) { + public QueryParamExtractor(String name, boolean single, boolean encoded, String separator) { this.name = name; this.single = single; this.encoded = encoded; + this.separator = separator; } @Override + @SuppressWarnings("unchecked") public Object extractParameter(ResteasyReactiveRequestContext context) { - return context.getQueryParameter(name, single, encoded); + Object queryParameter = context.getQueryParameter(name, single, encoded); + if (separator != null) { + if (queryParameter instanceof List) { // it's List + List list = (List) queryParameter; + List result = new ArrayList<>(list.size()); + for (int i = 0; i < list.size(); i++) { + String[] parts = list.get(i).split(separator); + result.addAll(Arrays.asList(parts)); + } + queryParameter = result; + } else if (queryParameter instanceof String) { + List result = new ArrayList<>(1); + String[] parts = ((String) queryParameter).split(separator); + result.addAll(Arrays.asList(parts)); + queryParameter = result; + } else { + // can't really happen + } + } + return queryParameter; } } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java index 3c2710fef8cf4..1b93682704e40 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java @@ -676,7 +676,7 @@ public ParameterExtractor parameterExtractor(Map pathParameterI case ASYNC_RESPONSE: return new AsyncResponseExtractor(); case QUERY: - extractor = new QueryParamExtractor(param.name, param.isSingle(), param.encoded); + extractor = new QueryParamExtractor(param.name, param.isSingle(), param.encoded, param.separator); return extractor; case BODY: return new BodyParamExtractor(); diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/ServerMethodParameter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/ServerMethodParameter.java index 9a24d48043bba..d866d393ea817 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/ServerMethodParameter.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/ServerMethodParameter.java @@ -21,10 +21,10 @@ public ServerMethodParameter(String name, String type, String declaredType, Stri ParameterConverterSupplier converter, String defaultValue, boolean obtainedAsCollection, boolean optional, boolean encoded, ParameterExtractor customParameterExtractor, - String mimeType) { + String mimeType, String separator) { super(name, type, declaredType, declaredUnresolvedType, signature, parameterType, single, defaultValue, obtainedAsCollection, optional, - encoded, mimeType, null /* not useful for server params */); + encoded, mimeType, null /* not useful for server params */, separator); this.converter = converter; this.customParameterExtractor = customParameterExtractor; }