From aaa8b8c0b3195e7e0884f0aa18e343b63aca2a63 Mon Sep 17 00:00:00 2001 From: Jose Date: Thu, 11 May 2023 09:05:09 +0200 Subject: [PATCH] Support Providers in REST Client Reactive from context Fix https://github.com/quarkusio/quarkus/issues/26003 --- .../main/asciidoc/rest-client-reactive.adoc | 73 +++++++++++++ .../reactive/ProvidersFromContextTest.java | 102 ++++++++++++++++++ .../client/impl/ClientRequestContextImpl.java | 8 ++ .../reactive/client/impl/ProvidersImpl.java | 60 +++++++++++ .../ResteasyReactiveClientRequestContext.java | 6 ++ .../ResteasyReactiveClientRequestFilter.java | 14 +++ .../ResteasyReactiveClientResponseFilter.java | 17 +++ .../common/jaxrs/ConfigurationImpl.java | 14 +++ 8 files changed, 294 insertions(+) create mode 100644 extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/ProvidersFromContextTest.java create mode 100644 independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ProvidersImpl.java diff --git a/docs/src/main/asciidoc/rest-client-reactive.adoc b/docs/src/main/asciidoc/rest-client-reactive.adoc index 83a97652fc686..9dbf627dd273e 100644 --- a/docs/src/main/asciidoc/rest-client-reactive.adoc +++ b/docs/src/main/asciidoc/rest-client-reactive.adoc @@ -953,6 +953,79 @@ public interface ExtensionsService { org.eclipse.microprofile.rest.client.propagateHeaders=Authorization,Proxy-Authorization ---- +== Customizing the request + +The REST Client Reactive supports further customization of the final request to be sent to the server via filters. The filters must implement either the interface `ClientRequestFilter` or `ResteasyReactiveClientRequestFilter`. + +A simple example of customizing the request would be to add a custom header: + +[source, java] +---- +@Provider +public class TestClientRequestFilter implements ClientRequestFilter { + + @Override + public void filter(ClientRequestContext requestContext) { + requestContext.getHeaders().add("my_header", "value"); + } +} +---- + +Next, you can register your filter using the `@RegisterProvider` annotation: + +[source, java] +---- +@Path("/extensions") +@RegisterProvider(TestClientRequestFilter.class) +public interface ExtensionsService { + + // ... +} +---- + +Or programmatically using the `.register()` method: + +[source, java] +---- +QuarkusRestClientBuilder.newBuilder() + .register(TestClientRequestFilter.class) + .build(ExtensionsService.class) +---- + +=== Injecting the `jakarta.ws.rs.ext.Providers` instance in filters + +The `jakarta.ws.rs.ext.Providers` is useful when we need to lookup the provider instances of the current client. + +We can get the `Providers` instance in our filters from the request context as follows: + +[source, java] +---- +@Provider +public class TestClientRequestFilter implements ClientRequestFilter { + + @Override + public void filter(ClientRequestContext requestContext) { + Providers providers = ((ResteasyReactiveClientRequestContext) requestContext).getProviders(); + // ... + } +} +---- + +Alternatively, you can implement the `ResteasyReactiveClientRequestFilter` interface instead of the `ClientRequestFilter` interface that will directly provide the `ResteasyReactiveClientRequestContext` context: + +[source, java] +---- +@Provider +public class TestClientRequestFilter implements ResteasyReactiveClientRequestFilter { + + @Override + public void filter(ResteasyReactiveClientRequestFilter requestContext) { + Providers providers = requestContext.getProviders(); + // ... + } +} +---- + == Exception handling The MicroProfile REST Client specification introduces the `org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper` whose purpose is to convert an HTTP response to an exception. diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/ProvidersFromContextTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/ProvidersFromContextTest.java new file mode 100644 index 0000000000000..eac99ad1181e8 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/ProvidersFromContextTest.java @@ -0,0 +1,102 @@ +package io.quarkus.rest.client.reactive; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URI; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ContextResolver; +import jakarta.ws.rs.ext.Provider; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestContext; +import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestFilter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; + +public class ProvidersFromContextTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest(); + + @TestHTTPResource + URI baseUri; + + private Client client; + + @BeforeEach + public void before() { + client = QuarkusRestClientBuilder.newBuilder() + .baseUri(baseUri) + .register(TestClientRequestFilter.class) + .register(MyContextResolver.class) + .build(Client.class); + } + + @Test + public void test() { + Response response = client.get(); + assertEquals(200, response.getStatus()); + } + + @RegisterRestClient + public interface Client { + + @GET + @Path("test") + Response get(); + } + + @Path("test") + public static class Endpoint { + + @GET + public Response get() { + return Response.ok().build(); + } + } + + public static class Person { + public String name; + } + + public static class MyContextResolver implements ContextResolver { + + @Override + public Person getContext(Class aClass) { + return new Person(); + } + } + + @Provider + public static class TestClientRequestFilter implements ResteasyReactiveClientRequestFilter { + + @Override + public void filter(ResteasyReactiveClientRequestContext requestContext) { + if (requestContext.getProviders() == null) { + throw new RuntimeException("Providers was not injected"); + } + + var readers = requestContext.getProviders().getMessageBodyReader(String.class, null, null, null); + if (readers == null) { + throw new RuntimeException("No readers were found"); + } + + var writers = requestContext.getProviders().getMessageBodyWriter(String.class, null, null, null); + if (writers == null) { + throw new RuntimeException("No writers were found"); + } + + ContextResolver contextResolver = requestContext.getProviders().getContextResolver(Person.class, null); + if (contextResolver == null) { + throw new RuntimeException("Context resolver was not found"); + } + } + } +} diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientRequestContextImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientRequestContextImpl.java index fcd63ed74d957..eb653ee587544 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientRequestContextImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientRequestContextImpl.java @@ -32,6 +32,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.Providers; import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestContext; import org.jboss.resteasy.reactive.common.NotImplementedYet; @@ -52,6 +53,7 @@ public class ClientRequestContextImpl implements ResteasyReactiveClientRequestCo private final RestClientRequestContext restClientRequestContext; private final ClientRequestHeadersMap headersMap; private final Context context; + private final Providers providers; public ClientRequestContextImpl(RestClientRequestContext restClientRequestContext, ClientImpl client, ConfigurationImpl configuration) { @@ -59,6 +61,7 @@ public ClientRequestContextImpl(RestClientRequestContext restClientRequestContex this.client = client; this.configuration = configuration; this.headersMap = new ClientRequestHeadersMap(); //restClientRequestContext.requestHeaders.getHeaders() + this.providers = new ProvidersImpl(restClientRequestContext); // TODO This needs to be challenged: // Always create a duplicated context because each REST Client invocation must have its own context @@ -73,6 +76,11 @@ public ClientRequestContextImpl(RestClientRequestContext restClientRequestContex restClientRequestContext.properties.put(VERTX_CONTEXT_PROPERTY, context); } + @Override + public Providers getProviders() { + return providers; + } + @Override public Context getContext() { return context; diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ProvidersImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ProvidersImpl.java new file mode 100644 index 0000000000000..e8169dbaaae72 --- /dev/null +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ProvidersImpl.java @@ -0,0 +1,60 @@ +package org.jboss.resteasy.reactive.client.impl; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.List; + +import jakarta.ws.rs.RuntimeType; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.ext.ContextResolver; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.MessageBodyReader; +import jakarta.ws.rs.ext.MessageBodyWriter; +import jakarta.ws.rs.ext.Providers; + +public class ProvidersImpl implements Providers { + + private final RestClientRequestContext context; + + public ProvidersImpl(RestClientRequestContext context) { + this.context = context; + } + + @Override + public MessageBodyReader getMessageBodyReader(Class type, Type genericType, Annotation[] annotations, + MediaType mediaType) { + List> readers = context.getRestClient().getClientContext().getSerialisers() + .findReaders(context.getConfiguration(), type, mediaType, RuntimeType.CLIENT); + for (MessageBodyReader reader : readers) { + if (reader.isReadable(type, genericType, annotations, mediaType)) { + return (MessageBodyReader) reader; + } + } + return null; + } + + @Override + public MessageBodyWriter getMessageBodyWriter(Class type, Type genericType, Annotation[] annotations, + MediaType mediaType) { + List> writers = context.getRestClient().getClientContext().getSerialisers() + .findWriters(context.getConfiguration(), type, mediaType, RuntimeType.CLIENT); + for (MessageBodyWriter writer : writers) { + if (writer.isWriteable(type, genericType, annotations, mediaType)) { + return (MessageBodyWriter) writer; + } + } + return null; + } + + @Override + public ExceptionMapper getExceptionMapper(Class type) { + throw new UnsupportedOperationException( + "`jakarta.ws.rs.ext.ExceptionMapper` are not supported in REST Client Reactive"); + } + + @Override + public ContextResolver getContextResolver(Class contextType, MediaType mediaType) { + // TODO: support getting context resolver by mediaType (which is provided using the `@Produces` annotation). + return context.getConfiguration().getContextResolver(contextType); + } +} diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ResteasyReactiveClientRequestContext.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ResteasyReactiveClientRequestContext.java index b1cbc2e7809e5..43f8327a9ed64 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ResteasyReactiveClientRequestContext.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ResteasyReactiveClientRequestContext.java @@ -2,6 +2,7 @@ import jakarta.ws.rs.client.ClientRequestContext; import jakarta.ws.rs.core.GenericType; +import jakarta.ws.rs.ext.Providers; import io.smallrye.stork.api.ServiceInstance; import io.vertx.core.Context; @@ -23,6 +24,11 @@ public interface ResteasyReactiveClientRequestContext extends ClientRequestConte void resume(Throwable t); + /** + * @return the context where to lookup all the provider instances of the current client. + */ + Providers getProviders(); + /** * @return the captured or created duplicated context. See {@link #VERTX_CONTEXT_PROPERTY} for details. */ diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ResteasyReactiveClientRequestFilter.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ResteasyReactiveClientRequestFilter.java index d4ca467842f8b..dc974a8f182e0 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ResteasyReactiveClientRequestFilter.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ResteasyReactiveClientRequestFilter.java @@ -5,12 +5,26 @@ import jakarta.ws.rs.client.ClientRequestContext; import jakarta.ws.rs.client.ClientRequestFilter; +/** + * An extension interface implemented by client request filters used by REST Client Reactive. + */ public interface ResteasyReactiveClientRequestFilter extends ClientRequestFilter { + /** + * Filter method called before a request has been dispatched to a client transport layer. + * + * @param requestContext the request context. + * @throws IOException if an I/O exception occurs. + */ @Override default void filter(ClientRequestContext requestContext) throws IOException { filter((ResteasyReactiveClientRequestContext) requestContext); } + /** + * Filter method called before a request has been dispatched to a client transport layer. + * + * @param requestContext the REST Client reactive request context. + */ void filter(ResteasyReactiveClientRequestContext requestContext); } diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ResteasyReactiveClientResponseFilter.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ResteasyReactiveClientResponseFilter.java index 112620fd9b588..139a3461079c9 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ResteasyReactiveClientResponseFilter.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ResteasyReactiveClientResponseFilter.java @@ -4,11 +4,28 @@ import jakarta.ws.rs.client.ClientResponseContext; import jakarta.ws.rs.client.ClientResponseFilter; +/** + * An extension interface implemented by client response filters used by REST Client Reactive. + */ public interface ResteasyReactiveClientResponseFilter extends ClientResponseFilter { + /** + * Filter method called after a response has been provided for a request (either by a request filter or when the HTTP + * invocation returns). + * + * @param requestContext the request context. + * @param responseContext the response context. + */ default void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) { filter((ResteasyReactiveClientRequestContext) requestContext, responseContext); } + /** + * Filter method called after a response has been provided for a request (either by a request filter or when the HTTP + * invocation returns). + * + * @param requestContext the REST Client reactive request context. + * @param responseContext the response context. + */ void filter(ResteasyReactiveClientRequestContext requestContext, ClientResponseContext responseContext); } diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/jaxrs/ConfigurationImpl.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/jaxrs/ConfigurationImpl.java index 588237450831f..72a269f62e419 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/jaxrs/ConfigurationImpl.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/jaxrs/ConfigurationImpl.java @@ -528,6 +528,20 @@ public RxInvokerProvider getRxInvokerProvider(Class wantedClass) { return null; } + public ContextResolver getContextResolver(Class wantedClass) { + MultivaluedMap> candidates = contextResolvers.get(wantedClass); + if (candidates == null) { + return null; + } + for (List> contextResolvers : candidates.values()) { + if (!contextResolvers.isEmpty()) { + return (ContextResolver) contextResolvers.get(0); + } + } + + return null; + } + public T getFromContext(Class wantedClass) { MultivaluedMap> candidates = contextResolvers.get(wantedClass); if (candidates == null) {