From 9dd7f7befb0bc4c7ce4be45e6f6a632e1d5de0af Mon Sep 17 00:00:00 2001 From: Jose Date: Wed, 2 Mar 2022 12:29:25 +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 | 46 +++++++++++++++++ .../simple/OptionalQueryParamResource.java | 49 +++++++++++++++++++ .../simple/SimpleQuarkusRestTestCase.java | 42 +++++++++++++++- .../common/processor/EndpointIndexer.java | 11 ++++- .../processor/ServerEndpointIndexer.java | 29 +++++++++-- .../converters/OptionalConverter.java | 10 +++- 6 files changed, 180 insertions(+), 7 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..8d4423ed029a9 --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/QueryParamTest.java @@ -0,0 +1,46 @@ +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 org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +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..6b702cd2c2f4b 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,8 @@ package io.quarkus.resteasy.reactive.server.test.simple; +import static io.quarkus.resteasy.reactive.server.test.simple.OptionalQueryParamResource.AND; +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 +11,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 +42,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 +436,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" + AND + "jose", RestAssured.given().queryParam("name", "albert", "jose") + .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" + AND + "jose", RestAssured.given().queryParam("name", "albert", "jose") + .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" + AND + "jose", RestAssured.given().queryParam("name", "albert", "jose") + .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..991e53d5b6413 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,29 @@ 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); + builder.setSingle(false); + } else if (SET.toString().equals(elementType)) { + converter = new SetConverter.SetSupplier(genericTypeConverter); + builder.setSingle(false); + } else if (SORTED_SET.toString().equals(elementType)) { + converter = new SortedSetConverter.SortedSetSupplier(genericTypeConverter); + builder.setSingle(false); + } + } + + 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)); } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/OptionalConverter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/OptionalConverter.java index 1c5dffe4dcbd5..2cd7ad96cb420 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/OptionalConverter.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/OptionalConverter.java @@ -2,6 +2,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Type; +import java.util.Collection; import java.util.Optional; import org.jboss.resteasy.reactive.server.model.ParamConverterProviders; @@ -18,7 +19,14 @@ public Object convert(Object parameter) { if (parameter == null) { return Optional.empty(); } else if (delegate != null) { - return Optional.ofNullable(delegate.convert(parameter)); + Object converted = delegate.convert(parameter); + if (converted != null + && converted instanceof Collection + && ((Collection) converted).isEmpty()) { + return Optional.empty(); + } else { + return Optional.ofNullable(converted); + } } else { return Optional.of(parameter); }