This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new 7e50fdb79d Allow rootUrl to be retrievable/overridable in RestClient
7e50fdb79d is described below
commit 7e50fdb79da95cf2411d07a15b065dfcbd67f6ea
Author: James Bognar <[email protected]>
AuthorDate: Thu Mar 19 10:23:51 2026 -0400
Allow rootUrl to be retrievable/overridable in RestClient
---
AGENTS.md | 4 +-
.../org/apache/juneau/rest/client/RestClient.java | 87 +++++++++++++++++++---
.../apache/juneau/rest/mock/MockRestClient.java | 13 ++--
.../client/RestClient_Config_RestClient_Test.java | 13 ++++
todo/TODO.md | 4 +-
5 files changed, 101 insertions(+), 20 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 8b15dde388..fbdb6192f5 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1564,8 +1564,8 @@ This document serves as the definitive guide for unit
testing in the Apache June
### Location
When asked to "add to the release notes", this refers to the current release
file located at:
- `/docs/pages/release-notes/<VERSION>.md`
-- **Current version**: `9.2.0`
-- **Current file**: `/docs/pages/release-notes/9.2.0.md`
+- **Current version**: `9.2.1`
+- **Current file**: `/docs/pages/release-notes/9.2.1.md`
### Structure
Release notes are organized into two main sections:
diff --git
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
index 87feb3fc33..816d3eba99 100644
---
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
+++
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
@@ -1116,7 +1116,7 @@ public class RestClient extends BeanContextable
implements HttpClient, Closeable
private Predicate<Integer> errorCodes = x -> x <= 0 || x >= 400;
private PrintStream console;
private SerializerSet.Builder serializers;
- private String rootUrl;
+ private Supplier<String> rootUrl;
private UrlEncodingSerializer.Builder urlEncodingSerializer;
List<RestCallInterceptor> interceptors;
@@ -2391,7 +2391,14 @@ public class RestClient extends BeanContextable
implements HttpClient, Closeable
*
* @return The root URI defined for this client.
*/
- public String getRootUri() { return rootUrl; }
+ public String getRootUri() { return rootUrl != null ?
rootUrl.get() : null; }
+
+ /**
+ * Returns the root URL supplier set on this builder.
+ *
+ * @return The root URL supplier, or <jk>null</jk> if not set.
+ */
+ public Supplier<String> getRootUrlSupplier() { return rootUrl; }
/**
* Appends a header to all requests.
@@ -4759,8 +4766,58 @@ public class RestClient extends BeanContextable
implements HttpClient, Closeable
rootUrl = null;
else if (s.indexOf("://") == -1)
throw rex("Invalid rootUrl value: ''{0}''.
Must be a valid absolute URL.", value);
- else
- rootUrl = s;
+ else {
+ final var url = s;
+ rootUrl = () -> url;
+ }
+ return this;
+ }
+
+ /**
+ * Sets the root URL using a supplier, allowing the URL to be
computed dynamically at request time.
+ *
+ * <p>
+ * The supplier is called on every request, so the URL can
change between requests.
+ * No build-time validation is performed; invalid values will
surface as errors when requests are made.
+ *
+ * <p>
+ * Three common patterns are supported:
+ * <ul>
+ * <li>
+ * <b>Swappable reference</b> — Use an
{@link java.util.concurrent.atomic.AtomicReference AtomicReference}
+ * when you need to point the client at a
different host at any time:
+ * <p class='bjava'>
+ * AtomicReference<String>
<jv>urlRef</jv> = <jk>new</jk>
AtomicReference<>(<js>"https://host1"</js>);
+ * RestClient <jv>client</jv> =
RestClient.<jsm>create</jsm>().rootUrl(<jv>urlRef</jv>::get).build();
+ *
+ * <jc>// Switch to a different host at
runtime.</jc>
+ *
<jv>urlRef</jv>.set(<js>"https://host2"</js>);
+ * </p>
+ * <li>
+ * <b>Refreshable cached value</b> — Use
{@link org.apache.juneau.commons.function.Memoizer Memoizer}
+ * (via {@link
org.apache.juneau.commons.utils.Utils#memoizer(java.util.function.Supplier)
Utils.memoizer()})
+ * when computing the URL is expensive (e.g.
service discovery) and you want it cached until explicitly refreshed:
+ * <p class='bjava'>
+ * Memoizer<String> <jv>url</jv> =
<jsm>memoizer</jsm>(() ->
serviceDiscovery.<jsm>findUrl</jsm>(<js>"my-service"</js>));
+ * RestClient <jv>client</jv> =
RestClient.<jsm>create</jsm>().rootUrl(<jv>url</jv>).build();
+ *
+ * <jc>// Force re-evaluation on the next
request (e.g. after a failover).</jc>
+ * <jv>url</jv>.reset();
+ * </p>
+ * <li>
+ * <b>Purely dynamic</b> — Use a plain
lambda when the URL must be re-evaluated on every request:
+ * <p class='bjava'>
+ * RestClient <jv>client</jv> =
RestClient.<jsm>create</jsm>().rootUrl(() ->
config.<jsm>getRootUrl</jsm>()).build();
+ * </p>
+ * </ul>
+ *
+ * @param value
+ * A supplier that returns the root URI to prefix to
relative URI strings.
+ * <br>Can be <jk>null</jk> (no root URL will be set).
+ * @return This object.
+ */
+ public Builder rootUrl(Supplier<String> value) {
+ rootUrl = value;
return this;
}
@@ -6286,7 +6343,7 @@ public class RestClient extends BeanContextable
implements HttpClient, Closeable
private final RestCallHandler callHandler;
private StackTraceElement[] closedStack;
private final StackTraceElement[] creationStack;
- private final String rootUrl;
+ private final Supplier<String> rootUrl;
private final boolean executorServiceShutdownOnClose;
private final boolean logToConsole;
private final AtomicBoolean isClosed = new AtomicBoolean(false);
@@ -6345,6 +6402,15 @@ public class RestClient extends BeanContextable
implements HttpClient, Closeable
init();
}
+ /**
+ * Returns the root URL set on this client.
+ *
+ * @return The root URL, or <jk>null</jk> if not set.
+ */
+ public String getRootUrl() {
+ return rootUrl != null ? rootUrl.get() : null;
+ }
+
/**
* Performs a REST call where the entire call is specified in a simple
string.
*
@@ -7049,7 +7115,7 @@ public class RestClient extends BeanContextable
implements HttpClient, Closeable
public <T> T getRemote(Class<T> interfaceClass, Object rootUrl,
Serializer serializer, Parser parser) {
if (rootUrl == null)
- rootUrl = this.rootUrl;
+ rootUrl = this.rootUrl != null ? this.rootUrl.get() :
null;
final String restUrl2 = trimSlashes(emptyIfNull(rootUrl));
@@ -7310,9 +7376,10 @@ public class RestClient extends BeanContextable
implements HttpClient, Closeable
var rm = new RrpcInterfaceMeta(interfaceClass, "");
var path = rm.getPath();
if (path.indexOf("://") == -1) {
- if (isEmpty(rootUrl))
+ var rootUrlValue = rootUrl != null ?
rootUrl.get() : null;
+ if (isEmpty(rootUrlValue))
throw new
RemoteMetadataException(interfaceClass, "Root URI has not been specified.
Cannot construct absolute path to remote interface.");
- path = trimSlashes(rootUrl) + '/' + path;
+ path = trimSlashes(rootUrlValue) + '/' + path;
}
uri = path;
}
@@ -8079,7 +8146,7 @@ public class RestClient extends BeanContextable
implements HttpClient, Closeable
.a(PROP_partParser, partParser)
.a(PROP_partSerializer, partSerializer)
.a(PROP_queryData, queryData)
- .a(PROP_rootUrl, rootUrl);
+ .a(PROP_rootUrl, rootUrl != null ? rootUrl.get() :
null);
}
/**
@@ -8107,7 +8174,7 @@ public class RestClient extends BeanContextable
implements HttpClient, Closeable
"RestClient.close() has already been called.
This client cannot be reused. Closed location stack trace can be displayed by
setting the system property
'org.apache.juneau.rest.client2.RestClient.trackCreation' to true.");
}
- var req = createRequest(toUri(op.getUri(), rootUrl),
op.getMethod(), op.hasContent());
+ var req = createRequest(toUri(op.getUri(), rootUrl != null ?
rootUrl.get() : null), op.getMethod(), op.hasContent());
onCallInit(req);
diff --git
a/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock/MockRestClient.java
b/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock/MockRestClient.java
index 7db4455375..d7f41ddc10 100644
---
a/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock/MockRestClient.java
+++
b/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock/MockRestClient.java
@@ -1833,9 +1833,6 @@ public class MockRestClient extends RestClient implements
HttpClientConnection {
var restBean = builder.restBean;
var contextPath = builder.contextPath;
var servletPath = builder.servletPath;
- var rootUrl = builder.getRootUri();
- if (rootUrl == null)
- rootUrl = "http://localhost";
var c = restBean instanceof Class restBean2 ?
(Class<?>)restBean2 : restBean.getClass();
if (! restContexts.containsKey(c)) {
@@ -1859,10 +1856,16 @@ public class MockRestClient extends RestClient
implements HttpClientConnection {
if (servletPath == null)
servletPath =
toValidContextPath(restBeanCtx.getFullPath());
- rootUrl = rootUrl + emptyIfNull(contextPath) +
emptyIfNull(servletPath);
+ final var suffix = emptyIfNull(contextPath) +
emptyIfNull(servletPath);
+ final var existingSupplier =
builder.getRootUrlSupplier();
+ if (existingSupplier != null) {
+ // Compose a new supplier that appends the
fixed path suffix to whatever the original supplier returns.
+ builder.rootUrl(() -> existingSupplier.get() +
suffix);
+ } else {
+ builder.rootUrl("http://localhost" + suffix);
+ }
builder.servletPath = servletPath;
- builder.rootUrl(rootUrl);
return builder;
} catch (Exception e) {
throw new ConfigException(e, "Could not initialize
MockRestClient");
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/rest/client/RestClient_Config_RestClient_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/rest/client/RestClient_Config_RestClient_Test.java
index f8b47cb712..fe6ac793cc 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/rest/client/RestClient_Config_RestClient_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/rest/client/RestClient_Config_RestClient_Test.java
@@ -482,6 +482,19 @@ class RestClient_Config_RestClient_Test extends TestBase {
assertEquals("http://foo:bar@localhost:8080/bean?foo=bar#baz",uri.toString());
}
+ @Test void a17_getRootUrl() {
+ var c = client().rootUrl("https://foo").build();
+ assertEquals("https://foo", c.getRootUrl());
+ }
+
+ @Test void a18_rootUrl_supplier() {
+ var url = new String[]{"https://host1"};
+ var c = client().rootUrl(() -> url[0]).build();
+ assertEquals("https://host1", c.getRootUrl());
+ url[0] = "https://host2";
+ assertEquals("https://host2", c.getRootUrl());
+ }
+
//------------------------------------------------------------------------------------------------------------------
// Helper methods.
//------------------------------------------------------------------------------------------------------------------
diff --git a/todo/TODO.md b/todo/TODO.md
index 9e8b2bff04..94557524b9 100644
--- a/todo/TODO.md
+++ b/todo/TODO.md
@@ -5,7 +5,7 @@
- Update REST server API to use new BeanStore2.
- Make sure @Beanp("*") works on plain fields.
- Need an easier way to specify this header:
-Content-Disposition: attachment; filename="example.pdf"
+ Content-Disposition: attachment; filename="example.pdf"
- Figure out why this needs a cast:
private static final Json5 JSON5_LENIENT = new
Json5(Json5Serializer.DEFAULT,
(Json5Parser)Json5Parser.create().ignoreUnknownBeanProperties().build());
@@ -19,8 +19,6 @@ Content-Disposition: attachment; filename="example.pdf"
- RestResponse needs a setSerializer() command.
- Verify that you can add @BeanIgnore on a private field with getters/setters.
-- RestClient needs a getRootUrl to see how it's set.
-- RestClient rootUrl should allow for a supplier to be used.
- On RestClient when logging with FULL, calling
RestREsponse.getContent().asString() causes a stream closed exception.
- Possibility of adding convenience classes for
okhttp3.mockwebserver.Dispatcher?