Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Providers in REST Client Reactive from context #32976

Merged
merged 1 commit into from
May 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
Sgitario marked this conversation as resolved.
Show resolved Hide resolved
// ...
}
}
----

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 @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
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