This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch feature/CAMEL-25514-xslt-braces-hint in repository https://gitbox.apache.org/repos/asf/camel.git
commit cb48c25bfb104a716cacf1907db5577235d13de3 Author: Claus Ibsen <[email protected]> AuthorDate: Sat Oct 10 07:59:10 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 (a function call, a path, an attribute or a variable), 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. The camel-jbang monitor's own write gives the note too. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m --- .../dsl/jbang/core/commands/ai/AuthoringTools.java | 12 ++++-- .../jbang/core/commands/ai/SourceValidator.java | 8 ++++ .../dsl/jbang/core/commands/ai/XmlChecks.java | 48 ++++++++++++++++++++++ .../commands/ai/SourceValidatorJavaXsltTest.java | 29 +++++++++++++ .../dsl/jbang/core/commands/tui/McpFacade.java | 11 ++++- 5 files changed, 104 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 453aed7c17bc..d80b7915ef8c 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); @@ -1047,13 +1047,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)); + } } } @@ -1117,7 +1123,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..85072308fda9 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,52 @@ 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")) { + 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; }
