From d119be2f7d946fefda3eed68167d100c42124c24 Mon Sep 17 00:00:00 2001
From: Georgios Andrianakis <geoand@gmail.com>
Date: Tue, 7 Sep 2021 22:31:26 +0300
Subject: [PATCH] Support RestResponse<T> as a return type for reactive rest
 client methods

Closes: #19966
---
 .../ClientResponseCompleteRestHandler.java    |  6 ++++-
 .../client/impl/AsyncInvokerImpl.java         | 23 ++++++++++++++-----
 .../client/impl/RestClientRequestContext.java | 22 +++++++++++++++---
 .../it/rest/client/main/AppleClient.java      |  9 ++++++++
 .../client/main/ClientCallingResource.java    | 20 +++++++++++++---
 .../rest/client/main/RestResponseClient.java  | 16 +++++++++++++
 .../io/quarkus/it/rest/client/BasicTest.java  | 10 ++++++--
 7 files changed, 91 insertions(+), 15 deletions(-)
 create mode 100644 integration-tests/resteasy-reactive-rest-client/src/main/java/io/quarkus/it/rest/client/main/RestResponseClient.java

diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientResponseCompleteRestHandler.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientResponseCompleteRestHandler.java
index e10c4dcf01e80..7879d8e2e088e 100644
--- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientResponseCompleteRestHandler.java
+++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientResponseCompleteRestHandler.java
@@ -2,6 +2,7 @@
 
 import java.io.IOException;
 import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
 import org.jboss.resteasy.reactive.client.impl.ClientResponseBuilderImpl;
 import org.jboss.resteasy.reactive.client.impl.ClientResponseContextImpl;
 import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext;
@@ -22,7 +23,10 @@ public static ResponseImpl mapToResponse(RestClientRequestContext context, boole
         builder.status(responseContext.getStatus(), responseContext.getReasonPhrase());
         builder.setAllHeaders(responseContext.getHeaders());
         builder.invocationState(context);
-        if (context.isResponseTypeSpecified() && parseContent) { // this case means that a specific response type was requested
+        if (context.isResponseTypeSpecified()
+                // when we are returning a RestResponse, we don't want to do any parsing
+                && (Response.Status.Family.familyOf(context.getResponseStatus()) == Response.Status.Family.SUCCESSFUL)
+                && parseContent) { // this case means that a specific response type was requested
             Object entity = context.readEntity(responseContext.getEntityStream(),
                     context.getResponseType(),
                     responseContext.getMediaType(),
diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/AsyncInvokerImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/AsyncInvokerImpl.java
index 2b84187250995..b5ef7e9d2b3ec 100644
--- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/AsyncInvokerImpl.java
+++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/AsyncInvokerImpl.java
@@ -16,6 +16,7 @@
 import javax.ws.rs.client.InvocationCallback;
 import javax.ws.rs.core.GenericType;
 import javax.ws.rs.core.Response;
+import org.jboss.resteasy.reactive.RestResponse;
 import org.jboss.resteasy.reactive.common.jaxrs.ConfigurationImpl;
 import org.jboss.resteasy.reactive.common.util.types.Types;
 import org.jboss.resteasy.reactive.spi.ThreadSetupAction;
@@ -280,12 +281,22 @@ private <T> Type getInvocationCallbackType(InvocationCallback<T> callback) {
     public <T> CompletableFuture<T> mapResponse(CompletableFuture<Response> res, Class<?> responseType) {
         if (responseType.equals(Response.class)) {
             return (CompletableFuture<T>) res;
+        } else if (responseType.equals(RestResponse.class)) {
+            return res.thenApply(new Function<>() {
+                @Override
+                public T apply(Response response) {
+                    return (T) RestResponse.ResponseBuilder.create(response.getStatusInfo(), response.getEntity())
+                            .replaceAll(response.getHeaders()).build();
+                }
+            });
+        } else {
+            return res.thenApply(new Function<>() {
+                @Override
+                public T apply(Response response) {
+                    return (T) response.getEntity();
+                }
+            });
         }
-        return res.thenApply(new Function<Response, T>() {
-            @Override
-            public T apply(Response response) {
-                return (T) response.getEntity();
-            }
-        });
+
     }
 }
diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java
index 467166cdf2dc9..6ceb958ca79e4 100644
--- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java
+++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java
@@ -9,6 +9,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.annotation.Annotation;
+import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
 import java.net.URI;
 import java.util.Arrays;
@@ -29,6 +30,7 @@
 import javax.ws.rs.ext.ReaderInterceptor;
 import javax.ws.rs.ext.WriterInterceptor;
 import org.jboss.resteasy.reactive.ClientWebApplicationException;
+import org.jboss.resteasy.reactive.RestResponse;
 import org.jboss.resteasy.reactive.client.spi.ClientRestHandler;
 import org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext;
 import org.jboss.resteasy.reactive.common.core.Serialisers;
@@ -102,9 +104,23 @@ public RestClientRequestContext(ClientImpl restClient,
             this.responseTypeSpecified = false;
         } else {
             this.responseType = responseType;
-            boolean isJaxResponse = responseType.getRawType().equals(Response.class);
-            this.checkSuccessfulFamily = !isJaxResponse;
-            this.responseTypeSpecified = !isJaxResponse;
+            if (responseType.getRawType().equals(Response.class)) {
+                this.checkSuccessfulFamily = false;
+                this.responseTypeSpecified = false;
+            } else if (responseType.getRawType().equals(RestResponse.class)) {
+                if (responseType.getType() instanceof ParameterizedType) {
+                    ParameterizedType type = (ParameterizedType) responseType.getType();
+                    if (type.getActualTypeArguments().length == 1) {
+                        Type restResponseType = type.getActualTypeArguments()[0];
+                        this.responseType = new GenericType<>(restResponseType);
+                    }
+                }
+                this.checkSuccessfulFamily = false;
+                this.responseTypeSpecified = true;
+            } else {
+                this.checkSuccessfulFamily = true;
+                this.responseTypeSpecified = true;
+            }
         }
         this.registerBodyHandler = registerBodyHandler;
         this.result = new CompletableFuture<>();
diff --git a/integration-tests/resteasy-reactive-rest-client/src/main/java/io/quarkus/it/rest/client/main/AppleClient.java b/integration-tests/resteasy-reactive-rest-client/src/main/java/io/quarkus/it/rest/client/main/AppleClient.java
index d03ae8cdc61f2..608df268226a2 100644
--- a/integration-tests/resteasy-reactive-rest-client/src/main/java/io/quarkus/it/rest/client/main/AppleClient.java
+++ b/integration-tests/resteasy-reactive-rest-client/src/main/java/io/quarkus/it/rest/client/main/AppleClient.java
@@ -9,6 +9,7 @@
 import javax.ws.rs.core.MediaType;
 
 import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
+import org.jboss.resteasy.reactive.RestResponse;
 
 import io.smallrye.mutiny.Uni;
 
@@ -54,4 +55,12 @@ public interface AppleClient {
     @POST
     @Produces(MediaType.APPLICATION_JSON)
     Uni<String> uniStringApple();
+
+    @POST
+    @Produces(MediaType.APPLICATION_JSON)
+    RestResponse<Apple> restResponseApple();
+
+    @POST
+    @Produces(MediaType.APPLICATION_JSON)
+    Uni<RestResponse<Apple>> uniRestResponseApple();
 }
diff --git a/integration-tests/resteasy-reactive-rest-client/src/main/java/io/quarkus/it/rest/client/main/ClientCallingResource.java b/integration-tests/resteasy-reactive-rest-client/src/main/java/io/quarkus/it/rest/client/main/ClientCallingResource.java
index 7dee0e453e1ee..2c67f67c4507c 100644
--- a/integration-tests/resteasy-reactive-rest-client/src/main/java/io/quarkus/it/rest/client/main/ClientCallingResource.java
+++ b/integration-tests/resteasy-reactive-rest-client/src/main/java/io/quarkus/it/rest/client/main/ClientCallingResource.java
@@ -3,6 +3,7 @@
 import java.net.URI;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 import javax.enterprise.context.ApplicationScoped;
@@ -12,6 +13,7 @@
 
 import org.eclipse.microprofile.rest.client.RestClientBuilder;
 import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.jboss.resteasy.reactive.RestResponse;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -74,12 +76,15 @@ void init(@Observes Router router) {
             Uni<Apple> apple8 = Uni.createFrom().completionStage(client.completionStringApple()).onItem()
                     .transform(this::toApple);
             Uni<Apple> apple9 = client.uniStringApple().onItem().transform(this::toApple);
-            Uni.combine().all().unis(apple1, apple2, apple3, apple4, apple5, apple6, apple7, apple8, apple9).asTuple()
+            Uni<Apple> apple10 = Uni.createFrom().item(client.restResponseApple().getEntity());
+            Uni<Apple> apple11 = client.uniRestResponseApple().onItem().transform(RestResponse::getEntity);
+            Uni.combine().all().unis(apple1, apple2, apple3, apple4, apple5, apple6, apple7, apple8, apple9, apple10, apple11)
+                    .combinedWith(Function.identity())
                     .subscribe()
-                    .with(tuple -> {
+                    .with(list -> {
                         try {
                             rc.response().putHeader("content-type", "application/json")
-                                    .end(mapper.writeValueAsString(tuple.asList()));
+                                    .end(mapper.writeValueAsString(list));
                         } catch (JsonProcessingException e) {
                             fail(rc, e.getMessage());
                         }
@@ -107,6 +112,15 @@ void init(@Observes Router router) {
             rc.response().end(greeting);
         });
 
+        router.route("/rest-response").blockingHandler(rc -> {
+            String url = rc.getBody().toString();
+            RestResponseClient client = RestClientBuilder.newBuilder().baseUri(URI.create(url))
+                    .property("microprofile.rest.client.disable.default.mapper", true)
+                    .build(RestResponseClient.class);
+            RestResponse<String> restResponse = client.response();
+            rc.response().end("" + restResponse.getStatus());
+        });
+
         router.route("/export-clear").blockingHandler(rc -> {
             inMemorySpanExporter.reset();
             rc.response().end();
diff --git a/integration-tests/resteasy-reactive-rest-client/src/main/java/io/quarkus/it/rest/client/main/RestResponseClient.java b/integration-tests/resteasy-reactive-rest-client/src/main/java/io/quarkus/it/rest/client/main/RestResponseClient.java
new file mode 100644
index 0000000000000..49ed4092650c4
--- /dev/null
+++ b/integration-tests/resteasy-reactive-rest-client/src/main/java/io/quarkus/it/rest/client/main/RestResponseClient.java
@@ -0,0 +1,16 @@
+package io.quarkus.it.rest.client.main;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import org.jboss.resteasy.reactive.RestResponse;
+
+@Path("")
+public interface RestResponseClient {
+
+    @Produces(MediaType.TEXT_PLAIN)
+    @GET
+    RestResponse<String> response();
+}
diff --git a/integration-tests/resteasy-reactive-rest-client/src/test/java/io/quarkus/it/rest/client/BasicTest.java b/integration-tests/resteasy-reactive-rest-client/src/test/java/io/quarkus/it/rest/client/BasicTest.java
index 78724c6e10cc5..82f666e6bad9e 100644
--- a/integration-tests/resteasy-reactive-rest-client/src/test/java/io/quarkus/it/rest/client/BasicTest.java
+++ b/integration-tests/resteasy-reactive-rest-client/src/test/java/io/quarkus/it/rest/client/BasicTest.java
@@ -41,6 +41,12 @@ public void shouldMakeTextRequest() {
         assertThat(response.asString()).isEqualTo("Hello, JohnJohn");
     }
 
+    @Test
+    public void restResponseShouldWorkWithNonSuccessfulResponse() {
+        Response response = RestAssured.with().body(helloUrl).post("/rest-response");
+        assertThat(response.asString()).isEqualTo("405");
+    }
+
     @SuppressWarnings({ "rawtypes", "unchecked" })
     @Test
     void shouldMakeJsonRequest() {
@@ -49,11 +55,11 @@ void shouldMakeJsonRequest() {
                 .statusCode(200)
                 .contentType("application/json")
                 .extract().body().jsonPath().getList(".", Map.class);
-        assertThat(results).hasSize(9).allSatisfy(m -> {
+        assertThat(results).hasSize(11).allSatisfy(m -> {
             assertThat(m).containsOnlyKeys("cultivar");
         });
         Map<Object, Long> valueByCount = results.stream().collect(Collectors.groupingBy(m -> m.get("cultivar"), counting()));
-        assertThat(valueByCount).containsOnly(entry("cortland", 3L), entry("lobo", 3L), entry("golden delicious", 3L));
+        assertThat(valueByCount).containsOnly(entry("cortland", 4L), entry("lobo", 4L), entry("golden delicious", 3L));
     }
 
     @Test