diff --git a/docs/src/main/asciidoc/rest-client-reactive.adoc b/docs/src/main/asciidoc/rest-client-reactive.adoc index 39b1c338f4e77..9418f0285aa0e 100644 --- a/docs/src/main/asciidoc/rest-client-reactive.adoc +++ b/docs/src/main/asciidoc/rest-client-reactive.adoc @@ -541,9 +541,13 @@ This difference comes from the laziness aspect of Mutiny and its subscription pr More details about this can be found in https://smallrye.io/smallrye-mutiny/#_uni_and_multi[the Mutiny documentation]. == Custom headers support -The MicroProfile REST client allows amending request headers by registering a `ClientHeadersFactory` with the `@RegisterClientHeaders` annotation. +There are a few ways in which you can specify custom headers for your REST calls: -Let's see it in action by adding a `@RegisterClientHeaders` annotation pointing to a `RequestUUIDHeaderFactory` class in our `CountriesService` REST interface: +- by registering a `ClientHeadersFactory` with the `@RegisterClientHeaders` annotation +- by specifying the value of the header with `@ClientHeaderParam` +- by specifying the value of the header by `@HeaderParam` + +The code below demonstrates how to use each of this techniques: [source, java] ---- @@ -560,12 +564,15 @@ import java.util.Set; @Path("/v2") @RegisterRestClient -@RegisterClientHeaders(RequestUUIDHeaderFactory.class) +@RegisterClientHeaders(RequestUUIDHeaderFactory.class) // <1> +@ClientHeaderParam(name = "my-header", value = "constant-header-value") // <2> +@ClientHeaderParam(name = "computed-header", value = "{org.acme.rest.client.Util.computeHeader}") // <3> public interface CountriesService { @GET @Path("/name/{name}") - Set getByName(@PathParam("name") String name); + @ClientHeaderParam(name = "header-from-properties", value = "${header.value}") // <4> + Set getByName(@PathParam("name") String name, @HeaderParam("jaxrs-style-header") String headerValue); // <5> @GET @Path("/name/{name}") @@ -573,7 +580,12 @@ public interface CountriesService { } ---- -And the `RequestUUIDHeaderFactory` would look like: +<1> There can be only one `ClientHeadersFactory` per class. With it, you can not only add custom headers, but you can also transform existing ones. See the `RequestUUIDHeaderFactory` class below for an example of the factory. +<2> `@ClientHeaderParam` can be used on the client interface and on methods. It can specify a constant header value... +<3> ... and a name of a method that should compute the value of the header. It can either be a static method or a default method in this interface +<4> ... as well as a value from your application's configuration + +A `ClientHeadersFactory` can look as follows: [source, java] ---- @@ -601,8 +613,16 @@ public class RequestUUIDHeaderFactory implements ClientHeadersFactory { As you see in the example above, you can make your `ClientHeadersFactory` implementation a CDI bean by annotating it with a scope-defining annotation, such as `@Singleton`, `@ApplicationScoped`, etc. +To specify a value for `${header.value}`, simply put the following in your `application.properties`: + +[source,properties] +---- +header.value=value of the header +---- + === Default header factory -You can also use `@RegisterClientHeaders` annotation without any custom factory specified. In that case the `DefaultClientHeadersFactoryImpl` factory will be used and all headers listed in `org.eclipse.microprofile.rest.client.propagateHeaders` configuration property will be amended. Individual header names are comma-separated. +The `@RegisterClientHeaders` annotation can also be used without any custom factory specified. In that case the `DefaultClientHeadersFactoryImpl` factory will be used. +If you make a REST client call from a REST resource, this fatory will propagate all the headers listed in `org.eclipse.microprofile.rest.client.propagateHeaders` configuration property from the resource request to the client request. Individual header names are comma-separated. [source, java] ---- diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/MicroProfileRestClientEnricher.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/MicroProfileRestClientEnricher.java index f5e90dc889c70..88ef8823de5b0 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/MicroProfileRestClientEnricher.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/MicroProfileRestClientEnricher.java @@ -53,6 +53,7 @@ import io.quarkus.gizmo.TryBlock; import io.quarkus.jaxrs.client.reactive.deployment.JaxrsClientReactiveEnricher; import io.quarkus.rest.client.reactive.HeaderFiller; +import io.quarkus.rest.client.reactive.runtime.ConfigUtils; import io.quarkus.rest.client.reactive.runtime.MicroProfileRestClientRequestFilter; import io.quarkus.rest.client.reactive.runtime.NoOpHeaderFiller; import io.quarkus.runtime.util.HashUtil; @@ -321,9 +322,19 @@ private void addHeaderParam(MethodInfo declaringMethod, MethodCreator fillHeader .trueBranch(); if (values.length > 1 || !(values[0].startsWith("{") && values[0].endsWith("}"))) { + boolean required = annotation.valueWithDefault(index, "required").asBoolean(); ResultHandle headerList = fillHeaders.newInstance(MethodDescriptor.ofConstructor(ArrayList.class)); for (String value : values) { - fillHeaders.invokeInterfaceMethod(LIST_ADD_METHOD, headerList, fillHeaders.load(value)); + if (value.startsWith("${") && value.endsWith("}")) { + ResultHandle headerValueFromConfig = fillHeaders.invokeStaticMethod( + MethodDescriptor.ofMethod(ConfigUtils.class, "getConfigValue", String.class, String.class, + boolean.class), + fillHeaders.load(value), fillHeaders.load(required)); + fillHeaders.ifNotNull(headerValueFromConfig) + .trueBranch().invokeInterfaceMethod(LIST_ADD_METHOD, headerList, headerValueFromConfig); + } else { + fillHeaders.invokeInterfaceMethod(LIST_ADD_METHOD, headerList, fillHeaders.load(value)); + } } fillHeaders.invokeInterfaceMethod(MAP_PUT_METHOD, headerMap, fillHeaders.load(headerName), headerList); diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/headers/ClientHeaderParamFromPropertyTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/headers/ClientHeaderParamFromPropertyTest.java new file mode 100644 index 0000000000000..6a3035988b107 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/headers/ClientHeaderParamFromPropertyTest.java @@ -0,0 +1,84 @@ +package io.quarkus.rest.client.reactive.headers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.net.URI; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +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 ClientHeaderParamFromPropertyTest { + private static final String HEADER_VALUE = "oifajrofijaeoir5gjaoasfaxcvcz"; + + @TestHTTPResource + URI baseUri; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(Client.class) + .addAsResource( + new StringAsset("my.property-value=" + HEADER_VALUE), + "application.properties")); + + @Test + void shouldSetHeaderFromProperties() { + Client client = RestClientBuilder.newBuilder().baseUri(baseUri) + .build(Client.class); + + assertThat(client.getWithHeader()).isEqualTo(HEADER_VALUE); + } + + @Test + void shouldFailOnMissingRequiredHeaderProperty() { + Client client = RestClientBuilder.newBuilder().baseUri(baseUri) + .build(Client.class); + + assertThatThrownBy(client::missingRequiredProperty) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void shouldSucceedOnMissingNonRequiredHeaderProperty() { + Client client = RestClientBuilder.newBuilder().baseUri(baseUri) + .build(Client.class); + + assertThat(client.missingNonRequiredProperty()).isEqualTo(HEADER_VALUE); + } + + @Path("/") + @ApplicationScoped + public static class Resource { + @GET + public String returnHeaderValue(@HeaderParam("my-header") String header) { + return header; + } + } + + @ClientHeaderParam(name = "my-header", value = "${my.property-value}") + public interface Client { + @GET + String getWithHeader(); + + @GET + @ClientHeaderParam(name = "some-other-header", value = "${non-existent-property}") + String missingRequiredProperty(); + + @GET + @ClientHeaderParam(name = "some-other-header", value = "${non-existent-property}", required = false) + String missingNonRequiredProperty(); + } +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/ConfigUtils.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/ConfigUtils.java new file mode 100644 index 0000000000000..f1596ddfb713f --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/ConfigUtils.java @@ -0,0 +1,41 @@ +package io.quarkus.rest.client.reactive.runtime; + +import java.util.NoSuchElementException; + +import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.logging.Logger; + +public class ConfigUtils { + + private static final Logger log = Logger.getLogger(ConfigUtils.class); + + public static String getConfigValue(String configProperty, boolean required) { + String propertyName = stripPrefixAndSuffix(configProperty); + try { + return ConfigProvider.getConfig().getValue(propertyName, String.class); + } catch (NoSuchElementException e) { + String message = "Failed to find value for config property " + configProperty + + " in application configuration. Please provide the value for the property, e.g. by adding " + + propertyName + "= to your application.properties"; + if (required) { + throw new IllegalArgumentException(message, e); + } else { + log.warn(message); + return null; + } + } catch (IllegalArgumentException e) { + String message = "Failed to convert value for property " + configProperty + " to String"; + if (required) { + throw new IllegalArgumentException(message, e); + } else { + log.warn(message); + return null; + } + } + } + + private static String stripPrefixAndSuffix(String configProperty) { + // by now we know that configProperty is of form ${...} + return configProperty.substring(2, configProperty.length() - 1); + } +}