This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch docs
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/docs by this push:
     new ded04f3056 docs: TODO-73 runtime-overridable @Rest(paths=...) — close 
out
ded04f3056 is described below

commit ded04f3056a11060aaaea74d2d1e5d6c8540c743
Author: James Bognar <[email protected]>
AuthorDate: Sun May 24 10:23:00 2026 -0400

    docs: TODO-73 runtime-overridable @Rest(paths=...) — close out
    
    Release notes + RestServerComposition topic page updates accompanying the
    juneau commit closing out TODO-73 (3-rung precedence chain — programmatic >
    getter > annotation, per-element SVL + comma-split on @Rest(paths=...) 
elements,
    getPaths() Object contract, RestContext.Args ctor consolidation, 
host-class-only
    semantics on @Rest.path() / @Rest.paths() javadoc).
    
    Co-authored-by: Cursor <[email protected]>
---
 pages/release-notes/9.5.0.md                 |  55 +++++++++++
 pages/topics/10.07a.RestServerComposition.md | 138 +++++++++++++++++++++++++++
 2 files changed, 193 insertions(+)

diff --git a/pages/release-notes/9.5.0.md b/pages/release-notes/9.5.0.md
index 5d51815214..1776792120 100644
--- a/pages/release-notes/9.5.0.md
+++ b/pages/release-notes/9.5.0.md
@@ -2056,6 +2056,61 @@ Users can now choose either:
 - **Recommended**: `@Rest(mixins=BasicHealthResource.class)` on the root 
resource (single servlet).
 - **Fallback**: standalone `HealthProbeConfiguration` auto-mount (separate 
servlet, explicit paths).
 
+#### Runtime-Overridable `@Rest(paths=...)` Resolution Chain (TODO-73)
+
+The `paths` array on `@Rest` is now a *default* in a three-rung 
**runtime-override resolution
+chain**, walked once during `RestContext` construction and exposed via 
`RestContext.getPaths()`.
+Apps can substitute the top-level mount paths at deploy time without 
recompiling the resource.
+
+Precedence (highest first):
+
+1. **Programmatic** — `RestContext.Args.paths()` / 
`RestContext.Builder.paths(String...)`.
+   `null` means "inherit from lower rungs"; `new String[0]` is the 
explicit-clear sentinel.
+2. **`getPaths()` virtual getter** — added to both `RestServlet` and 
`RestObject` (default
+   `null`). Returning `null` inherits the lower rung; `new String[0]` is the 
explicit-clear
+   sentinel; non-empty wins.
+3. **`@Rest(paths={"…"})` annotation default** — *new behavior:* each element 
runs through
+   [SVL](/docs/topics/RestServerSvlVariables) substitution and the resolved 
value is then
+   split on `,` (trim each piece, drop empties). A single template element can 
therefore
+   expand to zero, one, or many mount paths.
+
+The annotation-default rung now supports the same SVL surface as any other 
`@Rest` string member:
+
+```java
+@Rest(paths={"$C{health.paths}"})                      // single key, 
multi-value
+@Rest(paths={"/api", "$C{extra.paths}"})               // mix literal + 
resolved
+@Rest(paths={"$E{HEALTH_PATHS,/healthz,/readyz}"})     // env var w/ 
comma-separated default
+```
+
+SVL `$C{key}` consults whatever Juneau `Config` is registered on the bean 
store (test overlay,
+`@Bean` factory, or the framework's runtime `Config`); `$E{NAME,default}` and
+`$S{prop,default}` use the bootstrap variable catalog without needing any 
beans. An SVL failure
+or missing key with no default substitutes empty, which the comma-split then 
drops — same
+semantics as every other SVL-bearing `@Rest` member.
+
+The same chain runs whether the app is wired via Juneau auto-discovery 
(`JettyServerComponent`)
+or Spring Boot's manual registration. A new public mount-time helper
+`RestContext.resolveTopLevelPaths(Class, Object, BeanStore)` lets Spring Boot 
users compute the
+resolved paths inside their `@Bean ServletRegistrationBean<MyServlet>(...)` 
factories — the
+programmatic Builder rung is N/A for that flow (no `RestContext` exists at 
registration time;
+substitute via the getter or by registering a `Config` bean that the 
annotation's `$C{...}`
+elements can resolve through).
+
+> **Spring Boot `application.yaml` integration is planned under a separate 
work item.** Once
+> that lands, `$C{key}` in `@Rest(paths=...)` elements will transparently 
consult Spring's
+> `Environment` under Spring Boot via a Juneau-`Config`-to-`Environment` 
bridge — no API change
+> here, just an additional resolution source registered with the `Config` bean.
+
+See [REST Server — Mixins and Multi-Mount Paths § Runtime-overridable 
paths](/docs/topics/RestServerCompositionMixinsAndPaths#runtime-overridable-paths-since-950)
+for the full reference, including the precedence table, worked examples for 
each rung, the
+Spring Boot integration walkthrough, and the `null`-vs-empty-array semantic 
table.
+
+This change is purely additive — resources that don't override `getPaths()`, 
aren't constructed
+with a programmatic `Args.paths()`, and don't include SVL markers in their 
`@Rest(paths=...)`
+literals continue to behave exactly as they did under FINISHED-72 (the 
annotation default wins
+on its own, element-for-element). Pre-9.5.0 users who already adopted 
`@Rest(paths=...)` see no
+behavior change unless they introduce an SVL marker or an embedded comma in an 
element.
+
 #### OpenAPI 3.1 Emission + `apiFormat` Knob (TODO-63)
 
 `juneau-rest-server` now ships first-class OpenAPI 3.1 document generation 
alongside the existing Swagger v2 path:
diff --git a/pages/topics/10.07a.RestServerComposition.md 
b/pages/topics/10.07a.RestServerComposition.md
index 35cbaf3fd6..c049be2e72 100644
--- a/pages/topics/10.07a.RestServerComposition.md
+++ b/pages/topics/10.07a.RestServerComposition.md
@@ -367,6 +367,144 @@ probe resource, declare it explicitly 
(`paths={"/healthz","/healthz/extra/segmen
 take whatever path-spec the caller hands them, regardless of the `paths` 
attribute on the
 servlet class. The auto-mount honor is opt-in — `@Bean Servlet` is the trigger.
 
+## Runtime-overridable paths (since 9.5.0)
+
+The `paths` array on `@Rest` is a *default* — apps frequently need to 
substitute it at deploy
+time without recompiling the resource (e.g. probe URLs that vary by Kubernetes 
namespace, internal
+endpoints whose paths differ between the AWS and GCP clusters, etc.). Juneau 
resolves the
+top-level mount paths through a three-rung **runtime-override resolution 
chain**, walked once
+during `RestContext` construction.
+
+### Precedence (highest first)
+
+| Rung | Source                                         | Notes                
                                |
+|------|------------------------------------------------|------------------------------------------------------|
+| 1    | Programmatic — `RestContext.Args.paths()` / 
`RestContext.Builder.paths(String...)` | Highest precedence. `null` = inherit; 
`new String[0]` = explicit clear. |
+| 2    | `RestServlet.getPaths()` / `RestObject.getPaths()` virtual getter | 
`null` = inherit lower rung; non-`null` (incl. empty) wins. |
+| 3    | `@Rest(paths={"…"})` annotation default        | Lowest rung. Each 
element runs through SVL, then is split on `,` (trim, drop empties). |
+
+The resolved array is exposed via `RestContext.getPaths()` (and consumed at 
mount time by
+`JettyServerComponent` and the public mount-time helper
+`RestContext.resolveTopLevelPaths(Class, Object, BeanStore)` for Spring Boot's
+`ServletRegistrationBean` flow).
+
+### Worked examples
+
+**Rung 1 — programmatic override (test fixtures, custom mounts):**
+
+```java
+var args = new RestContext.Args(MyResource.class, null, null, MyResource::new, 
"", null, null,
+    new String[]{"/api/v2/healthz", "/api/v2/readyz"});
+var ctx = new RestContext(args);
+// ctx.getPaths() → {"/api/v2/healthz", "/api/v2/readyz"}
+```
+
+Pass `new String[0]` (not `null`) to explicitly clear all lower rungs and end 
up with no
+top-level mounts.
+
+**Rung 2 — `getPaths()` getter override on a subclass:**
+
+```java
+@Rest(paths={"/healthz","/readyz"})
+public class TenantHealthResource extends BasicHealthResource {
+    @Override
+    public String[] getPaths() {
+        var prefix = "/" + tenantId() + "/health";
+        return new String[]{ prefix + "/live", prefix + "/ready" };
+    }
+}
+```
+
+The getter runs at construction time (before the servlet container hits the 
resource), so the
+mounted URLs reflect the per-deployment tenant id without recompiling 
`BasicHealthResource`.
+
+**Rung 3 — `@Rest(paths={"…"})` with SVL + comma-split:**
+
+Each element of the `paths` array is a *template*. The resolver:
+
+1. Runs the element through SVL substitution (using the bootstrap 
`VarResolver` on the bean
+   store, so `$C{key}` consults the registered Juneau `Config`).
+2. Splits the post-SVL value on `,`, trims each piece, and drops empties.
+
+A single element can therefore expand to zero, one, or many mount paths. 
Examples:
+
+```java
+// 1. Single Config key whose value is a comma-separated list.
+@Rest(paths={"$C{health.paths}"})
+public class HealthResource extends BasicRestServlet { ... }
+```
+
+```ini
+# my-app.cfg
+health.paths = /probe/live, /probe/ready
+```
+
+```java
+// 2. Mix literal and resolved elements.
+@Rest(paths={"/api", "$C{extra.paths}"})
+public class ApiResource extends BasicRestServlet { ... }
+```
+
+```java
+// 3. Env var with comma-separated default baked in.
+@Rest(paths={"$E{HEALTH_PATHS,/healthz,/readyz}"})
+public class HealthResource extends BasicRestServlet { ... }
+```
+
+```bash
+# At deploy time:
+HEALTH_PATHS="/probe/live,/probe/ready" java -jar my-app.jar
+```
+
+The same SVL surface as any other `@Rest` string member is available: 
`$C{key[,default]}` for
+Juneau `Config`, `$E{NAME[,default]}` for environment variables, 
`$S{prop[,default]}` for
+system properties, and so on.
+
+Resolution-miss behavior is uniform across SVL: when a key resolves to nothing 
and no default is
+provided, the substitution is empty, and the comma-split then drops the empty 
piece. The
+annotation's own array still represents the floor — an element like 
`"/literal"` is unaffected
+by adjacent unresolved SVL elements.
+
+### Spring Boot integration
+
+Under `juneau-rest-server-springboot`, paths still resolve through the same 
three-rung chain.
+Spring Boot's manual `ServletRegistrationBean` flow uses the public mount-time 
helper:
+
+```java
+@Configuration
+class HealthBoot {
+    @Bean
+    ServletRegistrationBean<HealthResource> healthServletRegistration(
+            HealthResource servlet, ApplicationContext appContext) {
+        var store = new SpringBeanStore(appContext, null);
+        var paths = RestContext.resolveTopLevelPaths(HealthResource.class, 
servlet, store);
+        return new ServletRegistrationBean<>(servlet, paths);
+    }
+}
+```
+
+The same chain runs whether the app is wired via Juneau auto-discovery 
(`JettyServerComponent`)
+or Spring Boot's manual registration — the only difference is that the 
programmatic Builder rung
+is N/A in the manual flow (no `RestContext` exists at registration time; users 
substitute via
+the getter, by registering a `Config` bean whose contents drive `$C{...}` 
elements, or by
+providing system-property / env-var overrides for `$S{...}` / `$E{...}` 
elements).
+
+> **Spring Boot `application.yaml` integration is planned under a separate 
work item.** Once
+> that lands, `$C{key}` in `@Rest(paths=...)` elements will transparently 
consult Spring's
+> `Environment` under Spring Boot via a Juneau-`Config`-to-`Environment` 
bridge — no API change
+> here, just an additional resolution source registered with the `Config` bean.
+
+### Null vs. empty-array semantics
+
+| Value         | Programmatic (Rung 1) | Getter (Rung 2) |
+|---------------|-----------------------|-----------------|
+| `null`        | No override; lower rungs resolve. | Inherit lower rung. |
+| `new String[0]` | Explicit clear; no top-level mounts. | Explicit clear; no 
top-level mounts. |
+
+Picking the right sentinel matters: `null` lets the annotation default still 
resolve beneath
+you; `new String[0]` short-circuits and produces a resource with no mounts 
(which surfaces a
+clear "no mounts" error from the hosting runtime when nothing is reachable).
+
 ### Mixin operations don't surface in Swagger as "imported"
 
 The Swagger / OpenAPI emission walks the importing resource's full operation 
tree, so mixin

Reply via email to