From aad21d19963ceb76fefca44c2689d68b2ad64af0 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 11 Jun 2019 17:26:19 -0700 Subject: [PATCH] Polish "Support default headers with RestTemplateBuilder" Broaden the scope of customizer support so that instead of focusing just on headers, we can now customize any outgoing `HttpClientRequest`. Also update auto-configuration to automatically add any `RestTemplateRequestCustomizer` beans to the builder. See gh-17091 --- .../client/RestTemplateAutoConfiguration.java | 17 +- .../RestTemplateAutoConfigurationTests.java | 32 + .../web/client/TestRestTemplateTests.java | 6 +- ...stractHttpHeadersDefaultingCustomizer.java | 36 -- ...stomizer.java => BasicAuthentication.java} | 17 +- ...rsCustomizingClientHttpRequestFactory.java | 58 -- .../boot/web/client/RestTemplateBuilder.java | 576 ++++++++---------- ...uilderClientHttpRequestFactoryWrapper.java | 70 +++ ...ava => RestTemplateRequestCustomizer.java} | 21 +- .../SimpleHttpHeaderDefaultingCustomizer.java | 96 --- ...tomizingClientHttpRequestFactoryTests.java | 94 --- ...rClientHttpRequestFactoryWrapperTests.java | 115 ++++ .../web/client/RestTemplateBuilderTests.java | 40 +- ...pleHttpHeaderDefaultingCustomizerTest.java | 75 --- 14 files changed, 546 insertions(+), 707 deletions(-) delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/AbstractHttpHeadersDefaultingCustomizer.java rename spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/{BasicAuthenticationHeaderDefaultingCustomizer.java => BasicAuthentication.java} (66%) delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/HttpHeadersCustomizingClientHttpRequestFactory.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilderClientHttpRequestFactoryWrapper.java rename spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/{HttpHeadersCustomizer.java => RestTemplateRequestCustomizer.java} (53%) delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/SimpleHttpHeaderDefaultingCustomizer.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/HttpHeadersCustomizingClientHttpRequestFactoryTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RestTemplateBuilderClientHttpRequestFactoryWrapperTests.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/SimpleHttpHeaderDefaultingCustomizerTest.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java index a41463ad70..bd8536c3f0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java @@ -16,7 +16,9 @@ package org.springframework.boot.autoconfigure.web.client; +import java.util.Collection; import java.util.List; +import java.util.function.BiFunction; import java.util.stream.Collectors; import org.springframework.beans.factory.ObjectProvider; @@ -32,6 +34,7 @@ import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConf import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration.NotReactiveWebApplicationCondition; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.client.RestTemplateCustomizer; +import org.springframework.boot.web.client.RestTemplateRequestCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -53,15 +56,23 @@ public class RestTemplateAutoConfiguration { @Bean @ConditionalOnMissingBean public RestTemplateBuilder restTemplateBuilder(ObjectProvider messageConverters, - ObjectProvider restTemplateCustomizers) { + ObjectProvider restTemplateCustomizers, + ObjectProvider> restTemplateRequestCustomizers) { RestTemplateBuilder builder = new RestTemplateBuilder(); HttpMessageConverters converters = messageConverters.getIfUnique(); if (converters != null) { builder = builder.messageConverters(converters.getConverters()); } - List customizers = restTemplateCustomizers.orderedStream().collect(Collectors.toList()); + builder = addCustomizers(builder, restTemplateCustomizers, RestTemplateBuilder::customizers); + builder = addCustomizers(builder, restTemplateRequestCustomizers, RestTemplateBuilder::requestCustomizers); + return builder; + } + + private RestTemplateBuilder addCustomizers(RestTemplateBuilder builder, ObjectProvider objectProvider, + BiFunction, RestTemplateBuilder> method) { + List customizers = objectProvider.orderedStream().collect(Collectors.toList()); if (!customizers.isEmpty()) { - builder = builder.customizers(customizers); + return method.apply(builder, customizers); } return builder; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java index 7fc93af5c1..2d6420b356 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.autoconfigure.web.client; +import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Test; @@ -28,14 +29,21 @@ import org.springframework.boot.test.context.runner.ReactiveWebApplicationContex import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.client.RestTemplateCustomizer; +import org.springframework.boot.web.client.RestTemplateRequestCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.mock.http.client.MockClientHttpRequest; +import org.springframework.mock.http.client.MockClientHttpResponse; import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -109,6 +117,20 @@ class RestTemplateAutoConfigurationTests { }); } + @Test + void restTemplateShouldApplyRequestCustomizer() { + this.contextRunner.withUserConfiguration(RestTemplateRequestCustomizerConfig.class).run((context) -> { + RestTemplateBuilder builder = context.getBean(RestTemplateBuilder.class); + ClientHttpRequestFactory requestFactory = mock(ClientHttpRequestFactory.class); + MockClientHttpRequest request = new MockClientHttpRequest(); + request.setResponse(new MockClientHttpResponse(new byte[0], HttpStatus.OK)); + given(requestFactory.createRequest(any(), any())).willReturn(request); + RestTemplate restTemplate = builder.requestFactory(() -> requestFactory).build(); + restTemplate.getForEntity("http://localhost:8080/test", String.class); + assertThat(request.getHeaders()).containsEntry("spring", Collections.singletonList("boot")); + }); + } + @Test void builderShouldBeFreshForEachUse() { this.contextRunner.withUserConfiguration(DirtyRestTemplateConfig.class) @@ -189,6 +211,16 @@ class RestTemplateAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class RestTemplateRequestCustomizerConfig { + + @Bean + public RestTemplateRequestCustomizer restTemplateRequestCustomizer() { + return (request) -> request.getHeaders().add("spring", "boot"); + } + + } + static class CustomHttpMessageConverter extends StringHttpMessageConverter { } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/TestRestTemplateTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/TestRestTemplateTests.java index edab82eb5e..f7dcc228fb 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/TestRestTemplateTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/TestRestTemplateTests.java @@ -100,7 +100,7 @@ class TestRestTemplateTests { TestRestTemplate testRestTemplate = new TestRestTemplate(builder).withBasicAuth("test", "test"); RestTemplate restTemplate = testRestTemplate.getRestTemplate(); assertThat(restTemplate.getRequestFactory().getClass().getName()) - .contains("HttpHeadersCustomizingClientHttpRequestFactory"); + .contains("RestTemplateBuilderClientHttpRequestFactoryWrapper"); Object requestFactory = ReflectionTestUtils.getField(restTemplate.getRequestFactory(), "requestFactory"); assertThat(requestFactory).isEqualTo(customFactory).hasSameClassAs(customFactory); } @@ -208,7 +208,7 @@ class TestRestTemplateTests { TestRestTemplate basicAuth = original.withBasicAuth("user", "password"); assertThat(getConverterClasses(original)).containsExactlyElementsOf(getConverterClasses(basicAuth)); assertThat(basicAuth.getRestTemplate().getRequestFactory().getClass().getName()) - .contains("HttpHeadersCustomizingClientHttpRequestFactory"); + .contains("RestTemplateBuilderClientHttpRequestFactoryWrapper"); assertThat(ReflectionTestUtils.getField(basicAuth.getRestTemplate().getRequestFactory(), "requestFactory")) .isInstanceOf(CustomHttpComponentsClientHttpRequestFactory.class); assertThat(basicAuth.getRestTemplate().getInterceptors()).isEmpty(); @@ -221,7 +221,7 @@ class TestRestTemplateTests { TestRestTemplate basicAuth = original.withBasicAuth("user", "password"); assertThat(getConverterClasses(basicAuth)).containsExactlyElementsOf(getConverterClasses(original)); assertThat(basicAuth.getRestTemplate().getRequestFactory().getClass().getName()) - .contains("HttpHeadersCustomizingClientHttpRequestFactory"); + .contains("RestTemplateBuilderClientHttpRequestFactoryWrapper"); assertThat(ReflectionTestUtils.getField(basicAuth.getRestTemplate().getRequestFactory(), "requestFactory")) .isInstanceOf(CustomHttpComponentsClientHttpRequestFactory.class); assertThat(basicAuth.getRestTemplate().getInterceptors()).isEmpty(); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/AbstractHttpHeadersDefaultingCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/AbstractHttpHeadersDefaultingCustomizer.java deleted file mode 100644 index d4a85ab669..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/AbstractHttpHeadersDefaultingCustomizer.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.web.client; - -import org.springframework.http.HttpHeaders; - -/** - * {@link HttpHeadersCustomizer} that only adds headers that were not populated in the - * request. - * - * @author Ilya Lukyanovich - */ -public abstract class AbstractHttpHeadersDefaultingCustomizer implements HttpHeadersCustomizer { - - @Override - public void applyTo(HttpHeaders headers) { - createHeaders().forEach((key, value) -> headers.merge(key, value, (oldValue, ignored) -> oldValue)); - } - - protected abstract HttpHeaders createHeaders(); - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/BasicAuthenticationHeaderDefaultingCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/BasicAuthentication.java similarity index 66% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/BasicAuthenticationHeaderDefaultingCustomizer.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/BasicAuthentication.java index 5e9d2b49a3..2039822288 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/BasicAuthenticationHeaderDefaultingCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/BasicAuthentication.java @@ -22,14 +22,12 @@ import org.springframework.http.HttpHeaders; import org.springframework.util.Assert; /** - * {@link AbstractHttpHeadersDefaultingCustomizer} that applies basic authentication - * header unless it was provided in the request. + * Basic authentication details to be applied to {@link HttpHeaders}. * * @author Dmytro Nosan * @author Ilya Lukyanovich - * @see HttpHeadersCustomizingClientHttpRequestFactory */ -class BasicAuthenticationHeaderDefaultingCustomizer extends AbstractHttpHeadersDefaultingCustomizer { +class BasicAuthentication { private final String username; @@ -37,7 +35,7 @@ class BasicAuthenticationHeaderDefaultingCustomizer extends AbstractHttpHeadersD private final Charset charset; - BasicAuthenticationHeaderDefaultingCustomizer(String username, String password, Charset charset) { + BasicAuthentication(String username, String password, Charset charset) { Assert.notNull(username, "Username must not be null"); Assert.notNull(password, "Password must not be null"); this.username = username; @@ -45,11 +43,10 @@ class BasicAuthenticationHeaderDefaultingCustomizer extends AbstractHttpHeadersD this.charset = charset; } - @Override - protected HttpHeaders createHeaders() { - HttpHeaders headers = new HttpHeaders(); - headers.setBasicAuth(this.username, this.password, this.charset); - return headers; + public void applyTo(HttpHeaders headers) { + if (!headers.containsKey(HttpHeaders.AUTHORIZATION)) { + headers.setBasicAuth(this.username, this.password, this.charset); + } } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/HttpHeadersCustomizingClientHttpRequestFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/HttpHeadersCustomizingClientHttpRequestFactory.java deleted file mode 100644 index 9611a6ec2a..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/HttpHeadersCustomizingClientHttpRequestFactory.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.web.client; - -import java.io.IOException; -import java.net.URI; -import java.util.Collection; - -import org.jetbrains.annotations.NotNull; - -import org.springframework.http.HttpMethod; -import org.springframework.http.client.AbstractClientHttpRequestFactoryWrapper; -import org.springframework.http.client.ClientHttpRequest; -import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.util.Assert; - -/** - * {@link ClientHttpRequestFactory} to apply default headers to a request unless header - * values were provided. - * - * @author Ilya Lukyanovich - * @author Dmytro Nosan - */ -class HttpHeadersCustomizingClientHttpRequestFactory extends AbstractClientHttpRequestFactoryWrapper { - - private final Collection customizers; - - HttpHeadersCustomizingClientHttpRequestFactory(Collection customizers, - ClientHttpRequestFactory clientHttpRequestFactory) { - super(clientHttpRequestFactory); - Assert.notEmpty(customizers, "Customizers must not be empty"); - this.customizers = customizers; - } - - @NotNull - @Override - protected ClientHttpRequest createRequest(@NotNull URI uri, @NotNull HttpMethod httpMethod, - ClientHttpRequestFactory requestFactory) throws IOException { - ClientHttpRequest request = requestFactory.createRequest(uri, httpMethod); - this.customizers.forEach((customizer) -> customizer.applyTo(request.getHeaders())); - return request; - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilder.java index 77d327e7d2..9de267f268 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilder.java @@ -25,14 +25,17 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; +import reactor.netty.http.client.HttpClientRequest; + import org.springframework.beans.BeanUtils; -import org.springframework.http.HttpHeaders; import org.springframework.http.client.AbstractClientHttpRequestFactoryWrapper; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpRequestFactory; @@ -70,25 +73,29 @@ import org.springframework.web.util.UriTemplateHandler; */ public class RestTemplateBuilder { + private final RequestFactoryCustomizer requestFactoryCustomizer; + private final boolean detectRequestFactory; private final String rootUri; private final Set> messageConverters; - private final Supplier requestFactorySupplier; + private final Set interceptors; + + private final Supplier requestFactory; private final UriTemplateHandler uriTemplateHandler; private final ResponseErrorHandler errorHandler; - private final Set restTemplateCustomizers; + private final BasicAuthentication basicAuthentication; - private final RequestFactoryCustomizer requestFactoryCustomizer; + private final Map defaultHeaders; - private final Set interceptors; + private final Set customizers; - private final Set httpHeadersCustomizers; + private final Set> requestCustomizers; /** * Create a new {@link RestTemplateBuilder} instance. @@ -97,33 +104,38 @@ public class RestTemplateBuilder { */ public RestTemplateBuilder(RestTemplateCustomizer... customizers) { Assert.notNull(customizers, "Customizers must not be null"); + this.requestFactoryCustomizer = new RequestFactoryCustomizer(); this.detectRequestFactory = true; this.rootUri = null; this.messageConverters = null; - this.requestFactorySupplier = null; + this.interceptors = Collections.emptySet(); + this.requestFactory = null; this.uriTemplateHandler = null; this.errorHandler = null; - this.restTemplateCustomizers = Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(customizers))); - this.requestFactoryCustomizer = new RequestFactoryCustomizer(); - this.interceptors = Collections.emptySet(); - this.httpHeadersCustomizers = Collections.emptySet(); + this.basicAuthentication = null; + this.defaultHeaders = Collections.emptyMap(); + this.customizers = setOf(customizers); + this.requestCustomizers = Collections.emptySet(); } - private RestTemplateBuilder(boolean detectRequestFactory, String rootUri, - Set> messageConverters, Supplier requestFactorySupplier, + private RestTemplateBuilder(RequestFactoryCustomizer requestFactoryCustomizer, boolean detectRequestFactory, + String rootUri, Set> messageConverters, + Set interceptors, Supplier requestFactorySupplier, UriTemplateHandler uriTemplateHandler, ResponseErrorHandler errorHandler, - Set restTemplateCustomizers, RequestFactoryCustomizer requestFactoryCustomizer, - Set interceptors, Set httpHeadersCustomizers) { + BasicAuthentication basicAuthentication, Map defaultHeaders, + Set customizers, Set> requestCustomizers) { + this.requestFactoryCustomizer = requestFactoryCustomizer; this.detectRequestFactory = detectRequestFactory; this.rootUri = rootUri; this.messageConverters = messageConverters; - this.requestFactorySupplier = requestFactorySupplier; + this.interceptors = interceptors; + this.requestFactory = requestFactorySupplier; this.uriTemplateHandler = uriTemplateHandler; this.errorHandler = errorHandler; - this.restTemplateCustomizers = restTemplateCustomizers; - this.requestFactoryCustomizer = requestFactoryCustomizer; - this.interceptors = interceptors; - this.httpHeadersCustomizers = httpHeadersCustomizers; + this.basicAuthentication = basicAuthentication; + this.defaultHeaders = defaultHeaders; + this.customizers = customizers; + this.requestCustomizers = requestCustomizers; } /** @@ -134,9 +146,10 @@ public class RestTemplateBuilder { * @return a new builder instance */ public RestTemplateBuilder detectRequestFactory(boolean detectRequestFactory) { - return new RestTemplateBuilder(detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.restTemplateCustomizers, - this.requestFactoryCustomizer, this.interceptors, this.httpHeadersCustomizers); + return new RestTemplateBuilder(this.requestFactoryCustomizer, detectRequestFactory, this.rootUri, + this.messageConverters, this.interceptors, this.requestFactory, this.uriTemplateHandler, + this.errorHandler, this.basicAuthentication, this.defaultHeaders, this.customizers, + this.requestCustomizers); } /** @@ -146,9 +159,10 @@ public class RestTemplateBuilder { * @return a new builder instance */ public RestTemplateBuilder rootUri(String rootUri) { - return new RestTemplateBuilder(this.detectRequestFactory, rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.restTemplateCustomizers, - this.requestFactoryCustomizer, this.interceptors, this.httpHeadersCustomizers); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, rootUri, + this.messageConverters, this.interceptors, this.requestFactory, this.uriTemplateHandler, + this.errorHandler, this.basicAuthentication, this.defaultHeaders, this.customizers, + this.requestCustomizers); } /** @@ -176,10 +190,10 @@ public class RestTemplateBuilder { */ public RestTemplateBuilder messageConverters(Collection> messageConverters) { Assert.notNull(messageConverters, "MessageConverters must not be null"); - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, - Collections.unmodifiableSet(new LinkedHashSet>(messageConverters)), - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.restTemplateCustomizers, - this.requestFactoryCustomizer, this.interceptors, this.httpHeadersCustomizers); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + setOf(messageConverters), this.interceptors, this.requestFactory, this.uriTemplateHandler, + this.errorHandler, this.basicAuthentication, this.defaultHeaders, this.customizers, + this.requestCustomizers); } /** @@ -192,22 +206,7 @@ public class RestTemplateBuilder { */ public RestTemplateBuilder additionalMessageConverters(HttpMessageConverter... messageConverters) { Assert.notNull(messageConverters, "MessageConverters must not be null"); - return additionalMessageConverters(true, messageConverters); - } - - /** - * Add additional {@link HttpMessageConverter HttpMessageConverters} that should be - * used with the {@link RestTemplate}. Any converters configured on the builder will - * replace RestTemplate's default converters. - * @param append if true adds converters to the end otherwise to the beginning - * @param messageConverters the converters to add - * @return a new builder instance - * @see #messageConverters(HttpMessageConverter...) - */ - public RestTemplateBuilder additionalMessageConverters(boolean append, - HttpMessageConverter... messageConverters) { - Assert.notNull(messageConverters, "MessageConverters must not be null"); - return additionalMessageConverters(append, Arrays.asList(messageConverters)); + return additionalMessageConverters(Arrays.asList(messageConverters)); } /** @@ -221,26 +220,10 @@ public class RestTemplateBuilder { public RestTemplateBuilder additionalMessageConverters( Collection> messageConverters) { Assert.notNull(messageConverters, "MessageConverters must not be null"); - return additionalMessageConverters(true, messageConverters); - } - - /** - * Add additional {@link HttpMessageConverter HttpMessageConverters} that should be - * used with the {@link RestTemplate}. Any converters configured on the builder will - * replace RestTemplate's default converters. - * @param append if true adds converters to the end otherwise to the beginning - * @param messageConverters the converters to add - * @return a new builder instance - * @see #messageConverters(HttpMessageConverter...) - */ - public RestTemplateBuilder additionalMessageConverters(boolean append, - Collection> messageConverters) { - Assert.notNull(messageConverters, "MessageConverters must not be null"); - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, - append ? append(this.messageConverters, messageConverters) - : append(messageConverters, this.messageConverters), - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.restTemplateCustomizers, - this.requestFactoryCustomizer, this.interceptors, this.httpHeadersCustomizers); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + append(this.messageConverters, messageConverters), this.interceptors, this.requestFactory, + this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, this.defaultHeaders, + this.customizers, this.requestCustomizers); } /** @@ -251,10 +234,10 @@ public class RestTemplateBuilder { * @see #messageConverters(HttpMessageConverter...) */ public RestTemplateBuilder defaultMessageConverters() { - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, - Collections.unmodifiableSet(new LinkedHashSet<>(new RestTemplate().getMessageConverters())), - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.restTemplateCustomizers, - this.requestFactoryCustomizer, this.interceptors, this.httpHeadersCustomizers); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + setOf(new RestTemplate().getMessageConverters()), this.interceptors, this.requestFactory, + this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, this.defaultHeaders, + this.customizers, this.requestCustomizers); } /** @@ -282,10 +265,10 @@ public class RestTemplateBuilder { */ public RestTemplateBuilder interceptors(Collection interceptors) { Assert.notNull(interceptors, "interceptors must not be null"); - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.restTemplateCustomizers, - this.requestFactoryCustomizer, Collections.unmodifiableSet(new LinkedHashSet<>(interceptors)), - this.httpHeadersCustomizers); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + this.messageConverters, setOf(interceptors), this.requestFactory, this.uriTemplateHandler, + this.errorHandler, this.basicAuthentication, this.defaultHeaders, this.customizers, + this.requestCustomizers); } /** @@ -311,9 +294,10 @@ public class RestTemplateBuilder { */ public RestTemplateBuilder additionalInterceptors(Collection interceptors) { Assert.notNull(interceptors, "interceptors must not be null"); - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.restTemplateCustomizers, - this.requestFactoryCustomizer, append(this.interceptors, interceptors), this.httpHeadersCustomizers); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + this.messageConverters, append(this.interceptors, interceptors), this.requestFactory, + this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, this.defaultHeaders, + this.customizers, this.requestCustomizers); } /** @@ -341,15 +325,15 @@ public class RestTemplateBuilder { /** * Set the {@code Supplier} of {@link ClientHttpRequestFactory} that should be called * each time we {@link #build()} a new {@link RestTemplate} instance. - * @param requestFactorySupplier the supplier for the request factory + * @param requestFactory the supplier for the request factory * @return a new builder instance * @since 2.0.0 */ - public RestTemplateBuilder requestFactory(Supplier requestFactorySupplier) { - Assert.notNull(requestFactorySupplier, "RequestFactory Supplier must not be null"); - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.restTemplateCustomizers, - this.requestFactoryCustomizer, this.interceptors, this.httpHeadersCustomizers); + public RestTemplateBuilder requestFactory(Supplier requestFactory) { + Assert.notNull(requestFactory, "RequestFactory Supplier must not be null"); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + this.messageConverters, this.interceptors, requestFactory, this.uriTemplateHandler, this.errorHandler, + this.basicAuthentication, this.defaultHeaders, this.customizers, this.requestCustomizers); } /** @@ -360,9 +344,9 @@ public class RestTemplateBuilder { */ public RestTemplateBuilder uriTemplateHandler(UriTemplateHandler uriTemplateHandler) { Assert.notNull(uriTemplateHandler, "UriTemplateHandler must not be null"); - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, uriTemplateHandler, this.errorHandler, this.restTemplateCustomizers, - this.requestFactoryCustomizer, this.interceptors, this.httpHeadersCustomizers); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + this.messageConverters, this.interceptors, this.requestFactory, uriTemplateHandler, this.errorHandler, + this.basicAuthentication, this.defaultHeaders, this.customizers, this.requestCustomizers); } /** @@ -373,124 +357,9 @@ public class RestTemplateBuilder { */ public RestTemplateBuilder errorHandler(ResponseErrorHandler errorHandler) { Assert.notNull(errorHandler, "ErrorHandler must not be null"); - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, errorHandler, this.restTemplateCustomizers, - this.requestFactoryCustomizer, this.interceptors, this.httpHeadersCustomizers); - } - - /** - * Add a header to requests with the given value unless a custom header has been set - * before. - * @param header the header - * @param value the value - * @return a new builder instance - * @see #additionalHttpHeadersCustomizers(HttpHeadersCustomizer...) - */ - public RestTemplateBuilder defaultHeader(String header, String value) { - return additionalHttpHeadersCustomizers(SimpleHttpHeaderDefaultingCustomizer.singleHeader(header, value)); - } - - /** - * Add a headers to requests with the given value unless a custom header has been set - * before. - * @param headers the headers - * @return a new builder instance - * @see #additionalHttpHeadersCustomizers(HttpHeadersCustomizer...) - */ - public RestTemplateBuilder defaultHeaders(HttpHeaders headers) { - return additionalHttpHeadersCustomizers(new SimpleHttpHeaderDefaultingCustomizer(headers)); - } - - /** - * Set the {@link HttpHeadersCustomizer HttpHeadersCustomizers} that should be applied - * to the {@link HttpHeaders} on the request. Customizers are applied in the order - * that they were added. Setting this value will replace any previously configured - * http headers customizers. - * @param httpHeadersCustomizers the customizers to set - * @return a new builder instance - * @see #additionalHttpHeadersCustomizers(HttpHeadersCustomizer...) - */ - public RestTemplateBuilder httpHeadersCustomizers(HttpHeadersCustomizer... httpHeadersCustomizers) { - Assert.notNull(httpHeadersCustomizers, "HttpHeadersCustomizers must not be null"); - return httpHeadersCustomizers(Arrays.asList(httpHeadersCustomizers)); - } - - /** - * Set the {@link HttpHeadersCustomizer HttpHeadersCustomizers} that should be applied - * to the {@link HttpHeaders} on the request. Customizers are applied in the order - * that they were added. Setting this value will replace any previously configured - * http headers customizers. - * @param httpHeadersCustomizers the customizers to set - * @return a new builder instance - * @see #additionalHttpHeadersCustomizers(HttpHeadersCustomizer...) - */ - public RestTemplateBuilder httpHeadersCustomizers( - Collection httpHeadersCustomizers) { - Assert.notNull(httpHeadersCustomizers, "HttpHeadersCustomizers must not be null"); - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.restTemplateCustomizers, - this.requestFactoryCustomizer, this.interceptors, - Collections.unmodifiableSet(new LinkedHashSet<>(this.httpHeadersCustomizers))); - } - - /** - * Add the {@link HttpHeadersCustomizer HttpHeadersCustomizers} that should be applied - * to the {@link HttpHeaders} on the request. Customizers are applied in the order - * that they were added. - * @param httpHeadersCustomizers the customizers to set - * @return a new builder instance - * @see #httpHeadersCustomizers(HttpHeadersCustomizer...) - */ - public RestTemplateBuilder additionalHttpHeadersCustomizers(HttpHeadersCustomizer... httpHeadersCustomizers) { - Assert.notNull(httpHeadersCustomizers, "HttpHeadersCustomizers must not be null"); - return additionalHttpHeadersCustomizers(true, httpHeadersCustomizers); - } - - /** - * Add the {@link HttpHeadersCustomizer HttpHeadersCustomizers} that should be applied - * to the {@link HttpHeaders} on the request. Customizers are applied in the order - * that they were added. - * @param append if true adds customizers to the end otherwise to the beginning - * @param httpHeadersCustomizers the customizers to set - * @return a new builder instance - * @see #httpHeadersCustomizers(HttpHeadersCustomizer...) - */ - public RestTemplateBuilder additionalHttpHeadersCustomizers(boolean append, - HttpHeadersCustomizer... httpHeadersCustomizers) { - Assert.notNull(httpHeadersCustomizers, "HttpHeadersCustomizers must not be null"); - return additionalHttpHeadersCustomizers(append, Arrays.asList(httpHeadersCustomizers)); - } - - /** - * Add the {@link HttpHeadersCustomizer HttpHeadersCustomizers} that should be applied - * to the {@link HttpHeaders} on the request. Customizers are applied in the order - * that they were added. - * @param httpHeadersCustomizers the customizers to set - * @return a new builder instance - * @see #httpHeadersCustomizers(HttpHeadersCustomizer...) - */ - public RestTemplateBuilder additionalHttpHeadersCustomizers( - Collection httpHeadersCustomizers) { - return additionalHttpHeadersCustomizers(true, httpHeadersCustomizers); - } - - /** - * Add the {@link HttpHeadersCustomizer HttpHeadersCustomizers} that should be applied - * to the {@link HttpHeaders} on the request. Customizers are applied in the order - * that they were added. - * @param append if true adds customizers to the end otherwise to the beginning - * @param httpHeadersCustomizers the customizers to set - * @return a new builder instance - * @see #httpHeadersCustomizers(HttpHeadersCustomizer...) - */ - public RestTemplateBuilder additionalHttpHeadersCustomizers(boolean append, - Collection httpHeadersCustomizers) { - Assert.notNull(httpHeadersCustomizers, "HttpHeadersCustomizers must not be null"); - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.restTemplateCustomizers, - this.requestFactoryCustomizer, this.interceptors, - append ? append(this.httpHeadersCustomizers, httpHeadersCustomizers) - : append(httpHeadersCustomizers, this.httpHeadersCustomizers)); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + this.messageConverters, this.interceptors, this.requestFactory, this.uriTemplateHandler, errorHandler, + this.basicAuthentication, this.defaultHeaders, this.customizers, this.requestCustomizers); } /** @@ -508,113 +377,33 @@ public class RestTemplateBuilder { /** * Add HTTP Basic Authentication to requests with the given username/password pair, - * unless a custom Authorization header has been set before. Customizer is added to - * the beginning of the {@link HttpHeadersCustomizer HttpHeadersCustomizers} - * collection. + * unless a custom Authorization header has been set before. * @param username the user name * @param password the password * @param charset the charset to use * @return a new builder instance * @since 2.2.0 - * @see #additionalHttpHeadersCustomizers(HttpHeadersCustomizer...) - * @see SimpleHttpHeaderDefaultingCustomizer#basicAuthentication(String, String, - * Charset) */ public RestTemplateBuilder basicAuthentication(String username, String password, Charset charset) { - return additionalHttpHeadersCustomizers(false, - SimpleHttpHeaderDefaultingCustomizer.basicAuthentication(username, password, charset)); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + this.messageConverters, this.interceptors, this.requestFactory, this.uriTemplateHandler, + this.errorHandler, new BasicAuthentication(username, password, charset), this.defaultHeaders, + this.customizers, this.requestCustomizers); } /** - * Set the {@link RestTemplateCustomizer RestTemplateCustomizers} that should be - * applied to the {@link RestTemplate}. Customizers are applied in the order that they - * were added after builder configuration has been applied. Setting this value will - * replace any previously configured customizers. - * @param restTemplateCustomizers the customizers to set + * Add a default header that will be set if not already present on the outgoing + * {@link HttpClientRequest}. + * @param name the name of the header + * @param value the header value * @return a new builder instance - * @see #additionalCustomizers(RestTemplateCustomizer...) + * @since 2.2.0 */ - public RestTemplateBuilder customizers(RestTemplateCustomizer... restTemplateCustomizers) { - Assert.notNull(restTemplateCustomizers, "RestTemplateCustomizers must not be null"); - return customizers(Arrays.asList(restTemplateCustomizers)); - } - - /** - * Set the {@link RestTemplateCustomizer RestTemplateCustomizers} that should be - * applied to the {@link RestTemplate}. Customizers are applied in the order that they - * were added after builder configuration has been applied. Setting this value will - * replace any previously configured customizers. - * @param restTemplateCustomizers the customizers to set - * @return a new builder instance - * @see #additionalCustomizers(RestTemplateCustomizer...) - */ - public RestTemplateBuilder customizers(Collection restTemplateCustomizers) { - Assert.notNull(restTemplateCustomizers, "RestTemplateCustomizers must not be null"); - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, - - Collections.unmodifiableSet(new LinkedHashSet(restTemplateCustomizers)), - this.requestFactoryCustomizer, this.interceptors, this.httpHeadersCustomizers); - } - - /** - * Add {@link RestTemplateCustomizer RestTemplateCustomizers} that should be applied - * to the {@link RestTemplate}. Customizers are applied in the order that they were - * added after builder configuration has been applied. - * @param restTemplateCustomizers the customizers to add - * @return a new builder instance - * @see #customizers(RestTemplateCustomizer...) - */ - public RestTemplateBuilder additionalCustomizers(RestTemplateCustomizer... restTemplateCustomizers) { - Assert.notNull(restTemplateCustomizers, "RestTemplateCustomizers must not be null"); - return additionalCustomizers(true, restTemplateCustomizers); - } - - /** - * Add {@link RestTemplateCustomizer RestTemplateCustomizers} that should be applied - * to the {@link RestTemplate}. Customizers are applied in the order that they were - * added after builder configuration has been applied. - * @param append if true adds customizers to the end otherwise to the beginning - * @param restTemplateCustomizers the customizers to add - * @return a new builder instance - * @see #customizers(RestTemplateCustomizer...) - */ - public RestTemplateBuilder additionalCustomizers(boolean append, - RestTemplateCustomizer... restTemplateCustomizers) { - Assert.notNull(restTemplateCustomizers, "RestTemplateCustomizers must not be null"); - return additionalCustomizers(append, Arrays.asList(restTemplateCustomizers)); - } - - /** - * Add {@link RestTemplateCustomizer RestTemplateCustomizers} that should be applied - * to the {@link RestTemplate}. Customizers are applied in the order that they were - * added after builder configuration has been applied. - * @param customizers the customizers to add - * @return a new builder instance - * @see #customizers(RestTemplateCustomizer...) - */ - public RestTemplateBuilder additionalCustomizers(Collection customizers) { - Assert.notNull(customizers, "RestTemplateCustomizers must not be null"); - return additionalCustomizers(true, customizers); - } - - /** - * Add {@link RestTemplateCustomizer RestTemplateCustomizers} that should be applied - * to the {@link RestTemplate}. Customizers are applied in the order that they were - * added after builder configuration has been applied. - * @param append if true adds customizers to the end otherwise to the beginning - * @param customizers the customizers to add - * @return a new builder instance - * @see #customizers(RestTemplateCustomizer...) - */ - public RestTemplateBuilder additionalCustomizers(boolean append, - Collection customizers) { - Assert.notNull(customizers, "RestTemplateCustomizers must not be null"); - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, - append ? append(this.restTemplateCustomizers, customizers) - : append(customizers, this.restTemplateCustomizers), - this.requestFactoryCustomizer, this.interceptors, this.httpHeadersCustomizers); + public RestTemplateBuilder defaultHeader(String name, String value) { + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + this.messageConverters, this.interceptors, this.requestFactory, this.uriTemplateHandler, + this.errorHandler, this.basicAuthentication, append(this.defaultHeaders, name, value), this.customizers, + this.requestCustomizers); } /** @@ -624,10 +413,10 @@ public class RestTemplateBuilder { * @since 2.1.0 */ public RestTemplateBuilder setConnectTimeout(Duration connectTimeout) { - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.restTemplateCustomizers, - this.requestFactoryCustomizer.connectTimeout(connectTimeout), this.interceptors, - this.httpHeadersCustomizers); + return new RestTemplateBuilder(this.requestFactoryCustomizer.connectTimeout(connectTimeout), + this.detectRequestFactory, this.rootUri, this.messageConverters, this.interceptors, this.requestFactory, + this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, this.defaultHeaders, + this.customizers, this.requestCustomizers); } /** @@ -637,9 +426,10 @@ public class RestTemplateBuilder { * @since 2.1.0 */ public RestTemplateBuilder setReadTimeout(Duration readTimeout) { - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.restTemplateCustomizers, - this.requestFactoryCustomizer.readTimeout(readTimeout), this.interceptors, this.httpHeadersCustomizers); + return new RestTemplateBuilder(this.requestFactoryCustomizer.readTimeout(readTimeout), + this.detectRequestFactory, this.rootUri, this.messageConverters, this.interceptors, this.requestFactory, + this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, this.defaultHeaders, + this.customizers, this.requestCustomizers); } /** @@ -652,10 +442,136 @@ public class RestTemplateBuilder { * @see HttpComponentsClientHttpRequestFactory#setBufferRequestBody(boolean) */ public RestTemplateBuilder setBufferRequestBody(boolean bufferRequestBody) { - return new RestTemplateBuilder(this.detectRequestFactory, this.rootUri, this.messageConverters, - this.requestFactorySupplier, this.uriTemplateHandler, this.errorHandler, this.restTemplateCustomizers, - this.requestFactoryCustomizer.bufferRequestBody(bufferRequestBody), this.interceptors, - this.httpHeadersCustomizers); + return new RestTemplateBuilder(this.requestFactoryCustomizer.bufferRequestBody(bufferRequestBody), + this.detectRequestFactory, this.rootUri, this.messageConverters, this.interceptors, this.requestFactory, + this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, this.defaultHeaders, + this.customizers, this.requestCustomizers); + } + + /** + * Set the {@link RestTemplateCustomizer RestTemplateCustomizers} that should be + * applied to the {@link RestTemplate}. Customizers are applied in the order that they + * were added after builder configuration has been applied. Setting this value will + * replace any previously configured customizers. + * @param customizers the customizers to set + * @return a new builder instance + * @see #additionalCustomizers(RestTemplateCustomizer...) + */ + public RestTemplateBuilder customizers(RestTemplateCustomizer... customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return customizers(Arrays.asList(customizers)); + } + + /** + * Set the {@link RestTemplateCustomizer RestTemplateCustomizers} that should be + * applied to the {@link RestTemplate}. Customizers are applied in the order that they + * were added after builder configuration has been applied. Setting this value will + * replace any previously configured customizers. + * @param customizers the customizers to set + * @return a new builder instance + * @see #additionalCustomizers(RestTemplateCustomizer...) + */ + public RestTemplateBuilder customizers(Collection customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + this.messageConverters, this.interceptors, this.requestFactory, this.uriTemplateHandler, + this.errorHandler, this.basicAuthentication, this.defaultHeaders, setOf(customizers), + this.requestCustomizers); + } + + /** + * Add {@link RestTemplateCustomizer RestTemplateCustomizers} that should be applied + * to the {@link RestTemplate}. Customizers are applied in the order that they were + * added after builder configuration has been applied. + * @param customizers the customizers to add + * @return a new builder instance + * @see #customizers(RestTemplateCustomizer...) + */ + public RestTemplateBuilder additionalCustomizers(RestTemplateCustomizer... customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return additionalCustomizers(Arrays.asList(customizers)); + } + + /** + * Add {@link RestTemplateCustomizer RestTemplateCustomizers} that should be applied + * to the {@link RestTemplate}. Customizers are applied in the order that they were + * added after builder configuration has been applied. + * @param customizers the customizers to add + * @return a new builder instance + * @see #customizers(RestTemplateCustomizer...) + */ + public RestTemplateBuilder additionalCustomizers(Collection customizers) { + Assert.notNull(customizers, "RestTemplateCustomizers must not be null"); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + this.messageConverters, this.interceptors, this.requestFactory, this.uriTemplateHandler, + this.errorHandler, this.basicAuthentication, this.defaultHeaders, append(this.customizers, customizers), + this.requestCustomizers); + } + + /** + * Set the {@link RestTemplateRequestCustomizer RestTemplateRequestCustomizers} that + * should be applied to the {@link ClientHttpRequest}. Customizers are applied in the + * order that they were added. Setting this value will replace any previously + * configured request customizers. + * @param requestCustomizers the request customizers to set + * @return a new builder instance + * @since 2.2.0 + * @see #additionalRequestCustomizers(RestTemplateRequestCustomizer...) + */ + public RestTemplateBuilder requestCustomizers(RestTemplateRequestCustomizer... requestCustomizers) { + Assert.notNull(requestCustomizers, "RequestCustomizers must not be null"); + return requestCustomizers(Arrays.asList(requestCustomizers)); + } + + /** + * Set the {@link RestTemplateRequestCustomizer RestTemplateRequestCustomizers} that + * should be applied to the {@link ClientHttpRequest}. Customizers are applied in the + * order that they were added. Setting this value will replace any previously + * configured request customizers. + * @param requestCustomizers the request customizers to set + * @return a new builder instance + * @since 2.2.0 + * @see #additionalRequestCustomizers(RestTemplateRequestCustomizer...) + */ + public RestTemplateBuilder requestCustomizers( + Collection> requestCustomizers) { + Assert.notNull(requestCustomizers, "RequestCustomizers must not be null"); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + this.messageConverters, this.interceptors, this.requestFactory, this.uriTemplateHandler, + this.errorHandler, this.basicAuthentication, this.defaultHeaders, this.customizers, + setOf(requestCustomizers)); + } + + /** + * Add the {@link RestTemplateRequestCustomizer RestTemplateRequestCustomizers} that + * should be applied to the {@link ClientHttpRequest}. Customizers are applied in the + * order that they were added. + * @param requestCustomizers the request customizers to add + * @return a new builder instance + * @since 2.2.0 + * @see #requestCustomizers(RestTemplateRequestCustomizer...) + */ + public RestTemplateBuilder additionalRequestCustomizers(RestTemplateRequestCustomizer... requestCustomizers) { + Assert.notNull(requestCustomizers, "RequestCustomizers must not be null"); + return additionalRequestCustomizers(Arrays.asList(requestCustomizers)); + } + + /** + * Add the {@link RestTemplateRequestCustomizer RestTemplateRequestCustomizers} that + * should be applied to the {@link ClientHttpRequest}. Customizers are applied in the + * order that they were added. + * @param requestCustomizers the request customizers to add + * @return a new builder instance + * @since 2.2.0 + * @see #requestCustomizers(Collection) + */ + public RestTemplateBuilder additionalRequestCustomizers( + Collection> requestCustomizers) { + Assert.notNull(requestCustomizers, "RequestCustomizers must not be null"); + return new RestTemplateBuilder(this.requestFactoryCustomizer, this.detectRequestFactory, this.rootUri, + this.messageConverters, this.interceptors, this.requestFactory, this.uriTemplateHandler, + this.errorHandler, this.basicAuthentication, this.defaultHeaders, this.customizers, + append(this.requestCustomizers, requestCustomizers)); } /** @@ -694,9 +610,7 @@ public class RestTemplateBuilder { if (requestFactory != null) { restTemplate.setRequestFactory(requestFactory); } - if (!CollectionUtils.isEmpty(this.httpHeadersCustomizers)) { - configureHttpHeadersCustomizers(restTemplate); - } + addClientHttpRequestFactoryWrapper(restTemplate); if (!CollectionUtils.isEmpty(this.messageConverters)) { restTemplate.setMessageConverters(new ArrayList<>(this.messageConverters)); } @@ -710,8 +624,8 @@ public class RestTemplateBuilder { RootUriTemplateHandler.addTo(restTemplate, this.rootUri); } restTemplate.getInterceptors().addAll(this.interceptors); - if (!CollectionUtils.isEmpty(this.restTemplateCustomizers)) { - for (RestTemplateCustomizer customizer : this.restTemplateCustomizers) { + if (!CollectionUtils.isEmpty(this.customizers)) { + for (RestTemplateCustomizer customizer : this.customizers) { customizer.customize(restTemplate); } } @@ -726,8 +640,8 @@ public class RestTemplateBuilder { */ public ClientHttpRequestFactory buildRequestFactory() { ClientHttpRequestFactory requestFactory = null; - if (this.requestFactorySupplier != null) { - requestFactory = this.requestFactorySupplier.get(); + if (this.requestFactory != null) { + requestFactory = this.requestFactory.get(); } else if (this.detectRequestFactory) { requestFactory = new ClientHttpRequestFactorySupplier().get(); @@ -740,7 +654,10 @@ public class RestTemplateBuilder { return requestFactory; } - private void configureHttpHeadersCustomizers(RestTemplate restTemplate) { + private void addClientHttpRequestFactoryWrapper(RestTemplate restTemplate) { + if (this.basicAuthentication == null && this.defaultHeaders.isEmpty() && this.requestCustomizers.isEmpty()) { + return; + } List interceptors = null; if (!restTemplate.getInterceptors().isEmpty()) { // Stash and clear the interceptors so we can access the real factory @@ -748,14 +665,24 @@ public class RestTemplateBuilder { restTemplate.getInterceptors().clear(); } ClientHttpRequestFactory requestFactory = restTemplate.getRequestFactory(); - restTemplate.setRequestFactory( - new HttpHeadersCustomizingClientHttpRequestFactory(this.httpHeadersCustomizers, requestFactory)); + ClientHttpRequestFactory wrapper = new RestTemplateBuilderClientHttpRequestFactoryWrapper(requestFactory, + this.basicAuthentication, this.defaultHeaders, this.requestCustomizers); + restTemplate.setRequestFactory(wrapper); // Restore the original interceptors if (interceptors != null) { restTemplate.getInterceptors().addAll(interceptors); } } + @SuppressWarnings("unchecked") + private Set setOf(T... items) { + return setOf(Arrays.asList(items)); + } + + private Set setOf(Collection collection) { + return Collections.unmodifiableSet(new LinkedHashSet<>(collection)); + } + private static Set append(Collection collection, Collection additions) { Set result = new LinkedHashSet<>((collection != null) ? collection : Collections.emptySet()); if (additions != null) { @@ -764,6 +691,15 @@ public class RestTemplateBuilder { return Collections.unmodifiableSet(result); } + private static Map append(Map map, K key, V value) { + Map result = new LinkedHashMap<>((map != null) ? map : Collections.emptyMap()); + result.put(key, value); + return Collections.unmodifiableMap(result); + } + + /** + * Internal customizer used to apply {@link ClientHttpRequestFactory} settings. + */ private static class RequestFactoryCustomizer implements Consumer { private final Duration connectTimeout; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilderClientHttpRequestFactoryWrapper.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilderClientHttpRequestFactoryWrapper.java new file mode 100644 index 0000000000..bc3019ad65 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilderClientHttpRequestFactoryWrapper.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * 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 + * + * https://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.springframework.boot.web.client; + +import java.io.IOException; +import java.net.URI; +import java.util.Map; +import java.util.Set; + +import org.springframework.boot.util.LambdaSafe; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.AbstractClientHttpRequestFactoryWrapper; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpRequestFactory; + +/** + * {@link ClientHttpRequestFactory} to apply customizations from the + * {@link RestTemplateBuilder}. + * + * @author Dmytro Nosan + * @author Ilya Lukyanovich + */ +class RestTemplateBuilderClientHttpRequestFactoryWrapper extends AbstractClientHttpRequestFactoryWrapper { + + private final BasicAuthentication basicAuthentication; + + private final Map defaultHeaders; + + private final Set> requestCustomizers; + + RestTemplateBuilderClientHttpRequestFactoryWrapper(ClientHttpRequestFactory requestFactory, + BasicAuthentication basicAuthentication, Map defaultHeaders, + Set> requestCustomizers) { + super(requestFactory); + this.basicAuthentication = basicAuthentication; + this.defaultHeaders = defaultHeaders; + this.requestCustomizers = requestCustomizers; + } + + @Override + @SuppressWarnings("unchecked") + protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, ClientHttpRequestFactory requestFactory) + throws IOException { + ClientHttpRequest request = requestFactory.createRequest(uri, httpMethod); + HttpHeaders headers = request.getHeaders(); + if (this.basicAuthentication != null) { + this.basicAuthentication.applyTo(headers); + } + this.defaultHeaders.forEach(headers::addIfAbsent); + LambdaSafe.callbacks(RestTemplateRequestCustomizer.class, this.requestCustomizers, request) + .invoke((customizer) -> customizer.customize(request)); + return request; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/HttpHeadersCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateRequestCustomizer.java similarity index 53% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/HttpHeadersCustomizer.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateRequestCustomizer.java index cfa68532c8..2d7e0210d7 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/HttpHeadersCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateRequestCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,21 +16,26 @@ package org.springframework.boot.web.client; -import org.springframework.http.HttpHeaders; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.web.client.RestTemplate; /** - * Callback interface that can be used to customize a {@link HttpHeaders}. + * Callback interface that can be used to customize the {@link ClientHttpRequest} sent + * from a {@link RestTemplate}. * + * @param the {@link ClientHttpRequest} type * @author Ilya Lukyanovich - * @see HttpHeadersCustomizingClientHttpRequestFactory + * @author Phillip Webb + * @since 2.2.0 + * @see RestTemplateBuilder */ @FunctionalInterface -public interface HttpHeadersCustomizer { +public interface RestTemplateRequestCustomizer { /** - * Callback to customize a {@link HttpHeaders} instance. - * @param headers the headers to customize + * Customize the specified {@link ClientHttpRequest}. + * @param request the request to customize */ - void applyTo(HttpHeaders headers); + void customize(T request); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/SimpleHttpHeaderDefaultingCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/SimpleHttpHeaderDefaultingCustomizer.java deleted file mode 100644 index 3a4e6734e1..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/SimpleHttpHeaderDefaultingCustomizer.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.web.client; - -import java.nio.charset.Charset; - -import org.springframework.http.HttpHeaders; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; -import org.springframework.util.LinkedMultiValueMap; - -/** - * A {@link AbstractHttpHeadersDefaultingCustomizer} that uses provided - * {@link HttpHeaders} instance as default headers. - * - * @author Ilya Lukyanovich - * @see HttpHeadersCustomizingClientHttpRequestFactory - */ -public class SimpleHttpHeaderDefaultingCustomizer extends AbstractHttpHeadersDefaultingCustomizer { - - private final HttpHeaders httpHeaders; - - public SimpleHttpHeaderDefaultingCustomizer(HttpHeaders httpHeaders) { - Assert.notNull(httpHeaders, "Header must not be null"); - this.httpHeaders = httpHeaders; - } - - @Override - protected HttpHeaders createHeaders() { - return new HttpHeaders(new LinkedMultiValueMap<>(this.httpHeaders)); - } - - /** - * A factory method that creates a {@link SimpleHttpHeaderDefaultingCustomizer} with a - * single header and a single value. - * @param header the header - * @param value the value - * @return new {@link SimpleHttpHeaderDefaultingCustomizer} instance - * @see HttpHeaders#set(String, String) - */ - public static HttpHeadersCustomizer singleHeader(@NonNull String header, @NonNull String value) { - Assert.notNull(header, "Header must not be null empty"); - Assert.notNull(value, "Value must not be null empty"); - HttpHeaders headers = new HttpHeaders(); - headers.set(header, value); - return new SimpleHttpHeaderDefaultingCustomizer(headers); - } - - /** - * A factory method that creates a {@link SimpleHttpHeaderDefaultingCustomizer} for - * {@link HttpHeaders#AUTHORIZATION} header with pre-defined username and password - * pair. - * @param username the username - * @param password the password - * @return new {@link SimpleHttpHeaderDefaultingCustomizer} instance - * @see #basicAuthentication(String, String, Charset) - */ - public static HttpHeadersCustomizer basicAuthentication(@NonNull String username, @NonNull String password) { - return basicAuthentication(username, password, null); - } - - /** - * A factory method that creates a {@link SimpleHttpHeaderDefaultingCustomizer} for - * {@link HttpHeaders#AUTHORIZATION} header with pre-defined username and password - * pair. - * @param username the username - * @param password the password - * @param charset the header encoding charset - * @return new {@link SimpleHttpHeaderDefaultingCustomizer} instance - * @see HttpHeaders#setBasicAuth(String, String, Charset) - */ - public static HttpHeadersCustomizer basicAuthentication(@NonNull String username, @NonNull String password, - @Nullable Charset charset) { - Assert.notNull(username, "Username must not be null"); - Assert.notNull(password, "Password must not be null"); - HttpHeaders headers = new HttpHeaders(); - headers.setBasicAuth(username, password, charset); - return new SimpleHttpHeaderDefaultingCustomizer(headers); - } - -} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/HttpHeadersCustomizingClientHttpRequestFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/HttpHeadersCustomizingClientHttpRequestFactoryTests.java deleted file mode 100644 index e98a54bb7f..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/HttpHeadersCustomizingClientHttpRequestFactoryTests.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.web.client; - -import java.io.IOException; -import java.net.URI; -import java.util.Arrays; -import java.util.Collections; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.ClientHttpRequest; -import org.springframework.http.client.ClientHttpRequestFactory; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link HttpHeadersCustomizingClientHttpRequestFactory}. - * - * @author Dmytro Nosan - * @author Ilya Lukyanovich - */ -public class HttpHeadersCustomizingClientHttpRequestFactoryTests { - - private final HttpHeaders headers = new HttpHeaders(); - - private ClientHttpRequestFactory requestFactory; - - @BeforeEach - public void setUp() throws IOException { - this.requestFactory = mock(ClientHttpRequestFactory.class); - ClientHttpRequest request = mock(ClientHttpRequest.class); - given(this.requestFactory.createRequest(any(), any())).willReturn(request); - given(request.getHeaders()).willReturn(this.headers); - } - - @Test - void shouldAddAuthorizationHeader() throws IOException { - this.requestFactory = new HttpHeadersCustomizingClientHttpRequestFactory( - Collections.singleton(SimpleHttpHeaderDefaultingCustomizer.basicAuthentication("spring", "boot", null)), - this.requestFactory); - ClientHttpRequest request = createRequest(); - assertThat(request.getHeaders().get(HttpHeaders.AUTHORIZATION)).containsExactly("Basic c3ByaW5nOmJvb3Q="); - } - - @Test - void shouldNotAddAuthorizationHeaderAuthorizationAlreadySet() throws IOException { - this.headers.setBasicAuth("boot", "spring"); - this.requestFactory = new HttpHeadersCustomizingClientHttpRequestFactory( - Collections.singleton(SimpleHttpHeaderDefaultingCustomizer.basicAuthentication("spring", "boot", null)), - this.requestFactory); - ClientHttpRequest request = createRequest(); - assertThat(request.getHeaders().get(HttpHeaders.AUTHORIZATION)).doesNotContain("Basic c3ByaW5nOmJvb3Q="); - - } - - @Test - void shouldApplyCustomizersInTheProvidedOrder() throws IOException { - this.requestFactory = new HttpHeadersCustomizingClientHttpRequestFactory( - Arrays.asList((headers) -> headers.add("foo", "bar"), - SimpleHttpHeaderDefaultingCustomizer.basicAuthentication("spring", "boot", null), - SimpleHttpHeaderDefaultingCustomizer.singleHeader(HttpHeaders.AUTHORIZATION, "won't do")), - this.requestFactory); - ClientHttpRequest request = createRequest(); - assertThat(request.getHeaders()).containsOnlyKeys("foo", HttpHeaders.AUTHORIZATION); - assertThat(request.getHeaders().get("foo")).containsExactly("bar"); - assertThat(request.getHeaders().get(HttpHeaders.AUTHORIZATION)).containsExactly("Basic c3ByaW5nOmJvb3Q="); - } - - private ClientHttpRequest createRequest() throws IOException { - return this.requestFactory.createRequest(URI.create("https://localhost:8080"), HttpMethod.POST); - } - -} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RestTemplateBuilderClientHttpRequestFactoryWrapperTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RestTemplateBuilderClientHttpRequestFactoryWrapperTests.java new file mode 100644 index 0000000000..f136547137 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RestTemplateBuilderClientHttpRequestFactoryWrapperTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * 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 + * + * https://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.springframework.boot.web.client; + +import java.io.IOException; +import java.net.URI; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpRequestFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RestTemplateBuilderClientHttpRequestFactoryWrapper}. + * + * @author Dmytro Nosan + * @author Ilya Lukyanovich + * @author Phillip Webb + */ +public class RestTemplateBuilderClientHttpRequestFactoryWrapperTests { + + private ClientHttpRequestFactory requestFactory; + + private final HttpHeaders headers = new HttpHeaders(); + + @BeforeEach + public void setUp() throws IOException { + this.requestFactory = mock(ClientHttpRequestFactory.class); + ClientHttpRequest request = mock(ClientHttpRequest.class); + given(this.requestFactory.createRequest(any(), any())).willReturn(request); + given(request.getHeaders()).willReturn(this.headers); + } + + @Test + void createRequestWhenHasBasicAuthAndNoAuthHeaderAddsHeader() throws IOException { + this.requestFactory = new RestTemplateBuilderClientHttpRequestFactoryWrapper(this.requestFactory, + new BasicAuthentication("spring", "boot", null), Collections.emptyMap(), Collections.emptySet()); + ClientHttpRequest request = createRequest(); + assertThat(request.getHeaders().get(HttpHeaders.AUTHORIZATION)).containsExactly("Basic c3ByaW5nOmJvb3Q="); + } + + @Test + void createRequestWhenHasBasicAuthAndExistingAuthHeaderDoesNotAddHeader() throws IOException { + this.headers.setBasicAuth("boot", "spring"); + this.requestFactory = new RestTemplateBuilderClientHttpRequestFactoryWrapper(this.requestFactory, + new BasicAuthentication("spring", "boot", null), Collections.emptyMap(), Collections.emptySet()); + ClientHttpRequest request = createRequest(); + assertThat(request.getHeaders().get(HttpHeaders.AUTHORIZATION)).doesNotContain("Basic c3ByaW5nOmJvb3Q="); + } + + @Test + void createRequestWhenHasDefaultHeadersAddsMissing() throws IOException { + this.headers.add("one", "existing"); + Map defaultHeaders = new LinkedHashMap<>(); + defaultHeaders.put("one", "1"); + defaultHeaders.put("two", "2"); + defaultHeaders.put("three", "3"); + this.requestFactory = new RestTemplateBuilderClientHttpRequestFactoryWrapper(this.requestFactory, null, + defaultHeaders, Collections.emptySet()); + ClientHttpRequest request = createRequest(); + assertThat(request.getHeaders().get("one")).containsExactly("existing"); + assertThat(request.getHeaders().get("two")).containsExactly("2"); + assertThat(request.getHeaders().get("three")).containsExactly("3"); + } + + @Test + @SuppressWarnings("unchecked") + void createRequestWhenHasRequestCustomizersAppliesThemInOrder() throws IOException { + Set> customizers = new LinkedHashSet<>(); + customizers.add(mock(RestTemplateRequestCustomizer.class)); + customizers.add(mock(RestTemplateRequestCustomizer.class)); + customizers.add(mock(RestTemplateRequestCustomizer.class)); + this.requestFactory = new RestTemplateBuilderClientHttpRequestFactoryWrapper(this.requestFactory, null, + Collections.emptyMap(), customizers); + ClientHttpRequest request = createRequest(); + InOrder inOrder = inOrder(customizers.toArray()); + for (RestTemplateRequestCustomizer customizer : customizers) { + inOrder.verify((RestTemplateRequestCustomizer) customizer).customize(request); + } + } + + private ClientHttpRequest createRequest() throws IOException { + return this.requestFactory.createRequest(URI.create("https://localhost:8080"), HttpMethod.POST); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RestTemplateBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RestTemplateBuilderTests.java index cf562f9425..818526780e 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RestTemplateBuilderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RestTemplateBuilderTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.web.client; +import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -52,6 +53,7 @@ import org.springframework.web.util.UriTemplateHandler; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.entry; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -69,6 +71,7 @@ import static org.springframework.test.web.client.response.MockRestResponseCreat * @author Andy Wilkinson * @author Dmytro Nosan * @author Kevin Strijbos + * @author Ilya Lukyanovich */ class RestTemplateBuilderTests { @@ -310,17 +313,45 @@ class RestTemplateBuilderTests { assertThat(request.getHeaders().get(HttpHeaders.AUTHORIZATION)).containsExactly("Basic c3ByaW5nOmJvb3Q="); } + @Test + void defaultHeaderAddsHeader() throws IOException { + RestTemplate template = this.builder.defaultHeader("spring", "boot").build(); + ClientHttpRequestFactory requestFactory = template.getRequestFactory(); + ClientHttpRequest request = requestFactory.createRequest(URI.create("http://localhost"), HttpMethod.GET); + assertThat(request.getHeaders()).contains(entry("spring", Collections.singletonList("boot"))); + } + + @Test + void requestCustomizersAddsCustomizers() throws IOException { + RestTemplate template = this.builder + .requestCustomizers((request) -> request.getHeaders().add("spring", "framework")).build(); + ClientHttpRequestFactory requestFactory = template.getRequestFactory(); + ClientHttpRequest request = requestFactory.createRequest(URI.create("http://localhost"), HttpMethod.GET); + assertThat(request.getHeaders()).contains(entry("spring", Collections.singletonList("framework"))); + } + + @Test + void additionalRequestCustomizersAddsCustomizers() throws IOException { + RestTemplate template = this.builder + .requestCustomizers((request) -> request.getHeaders().add("spring", "framework")) + .additionalRequestCustomizers((request) -> request.getHeaders().add("for", "java")).build(); + ClientHttpRequestFactory requestFactory = template.getRequestFactory(); + ClientHttpRequest request = requestFactory.createRequest(URI.create("http://localhost"), HttpMethod.GET); + assertThat(request.getHeaders()).contains(entry("spring", Collections.singletonList("framework"))) + .contains(entry("for", Collections.singletonList("java"))); + } + @Test void customizersWhenCustomizersAreNullShouldThrowException() { assertThatIllegalArgumentException().isThrownBy(() -> this.builder.customizers((RestTemplateCustomizer[]) null)) - .withMessageContaining("RestTemplateCustomizers must not be null"); + .withMessageContaining("Customizers must not be null"); } @Test void customizersCollectionWhenCustomizersAreNullShouldThrowException() { assertThatIllegalArgumentException() .isThrownBy(() -> this.builder.customizers((Set) null)) - .withMessageContaining("RestTemplateCustomizers must not be null"); + .withMessageContaining("Customizers must not be null"); } @Test @@ -352,7 +383,7 @@ class RestTemplateBuilderTests { void additionalCustomizersWhenCustomizersAreNullShouldThrowException() { assertThatIllegalArgumentException() .isThrownBy(() -> this.builder.additionalCustomizers((RestTemplateCustomizer[]) null)) - .withMessageContaining("RestTemplateCustomizers must not be null"); + .withMessageContaining("Customizers must not be null"); } @Test @@ -387,7 +418,8 @@ class RestTemplateBuilderTests { assertThat(actualRequestFactory).isInstanceOf(InterceptingClientHttpRequestFactory.class); ClientHttpRequestFactory authRequestFactory = (ClientHttpRequestFactory) ReflectionTestUtils .getField(actualRequestFactory, "requestFactory"); - assertThat(authRequestFactory).isInstanceOf(HttpHeadersCustomizingClientHttpRequestFactory.class); + assertThat(authRequestFactory) + .isInstanceOf(RestTemplateBuilderClientHttpRequestFactoryWrapper.class); assertThat(authRequestFactory).hasFieldOrPropertyWithValue("requestFactory", requestFactory); }).build(); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/SimpleHttpHeaderDefaultingCustomizerTest.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/SimpleHttpHeaderDefaultingCustomizerTest.java deleted file mode 100644 index 2687da2956..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/SimpleHttpHeaderDefaultingCustomizerTest.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * 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 - * - * https://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.springframework.boot.web.client; - -import org.junit.jupiter.api.Test; - -import org.springframework.http.HttpHeaders; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link SimpleHttpHeaderDefaultingCustomizer}. - * - * @author Ilya Lukyanovich - */ -class SimpleHttpHeaderDefaultingCustomizerTest { - - @Test - void testApplyTo_shouldAddAllHeaders() { - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.add("foo", "bar"); - httpHeaders.add("donut", "42"); - SimpleHttpHeaderDefaultingCustomizer customizer = new SimpleHttpHeaderDefaultingCustomizer(httpHeaders); - HttpHeaders provided = new HttpHeaders(); - customizer.applyTo(provided); - assertThat(provided).containsOnlyKeys("foo", "donut"); - assertThat(provided.get("foo")).containsExactly("bar"); - assertThat(provided.get("donut")).containsExactly("42"); - } - - @Test - void testApplyTo_shouldIgnoreProvided() { - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.add("foo", "bar"); - httpHeaders.add("donut", "42"); - SimpleHttpHeaderDefaultingCustomizer customizer = new SimpleHttpHeaderDefaultingCustomizer(httpHeaders); - HttpHeaders provided = new HttpHeaders(); - provided.add("donut", "touchme"); - customizer.applyTo(provided); - assertThat(provided).containsOnlyKeys("foo", "donut"); - assertThat(provided.get("foo")).containsExactly("bar"); - assertThat(provided.get("donut")).containsExactly("touchme"); - } - - @Test - void testSingleHeader() { - HttpHeaders provided = new HttpHeaders(); - SimpleHttpHeaderDefaultingCustomizer.singleHeader("foo", "bar").applyTo(provided); - assertThat(provided).containsOnlyKeys("foo"); - assertThat(provided.get("foo")).containsExactly("bar"); - } - - @Test - void testBasicAuthentication() { - HttpHeaders provided = new HttpHeaders(); - SimpleHttpHeaderDefaultingCustomizer.basicAuthentication("spring", "boot").applyTo(provided); - assertThat(provided).containsOnlyKeys(HttpHeaders.AUTHORIZATION); - assertThat(provided.get(HttpHeaders.AUTHORIZATION)).containsExactly("bar"); - } - -}