Skip to content

Commit 212f022

Browse files
committed
GH-5723: add RDF4JHttpClientConfig#toBuilder() and refactor
SharedHttpClientSessionManager - Add toBuilder() to RDF4JHttpClientConfig to allow creating modified copies without re-specifying all fields - Replace the three separate timeout fields in SharedHttpClientSessionManager with a single volatile RDF4JHttpClientConfig, so that setHttpClientConfig() now propagates the full config (not just the three timeout values) - Use toBuilder() in setDefaultSparqlServiceTimeouts() and setDefaultTimeouts() to apply only the timeout overrides while preserving all other config settings
1 parent dd4b2bb commit 212f022

3 files changed

Lines changed: 158 additions & 23 deletions

File tree

core/http/client-api/src/main/java/org/eclipse/rdf4j/http/client/spi/RDF4JHttpClientConfig.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,27 @@ public List<HttpHeader> getDefaultHeaders() {
172172
return defaultHeaders;
173173
}
174174

175+
/**
176+
* Returns a new {@link Builder} pre-populated with all settings from this config. Useful for creating a modified
177+
* copy without having to re-specify every field.
178+
*
179+
* @return a {@link Builder} seeded from this config
180+
*/
181+
public Builder toBuilder() {
182+
return new Builder()
183+
.connectTimeoutMs(connectTimeoutMs)
184+
.socketTimeoutMs(socketTimeoutMs)
185+
.connectionRequestTimeoutMs(connectionRequestTimeoutMs)
186+
.maxConnectionsPerRoute(maxConnectionsPerRoute)
187+
.maxConnectionsTotal(maxConnectionsTotal)
188+
.maxRedirects(maxRedirects)
189+
.followRedirects(followRedirects)
190+
.idleConnectionTimeoutMs(idleConnectionTimeoutMs)
191+
.sslContext(sslContext)
192+
.disableHostnameVerification(disableHostnameVerification)
193+
.defaultHeaders(defaultHeaders);
194+
}
195+
175196
/**
176197
* Returns a new {@link RDF4JHttpClientConfig} with all settings at their default values.
177198
*
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Eclipse RDF4J contributors.
3+
*
4+
* All rights reserved. This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Distribution License v1.0
6+
* which accompanies this distribution, and is available at
7+
* http://www.eclipse.org/org/documents/edl-v10.php.
8+
*
9+
* SPDX-License-Identifier: BSD-3-Clause
10+
*******************************************************************************/
11+
package org.eclipse.rdf4j.http.client.spi;
12+
13+
import static org.assertj.core.api.Assertions.assertThat;
14+
import static org.assertj.core.groups.Tuple.tuple;
15+
16+
import java.util.List;
17+
18+
import org.junit.jupiter.api.Test;
19+
20+
class RDF4JHttpClientConfigTest {
21+
22+
@Test
23+
void toBuilderCopiesAllFields() {
24+
RDF4JHttpClientConfig original = RDF4JHttpClientConfig.newBuilder()
25+
.connectTimeoutMs(1_000)
26+
.socketTimeoutMs(2_000)
27+
.connectionRequestTimeoutMs(3_000)
28+
.maxConnectionsPerRoute(10)
29+
.maxConnectionsTotal(20)
30+
.maxRedirects(3)
31+
.followRedirects(false)
32+
.idleConnectionTimeoutMs(60_000L)
33+
.disableHostnameVerification(true)
34+
.defaultHeaders(List.of(HttpHeader.of("X-Custom", "value")))
35+
.build();
36+
37+
RDF4JHttpClientConfig copy = original.toBuilder().build();
38+
39+
assertThat(copy.getConnectTimeoutMs()).isEqualTo(original.getConnectTimeoutMs());
40+
assertThat(copy.getSocketTimeoutMs()).isEqualTo(original.getSocketTimeoutMs());
41+
assertThat(copy.getConnectionRequestTimeoutMs()).isEqualTo(original.getConnectionRequestTimeoutMs());
42+
assertThat(copy.getMaxConnectionsPerRoute()).isEqualTo(original.getMaxConnectionsPerRoute());
43+
assertThat(copy.getMaxConnectionsTotal()).isEqualTo(original.getMaxConnectionsTotal());
44+
assertThat(copy.getMaxRedirects()).isEqualTo(original.getMaxRedirects());
45+
assertThat(copy.isFollowRedirects()).isEqualTo(original.isFollowRedirects());
46+
assertThat(copy.getIdleConnectionTimeoutMs()).isEqualTo(original.getIdleConnectionTimeoutMs());
47+
assertThat(copy.isDisableHostnameVerification()).isEqualTo(original.isDisableHostnameVerification());
48+
assertThat(copy.getDefaultHeaders())
49+
.usingRecursiveFieldByFieldElementComparator()
50+
.isEqualTo(original.getDefaultHeaders());
51+
assertThat(copy.getSslContext()).isEqualTo(original.getSslContext());
52+
}
53+
54+
@Test
55+
void toBuilderOverrideDoesNotAffectOriginal() {
56+
RDF4JHttpClientConfig original = RDF4JHttpClientConfig.newBuilder()
57+
.connectTimeoutMs(5_000)
58+
.socketTimeoutMs(10_000)
59+
.connectionRequestTimeoutMs(15_000)
60+
.defaultHeader("X-Original", "yes")
61+
.build();
62+
63+
RDF4JHttpClientConfig modified = original.toBuilder()
64+
.connectTimeoutMs(1_000)
65+
.socketTimeoutMs(2_000)
66+
.connectionRequestTimeoutMs(3_000)
67+
.defaultHeaders(List.of(HttpHeader.of("X-Modified", "yes")))
68+
.build();
69+
70+
// original is unchanged
71+
assertThat(original.getConnectTimeoutMs()).isEqualTo(5_000);
72+
assertThat(original.getSocketTimeoutMs()).isEqualTo(10_000);
73+
assertThat(original.getConnectionRequestTimeoutMs()).isEqualTo(15_000);
74+
assertThat(original.getDefaultHeaders())
75+
.extracting(HttpHeader::getName, HttpHeader::getValue)
76+
.containsExactly(tuple("X-Original", "yes"));
77+
78+
// modified has new values
79+
assertThat(modified.getConnectTimeoutMs()).isEqualTo(1_000);
80+
assertThat(modified.getSocketTimeoutMs()).isEqualTo(2_000);
81+
assertThat(modified.getConnectionRequestTimeoutMs()).isEqualTo(3_000);
82+
assertThat(modified.getDefaultHeaders())
83+
.extracting(HttpHeader::getName, HttpHeader::getValue)
84+
.containsExactly(tuple("X-Modified", "yes"));
85+
}
86+
87+
@Test
88+
void toBuilderDefaultHeadersListIsIndependent() {
89+
RDF4JHttpClientConfig original = RDF4JHttpClientConfig.newBuilder()
90+
.defaultHeader("X-A", "1")
91+
.build();
92+
93+
// Adding a header via toBuilder should not affect the original
94+
RDF4JHttpClientConfig withExtra = original.toBuilder()
95+
.defaultHeader("X-B", "2")
96+
.build();
97+
98+
assertThat(original.getDefaultHeaders()).hasSize(1);
99+
assertThat(original.getDefaultHeaders())
100+
.extracting(HttpHeader::getName, HttpHeader::getValue)
101+
.containsExactly(tuple("X-A", "1"));
102+
assertThat(withExtra.getDefaultHeaders()).hasSize(2);
103+
}
104+
}

core/http/client/src/main/java/org/eclipse/rdf4j/http/client/SharedHttpClientSessionManager.java

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -283,11 +283,9 @@ public class SharedHttpClientSessionManager implements HttpClientSessionManager,
283283
public static final int SPARQL_SOCKET_TIMEOUT = Integer.parseInt(
284284
System.getProperty(SPARQL_SOCKET_TIMEOUT_PROPERTY, String.valueOf(DEFAULT_SPARQL_SOCKET_TIMEOUT)));
285285

286-
// Variables for the currently used timeouts
286+
// Full config used when lazily creating the internal HTTP client
287287

288-
private int currentConnectionTimeout = CONNECTION_TIMEOUT;
289-
private int currentConnectionRequestTimeout = CONNECTION_REQUEST_TIMEOUT;
290-
private int currentSocketTimeout = SOCKET_TIMEOUT;
288+
private volatile RDF4JHttpClientConfig currentConfig = buildDefaultConfig();
291289

292290
private final Logger logger = LoggerFactory.getLogger(SharedHttpClientSessionManager.class);
293291

@@ -358,15 +356,20 @@ public void setHttpClient(RDF4JHttpClient httpClient) {
358356
}
359357

360358
/**
361-
* Set an optional {@link RDF4JHttpClientConfig} to be used when creating the inner HTTP client (if not provided
362-
* externally as dependent client).
359+
* Sets the {@link RDF4JHttpClientConfig} to be used when creating the internal HTTP client (if not provided
360+
* externally via {@link #setHttpClient(RDF4JHttpClient)}). The full configuration is applied, including timeouts,
361+
* connection limits, SSL context, default headers, and redirect policy.
363362
*
364-
* @param config the configuration for the managed HTTP client
363+
* <p>
364+
* This method always updates the stored configuration. However, if an external HTTP client has already been
365+
* assigned via {@link #setHttpClient(RDF4JHttpClient)}, or if the internal client has already been lazily
366+
* initialised, the new configuration will <em>not</em> be applied to the already-created client. In that case
367+
* {@link #getDefaultHttpClientConfig()} will return the updated config even though it is not in use.
368+
*
369+
* @param config the configuration for the managed HTTP client; must not be {@code null}
365370
*/
366371
public void setHttpClientConfig(RDF4JHttpClientConfig config) {
367-
this.currentConnectionTimeout = config.getConnectTimeoutMs();
368-
this.currentConnectionRequestTimeout = config.getConnectionRequestTimeoutMs();
369-
this.currentSocketTimeout = config.getSocketTimeoutMs();
372+
this.currentConfig = Objects.requireNonNull(config, "config must not be null");
370373
}
371374

372375
@Override
@@ -457,20 +460,23 @@ protected final ExecutorService getExecutorService() {
457460
}
458461

459462
private RDF4JHttpClient createHttpClient() {
460-
RDF4JHttpClientConfig config = getDefaultHttpClientConfig();
461-
return RDF4JHttpClients.newDefaultClient(config);
463+
return RDF4JHttpClients.newDefaultClient(currentConfig);
462464
}
463465

464466
/**
465-
* Returns the default {@link RDF4JHttpClientConfig} using the currently set timeout values.
467+
* Returns the {@link RDF4JHttpClientConfig} that will be used when lazily creating the internal HTTP client.
466468
*
467-
* @return a configured {@link RDF4JHttpClientConfig} with the current timeouts.
469+
* @return the current {@link RDF4JHttpClientConfig}; never {@code null}
468470
*/
469471
public RDF4JHttpClientConfig getDefaultHttpClientConfig() {
472+
return currentConfig;
473+
}
474+
475+
private static RDF4JHttpClientConfig buildDefaultConfig() {
470476
return RDF4JHttpClientConfig.newBuilder()
471-
.connectTimeoutMs(currentConnectionTimeout)
472-
.connectionRequestTimeoutMs(currentConnectionRequestTimeout)
473-
.socketTimeoutMs(currentSocketTimeout)
477+
.connectTimeoutMs(CONNECTION_TIMEOUT)
478+
.connectionRequestTimeoutMs(CONNECTION_REQUEST_TIMEOUT)
479+
.socketTimeoutMs(SOCKET_TIMEOUT)
474480
.maxConnectionsPerRoute(MAX_CONN_PER_ROUTE)
475481
.maxConnectionsTotal(MAX_CONN_TOTAL)
476482
.followRedirects(true)
@@ -488,9 +494,11 @@ public RDF4JHttpClientConfig getDefaultHttpClientConfig() {
488494
* </p>
489495
*/
490496
public void setDefaultSparqlServiceTimeouts() {
491-
this.currentConnectionTimeout = SPARQL_CONNECTION_TIMEOUT;
492-
this.currentConnectionRequestTimeout = SPARQL_CONNECTION_REQUEST_TIMEOUT;
493-
this.currentSocketTimeout = SPARQL_SOCKET_TIMEOUT;
497+
this.currentConfig = currentConfig.toBuilder()
498+
.connectTimeoutMs(SPARQL_CONNECTION_TIMEOUT)
499+
.connectionRequestTimeoutMs(SPARQL_CONNECTION_REQUEST_TIMEOUT)
500+
.socketTimeoutMs(SPARQL_SOCKET_TIMEOUT)
501+
.build();
494502
}
495503

496504
/**
@@ -503,9 +511,11 @@ public void setDefaultSparqlServiceTimeouts() {
503511
* </p>
504512
*/
505513
public void setDefaultTimeouts() {
506-
this.currentConnectionTimeout = CONNECTION_TIMEOUT;
507-
this.currentConnectionRequestTimeout = CONNECTION_REQUEST_TIMEOUT;
508-
this.currentSocketTimeout = SOCKET_TIMEOUT;
514+
this.currentConfig = currentConfig.toBuilder()
515+
.connectTimeoutMs(CONNECTION_TIMEOUT)
516+
.connectionRequestTimeoutMs(CONNECTION_REQUEST_TIMEOUT)
517+
.socketTimeoutMs(SOCKET_TIMEOUT)
518+
.build();
509519
}
510520

511521
}

0 commit comments

Comments
 (0)