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 config properties in @ClientHeaderParam #19830

Merged
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
32 changes: 26 additions & 6 deletions docs/src/main/asciidoc/rest-client-reactive.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
----
Expand All @@ -560,20 +564,28 @@ 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<Country> getByName(@PathParam("name") String name);
@ClientHeaderParam(name = "header-from-properties", value = "${header.value}") // <4>
Set<Country> getByName(@PathParam("name") String name, @HeaderParam("jaxrs-style-header") String headerValue); // <5>

@GET
@Path("/name/{name}")
CompletionStage<Set<Country>> getByNameAsync(@PathParam("name") String name);
}
----

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]
----
Expand Down Expand Up @@ -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]
----

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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 + "=<desired-value> 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);
}
}