You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/10/10 06:41:16 UTC

(camel) branch main updated: CAMEL-25514: camel-jbang - the validator notes a {expression} in element text of an XSLT stylesheet

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 <cl...@gmail.com>
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) <no...@anthropic.com>
---
 .../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;
     }