You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2026/08/19 21:46:06 UTC

(juneau) branch master updated (e145337f99 -> 4ba9524e6c)

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

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


    from e145337f99 Reactor-wide Eclipse resource-warning cleanup
     new 85d95678a6 TODO-432: Add composition-time var(--jc-name) Theme references, resolved to literals at build()
     new 4ba9524e6c Ignore JMH-generated benchmark sources under juneau-integration-tests

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 juneau-integration-tests/.gitignore                |   7 +
 .../rest/server/console/ConsoleChromeMixin.java    |  15 +-
 .../rest/server/console/CssValueGrammar.java       |  35 +++-
 .../apache/juneau/rest/server/console/Theme.java   | 133 +++++++++++++-
 .../server/console/ConsoleChromeMixin_Test.java    |  28 +++
 .../rest/server/console/CssValueGrammar_Test.java  |  32 +++-
 .../server/console/Theme_VarReferences_Test.java   | 191 +++++++++++++++++++++
 7 files changed, 420 insertions(+), 21 deletions(-)
 create mode 100644 juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/Theme_VarReferences_Test.java


(juneau) 02/02: Ignore JMH-generated benchmark sources under juneau-integration-tests

Posted by ja...@apache.org.
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

commit 4ba9524e6c249829ac461b5e8c6e9da86db42717
Author: James Bognar <ja...@apache.org>
AuthorDate: Wed Aug 19 17:45:56 2026 -0400

    Ignore JMH-generated benchmark sources under juneau-integration-tests
    
    Running the microbench profile's exec:java outside a full mvn install can make
    jmh-generator-annprocess emit its generated *_jmhType/*_jmhTest classes at the module root
    (org/...) instead of target/generated-sources/annotations. All real source under this module
    lives under src/main/java and src/test/java, and these files regenerate on the next benchmark
    compile, so ignore them rather than letting them show up as untracked.
---
 juneau-integration-tests/.gitignore | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/juneau-integration-tests/.gitignore b/juneau-integration-tests/.gitignore
index 34acf885cb..d12beea76c 100644
--- a/juneau-integration-tests/.gitignore
+++ b/juneau-integration-tests/.gitignore
@@ -4,3 +4,10 @@
 .project
 /.settings/
 /bin/
+
+# JMH annotation-processor generated benchmark sources. All real source under this module lives
+# under src/main/java and src/test/java; running the `microbench` profile's exec:java outside a
+# full `mvn install` can make jmh-generator-annprocess emit its generated *_jmhType/*_jmhTest
+# classes at the module root (org/...) instead of target/generated-sources/annotations. They
+# regenerate on the next benchmark compile.
+/org/**/jmh_generated/


(juneau) 01/02: TODO-432: Add composition-time var(--jc-name) Theme references, resolved to literals at build()

Posted by ja...@apache.org.
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

commit 85d95678a6a087a16e4c02f54a8d6f8d20d87012
Author: James Bognar <ja...@apache.org>
AuthorDate: Wed Aug 19 17:45:47 2026 -0400

    TODO-432: Add composition-time var(--jc-name) Theme references, resolved to literals at build()
    
    A Theme token value may now be a var(--jc-name) reference to another known token (own map first,
    falling back to Theme.OPEN). Theme.Builder recognizes references one layer above CssValueGrammar
    and resolves each to a concrete literal at build() time -- own tokens shadowing OPEN -- so the
    substring "var(" never appears in any getTokens() value; only the re-validated resolved literal
    is ever emitted. Resolution is fail-closed with distinct messages for an unknown reference, a
    cycle, or a chain past the depth cap, and a failed build() leaves the builder's own map unresolved
    and retryable.
    
    CssValueGrammar splits its normalization belt (comment-stripping, control-char/url() rejection)
    out of normalizeAndValidate() into a shared normalize(), so the reference recognizer runs the
    exact same belt as the grammar with no second normalization pass to keep in sync. The six
    allowed-shape productions in isAllowedShape() are unchanged, and var() is still never a grammar
    value shape -- it stays Theme-layer syntax handled one layer above CssValueGrammar.
---
 .../rest/server/console/ConsoleChromeMixin.java    |  15 +-
 .../rest/server/console/CssValueGrammar.java       |  35 +++-
 .../apache/juneau/rest/server/console/Theme.java   | 133 +++++++++++++-
 .../server/console/ConsoleChromeMixin_Test.java    |  28 +++
 .../rest/server/console/CssValueGrammar_Test.java  |  32 +++-
 .../server/console/Theme_VarReferences_Test.java   | 191 +++++++++++++++++++++
 6 files changed, 413 insertions(+), 21 deletions(-)

diff --git a/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin.java b/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin.java
index ff93efb567..e378989ba8 100644
--- a/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin.java
+++ b/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin.java
@@ -132,11 +132,16 @@ public class ConsoleChromeMixin {
 	 *
 	 * <p>
 	 * These aliases are <b>permanent</b> (no deprecation window) and are deliberately <i>not</i>
-	 * {@link Theme#OPEN} tokens: {@code CssValueGrammar} rejects {@code var()} in every spelling, so a
-	 * {@code var(--jc-*)} alias expressed as a {@code Theme} token would throw at class-initialization time. This
-	 * is framework-authored literal text and never passes through that grammar (which exists to validate
-	 * <i>consumer</i> input, not the framework's own stylesheet). {@code Theme.OPEN} owns leaf values; this block
-	 * owns derived values; no token name is declared by both.
+	 * {@link Theme#OPEN} tokens. A {@code var(--jc-*)}-valued token IS legal Theme-layer syntax &mdash;
+	 * {@code Theme.Builder} recognizes it as a reference and resolves it to a concrete literal at {@code build()}
+	 * time &mdash; but that resolution scope deliberately excludes these role aliases, which are appended here
+	 * outside {@code Theme.OPEN}'s token map. Making them {@code Theme.OPEN} tokens instead would resolve each alias
+	 * to a <i>fixed literal</i> at composition time, snapshotting the live CSS cascade (the dark-mode /
+	 * user-agent overrides that reach these role tokens at use time) into a frozen value &mdash; so
+	 * {@code Theme.OPEN} is kept all-literal and the aliases stay here as framework-authored literal text that is
+	 * emitted verbatim (never routed through {@code CssValueGrammar}, which exists to validate <i>consumer</i>
+	 * input, not the framework's own stylesheet). {@code Theme.OPEN} owns leaf values; this block owns derived
+	 * values; no token name is declared by both.
 	 */
 	static final String OPEN_ROLE_ALIASES = String.join("",
 		"--jc-surface:var(--jc-white);",
diff --git a/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/CssValueGrammar.java b/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/CssValueGrammar.java
index 935c8807dd..088cf6d2fa 100644
--- a/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/CssValueGrammar.java
+++ b/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/CssValueGrammar.java
@@ -62,15 +62,23 @@ final class CssValueGrammar {
 	private static final Pattern URL_REJECT = Pattern.compile("(?i)url\\s*\\(");
 
 	/**
-	 * Normalizes and validates a token value, returning the normalized (comment-stripped, trimmed) value if it is
-	 * accepted.
+	 * Runs the normalization belt on a token value and returns the normalized (comment-stripped, trimmed) value,
+	 * <b>without</b> the allowlist-grammar shape check.
+	 *
+	 * <p>
+	 * This is the shared belt that both {@link #normalizeAndValidate(String)} and the {@code Theme}-layer
+	 * {@code var(--jc-name)} reference recognizer (see {@link Theme.Builder#token(String, String)}) run on, so the
+	 * reference recognizer sees exactly the same control-character-rejected, comment-stripped, {@code url(}-rejected
+	 * string as the grammar does &mdash; there is no second, reference-specific normalization pass to keep in sync.
+	 * The belt does <b>not</b> recognize or accept {@code var()}: that is Theme-layer syntax handled one layer above
+	 * this class.
 	 *
 	 * @param value The raw candidate value.
-	 * @return The normalized value.
-	 * @throws IllegalArgumentException If the value is <jk>null</jk>, contains a control character, contains a
-	 * 	{@code url(} production in any spelling, or does not match one of the allowed CSS value shapes.
+	 * @return The normalized (comment-stripped, trimmed) value.
+	 * @throws IllegalArgumentException If the value is <jk>null</jk>, contains a control character, or contains a
+	 * 	{@code url(} production in any spelling.
 	 */
-	static String normalizeAndValidate(String value) {
+	static String normalize(String value) {
 		if (value == null)
 			throw iaex("Theme token value must not be null.");
 
@@ -89,6 +97,21 @@ final class CssValueGrammar {
 		if (URL_REJECT.matcher(stripped).find())
 			throw iaex("Theme token value must not contain a url() production.");
 
+		return stripped;
+	}
+
+	/**
+	 * Normalizes and validates a token value, returning the normalized (comment-stripped, trimmed) value if it is
+	 * accepted.
+	 *
+	 * @param value The raw candidate value.
+	 * @return The normalized value.
+	 * @throws IllegalArgumentException If the value is <jk>null</jk>, contains a control character, contains a
+	 * 	{@code url(} production in any spelling, or does not match one of the allowed CSS value shapes.
+	 */
+	static String normalizeAndValidate(String value) {
+		var stripped = normalize(value);
+
 		if (isAllowedShape(stripped))
 			return stripped;
 
diff --git a/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/Theme.java b/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/Theme.java
index f8e058ba44..2ceb74c70b 100644
--- a/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/Theme.java
+++ b/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/Theme.java
@@ -19,6 +19,7 @@ package org.apache.juneau.rest.server.console;
 import static org.apache.juneau.commons.utils.Shorts.*;
 
 import java.util.*;
+import java.util.regex.*;
 
 /**
  * An immutable, named set of CSS custom-property ("theme token") overrides for the admin-console chrome.
@@ -37,6 +38,14 @@ import java.util.*;
  * {@code ConsoleChromeMixin}). {@link #OPEN} is a placeholder empty theme in this revision; its real token set
  * lands together with {@code chrome.css}.
  *
+ * <p>
+ * A token value may also be a {@code var(--jc-name)} <b>reference</b> to another known token. References are
+ * <i>not</i> a {@code CssValueGrammar} value shape: {@code Theme.Builder} recognizes them one layer above the
+ * grammar and resolves each to a concrete literal at {@link Builder#build() build()} time (own tokens shadowing
+ * {@link #OPEN}'s), so the substring {@code var(} never appears in any {@link #getTokens()} value &mdash; only the
+ * resolved literal, which is itself re-validated by the grammar, is ever emitted. An unknown reference, a cycle,
+ * or a chain longer than the resolution depth cap is a loud {@code build()} failure, never a silent fallback.
+ *
  * <h5 class='section'>Example:</h5>
  * <p class='bjava'>
  * 	Theme <jv>salesforce</jv> = Theme.<jsm>create</jsm>(<js>"salesforce"</js>)
@@ -55,6 +64,24 @@ public final class Theme {
 	/** Anchored (full-string) guard for a CSS custom-property token name. */
 	private static final String TOKEN_NAME_PATTERN = "^--jc-[a-z0-9-]+$";
 
+	/**
+	 * Anchored recognizer for a {@code var(--jc-name)} Theme-layer reference, run on the post-belt (control-char
+	 * rejected, comment-stripped, trimmed) value.
+	 *
+	 * <p>
+	 * Fully anchored ({@code ^...$}) so a reference can only ever be the <b>entire</b> value, never a prefix or
+	 * suffix of some larger value: {@code linear-gradient(var(--jc-a), #fff)} never matches this and is rejected by
+	 * the unchanged grammar. There is no fallback branch &mdash; {@code var(--jc-x, #fff)} simply fails to match
+	 * (comma inside the parens) and falls through to grammar rejection.
+	 */
+	private static final Pattern VAR_REFERENCE = Pattern.compile("^(?i:var)\\(\\s*(--jc-[a-z0-9-]+)\\s*\\)$");
+
+	/**
+	 * The maximum number of reference hops resolved before {@code build()} fails, enforced independently of cycle
+	 * detection so a long <i>acyclic</i> chain is bounded too.
+	 */
+	private static final int MAX_REFERENCE_HOPS = 8;
+
 	/**
 	 * The default, "open" theme.
 	 *
@@ -176,32 +203,120 @@ public final class Theme {
 		/**
 		 * Adds (or overrides) a single CSS custom-property token.
 		 *
+		 * <p>
+		 * The value may be either a <b>literal</b> or a {@code var(--jc-name)} <b>reference</b> to another known
+		 * token:
+		 * <ul class='spaced-list'>
+		 * 	<li>A literal is validated eagerly here by {@code CssValueGrammar}'s accept-known-safe allowlist grammar
+		 * 		after normalization (trim &rarr; reject C0/C1/DEL &rarr; strip comments &rarr; reject {@code url(} in
+		 * 		any spelling &rarr; grammar).
+		 * 	<li>A value that, after that same normalization belt, matches {@code var(--jc-name)} is recognized as a
+		 * 		reference and stored <i>unresolved</i> &mdash; it is resolved to a concrete literal at {@link #build()}
+		 * 		time (its target may not be defined yet, e.g. a forward reference). {@code var()} is never a
+		 * 		{@code CssValueGrammar} value shape; it is Theme-layer syntax recognized one layer above the grammar.
+		 * </ul>
+		 * Escaping happens later, at emission time, via {@code CssValueEscaper} (see {@code ConsoleChromeMixin}).
+		 *
 		 * @param name The token name. Must not be <jk>null</jk> and must match {@code ^--jc-[a-z0-9-]+$}.
 		 * @param value
-		 * 	The CSS value.  Validated (not escaped) by {@code CssValueGrammar}'s accept-known-safe allowlist
-		 * 	grammar after normalization (trim &rarr; reject C0/C1/DEL &rarr; strip comments &rarr; reject
-		 * 	{@code url(} in any spelling &rarr; grammar).  Escaping happens later, at emission time, via
-		 * 	{@code CssValueEscaper} (see {@code ConsoleChromeMixin}).
+		 * 	The CSS value &mdash; a literal, or a {@code var(--jc-name)} reference.
 		 * @return This object.
 		 * @throws IllegalArgumentException
 		 * 	If the name is not in the legal shape (full-string {@code String.matches(...)}, not {@code find(...)}
 		 * 	&mdash; {@code "--jc-foo;--bar"} must REJECT even though {@code "--jc-foo"} matches as a leading
-		 * 	substring), or if the value is not one of the allowlisted CSS value shapes.
+		 * 	substring), if the value contains a control character or a {@code url(} production, or if the value is
+		 * 	neither a {@code var(--jc-name)} reference nor one of the allowlisted CSS value shapes. A reference whose
+		 * 	target is unknown, cyclic, or too deeply chained is instead reported at {@link #build()} time.
 		 */
 		public Builder token(String name, String value) {
 			if (name == null || ! name.matches(TOKEN_NAME_PATTERN))
 				throw iaex("Invalid theme token name: '%s'.  Must match %s.", name, TOKEN_NAME_PATTERN);
-			tokens.put(name, CssValueGrammar.normalizeAndValidate(value));
+			// Recognition runs on the SAME post-belt string the grammar would see (one shared belt, no second
+			// comment-stripping pass).  A reference is stored unresolved; a literal is validated eagerly, exactly
+			// as before this feature existed.
+			var normalized = CssValueGrammar.normalize(value);
+			if (referencedName(normalized) != null)
+				tokens.put(name, normalized);
+			else
+				tokens.put(name, CssValueGrammar.normalizeAndValidate(value));
 			return this;
 		}
 
 		/**
-		 * Builds the immutable {@link Theme}.
+		 * Builds the immutable {@link Theme}, resolving every {@code var(--jc-name)} reference to a concrete literal.
+		 *
+		 * <p>
+		 * Resolution scope is this builder's own tokens, shadowing {@link Theme#OPEN}'s tokens (exact shadowing: a
+		 * name defined on this builder wins outright, even if resolving that entry then fails &mdash; there is no
+		 * silent fall-through to {@code Theme.OPEN}'s value for a shadowed name). Resolution runs on a copy of the
+		 * token map, so a failed {@code build()} leaves this builder unchanged and retryable.
 		 *
-		 * @return A new {@link Theme}.
+		 * @return A new {@link Theme} whose every token value is a resolved six-shape literal (the substring
+		 * 	{@code var(} appears in none of them).
+		 * @throws IllegalArgumentException
+		 * 	If a reference names an unknown token, forms a cycle (the message carries the cycle path), or exceeds the
+		 * 	resolution depth cap.
 		 */
 		public Theme build() {
-			return new Theme(name, tokens);
+			return new Theme(name, resolveReferences());
+		}
+
+		/** Resolves every reference in a copy of the token map, preserving declaration order; never mutates {@link #tokens}. */
+		private Map<String,String> resolveReferences() {
+			var resolved = new LinkedHashMap<String,String>();
+			for (var e : tokens.entrySet()) {
+				var target = referencedName(e.getValue());
+				resolved.put(e.getKey(), target == null ? e.getValue() : resolveReference(e.getKey(), target));
+			}
+			return resolved;
+		}
+
+		/**
+		 * Iteratively walks a reference chain from {@code firstTarget} to the literal it names, with exact shadowing
+		 * (own tokens win over {@link Theme#OPEN}'s), cycle detection, and an independent hop cap; re-validates the
+		 * resolved literal against the grammar as defense-in-depth.
+		 */
+		private String resolveReference(String definingName, String firstTarget) {
+			// Theme.OPEN is null only while Theme.OPEN itself is being built - and it has no references, so this
+			// fallback map is never actually consulted during that construction.
+			var openTokens = Theme.OPEN == null ? Collections.<String,String>emptyMap() : Theme.OPEN.getTokens();
+			var visited = new LinkedHashSet<String>();
+			visited.add(definingName);
+			var target = firstTarget;
+			var hops = 0;
+			while (true) {
+				if (visited.contains(target))
+					throw iaex("Theme token '%s' contains a cyclic reference: %s.", definingName, cyclePath(visited, target));
+				if (++hops > MAX_REFERENCE_HOPS)
+					throw iaex("Theme token '%s' exceeds the maximum reference depth of %d hops.", definingName, MAX_REFERENCE_HOPS);
+				visited.add(target);
+
+				String targetValue;
+				if (tokens.containsKey(target))
+					targetValue = tokens.get(target);          // own map wins outright (exact shadowing)
+				else if (openTokens.containsKey(target))
+					targetValue = openTokens.get(target);
+				else
+					throw iaex("Theme token '%s' references unknown token '%s'.", definingName, target);
+
+				var next = referencedName(targetValue);
+				if (next == null)
+					// The resolved literal - and only that literal, with no var() present - is re-validated by the
+					// unchanged grammar as defense-in-depth against a bad value hiding under a referenceable name.
+					return CssValueGrammar.normalizeAndValidate(targetValue);
+				target = next;
+			}
+		}
+
+		/** Returns the {@code --jc-name} a {@code var(--jc-name)} reference points at, or <jk>null</jk> if {@code value} is not a reference. */
+		private static String referencedName(String value) {
+			var m = VAR_REFERENCE.matcher(value);
+			return m.matches() ? m.group(1) : null;
+		}
+
+		/** Renders a cycle path like {@code --jc-a -> --jc-b -> --jc-a} for a build-failure message. */
+		private static String cyclePath(Set<String> visited, String repeated) {
+			return String.join(" -> ", visited) + " -> " + repeated;
 		}
 	}
 }
diff --git a/juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin_Test.java b/juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin_Test.java
index afcef00020..e5fe833842 100644
--- a/juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin_Test.java
+++ b/juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin_Test.java
@@ -849,6 +849,34 @@ class ConsoleChromeMixin_Test extends TestBase {
 		assertEquals(bodyOf(MockRestClient.buildLax(DefaultHost.class)), bodyOf(MockRestClient.buildLax(DefaultHost.class)));
 	}
 
+	//-----------------------------------------------------------------------------------------------------------------
+	// p) var(--jc-name) reference resolution reaches the served body as a literal
+	//-----------------------------------------------------------------------------------------------------------------
+
+	/**
+	 * Mirrors the {@code ReleaseManagerTheme} acceptance case: a derived token expressed as
+	 * {@code var(--jc-danger)} on the SAME builder as its target, so the served override block carries the resolved
+	 * literal, never the unresolved reference.
+	 */
+	private static final Theme DERIVED_THEME = Theme.create("derived")
+		.token("--jc-danger", "#c23934")
+		.token("--jc-tag-red-text", "var(--jc-danger)")
+		.build();
+
+	@Rest(mixins=ConsoleChromeMixin.class)
+	public static class DerivedHost extends BasicRestServlet {
+		private static final long serialVersionUID = 1L;
+		@Bean public ConsoleChromeMixin console() { return ConsoleChromeMixin.create().theme(DERIVED_THEME).build(); }
+	}
+
+	@Test void p01_varReference_isResolvedToItsLiteral_inTheServedOverrideBlock() throws Exception {
+		var body = bodyOf(MockRestClient.buildLax(DerivedHost.class));
+		assertTrue(body.contains("--jc-tag-red-text:#c23934;"), () -> "reference not resolved to its literal in the served body:\n" + body);
+		// The DECLARATION must be the literal, not a var() reference (chrome.css legitimately uses var(--jc-danger)
+		// at use-sites, so we pin the declaration form rather than a blanket substring).
+		assertFalse(body.contains("--jc-tag-red-text:var("), () -> "unresolved var() reference leaked into the served declaration:\n" + body);
+	}
+
 	//-----------------------------------------------------------------------------------------------------------------
 	// Test helpers
 	//-----------------------------------------------------------------------------------------------------------------
diff --git a/juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/CssValueGrammar_Test.java b/juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/CssValueGrammar_Test.java
index 57cde15f82..d937646d2d 100644
--- a/juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/CssValueGrammar_Test.java
+++ b/juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/CssValueGrammar_Test.java
@@ -25,7 +25,16 @@ import org.junit.jupiter.params.provider.*;
 
 /**
  * Phase 2 gate: {@code CssValueGrammar}'s allowlist-grammar accept/reject sweep, tested through
- * {@link Theme.Builder#token(String, String)} (the only call site).
+ * {@link Theme.Builder#token(String, String)}.
+ *
+ * <p>
+ * {@code token(...)} is no longer the <i>only</i> production call site for {@code CssValueGrammar}: with the
+ * {@code var(--jc-name)} Theme-layer reference feature, {@code Theme.Builder.build()} re-validates each resolved
+ * literal too (defense-in-depth). {@code var()} itself is never a grammar value shape &mdash; it is recognized and
+ * resolved one layer above the grammar &mdash; so the {@code var()} reject vectors below still fail here exactly as
+ * any other out-of-grammar value: a malformed {@code var(...)} (fallback, non-{@code --jc-} target, empty, or
+ * nested inside a gradient) never matches the anchored reference recognizer and falls straight through to grammar
+ * rejection at {@code token()} time.
  *
  * <p>
  * The GREEN bypass-vector sweep below is the B1 close-out: it must beat every named vector the plan-review
@@ -157,4 +166,25 @@ class CssValueGrammar_Test extends TestBase {
 		assertEquals("NONE", Theme.create("x").token("--jc-page-bg", "NONE").build().getTokens().get("--jc-page-bg"));
 		assertEquals("None", Theme.create("x").token("--jc-page-bg", "None").build().getTokens().get("--jc-page-bg"));
 	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// GREEN: malformed var(...) forms are NOT the anchored var(--jc-name) reference, so they never reach the
+	// reference recognizer and REJECT at the grammar layer at token() time, exactly like any other non-shape value.
+	// A well-formed var(--jc-name) reference is deliberately absent here - it is ACCEPTED (deferred) at token() and
+	// is exercised by Theme_VarReferences_Test instead; it must NOT be added to a07's positive (verbatim) sweep.
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@ParameterizedTest
+	@ValueSource(strings = {
+		"var(--jc-x, #fff)",                    // fallback branch - comma inside parens, never matches the recognizer
+		"VAR(--jc-x, red)",                     // case-insensitive fallback - still a fallback, still rejected
+		"var(--not-jc-x)",                      // target is not a --jc- name
+		"var()",                                // no argument
+		"var(--jc-x) #fff",                     // reference is not the ENTIRE value (recognizer is fully anchored)
+		"linear-gradient(var(--jc-a), #fff)",   // nested var() - grammar-layer reject (var not in the nested allowlist)
+	})
+	void a12_malformedOrNestedVar_rejectedAtGrammarLayer(String payload) {
+		var b = Theme.create("x");
+		assertThrows(IllegalArgumentException.class, () -> b.token("--jc-accent", payload));
+	}
 }
diff --git a/juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/Theme_VarReferences_Test.java b/juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/Theme_VarReferences_Test.java
new file mode 100644
index 0000000000..1f5bcef010
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-console-ui/src/test/java/org/apache/juneau/rest/server/console/Theme_VarReferences_Test.java
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *      http://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.apache.juneau.rest.server.console;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * {@code var(--jc-name)} Theme-layer reference recognition and composition-time ({@code build()}) resolution.
+ *
+ * <p>
+ * The load-bearing security property here is that {@code var()} is <b>never</b> a {@code CssValueGrammar} value
+ * shape: it is recognized one layer above the grammar and resolved to a concrete literal at {@code build()} time,
+ * so {@code getTokens()} never carries the substring {@code var(} (see {@code d02}). Resolution is fail-closed:
+ * an unknown reference, a cycle, or a chain past the depth cap is a loud {@code build()} failure with its own
+ * distinct message, never a silent fall-through (see the {@code c} group).
+ */
+class Theme_VarReferences_Test extends TestBase {
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// a) Recognition + resolution positives
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test void a01_ownMapReference_resolvesToTheReferencedLiteral() {
+		// The acceptance case (mirrors ReleaseManagerTheme): both tokens on the SAME builder, so this exercises
+		// own-map resolution, not the Theme.OPEN fallback.
+		var theme = Theme.create("derived")
+			.token("--jc-danger", "#c23934")
+			.token("--jc-tag-red-text", "var(--jc-danger)")
+			.build();
+		assertEquals("#c23934", theme.getTokens().get("--jc-tag-red-text"));
+		assertEquals("#c23934", theme.getTokens().get("--jc-danger"));
+	}
+
+	@Test void a02_forwardReference_resolvesRegardlessOfDeclarationOrder() {
+		// Reference declared BEFORE its target - only resolvable at build(), the whole point of deferring.
+		var theme = Theme.create("fwd")
+			.token("--jc-a", "var(--jc-b)")
+			.token("--jc-b", "#abcabc")
+			.build();
+		assertEquals("#abcabc", theme.getTokens().get("--jc-a"));
+	}
+
+	@Test void a03_fallbackToThemeOpen_resolvesAgainstTheFrameworkTokenSet() {
+		// Custom theme references a --jc-* name it does not itself define; resolution finds it on Theme.OPEN.
+		var theme = Theme.create("fb").token("--jc-a", "var(--jc-danger)").build();
+		assertEquals("#c23934", theme.getTokens().get("--jc-a"));
+	}
+
+	@Test void a04_multiHopChain_withinCap_resolvesToTheTerminalLiteral() {
+		var theme = Theme.create("hop")
+			.token("--jc-a", "var(--jc-b)")
+			.token("--jc-b", "var(--jc-c)")
+			.token("--jc-c", "#0a0b0c")
+			.build();
+		assertEquals("#0a0b0c", theme.getTokens().get("--jc-a"));
+		assertEquals("#0a0b0c", theme.getTokens().get("--jc-b"));
+	}
+
+	@Test void a05_recognitionVariants_shareTheSameNormalizationBelt() {
+		// Comment-stripping, case-insensitivity, and interior whitespace are handled by the SAME belt every other
+		// value runs through - there is no second, reference-specific normalization pass.
+		var theme = Theme.create("rec")
+			.token("--jc-x", "#123456")
+			.token("--jc-comment", "var/**/(--jc-x)")
+			.token("--jc-inner-comment", "var(--jc-/**/x)")
+			.token("--jc-upper", "VAR(--jc-x)")
+			.token("--jc-space", "var( --jc-x )")
+			.build();
+		assertEquals("#123456", theme.getTokens().get("--jc-comment"));
+		assertEquals("#123456", theme.getTokens().get("--jc-inner-comment"));
+		assertEquals("#123456", theme.getTokens().get("--jc-upper"));
+		assertEquals("#123456", theme.getTokens().get("--jc-space"));
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// b) Deferred timing / atomicity
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test void b01_referenceIsAcceptedAtTokenTime_evenWhenTargetIsNotYetDefined() {
+		// A well-formed reference must NOT throw at token() time (its target may be a forward reference).
+		assertDoesNotThrow(() -> Theme.create("t").token("--jc-a", "var(--jc-later)"));
+	}
+
+	@Test void b02_failedBuild_leavesBuilderUnchanged_andRetryable() {
+		// Resolution runs on a COPY: a failed build() must not half-resolve the builder's own map.
+		var b = Theme.create("atom").token("--jc-a", "var(--jc-missing)");
+		assertThrows(IllegalArgumentException.class, b::build);
+		// The reference survived intact, so defining its target and rebuilding now succeeds.
+		var theme = b.token("--jc-missing", "#123123").build();
+		assertEquals("#123123", theme.getTokens().get("--jc-a"));
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// c) Fail-closed resolution: unknown / cycle / depth-cap, each with a DISTINCT message
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test void c01_unknownReference_isABuildFailure_namingBothTokens() {
+		var ex = assertThrows(IllegalArgumentException.class,
+			() -> Theme.create("u").token("--jc-a", "var(--jc-missing)").build());
+		assertTrue(ex.getMessage().contains("--jc-a"), () -> "message must name the defining token: " + ex.getMessage());
+		assertTrue(ex.getMessage().contains("--jc-missing"), () -> "message must name the missing target: " + ex.getMessage());
+		assertTrue(ex.getMessage().contains("unknown"), () -> "message: " + ex.getMessage());
+		// Must NOT reuse the pre-existing "not one of the allowed CSS value shapes" message.
+		assertFalse(ex.getMessage().contains("allowed CSS value shapes"), () -> "message: " + ex.getMessage());
+	}
+
+	@Test void c02_shadowedBrokenOverride_failsInsteadOfSilentlyFallingBackToThemeOpen() {
+		// --jc-danger EXISTS on Theme.OPEN (#c23934). Shadowing it here with a broken reference must FAIL the build,
+		// NOT silently resolve through to Theme.OPEN's value - the exact bug the reference feature exists to catch.
+		var ex = assertThrows(IllegalArgumentException.class,
+			() -> Theme.create("shadow").token("--jc-danger", "var(--jc-nonexistent)").build());
+		assertTrue(ex.getMessage().contains("--jc-nonexistent"), () -> "message: " + ex.getMessage());
+		assertTrue(ex.getMessage().contains("unknown"), () -> "message: " + ex.getMessage());
+	}
+
+	@Test void c03_selfCycle_isABuildFailure_withTheCyclePath() {
+		var ex = assertThrows(IllegalArgumentException.class,
+			() -> Theme.create("cyc").token("--jc-a", "var(--jc-a)").build());
+		assertTrue(ex.getMessage().contains("cyclic"), () -> "message: " + ex.getMessage());
+		assertTrue(ex.getMessage().contains("--jc-a -> --jc-a"), () -> "message must carry the cycle path: " + ex.getMessage());
+	}
+
+	@Test void c04_twoNodeCycle_isABuildFailure_withTheFullCyclePath() {
+		var ex = assertThrows(IllegalArgumentException.class,
+			() -> Theme.create("cyc2")
+				.token("--jc-a", "var(--jc-b)")
+				.token("--jc-b", "var(--jc-a)")
+				.build());
+		assertTrue(ex.getMessage().contains("cyclic"), () -> "message: " + ex.getMessage());
+		assertTrue(ex.getMessage().contains("--jc-a -> --jc-b -> --jc-a"), () -> "message must carry the full cycle path: " + ex.getMessage());
+	}
+
+	@Test void c05_depthCapBreach_isADistinctFailure_fromCycleAndUnknown() {
+		// A long ACYCLIC chain (all names distinct, terminal token defined) must still be bounded by the hop cap -
+		// so this fails for depth, not cycle (no repeat) and not unknown (--jc-c10 exists).
+		var b = Theme.create("deep");
+		for (var i = 0; i < 10; i++)
+			b.token("--jc-c" + i, "var(--jc-c" + (i + 1) + ")");
+		b.token("--jc-c10", "#000000");
+		var ex = assertThrows(IllegalArgumentException.class, b::build);
+		assertTrue(ex.getMessage().contains("maximum reference depth"), () -> "message: " + ex.getMessage());
+		assertFalse(ex.getMessage().contains("cyclic"), () -> "depth-cap message must be distinct from the cycle message: " + ex.getMessage());
+		assertFalse(ex.getMessage().contains("unknown"), () -> "depth-cap message must be distinct from the unknown-name message: " + ex.getMessage());
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// d) Defense-in-depth + post-build invariant
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test void d01_resolvedLiteralThatTheGrammarRejects_wouldFailTheReValidation() {
+		// The resolved literal (never an intermediate var()) is re-validated by the unchanged grammar. Through the
+		// public API every literal is already grammar-validated at token() time, so this path cannot be driven to a
+		// failure here - it is exercised (every resolved value passes back through normalizeAndValidate) rather than
+		// tripped. This test pins that a reference to a plain literal round-trips byte-for-byte through that
+		// re-validation.
+		var theme = Theme.create("dd")
+			.token("--jc-a", "linear-gradient(135deg, rgb(21,137,238), rgba(0,0,0,0.2))")
+			.token("--jc-b", "var(--jc-a)")
+			.build();
+		assertEquals("linear-gradient(135deg, rgb(21,137,238), rgba(0,0,0,0.2))", theme.getTokens().get("--jc-b"));
+	}
+
+	@Test void d02_afterBuild_noTokenValueContainsTheVarSubstring() {
+		var theme = Theme.create("inv")
+			.token("--jc-danger", "#c23934")
+			.token("--jc-tag-red-text", "var(--jc-danger)")   // own-map reference
+			.token("--jc-a", "var(--jc-accent)")               // Theme.OPEN fallback reference
+			.token("--jc-b", "var(--jc-danger)")               // chained onto another own token
+			.token("--jc-lit", "#1589EE")                      // plain literal, untouched
+			.build();
+		for (var v : theme.getTokens().values())
+			assertFalse(v.contains("var("), () -> "resolved token value still contains 'var(': " + v);
+	}
+}