diff --git a/api/src/main/java/org/eclipse/microprofile/rest/client/RestClientBuilder.java b/api/src/main/java/org/eclipse/microprofile/rest/client/RestClientBuilder.java index 7279c06..a28590f 100644 --- a/api/src/main/java/org/eclipse/microprofile/rest/client/RestClientBuilder.java +++ b/api/src/main/java/org/eclipse/microprofile/rest/client/RestClientBuilder.java @@ -269,6 +269,20 @@ default RestClientBuilder baseUri(String uri) { */ RestClientBuilder queryParamStyle(QueryParamStyle style); + /** + * Add an arbitrary header. + * + * @param name + * - the name of the header + * @param name + * - the value of the HTTP header to add to the request. + * @return the current builder with the header added to the request. + * @throws NullPointerException + * if the value is null. + * @since 4.0 + */ + RestClientBuilder header(String name, Object value); + /** * Based on the configured RestClientBuilder, creates a new instance of the given REST interface to invoke API calls * against. diff --git a/api/src/test/java/org/eclipse/microprofile/rest/client/BuilderImpl1.java b/api/src/test/java/org/eclipse/microprofile/rest/client/BuilderImpl1.java index 1a38d65..daac7ad 100644 --- a/api/src/test/java/org/eclipse/microprofile/rest/client/BuilderImpl1.java +++ b/api/src/test/java/org/eclipse/microprofile/rest/client/BuilderImpl1.java @@ -90,6 +90,11 @@ public RestClientBuilder queryParamStyle(QueryParamStyle style) { throw new IllegalStateException("not implemented"); } + @Override + public RestClientBuilder header(String name, Object value) { + throw new IllegalStateException("not implemented"); + } + @Override public <T> T build(Class<T> clazz) { throw new IllegalStateException("not implemented"); diff --git a/api/src/test/java/org/eclipse/microprofile/rest/client/BuilderImpl2.java b/api/src/test/java/org/eclipse/microprofile/rest/client/BuilderImpl2.java index 64051ba..83f9322 100644 --- a/api/src/test/java/org/eclipse/microprofile/rest/client/BuilderImpl2.java +++ b/api/src/test/java/org/eclipse/microprofile/rest/client/BuilderImpl2.java @@ -90,6 +90,11 @@ public RestClientBuilder queryParamStyle(QueryParamStyle style) { throw new IllegalStateException("not implemented"); } + @Override + public RestClientBuilder header(String name, Object value) { + throw new IllegalStateException("not implemented"); + } + @Override public <T> T build(Class<T> clazz) { throw new IllegalStateException("not implemented"); diff --git a/spec/src/main/asciidoc/clientexamples.asciidoc b/spec/src/main/asciidoc/clientexamples.asciidoc index 2b88a5c..2e100a1 100644 --- a/spec/src/main/asciidoc/clientexamples.asciidoc +++ b/spec/src/main/asciidoc/clientexamples.asciidoc @@ -149,6 +149,19 @@ implementation must invoke the `DefaultClientHeadersFactoryImpl`. This default f `org.eclipse.microprofile.rest.client.propagateHeaders` +You can also configure headers on a per instance basis using the `RestClientBuilder.header(String name, Object value)` method. Headers added via this method will be merged with the headers added via `@ClientHeaderParam` annotations, `@HeaderParam` annotations, and `ClientHeadersFactory` implementations. +**Note: The method will throw a `NullPointerException` if the value is `null`.** + +Example: + +[source, java] +---- +RedirectClient client = RestClientBuilder.newBuilder() + .baseUri(someUri) + .header("Some-Header", headerValueObj) + .build(SomeClient.class); +---- + === Following Redirect Responses By default, a Rest Client instance will not automatically follow redirect responses. Redirect responses are typically responses with status codes in the 300 range and include `Location` header that indicates the URL of the redirected resource. diff --git a/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/ClientBuilderHeaderTest.java b/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/ClientBuilderHeaderTest.java new file mode 100644 index 0000000..ed94902 --- /dev/null +++ b/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/ClientBuilderHeaderTest.java @@ -0,0 +1,109 @@ +/* + * Copyright 2024 Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.eclipse.microprofile.rest.client.tck; + +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.eclipse.microprofile.rest.client.tck.interfaces.ClientBuilderHeaderClient; +import org.eclipse.microprofile.rest.client.tck.interfaces.ClientBuilderHeaderMethodClient; +import org.eclipse.microprofile.rest.client.tck.providers.ReturnWithAllDuplicateClientHeadersFilter; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.testng.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.testng.Assert; +import org.testng.annotations.Test; + +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonString; +import jakarta.json.JsonValue; + +public class ClientBuilderHeaderTest extends Arquillian { + @Deployment + public static Archive<?> createDeployment() { + return ShrinkWrap.create(WebArchive.class, ClientBuilderHeaderTest.class.getSimpleName() + ".war") + .addClasses( + ClientBuilderHeaderMethodClient.class, + ReturnWithAllDuplicateClientHeadersFilter.class); + } + + @Test + public void testHeaderBuilderMethod() { + + RestClientBuilder builder = RestClientBuilder.newBuilder().baseUri("http://localhost:8080/"); + builder.register(ReturnWithAllDuplicateClientHeadersFilter.class); + builder.header("InterfaceAndBuilderHeader", "builder"); + ClientBuilderHeaderMethodClient client = builder.build(ClientBuilderHeaderMethodClient.class); + + checkHeaders(client.getAllHeaders("headerparam"), "method"); + } + + @Test + public void testHeaderBuilderInterface() { + + RestClientBuilder builder = RestClientBuilder.newBuilder().baseUri("http://localhost:8080/"); + builder.register(ReturnWithAllDuplicateClientHeadersFilter.class); + builder.header("InterfaceAndBuilderHeader", "builder"); + ClientBuilderHeaderClient client = builder.build(ClientBuilderHeaderClient.class); + + checkHeaders(client.getAllHeaders("headerparam"), "interface"); + } + + @Test + public void testHeaderBuilderMethodNullValue() { + + RestClientBuilder builder = RestClientBuilder.newBuilder().baseUri("http://localhost:8080/"); + try { + builder.header("BuilderHeader", null); + } catch (NullPointerException npe) { + return; + } + fail("header(\"builderHeader\", null) should have thrown a NullPointerException"); + } + + private static void checkHeaders(final JsonObject headers, final String clientHeaderParamName) { + final List<String> clientRequestHeaders = headerValues(headers, "InterfaceAndBuilderHeader"); + + assertTrue(clientRequestHeaders.contains("builder"), + "Header InterfaceAndBuilderHeader did not container \"builder\": " + clientRequestHeaders); + assertTrue(clientRequestHeaders.contains(clientHeaderParamName), + "Header InterfaceAndBuilderHeader did not container \"" + clientHeaderParamName + "\": " + + clientRequestHeaders); + + final List<String> headerParamHeaders = headerValues(headers, "HeaderParam"); + assertTrue(headerParamHeaders.contains("headerparam"), + "Header HeaderParam did not container \"headerparam\": " + headerParamHeaders); + } + + private static List<String> headerValues(final JsonObject headers, final String headerName) { + final JsonArray headerValues = headers.getJsonArray(headerName); + Assert.assertNotNull(headerValues, + String.format("Expected header '%s' to be present in %s", headerName, headers)); + return headerValues.stream().map( + v -> (v.getValueType() == JsonValue.ValueType.STRING ? ((JsonString) v).getString() : v.toString())) + .collect(Collectors.toList()); + } +} diff --git a/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/interfaces/ClientBuilderHeaderClient.java b/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/interfaces/ClientBuilderHeaderClient.java new file mode 100644 index 0000000..da92804 --- /dev/null +++ b/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/interfaces/ClientBuilderHeaderClient.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.eclipse.microprofile.rest.client.tck.interfaces; + +import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam; + +import jakarta.json.JsonObject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.Path; + +@ClientHeaderParam(name = "InterfaceAndBuilderHeader", value = "interface") +@Path("/") +public interface ClientBuilderHeaderClient { + + @GET + JsonObject getAllHeaders(@HeaderParam("HeaderParam") String param); +} diff --git a/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/interfaces/ClientBuilderHeaderMethodClient.java b/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/interfaces/ClientBuilderHeaderMethodClient.java new file mode 100644 index 0000000..00795f5 --- /dev/null +++ b/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/interfaces/ClientBuilderHeaderMethodClient.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.eclipse.microprofile.rest.client.tck.interfaces; + +import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam; + +import jakarta.json.JsonObject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.Path; + +@Path("/") +public interface ClientBuilderHeaderMethodClient { + + @GET + @ClientHeaderParam(name = "InterfaceAndBuilderHeader", value = "method") + JsonObject getAllHeaders(@HeaderParam("HeaderParam") String param); +} diff --git a/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/providers/ReturnWithAllDuplicateClientHeadersFilter.java b/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/providers/ReturnWithAllDuplicateClientHeadersFilter.java new file mode 100644 index 0000000..94d5a83 --- /dev/null +++ b/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/providers/ReturnWithAllDuplicateClientHeadersFilter.java @@ -0,0 +1,47 @@ +/* + * Copyright 2018, 2021 Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.eclipse.microprofile.rest.client.tck.providers; + +import java.io.IOException; +import java.util.List; + +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObjectBuilder; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; + +public class ReturnWithAllDuplicateClientHeadersFilter implements ClientRequestFilter { + + @Override + public void filter(ClientRequestContext clientRequestContext) throws IOException { + JsonObjectBuilder allClientHeaders = Json.createObjectBuilder(); + MultivaluedMap<String, Object> clientHeaders = clientRequestContext.getHeaders(); + for (String headerName : clientHeaders.keySet()) { + List<Object> header = clientHeaders.get(headerName); + final JsonArrayBuilder headerValues = Json.createArrayBuilder(); + header.forEach(h -> headerValues.add(h.toString())); + allClientHeaders.add(headerName, headerValues); + } + clientRequestContext.abortWith(Response.ok(allClientHeaders.build()).build()); + } + +}