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;
     }

Reply via email to