From fa6e7b493fbadf36814d55aea0931505f368f8df Mon Sep 17 00:00:00 2001 From: Jose Date: Fri, 28 Apr 2023 09:14:12 +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 ++ .../common/jaxrs/ConfigurationImpl.java | 14 +++ 6 files changed, 263 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 83a97652fc6864..9dbf627dd273e0 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 00000000000000..eac99ad1181e88 --- /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 fcd63ed74d9572..eb653ee5875445 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 00000000000000..e8169dbaaae72d --- /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 b1cbc2e7809e5c..43f8327a9ed64a 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/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 588237450831fb..72a269f62e4194 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) {