From 764f8d627020a87147957613c8a394d81a01ba41 Mon Sep 17 00:00:00 2001 From: Jose Date: Fri, 17 Mar 2023 10:11:31 +0100 Subject: [PATCH] Support of generic collections in RESTEasy Reactive server and client There were some issues when supporting List, Set and SortedSet: - In the server, List, Set and SortedSet types without the element type was not working - In the client, it was unsopported as it was always trying to cast the header to string (and hence failing). Fix https://github.com/quarkusio/quarkus/issues/31866 (cherry picked from commit 41aba276f020d510c9a2a0435ad9ad332ebf0a97) --- .../JaxrsClientReactiveProcessor.java | 6 +-- .../simple/SimpleQuarkusRestResource.java | 44 +++++++++++++++ .../simple/SimpleQuarkusRestTestCase.java | 30 +++++++++++ .../client/reactive/headers/HeaderTest.java | 54 +++++++++++++++++++ .../MicroProfileRestClientRequestFilter.java | 5 +- .../common/processor/EndpointIndexer.java | 17 +++++- 6 files changed, 151 insertions(+), 5 deletions(-) diff --git a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java index db778f22b64cb..f58d6841b0dca 100644 --- a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java @@ -941,7 +941,7 @@ A more full example of generated client (with sub-resource) can is at the bottom MethodDescriptor handleHeaderDescriptor = MethodDescriptor.ofMethod(name, method.getName() + "$$" + methodIndex + "$$handleHeader$$" + paramIdx, Invocation.Builder.class, - Invocation.Builder.class, param.type); + Invocation.Builder.class, param.declaredType); MethodCreator handleHeaderMethod = classContext.classCreator.getMethodCreator( handleHeaderDescriptor).setModifiers(Modifier.PRIVATE); @@ -1358,7 +1358,7 @@ private void handleSubResourceMethod(List subMethod.getName() + "$$" + subMethodIndex + "$$handleHeader$$param" + inheritedParamIndex + "$" + subParamField.paramIndex, Invocation.Builder.class, - Invocation.Builder.class, param.type); + Invocation.Builder.class, param.declaredType); MethodCreator handleHeaderMethod = subContext.classCreator.getMethodCreator( handleHeaderDescriptor).setModifiers(Modifier.PRIVATE); @@ -1464,7 +1464,7 @@ private void handleSubResourceMethod(List MethodDescriptor handleHeaderDescriptor = MethodDescriptor.ofMethod(subName, subMethod.getName() + "$$" + subMethodIndex + "$$handleHeader$$" + paramIdx, Invocation.Builder.class, - Invocation.Builder.class, param.type); + Invocation.Builder.class, param.declaredType); MethodCreator handleHeaderMethod = subContext.classCreator.getMethodCreator( handleHeaderDescriptor).setModifiers(Modifier.PRIVATE); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/SimpleQuarkusRestResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/SimpleQuarkusRestResource.java index 895a728bbd143..d186fce0e127a 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/SimpleQuarkusRestResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/SimpleQuarkusRestResource.java @@ -2,9 +2,13 @@ import java.math.BigDecimal; import java.util.Arrays; +import java.util.Collection; import java.util.List; +import java.util.Set; +import java.util.SortedSet; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.json.JsonArray; @@ -143,6 +147,42 @@ public Response filters(@Context HttpHeaders headers, @RestHeader("filter-reques return Response.ok().header("filter-request", header).build(); } + @GET + @Path("header-param-list") + public Object headerParamWithList(@HeaderParam("header") List list) { + return collectionToString(list); + } + + @GET + @Path("rest-header-list") + public Object restHeaderWithList(@RestHeader List header) { + return collectionToString(header); + } + + @GET + @Path("header-param-set") + public Object headerParamWithSet(@HeaderParam("header") Set list) { + return collectionToString(list); + } + + @GET + @Path("rest-header-set") + public Object restHeaderWithSet(@RestHeader Set header) { + return collectionToString(header); + } + + @GET + @Path("header-param-sorted-set") + public Object headerParamWithSortedSet(@HeaderParam("header") SortedSet list) { + return collectionToString(list); + } + + @GET + @Path("rest-header-sorted-set") + public String restHeaderWithSortedSet(@RestHeader SortedSet header) { + return collectionToString(header); + } + @GET @Path("feature-filters") public Response featureFilters(@Context HttpHeaders headers) { @@ -380,4 +420,8 @@ public String simplifiedResourceInfo(@Context SimpleResourceInfo simplifiedResou public String bigDecimalConverter(BigDecimal val) { return val.toString(); } + + private String collectionToString(Collection list) { + return (String) list.stream().map(Object::toString).collect(Collectors.joining(", ")); + } } 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 ecadb812402d4..0281067a255c7 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 @@ -17,6 +17,8 @@ import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import io.quarkus.test.QuarkusUnitTest; import io.restassured.RestAssured; @@ -313,6 +315,34 @@ public void testHeaderParamInCtor() { .then().body(Matchers.is(emptyString())); } + @ParameterizedTest + @ValueSource(strings = { + "rest-header-list", + "rest-header-set", + "rest-header-sorted-set" + }) + public void testRestHeaderUsingCollection(String path) { + RestAssured.with().header("header", "a", "b") + .get("/simple/" + path) + .then() + .statusCode(HttpStatus.SC_OK) + .body(Matchers.equalTo("a, b")); + } + + @ParameterizedTest + @ValueSource(strings = { + "header-param-list", + "header-param-set", + "header-param-sorted-set" + }) + public void testHeaderParamUsingCollection(String path) { + RestAssured.with().header("header", "a", "b") + .get("/simple/" + path) + .then() + .statusCode(HttpStatus.SC_OK) + .body(Matchers.equalTo("a, b")); + } + @Test public void testFormMap() { RestAssured diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/headers/HeaderTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/headers/HeaderTest.java index 22d902c8e8406..24c585cf67b8c 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/headers/HeaderTest.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/headers/HeaderTest.java @@ -3,6 +3,13 @@ import static org.assertj.core.api.Assertions.assertThat; import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.stream.Collectors; import javax.enterprise.context.ApplicationScoped; import javax.ws.rs.GET; @@ -40,6 +47,15 @@ void testNullHeaders() { assertThat(client.cookieSub("bar", null).send(null, "bar4", "dummy")).isEqualTo("bar:null:null:bar4:X-My-Header/dummy"); } + @Test + void testHeadersWithCollections() { + String expected = "a, b, c, d"; + Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); + assertThat(client.headersList(List.of("a", "b"), List.of("c", "d"))).isEqualTo(expected); + assertThat(client.headersSet(Set.of("a", "b"), Set.of("c", "d"))).isEqualTo(expected); + assertThat(client.headersSet(new TreeSet(List.of("a", "b")), new TreeSet(List.of("c", "d")))).isEqualTo(expected); + } + @Path("/") @ApplicationScoped public static class Resource { @@ -51,12 +67,50 @@ public String returnHeaders(@HeaderParam("foo") String header, @HeaderParam("foo return header + ":" + header2 + ":" + header3 + ":" + header4 + ":" + myHeaderName + "/" + headers.getHeaderString("X-My-Header"); } + + @GET + @Path("/headers-list") + public String headersList(@HeaderParam("foo") List foo, @RestHeader List header) { + return joiningCollections(foo, header); + } + + @GET + @Path("/headers-set") + public String headersSet(@HeaderParam("foo") Set foo, @RestHeader Set header) { + return joiningCollections(foo, header); + } + + @GET + @Path("/headers-sorted-set") + public String headersSortedSet(@HeaderParam("foo") SortedSet foo, @RestHeader SortedSet header) { + return joiningCollections(foo, header); + } + + private String joiningCollections(Collection... collections) { + List allHeaders = new ArrayList<>(); + for (Collection collection : collections) { + collection.forEach(v -> allHeaders.add((String) v)); + } + return allHeaders.stream().collect(Collectors.joining(", ")); + } } public interface Client { @Path("/") SubClient cookieSub(@HeaderParam("foo") String cookie, @HeaderParam("foo2") String cookie2); + + @GET + @Path("/headers-list") + String headersList(@HeaderParam("foo") List foo, @RestHeader List header); + + @GET + @Path("/headers-set") + String headersSet(@HeaderParam("foo") Set foo, @RestHeader Set header); + + @GET + @Path("/headers-sorted-set") + String headersSortedSet(@HeaderParam("foo") SortedSet foo, @RestHeader SortedSet header); } public interface SubClient { diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/MicroProfileRestClientRequestFilter.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/MicroProfileRestClientRequestFilter.java index 89982a4c8caaa..7506bccc7cfc7 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/MicroProfileRestClientRequestFilter.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/MicroProfileRestClientRequestFilter.java @@ -1,6 +1,7 @@ package io.quarkus.rest.client.reactive.runtime; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -81,11 +82,13 @@ public void filter(ResteasyReactiveClientRequestContext requestContext) { } } - private static List castToListOfStrings(List values) { + private static List castToListOfStrings(Collection values) { List result = new ArrayList<>(); for (Object value : values) { if (value instanceof String) { result.add((String) value); + } else if (value instanceof Collection) { + result.addAll(castToListOfStrings((Collection) value)); } else { result.add(String.valueOf(value)); } 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 b279df0c8784b..cb56e0d49c651 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 @@ -1362,13 +1362,28 @@ && isParameterContainerType(paramType.asClassType())) { elementType = paramType.name().toString(); handleTemporalParam(builder, paramType.name(), anns, currentMethodInfo); typeHandled = true; - } else if (paramType.name().equals(LIST) && (type == ParameterType.QUERY)) { // RESTEasy Classic handles the non-generic List type + } else if (paramType.name().equals(LIST) && (type == ParameterType.QUERY + || type == ParameterType.HEADER)) { // RESTEasy Classic handles the non-generic List type elementType = String.class.getName(); typeHandled = true; builder.setSingle(false); if (convertible) { handleListParam(existingConverters, errorLocation, hasRuntimeConverters, builder, elementType); } + } else if (paramType.name().equals(SET) && type == ParameterType.HEADER) { // RESTEasy Classic handles the non-generic Set type + elementType = String.class.getName(); + typeHandled = true; + builder.setSingle(false); + if (convertible) { + handleSetParam(existingConverters, errorLocation, hasRuntimeConverters, builder, elementType); + } + } else if (paramType.name().equals(SORTED_SET) && type == ParameterType.HEADER) { // RESTEasy Classic handles the non-generic SortedSet type + elementType = String.class.getName(); + typeHandled = true; + builder.setSingle(false); + if (convertible) { + handleSortedSetParam(existingConverters, errorLocation, hasRuntimeConverters, builder, elementType); + } } else if (paramType.kind() == Kind.ARRAY) { ArrayType at = paramType.asArrayType(); typeHandled = true;