This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 13fe67091892 CAMEL-25514: camel-jbang - the validator notes a
{expression} in element text of an XSLT stylesheet
13fe67091892 is described below
commit 13fe6709189218f338d195b4b9508856c5e19e4e
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Oct 10 08:41:08 2026 +0200
CAMEL-25514: camel-jbang - the validator notes a {expression} in element
text of an XSLT stylesheet
An attribute value template is evaluated only in an attribute value. In
element content of an XSLT 1.0 or 2.0 stylesheet (camel-xslt is XSLT 1.0),
<pieces>{sum(line/@qty)}</pieces> writes the braces and the expression out
as text, with no error. A local model wrote this in the benchmark's
transform-xslt example and did not find the cause in 20 tool calls.
A write, an edit or camel_validate_source of a .xsl file now answers with
a note (it does not refuse the write) for text in an output element that
holds a {expression} reading as XPath, with the xsl:value-of to write
instead. Braces around text with quotes (JSON), in xsl:text, or in a
stylesheet with expand-text are left alone.
Closes #27648
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../dsl/jbang/core/commands/ai/AuthoringTools.java | 12 ++++--
.../jbang/core/commands/ai/SourceValidator.java | 8 ++++
.../dsl/jbang/core/commands/ai/XmlChecks.java | 50 ++++++++++++++++++++++
.../commands/ai/SourceValidatorJavaXsltTest.java | 29 +++++++++++++
.../dsl/jbang/core/commands/tui/McpFacade.java | 11 ++++-
5 files changed, 106 insertions(+), 4 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
index cedd14b6a98e..61e105595dc7 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
@@ -541,7 +541,7 @@ public final class AuthoringTools {
result.put("file", file);
result.put("errors", new JsonArray(errors));
putKameletGuide(result, file, errors);
- putKameletNotes(result, file, content);
+ putFileNotes(result, file, content);
// the problems whose fix is certain, as edits an agent can apply
(camel_edit_file find/replace)
JsonArray fixes = new JsonArray();
String[] lines = content.split("\n", -1);
@@ -1049,13 +1049,19 @@ public final class AuthoringTools {
* What a Kamelet file does that works but is not right, which does not
refuse a write: a camel: dependency its
* template does not use (CAMEL-25403).
*/
- private static void putKameletNotes(JsonObject result, String file, String
content) {
+ private static void putFileNotes(JsonObject result, String file, String
content) {
String name = file != null ? file.toLowerCase(Locale.ROOT) : "";
if (name.endsWith(".kamelet.yaml") || name.endsWith(".kamelet.yml")) {
List<String> notes = KameletChecks.unusedDependencies(content);
if (!notes.isEmpty()) {
result.put("notes", new JsonArray(notes));
}
+ } else if (name.endsWith(".xsl") || name.endsWith(".xslt")) {
+ // a {expression} in element content is written out as text
(CAMEL-25514)
+ List<String> notes = SourceValidator.xsltNotes(content);
+ if (!notes.isEmpty()) {
+ result.put("notes", new JsonArray(notes));
+ }
}
}
@@ -1133,7 +1139,7 @@ public final class AuthoringTools {
}
UnconsumedDirect waiting = null;
if (validate) {
- putKameletNotes(result, file, content);
+ putFileNotes(result, file, content);
waiting = unconsumedDirect(dir, file, content, ctx.catalog());
if (waiting != null) {
JsonArray notes = result.get("notes") instanceof JsonArray
existing ? existing : new JsonArray();
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
index 807b5a18217a..98e3b451555a 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
@@ -522,6 +522,14 @@ public final class SourceValidator {
return XmlChecks.validateXslt(content);
}
+ /**
+ * What a stylesheet does that works but is not what was meant, which does
not refuse a write: a {expression} in
+ * element content, written out as text (CAMEL-25514).
+ */
+ public static List<String> xsltNotes(String content) {
+ return XmlChecks.literalBraceNotes(content);
+ }
+
/** Checks that the XML is well formed (an input file, a Camel XML DSL
file or any other XML). */
public static List<String> validateXml(String content) {
return XmlChecks.validateXml(content);
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/XmlChecks.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/XmlChecks.java
index 523e51b83bae..32de946a5704 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/XmlChecks.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/XmlChecks.java
@@ -187,4 +187,54 @@ final class XmlChecks {
return msgs;
}
+ private static final String XSL_NS =
"http://www.w3.org/1999/XSL/Transform";
+ /** A {...} in text that reads as an XPath expression: a function call, a
path, an attribute or a variable. */
+ private static final Pattern LITERAL_BRACES =
Pattern.compile("\\{([^{}\"']*(?:\\w\\(|/|@|\\$)[^{}\"']*)}");
+
+ /**
+ * Text in an element the stylesheet writes out (not inside an xsl:
instruction) that holds a {expression}: in XSLT
+ * 1.0 and 2.0 an attribute value template is evaluated only in an
attribute value, so in element content the braces
+ * and the expression are written as they are, with no error
(CAMEL-25514). Not reported when the stylesheet turns
+ * on expand-text (XSLT 3.0), nor for braces around text with quotes (JSON
written by the stylesheet).
+ */
+ static List<String> literalBraceNotes(String content) {
+ List<String> notes = new ArrayList<>();
+ if (content == null || content.isBlank() ||
content.contains("expand-text")) {
+ // coarse on purpose: a stylesheet that mentions expand-text uses
XSLT 3.0 text value templates somewhere,
+ // and telling per element would need the inherited
xsl:expand-text of every ancestor
+ return notes;
+ }
+ org.w3c.dom.Document doc;
+ try {
+ javax.xml.parsers.DocumentBuilderFactory dbf =
javax.xml.parsers.DocumentBuilderFactory.newInstance();
+
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+ dbf.setNamespaceAware(true);
+ javax.xml.parsers.DocumentBuilder db = dbf.newDocumentBuilder();
+ db.setErrorHandler(null);
+ doc = db.parse(new org.xml.sax.InputSource(new
java.io.StringReader(content)));
+ } catch (Exception e) {
+ return notes; // not well formed: the validation says so
+ }
+ collectLiteralBraces(doc.getDocumentElement(), notes);
+ return notes;
+ }
+
+ private static void collectLiteralBraces(org.w3c.dom.Element element,
List<String> notes) {
+ boolean xsl = XSL_NS.equals(element.getNamespaceURI());
+ org.w3c.dom.NodeList children = element.getChildNodes();
+ for (int i = 0; i < children.getLength(); i++) {
+ org.w3c.dom.Node child = children.item(i);
+ if (child instanceof org.w3c.dom.Element e) {
+ collectLiteralBraces(e, notes);
+ } else if (!xsl && child.getNodeType() ==
org.w3c.dom.Node.TEXT_NODE) {
+ Matcher m = LITERAL_BRACES.matcher(child.getNodeValue());
+ while (m.find()) {
+ String expr = m.group(1).trim();
+ notes.add("<" + element.getNodeName() + "> holds the text
{" + expr + "}, which is written out as it"
+ + " is: a {expression} is evaluated only in an
attribute value; in element content write"
+ + " <xsl:value-of select=\"" + expr + "\"/>");
+ }
+ }
+ }
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorJavaXsltTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorJavaXsltTest.java
index 6c82698c8c2a..8cce02386207 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorJavaXsltTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorJavaXsltTest.java
@@ -263,4 +263,33 @@ public class SourceValidatorJavaXsltTest {
assertThat(msgs.get(0)).contains("cannot find
symbol").contains("Exchange has no setHeader")
.contains("exchange.getMessage().setHeader(...)").doesNotContain("//DEPS");
}
+
+ private static String stylesheet(String body) {
+ return "<xsl:stylesheet version=\"1.0\"
xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\n"
+ + " <xsl:template match=\"/order\">\n" + body + "\n
</xsl:template>\n</xsl:stylesheet>\n";
+ }
+
+ /** CAMEL-25514: a {expression} in element content is written out as text.
*/
+ @Test
+ void aBraceExpressionInElementContentIsNoted() {
+ List<String> notes =
SourceValidator.xsltNotes(stylesheet("<pieces>{sum(line/@qty)}</pieces>"));
+
+ assertThat(notes).singleElement().asString()
+ .startsWith("<pieces> holds the text {sum(line/@qty)}, which
is written out as it is")
+ .endsWith("<xsl:value-of select=\"sum(line/@qty)\"/>");
+ }
+
+ @Test
+ void bracesThatAreMeantAreNotNoted() {
+ // in an attribute value the braces are evaluated
+ assertThat(SourceValidator.xsltNotes(stylesheet("<slip
id=\"{@id}\"/>"))).isEmpty();
+ // JSON the stylesheet writes out
+ assertThat(SourceValidator.xsltNotes(stylesheet("<json>{\"sku\":
\"x\"}</json>"))).isEmpty();
+ // xsl:text is literal on purpose
+
assertThat(SourceValidator.xsltNotes(stylesheet("<xsl:text>{a/b}</xsl:text>"))).isEmpty();
+ // a plain word in braces is not an expression for sure
+
assertThat(SourceValidator.xsltNotes(stylesheet("<p>{customer}</p>"))).isEmpty();
+ // XSLT 3.0 text value templates
+ assertThat(SourceValidator.xsltNotes(stylesheet("<p
xsl:expand-text=\"yes\">{sum(line/@qty)}</p>"))).isEmpty();
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
index 6b7bbd6a734a..9a7205e24cbe 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
@@ -1389,9 +1389,18 @@ class McpFacade {
result.put("lines", lines);
result.put("bytes", content.getBytes(StandardCharsets.UTF_8).length);
// a route sending to a direct: endpoint no route consumes yet: its
route goes in the same call (CAMEL-25501)
+ List<String> notes = new ArrayList<>();
AuthoringTools.UnconsumedDirect waiting =
AuthoringTools.unconsumedDirect(dir, file, content, null);
if (waiting != null) {
- result.put("notes", new JsonArray(List.of(waiting.note())));
+ notes.add(waiting.note());
+ }
+ String lower = file.toLowerCase(Locale.ROOT);
+ if (lower.endsWith(".xsl") || lower.endsWith(".xslt")) {
+ // a {expression} in element content is written out as text
(CAMEL-25514)
+ notes.addAll(SourceValidator.xsltNotes(content));
+ }
+ if (!notes.isEmpty()) {
+ result.put("notes", new JsonArray(notes));
}
return result;
}