Skip to content

Commit

Permalink
Support Providers in REST Client Reactive from context
Browse files Browse the repository at this point in the history
Fix #26003
  • Loading branch information
Sgitario committed May 11, 2023
1 parent 1090afd commit fa6e7b4
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 0 deletions.
73 changes: 73 additions & 0 deletions docs/src/main/asciidoc/rest-client-reactive.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Person> {

@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<Person> contextResolver = requestContext.getProviders().getContextResolver(Person.class, null);
if (contextResolver == null) {
throw new RuntimeException("Context resolver was not found");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -52,13 +53,15 @@ 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) {
this.restClientRequestContext = restClientRequestContext;
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
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <T> MessageBodyReader<T> getMessageBodyReader(Class<T> type, Type genericType, Annotation[] annotations,
MediaType mediaType) {
List<MessageBodyReader<?>> 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<T>) reader;
}
}
return null;
}

@Override
public <T> MessageBodyWriter<T> getMessageBodyWriter(Class<T> type, Type genericType, Annotation[] annotations,
MediaType mediaType) {
List<MessageBodyWriter<?>> 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<T>) writer;
}
}
return null;
}

@Override
public <T extends Throwable> ExceptionMapper<T> getExceptionMapper(Class<T> type) {
throw new UnsupportedOperationException(
"`jakarta.ws.rs.ext.ExceptionMapper` are not supported in REST Client Reactive");
}

@Override
public <T> ContextResolver<T> getContextResolver(Class<T> contextType, MediaType mediaType) {
// TODO: support getting context resolver by mediaType (which is provided using the `@Produces` annotation).
return context.getConfiguration().getContextResolver(contextType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,20 @@ public RxInvokerProvider<?> getRxInvokerProvider(Class<?> wantedClass) {
return null;
}

public <T> ContextResolver<T> getContextResolver(Class<T> wantedClass) {
MultivaluedMap<Integer, ContextResolver<?>> candidates = contextResolvers.get(wantedClass);
if (candidates == null) {
return null;
}
for (List<ContextResolver<?>> contextResolvers : candidates.values()) {
if (!contextResolvers.isEmpty()) {
return (ContextResolver<T>) contextResolvers.get(0);
}
}

return null;
}

public <T> T getFromContext(Class<T> wantedClass) {
MultivaluedMap<Integer, ContextResolver<?>> candidates = contextResolvers.get(wantedClass);
if (candidates == null) {
Expand Down

0 comments on commit fa6e7b4

Please sign in to comment.