From 02dc081ff20a5893ddcc17f84694424d6c0cd424 Mon Sep 17 00:00:00 2001 From: Jose Date: Wed, 2 Mar 2022 11:57:21 +0100 Subject: [PATCH] Resteasy Reactive: Support use of optional with list/set/sortedset Support use when using query params with some collections. Example: ```java @Path("/list") @GET public String sayHelloToList(@QueryParam("name") final Optional> names) { return doSayHelloToCollection(names); } ``` fix https://github.com/quarkusio/quarkus/issues/23898 --- .../quarkus/resteasy/test/QueryParamTest.java | 52 +++++++++++++++++++ .../simple/OptionalQueryParamResource.java | 49 +++++++++++++++++ .../simple/SimpleQuarkusRestTestCase.java | 41 ++++++++++++++- .../common/processor/EndpointIndexer.java | 11 +++- .../processor/ServerEndpointIndexer.java | 26 ++++++++-- 5 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/QueryParamTest.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/OptionalQueryParamResource.java diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/QueryParamTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/QueryParamTest.java new file mode 100644 index 0000000000000..769fdd9dc464a --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/QueryParamTest.java @@ -0,0 +1,52 @@ +package io.quarkus.resteasy.test; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.SecurityContext; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.ext.web.RoutingContext; + +public class QueryParamTest { + + private static final String HELLO = "hello "; + private static final String NOBODY = "nobody"; + private static final String ALBERT = "albert"; + private static final String AND = " and "; + private static final String JOSE = "jose"; + private static final String NAME = "name"; + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar.addClasses(MyResource.class)); + + @Test + public void testWithSomeNames() { + Assertions.assertEquals(HELLO + ALBERT + AND + JOSE, RestAssured.given().queryParam(NAME, ALBERT, JOSE).get("/greetings").asString()); + } + + @Path("/greetings") + public static class MyResource { + + @GET + public String sayHello(@QueryParam("name") final Optional> names) { + return HELLO + names.map(l -> l.stream().collect(Collectors.joining(AND))) + .orElse(NOBODY); + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/OptionalQueryParamResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/OptionalQueryParamResource.java new file mode 100644 index 0000000000000..a27c34bd9fb0a --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/OptionalQueryParamResource.java @@ -0,0 +1,49 @@ +package io.quarkus.resteasy.reactive.server.test.simple; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.SortedSet; +import java.util.stream.Collectors; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; + +@Path("/optional-query/greetings") +public class OptionalQueryParamResource { + + public static final String HELLO = "hello "; + public static final String NOBODY = "nobody"; + public static final String AND = " and "; + + @Path("/one") + @GET + public String sayHelloToValue(@QueryParam("name") final Optional name) { + return HELLO + name.orElse(NOBODY); + } + + @Path("/list") + @GET + public String sayHelloToList(@QueryParam("name") final Optional> names) { + return doSayHelloToCollection(names); + } + + @Path("/set") + @GET + public String sayHelloToSet(@QueryParam("name") final Optional> names) { + return doSayHelloToCollection(names); + } + + @Path("/sortedset") + @GET + public String sayHelloToSortedSet(@QueryParam("name") final Optional> names) { + return doSayHelloToCollection(names); + } + + private String doSayHelloToCollection(final Optional> names) { + return HELLO + names.map(l -> l.stream().collect(Collectors.joining(AND))) + .orElse(NOBODY); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/SimpleQuarkusRestTestCase.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/SimpleQuarkusRestTestCase.java index 8fb55c2904a31..153a8a792b378 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/SimpleQuarkusRestTestCase.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/SimpleQuarkusRestTestCase.java @@ -1,5 +1,7 @@ package io.quarkus.resteasy.reactive.server.test.simple; +import static io.quarkus.resteasy.reactive.server.test.simple.OptionalQueryParamResource.HELLO; +import static io.quarkus.resteasy.reactive.server.test.simple.OptionalQueryParamResource.NOBODY; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.emptyString; @@ -8,6 +10,7 @@ import org.hamcrest.Matchers; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.OS; @@ -38,7 +41,7 @@ public JavaArchive get() { FeatureResponseFilter.class, DynamicFeatureRequestFilterWithLowPriority.class, TestFeature.class, TestDynamicFeature.class, SubResource.class, RootAResource.class, RootBResource.class, - QueryParamResource.class, HeaderParamResource.class, + QueryParamResource.class, HeaderParamResource.class, OptionalQueryParamResource.class, TestWriter.class, TestClass.class, SimpleBeanParam.class, OtherBeanParam.class, FieldInjectedResource.class, ParameterWithFromString.class, BeanParamSubClass.class, FieldInjectedSubClassResource.class, @@ -432,4 +435,40 @@ public void testInterfaceResource() { RestAssured.get("/iface") .then().statusCode(200).body(Matchers.equalTo("Hello")); } + + @Test + public void testQueryParamWithOptionalSingleValue() { + // verify with empty + Assertions.assertEquals(HELLO + NOBODY, RestAssured.get("/optional-query/greetings/one").asString()); + // verify with values + Assertions.assertEquals(HELLO + "albert", RestAssured.given().queryParam("name", "albert") + .get("/optional-query/greetings/one").asString()); + } + + @Test + public void testQueryParamWithOptionalList() { + // verify with empty + Assertions.assertEquals(HELLO + NOBODY, RestAssured.get("/optional-query/greetings/list").asString()); + // verify with values + Assertions.assertEquals(HELLO + "albert", RestAssured.given().queryParam("name", "albert") + .get("/optional-query/greetings/list").asString()); + } + + @Test + public void testQueryParamWithOptionalSet() { + // verify with empty + Assertions.assertEquals(HELLO + NOBODY, RestAssured.get("/optional-query/greetings/set").asString()); + // verify with values + Assertions.assertEquals(HELLO + "albert", RestAssured.given().queryParam("name", "albert") + .get("/optional-query/greetings/set").asString()); + } + + @Test + public void testQueryParamWithOptionalSortedSet() { + // verify with empty + Assertions.assertEquals(HELLO + NOBODY, RestAssured.get("/optional-query/greetings/sortedset").asString()); + // verify with values + Assertions.assertEquals(HELLO + "albert", RestAssured.given().queryParam("name", "albert") + .get("/optional-query/greetings/sortedset").asString()); + } } 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 71ed66b72b71b..9e6298c228940 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 @@ -1136,7 +1136,14 @@ && isContextType(paramType.asClassType())) { typeHandled = true; elementType = toClassName(pt.arguments().get(0), currentClassInfo, actualEndpointInfo, index); if (convertible) { - handleOptionalParam(existingConverters, errorLocation, hasRuntimeConverters, builder, elementType); + String genericElementType = null; + if (pt.arguments().get(0).kind() == Kind.PARAMETERIZED_TYPE) { + genericElementType = toClassName(pt.arguments().get(0).asParameterizedType().arguments().get(0), + currentClassInfo, actualEndpointInfo, index); + } + + handleOptionalParam(existingConverters, errorLocation, hasRuntimeConverters, builder, elementType, + genericElementType); } builder.setOptional(true); } else if (convertible) { @@ -1227,7 +1234,7 @@ protected void handleSortedSetParam(Map existingConverters, Stri } protected void handleOptionalParam(Map existingConverters, String errorLocation, - boolean hasRuntimeConverters, PARAM builder, String elementType) { + boolean hasRuntimeConverters, PARAM builder, String elementType, String genericElementType) { } protected void handleSetParam(Map existingConverters, String errorLocation, boolean hasRuntimeConverters, 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 6d44b35600c0c..b456ed247cbb5 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 @@ -10,12 +10,15 @@ import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.JSONP_JSON_STRING; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.JSONP_JSON_STRUCTURE; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.JSONP_JSON_VALUE; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.LIST; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.LOCAL_DATE; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.LOCAL_DATE_TIME; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.LOCAL_TIME; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MULTI_VALUED_MAP; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.OFFSET_DATE_TIME; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.OFFSET_TIME; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.SET; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.SORTED_SET; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.ZONED_DATE_TIME; import java.util.ArrayList; @@ -326,9 +329,26 @@ protected void handleSortedSetParam(Map existingConverters, Stri } protected void handleOptionalParam(Map existingConverters, String errorLocation, - boolean hasRuntimeConverters, ServerIndexedParameter builder, String elementType) { - ParameterConverterSupplier converter = extractConverter(elementType, index, - existingConverters, errorLocation, hasRuntimeConverters); + boolean hasRuntimeConverters, ServerIndexedParameter builder, String elementType, String genericElementType) { + ParameterConverterSupplier converter = null; + + if (genericElementType != null) { + ParameterConverterSupplier genericTypeConverter = extractConverter(genericElementType, index, existingConverters, + errorLocation, hasRuntimeConverters); + if (LIST.toString().equals(elementType)) { + converter = new ListConverter.ListSupplier(genericTypeConverter); + } else if (SET.toString().equals(elementType)) { + converter = new SetConverter.SetSupplier(genericTypeConverter); + } else if (SORTED_SET.toString().equals(elementType)) { + converter = new SortedSetConverter.SortedSetSupplier(genericTypeConverter); + } + } + + if (converter == null) { + // If no generic type provided or element type is not supported, then we try to use a custom runtime converter: + converter = extractConverter(elementType, index, existingConverters, errorLocation, hasRuntimeConverters); + } + builder.setConverter(new OptionalConverter.OptionalSupplier(converter)); }