Skip to content

Commit

Permalink
Support registering a ClientHeadersFactory in REST Client Reactive API
Browse files Browse the repository at this point in the history
  • Loading branch information
Sgitario committed Apr 25, 2023
1 parent 585ebb2 commit b770e4b
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/src/main/asciidoc/rest-client-reactive.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,7 @@ More details about this can be found in https://smallrye.io/smallrye-mutiny/late
There are a few ways in which you can specify custom headers for your REST calls:

- by registering a `ClientHeadersFactory` or a `ReactiveClientHeadersFactory` with the `@RegisterClientHeaders` annotation
- by programmatically registering a `ClientHeadersFactory` or a `ReactiveClientHeadersFactory` with the `QuarkusRestClientBuilder.clientHeadersFactory(factory)` method
- by specifying the value of the header with `@ClientHeaderParam`
- by specifying the value of the header by `@HeaderParam`

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package io.quarkus.rest.client.reactive.headers;

import static io.restassured.RestAssured.given;
import static java.lang.String.format;
import static org.assertj.core.api.Assertions.assertThat;

import java.net.URI;
import java.util.List;
import java.util.Map;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MultivaluedHashMap;
import jakarta.ws.rs.core.MultivaluedMap;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.arc.Unremovable;
import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder;
import io.quarkus.rest.client.reactive.ReactiveClientHeadersFactory;
import io.quarkus.rest.client.reactive.TestJacksonBasicMessageBodyReader;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.common.http.TestHTTPResource;
import io.smallrye.common.annotation.Blocking;
import io.smallrye.mutiny.Uni;
import io.vertx.core.Vertx;

public class ReactiveClientHeadersFromBuilderTest {
private static final String HEADER_NAME = "my-header";
private static final String HEADER_VALUE = "oifajrofijaeoir5gjaoasfaxcvcz";
public static final String COPIED_INCOMING_HEADER = "copied-incoming-header";
public static final String INCOMING_HEADER = "incoming-header";
public static final String DIRECT_HEADER_PARAM = "direct-header-param";
public static final String DIRECT_HEADER_PARAM_VAL = "direct-header-param-val";

@TestHTTPResource
URI baseUri;

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar.addClasses(Client.class, TestJacksonBasicMessageBodyReader.class)
.addAsResource(
new StringAsset("my.property-value=" + HEADER_VALUE),
"application.properties"));

@Test
void shouldPropagateHeaders() {
// we're calling a resource that sets "incoming-header" header
// this header should be dropped by the client and its value should be put into copied-incoming-header
String propagatedHeaderValue = "propag8ed header";
// @formatter:off
var response =
given()
.header(INCOMING_HEADER, propagatedHeaderValue)
.body(baseUri.toString())
.when()
.post("/call-client")
.thenReturn();
// @formatter:on
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.jsonPath().getString(INCOMING_HEADER)).isNull();
assertThat(response.jsonPath().getString(COPIED_INCOMING_HEADER)).isEqualTo(format("[%s]", propagatedHeaderValue));
assertThat(response.jsonPath().getString(HEADER_NAME)).isEqualTo(format("[%s]", HEADER_VALUE));
assertThat(response.jsonPath().getString(DIRECT_HEADER_PARAM)).isEqualTo(format("[%s]", DIRECT_HEADER_PARAM_VAL));
}

@Path("/")
@ApplicationScoped
public static class Resource {

@GET
@Produces("application/json")
public Map<String, List<String>> returnHeaderValues(@Context HttpHeaders headers) {
return headers.getRequestHeaders();
}

@Path("/call-client")
@POST
public Map<String, List<String>> callClient(String uri) {
ReactiveClientHeadersFromBuilderTest.Client client = QuarkusRestClientBuilder.newBuilder()
.baseUri(URI.create(uri))
.clientHeadersFactory(CustomReactiveClientHeadersFactory.class)
.register(new TestJacksonBasicMessageBodyReader())
.build(ReactiveClientHeadersFromBuilderTest.Client.class);
return client.getWithHeader(DIRECT_HEADER_PARAM_VAL);
}
}

@ApplicationScoped
public static class Service {
@Blocking
public String getValue() {
return HEADER_VALUE;
}
}

public interface Client {
@GET
Map<String, List<String>> getWithHeader(@HeaderParam(DIRECT_HEADER_PARAM) String directHeaderParam);
}

@Unremovable
@ApplicationScoped
public static class CustomReactiveClientHeadersFactory extends ReactiveClientHeadersFactory {

@Inject
Service service;

@Inject
Vertx vertx;

@Override
public Uni<MultivaluedMap<String, String>> getHeaders(MultivaluedMap<String, String> incomingHeaders,
MultivaluedMap<String, String> clientOutgoingHeaders) {
return Uni.createFrom().emitter(e -> {
MultivaluedHashMap<String, String> newHeaders = new MultivaluedHashMap<>();
newHeaders.add(HEADER_NAME, service.getValue());
newHeaders.add(COPIED_INCOMING_HEADER, incomingHeaders.getFirst(INCOMING_HEADER));
vertx.setTimer(100L, id -> e.complete(newHeaders));
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import org.eclipse.microprofile.rest.client.RestClientBuilder;
import org.eclipse.microprofile.rest.client.RestClientDefinitionException;
import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory;
import org.eclipse.microprofile.rest.client.ext.QueryParamStyle;
import org.eclipse.microprofile.rest.client.spi.RestClientBuilderListener;

Expand Down Expand Up @@ -216,6 +217,22 @@ static QuarkusRestClientBuilder newBuilder() {
*/
QuarkusRestClientBuilder queryParamStyle(QueryParamStyle style);

/**
* Specifies the client headers factory to use.
*
* @param clientHeadersFactoryClass the client headers factory class to use.
* @return the current builder
*/
QuarkusRestClientBuilder clientHeadersFactory(Class<? extends ClientHeadersFactory> clientHeadersFactoryClass);

/**
* Specifies the client headers factory to use.
*
* @param clientHeadersFactory the client headers factory to use.
* @return the current builder
*/
QuarkusRestClientBuilder clientHeadersFactory(ClientHeadersFactory clientHeadersFactory);

/**
* Based on the configured QuarkusRestClientBuilder, creates a new instance of the given REST interface to invoke API calls
* against.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory;
import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestContext;
import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestFilter;
import org.jboss.resteasy.reactive.common.jaxrs.ConfigurationImpl;

import io.quarkus.arc.Arc;
import io.quarkus.rest.client.reactive.HeaderFiller;
Expand Down Expand Up @@ -65,6 +66,7 @@ public void filter(ResteasyReactiveClientRequestContext requestContext) {
requestContext.getHeaders().put(headerEntry.getKey(), castToListOfObjects(headerEntry.getValue()));
}

ClientHeadersFactory clientHeadersFactory = clientHeadersFactory(requestContext);
if (clientHeadersFactory != null) {
if (clientHeadersFactory instanceof ReactiveClientHeadersFactory) {
// reactive
Expand All @@ -87,6 +89,18 @@ public void filter(ResteasyReactiveClientRequestContext requestContext) {
}
}

private ClientHeadersFactory clientHeadersFactory(ResteasyReactiveClientRequestContext requestContext) {
if (requestContext.getConfiguration() instanceof ConfigurationImpl) {
ConfigurationImpl configuration = (ConfigurationImpl) requestContext.getConfiguration();
ClientHeadersFactory localHeadersFactory = configuration.getFromContext(ClientHeadersFactory.class);
if (localHeadersFactory != null) {
return localHeadersFactory;
}
}

return clientHeadersFactory;
}

private static List<String> castToListOfStrings(Collection<Object> values) {
List<String> result = new ArrayList<>();
for (Object value : values) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
import jakarta.ws.rs.core.Configuration;

import org.eclipse.microprofile.rest.client.RestClientDefinitionException;
import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory;
import org.eclipse.microprofile.rest.client.ext.QueryParamStyle;

import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder;
import io.quarkus.rest.client.reactive.runtime.context.ClientHeadersFactoryContextResolver;

public class QuarkusRestClientBuilderImpl implements QuarkusRestClientBuilder {

Expand Down Expand Up @@ -179,6 +181,23 @@ public QuarkusRestClientBuilder register(Object component, Map<Class<?>, Integer
return this;
}

@Override
public QuarkusRestClientBuilder clientHeadersFactory(Class<? extends ClientHeadersFactory> clientHeadersFactoryClass) {
ClientHeadersFactory bean = BeanGrabber.getBeanIfDefined(clientHeadersFactoryClass);
if (bean == null) {
throw new IllegalArgumentException("Failed to instantiate the client headers factory " + clientHeadersFactoryClass
+ ". Make sure the bean is properly configured for CDI injection.");
}

return clientHeadersFactory(bean);
}

@Override
public QuarkusRestClientBuilder clientHeadersFactory(ClientHeadersFactory clientHeadersFactory) {
proxy.register(new ClientHeadersFactoryContextResolver(clientHeadersFactory));
return this;
}

@Override
public <T> T build(Class<T> clazz) throws IllegalStateException, RestClientDefinitionException {
return proxy.build(clazz);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.quarkus.rest.client.reactive.runtime.context;

import jakarta.ws.rs.ext.ContextResolver;

import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory;

public class ClientHeadersFactoryContextResolver implements ContextResolver<ClientHeadersFactory> {

private final ClientHeadersFactory component;

public ClientHeadersFactoryContextResolver(ClientHeadersFactory component) {
this.component = component;
}

@Override
public ClientHeadersFactory getContext(Class<?> wantedClass) {
if (wantedClass.equals(ClientHeadersFactory.class)) {
return component;
}

return null;
}
}

0 comments on commit b770e4b

Please sign in to comment.