Skip to content

Commit a4ec819

Browse files
authored
DATAES-801 - Implement callback to enable adding custom headers in the REST HTTP request.
Original PR: spring-projects#442
1 parent 65f89f9 commit a4ec819

File tree

13 files changed

+347
-70
lines changed

13 files changed

+347
-70
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@
252252
<dependency>
253253
<groupId>com.github.tomakehurst</groupId>
254254
<artifactId>wiremock-jre8</artifactId>
255-
<version>2.25.1</version>
255+
<version>2.26.3</version>
256256
<scope>test</scope>
257257
<exclusions>
258258
<!-- these exclusions are needed because of Elasticsearch JarHell-->

src/main/asciidoc/reference/elasticsearch-clients.adoc

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -143,40 +143,47 @@ NOTE: The ReactiveClient response, especially for search operations, is bound to
143143
[[elasticsearch.clients.configuration]]
144144
== Client Configuration
145145

146-
Client behaviour can be changed via the `ClientConfiguration` that allows to set options for SSL, connect and socket timeouts.
146+
Client behaviour can be changed via the `ClientConfiguration` that allows to set options for SSL, connect and socket timeouts, headers and other parameters.
147147

148148
.Client Configuration
149149
====
150150
[source,java]
151151
----
152-
// optional if Basic Auhtentication is needed
153152
HttpHeaders httpHeaders = new HttpHeaders();
154-
httpHeaders.add("es-security-runas-user", "some-user") <1>
153+
httpHeaders.add("some-header", "on every request") <1>
155154
156155
ClientConfiguration clientConfiguration = ClientConfiguration.builder()
157156
.connectedTo("localhost:9200", "localhost:9291") <2>
158-
.withProxy("localhost:8888") <3>
159-
.withPathPrefix("ela") <4>
160-
.withConnectTimeout(Duration.ofSeconds(5)) <5>
161-
.withSocketTimeout(Duration.ofSeconds(3)) <6>
162-
.useSsl() <7>
157+
.useSsl() <3>
158+
.withProxy("localhost:8888") <4>
159+
.withPathPrefix("ela") <5>
160+
.withConnectTimeout(Duration.ofSeconds(5)) <6>
161+
.withSocketTimeout(Duration.ofSeconds(3)) <7>
163162
.withDefaultHeaders(defaultHeaders) <8>
164163
.withBasicAuth(username, password) <9>
164+
.withHeaders(() -> { <10>
165+
HttpHeaders headers = new HttpHeaders();
166+
headers.add("currentTime", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
167+
return headers;
168+
})
165169
. // ... other options
166170
.build();
167171
168172
----
169173
<1> Define default headers, if they need to be customized
170174
<2> Use the builder to provide cluster addresses, set default `HttpHeaders` or enable SSL.
171-
<3> Optionally set a proxy footnote:notreactive[not yet implemented for the reactive client].
172-
<4> Optionally set a path prefix, mostly used when different clusters a behind some reverse proxy.
173-
<5> Set the connection timeout. Default is 10 sec.
174-
<6> Set the socket timeout. Default is 5 sec.
175-
<7> Optionally enable SSL.
175+
<3> Optionally enable SSL.
176+
<4> Optionally set a proxy.
177+
<5> Optionally set a path prefix, mostly used when different clusters a behind some reverse proxy.
178+
<6> Set the connection timeout. Default is 10 sec.
179+
<7> Set the socket timeout. Default is 5 sec.
176180
<8> Optionally set headers.
177181
<9> Add basic authentication.
182+
<10> A `Supplier<Header>` function can be specified which is called every time before a request is sent to Elasticsearch - here, as an example, the current time is written in a header.
178183
====
179184

185+
IMPORTANT: Adding a Header supplier as shown in above example allows to inject headers that may change over the time, like authentication JWT tokens. If this is used in the reactive setup, the supplier function *must not* block!
186+
180187
[[elasticsearch.clients.logging]]
181188
== Client Logging
182189

src/main/java/org/springframework/data/elasticsearch/client/ClientConfiguration.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.List;
2222
import java.util.Optional;
2323
import java.util.function.Function;
24+
import java.util.function.Supplier;
2425

2526
import javax.net.ssl.HostnameVerifier;
2627
import javax.net.ssl.SSLContext;
@@ -170,6 +171,11 @@ static ClientConfiguration create(InetSocketAddress socketAddress) {
170171
*/
171172
Function<WebClient, WebClient> getWebClientConfigurer();
172173

174+
/**
175+
* @return the supplier for custom headers.
176+
*/
177+
Supplier<HttpHeaders> getHeadersSupplier();
178+
173179
/**
174180
* @author Christoph Strobl
175181
*/
@@ -335,6 +341,19 @@ default TerminalClientConfigurationBuilder withSocketTimeout(long millis) {
335341
*/
336342
TerminalClientConfigurationBuilder withWebClientConfigurer(Function<WebClient, WebClient> webClientConfigurer);
337343

344+
/**
345+
* set a supplier for custom headers. This is invoked for every HTTP request to Elasticsearch to retrieve headers
346+
* that should be sent with the request. A common use case is passing in authentication headers that may change.
347+
* <br/>
348+
* Note: When used in a reactive environment, the calling of {@link Supplier#get()} function must not do any
349+
* blocking operations. It may return {@literal null}.
350+
*
351+
* @param headers supplier function for headers, must not be {@literal null}
352+
* @return the {@link TerminalClientConfigurationBuilder}.
353+
* @since 4.0
354+
*/
355+
TerminalClientConfigurationBuilder withHeaders(Supplier<HttpHeaders> headers);
356+
338357
/**
339358
* Build the {@link ClientConfiguration} object.
340359
*

src/main/java/org/springframework/data/elasticsearch/client/ClientConfigurationBuilder.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.Arrays;
2222
import java.util.List;
2323
import java.util.function.Function;
24+
import java.util.function.Supplier;
2425
import java.util.stream.Collectors;
2526

2627
import javax.net.ssl.HostnameVerifier;
@@ -58,7 +59,8 @@ class ClientConfigurationBuilder
5859
private @Nullable String password;
5960
private @Nullable String pathPrefix;
6061
private @Nullable String proxy;
61-
private @Nullable Function<WebClient, WebClient> webClientConfigurer;
62+
private Function<WebClient, WebClient> webClientConfigurer = Function.identity();
63+
private Supplier<HttpHeaders> headersSupplier = () -> HttpHeaders.EMPTY;
6264

6365
/*
6466
* (non-Javadoc)
@@ -196,14 +198,24 @@ public TerminalClientConfigurationBuilder withPathPrefix(String pathPrefix) {
196198
}
197199

198200
@Override
199-
public TerminalClientConfigurationBuilder withWebClientConfigurer(Function<WebClient, WebClient> webClientConfigurer) {
201+
public TerminalClientConfigurationBuilder withWebClientConfigurer(
202+
Function<WebClient, WebClient> webClientConfigurer) {
200203

201204
Assert.notNull(webClientConfigurer, "webClientConfigurer must not be null");
202205

203206
this.webClientConfigurer = webClientConfigurer;
204207
return this;
205208
}
206209

210+
@Override
211+
public TerminalClientConfigurationBuilder withHeaders(Supplier<HttpHeaders> headers) {
212+
213+
Assert.notNull(headers, "headersSupplier must not be null");
214+
215+
this.headersSupplier = headers;
216+
return this;
217+
}
218+
207219
/*
208220
* (non-Javadoc)
209221
* @see org.springframework.data.elasticsearch.client.ClientConfiguration.ClientConfigurationBuilderWithOptionalDefaultHeaders#build()
@@ -219,7 +231,7 @@ public ClientConfiguration build() {
219231
}
220232

221233
return new DefaultClientConfiguration(hosts, headers, useSsl, sslContext, soTimeout, connectTimeout, pathPrefix,
222-
hostnameVerifier, proxy, webClientConfigurer);
234+
hostnameVerifier, proxy, webClientConfigurer, headersSupplier);
223235
}
224236

225237
private static InetSocketAddress parse(String hostAndPort) {

src/main/java/org/springframework/data/elasticsearch/client/ClientLogger.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.slf4j.Logger;
2121
import org.slf4j.LoggerFactory;
2222
import org.springframework.http.HttpStatus;
23+
import org.springframework.lang.Nullable;
2324
import org.springframework.util.ObjectUtils;
2425

2526
/**
@@ -90,7 +91,7 @@ public static void logRequest(String logId, String method, String endpoint, Obje
9091
* @param logId the correlation Id, see {@link #newLogId()}.
9192
* @param statusCode the HTTP status code.
9293
*/
93-
public static void logRawResponse(String logId, HttpStatus statusCode) {
94+
public static void logRawResponse(String logId, @Nullable HttpStatus statusCode) {
9495

9596
if (isEnabled()) {
9697
WIRE_LOGGER.trace("[{}] Received raw response: {}", logId, statusCode);

src/main/java/org/springframework/data/elasticsearch/client/DefaultClientConfiguration.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.List;
2323
import java.util.Optional;
2424
import java.util.function.Function;
25+
import java.util.function.Supplier;
2526

2627
import javax.net.ssl.HostnameVerifier;
2728
import javax.net.ssl.SSLContext;
@@ -50,12 +51,13 @@ class DefaultClientConfiguration implements ClientConfiguration {
5051
private final @Nullable String pathPrefix;
5152
private final @Nullable HostnameVerifier hostnameVerifier;
5253
private final @Nullable String proxy;
53-
private final @Nullable Function<WebClient, WebClient> webClientConfigurer;
54+
private final Function<WebClient, WebClient> webClientConfigurer;
55+
private final Supplier<HttpHeaders> headersSupplier;
5456

5557
DefaultClientConfiguration(List<InetSocketAddress> hosts, HttpHeaders headers, boolean useSsl,
5658
@Nullable SSLContext sslContext, Duration soTimeout, Duration connectTimeout, @Nullable String pathPrefix,
5759
@Nullable HostnameVerifier hostnameVerifier, @Nullable String proxy,
58-
@Nullable Function<WebClient, WebClient> webClientConfigurer) {
60+
Function<WebClient, WebClient> webClientConfigurer, Supplier<HttpHeaders> headersSupplier) {
5961

6062
this.hosts = Collections.unmodifiableList(new ArrayList<>(hosts));
6163
this.headers = new HttpHeaders(headers);
@@ -67,6 +69,7 @@ class DefaultClientConfiguration implements ClientConfiguration {
6769
this.hostnameVerifier = hostnameVerifier;
6870
this.proxy = proxy;
6971
this.webClientConfigurer = webClientConfigurer;
72+
this.headersSupplier = headersSupplier;
7073
}
7174

7275
@Override
@@ -117,6 +120,11 @@ public Optional<String> getProxy() {
117120

118121
@Override
119122
public Function<WebClient, WebClient> getWebClientConfigurer() {
120-
return webClientConfigurer != null ? webClientConfigurer : Function.identity();
123+
return webClientConfigurer;
124+
}
125+
126+
@Override
127+
public Supplier<HttpHeaders> getHeadersSupplier() {
128+
return headersSupplier;
121129
}
122130
}

src/main/java/org/springframework/data/elasticsearch/client/RestClients.java

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,11 @@
2020
import java.io.IOException;
2121
import java.net.InetSocketAddress;
2222
import java.time.Duration;
23+
import java.util.Arrays;
2324
import java.util.List;
24-
import java.util.Optional;
25+
import java.util.function.Supplier;
2526
import java.util.stream.Collectors;
2627

27-
import javax.net.ssl.HostnameVerifier;
28-
import javax.net.ssl.SSLContext;
29-
3028
import org.apache.http.Header;
3129
import org.apache.http.HttpEntity;
3230
import org.apache.http.HttpEntityEnclosingRequest;
@@ -87,38 +85,32 @@ public static ElasticsearchRestClient create(ClientConfiguration clientConfigura
8785
HttpHeaders headers = clientConfiguration.getDefaultHeaders();
8886

8987
if (!headers.isEmpty()) {
90-
91-
Header[] httpHeaders = headers.toSingleValueMap().entrySet().stream()
92-
.map(it -> new BasicHeader(it.getKey(), it.getValue())).toArray(Header[]::new);
93-
builder.setDefaultHeaders(httpHeaders);
88+
builder.setDefaultHeaders(toHeaderArray(headers));
9489
}
9590

9691
builder.setHttpClientConfigCallback(clientBuilder -> {
97-
98-
Optional<SSLContext> sslContext = clientConfiguration.getSslContext();
99-
Optional<HostnameVerifier> hostNameVerifier = clientConfiguration.getHostNameVerifier();
100-
sslContext.ifPresent(clientBuilder::setSSLContext);
101-
hostNameVerifier.ifPresent(clientBuilder::setSSLHostnameVerifier);
92+
clientConfiguration.getSslContext().ifPresent(clientBuilder::setSSLContext);
93+
clientConfiguration.getHostNameVerifier().ifPresent(clientBuilder::setSSLHostnameVerifier);
94+
clientBuilder.addInterceptorLast(new CustomHeaderInjector(clientConfiguration.getHeadersSupplier()));
10295

10396
if (ClientLogger.isEnabled()) {
104-
10597
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
10698

10799
clientBuilder.addInterceptorLast((HttpRequestInterceptor) interceptor);
108100
clientBuilder.addInterceptorLast((HttpResponseInterceptor) interceptor);
109101
}
110102

111-
Duration connectTimeout = clientConfiguration.getConnectTimeout();
112-
Duration timeout = clientConfiguration.getSocketTimeout();
113-
114103
Builder requestConfigBuilder = RequestConfig.custom();
104+
Duration connectTimeout = clientConfiguration.getConnectTimeout();
115105

116106
if (!connectTimeout.isNegative()) {
117107

118108
requestConfigBuilder.setConnectTimeout(Math.toIntExact(connectTimeout.toMillis()));
119109
requestConfigBuilder.setConnectionRequestTimeout(Math.toIntExact(connectTimeout.toMillis()));
120110
}
121111

112+
Duration timeout = clientConfiguration.getSocketTimeout();
113+
122114
if (!timeout.isNegative()) {
123115
requestConfigBuilder.setSocketTimeout(Math.toIntExact(timeout.toMillis()));
124116
}
@@ -134,8 +126,16 @@ public static ElasticsearchRestClient create(ClientConfiguration clientConfigura
134126
return () -> client;
135127
}
136128

129+
private static Header[] toHeaderArray(HttpHeaders headers) {
130+
return headers.entrySet().stream() //
131+
.flatMap(entry -> entry.getValue().stream() //
132+
.map(value -> new BasicHeader(entry.getKey(), value))) //
133+
.toArray(Header[]::new);
134+
}
135+
137136
private static List<String> formattedHosts(List<InetSocketAddress> hosts, boolean useSsl) {
138-
return hosts.stream().map(it -> (useSsl ? "https" : "http") + "://" + it.getHostString() + ":" + it.getPort()).collect(Collectors.toList());
137+
return hosts.stream().map(it -> (useSsl ? "https" : "http") + "://" + it.getHostString() + ":" + it.getPort())
138+
.collect(Collectors.toList());
139139
}
140140

141141
/**
@@ -180,7 +180,6 @@ public void process(HttpRequest request, HttpContext context) throws IOException
180180
String logId = (String) context.getAttribute(RestClients.LOG_ID_ATTRIBUTE);
181181

182182
if (logId == null) {
183-
184183
logId = ClientLogger.newLogId();
185184
context.setAttribute(RestClients.LOG_ID_ATTRIBUTE, logId);
186185
}
@@ -205,10 +204,31 @@ public void process(HttpRequest request, HttpContext context) throws IOException
205204

206205
@Override
207206
public void process(HttpResponse response, HttpContext context) {
208-
209207
String logId = (String) context.getAttribute(RestClients.LOG_ID_ATTRIBUTE);
210-
211208
ClientLogger.logRawResponse(logId, HttpStatus.resolve(response.getStatusLine().getStatusCode()));
212209
}
213210
}
211+
212+
/**
213+
* Interceptor to inject custom supplied headers.
214+
*
215+
* @since 4.0
216+
*/
217+
private static class CustomHeaderInjector implements HttpRequestInterceptor {
218+
219+
public CustomHeaderInjector(Supplier<HttpHeaders> headersSupplier) {
220+
this.headersSupplier = headersSupplier;
221+
}
222+
223+
private final Supplier<HttpHeaders> headersSupplier;
224+
225+
@Override
226+
public void process(HttpRequest request, HttpContext context) {
227+
HttpHeaders httpHeaders = headersSupplier.get();
228+
229+
if (httpHeaders != null && httpHeaders != HttpHeaders.EMPTY) {
230+
Arrays.stream(toHeaderArray(httpHeaders)).forEach(request::addHeader);
231+
}
232+
}
233+
}
214234
}

0 commit comments

Comments
 (0)