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 d404f4f3825d CAMEL-25224: camel-xslt: Preserve document context for 
implicitly converted sources (#27154)
d404f4f3825d is described below

commit d404f4f3825d32c330f12ac0966d7d77f9c36108
Author: Salvatore Mongiardo <[email protected]>
AuthorDate: Thu Oct 1 17:41:14 2026 +0200

    CAMEL-25224: camel-xslt: Preserve document context for implicitly converted 
sources (#27154)
---
 .../xslt/saxon/XsltSaxonDomSourceTest.java         | 290 +++++++++++++++++++++
 .../camel/component/xslt/saxon/dom-source-root.xsl |  38 +++
 .../xslt/XmlSourceHandlerFactoryImpl.java          |  29 ++-
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  15 ++
 4 files changed, 363 insertions(+), 9 deletions(-)

diff --git 
a/components/camel-xslt-saxon/src/test/java/org/apache/camel/component/xslt/saxon/XsltSaxonDomSourceTest.java
 
b/components/camel-xslt-saxon/src/test/java/org/apache/camel/component/xslt/saxon/XsltSaxonDomSourceTest.java
new file mode 100644
index 000000000000..082e47fb66b9
--- /dev/null
+++ 
b/components/camel-xslt-saxon/src/test/java/org/apache/camel/component/xslt/saxon/XsltSaxonDomSourceTest.java
@@ -0,0 +1,290 @@
+/*
+ * 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.camel.component.xslt.saxon;
+
+import java.io.StringReader;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.transform.Source;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.sax.SAXSource;
+import javax.xml.transform.stax.StAXSource;
+import javax.xml.transform.stream.StreamSource;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import org.xml.sax.InputSource;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.Expression;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.builder.ExpressionBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.support.TypeConverterSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class XsltSaxonDomSourceTest {
+
+    private static final String ENDPOINT = 
"xslt-saxon:org/apache/camel/component/xslt/saxon/dom-source-root.xsl";
+    private static final String INPUT = """
+            <req:Request xmlns:req="urn:request">
+              <req:Header>SOURCE_SYSTEM_VALUE</req:Header>
+              <req:Authentication>
+                <req:UserName>test-user</req:UserName>
+                <req:Password>test-password</req:Password>
+              </req:Authentication>
+            </req:Request>
+            """;
+
+    private DefaultCamelContext context;
+    private ProducerTemplate template;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        context = new DefaultCamelContext();
+        // Force the DOM and generic Source fallback paths without depending 
on converter discovery order.
+        TypeConverterSupport converter = new TypeConverterSupport() {
+            @Override
+            public <T> T convertTo(Class<T> type, Exchange exchange, Object 
value) {
+                Payload payload = (Payload) value;
+                if (type == Source.class || type == payload.source.getClass() 
&& !payload.genericSourceOnly) {
+                    payload.conversions++;
+                    return type.cast(payload.source);
+                }
+                return null;
+            }
+        };
+        for (Class<?> type : new Class<?>[] {
+                Source.class, DOMSource.class, SAXSource.class,
+                StreamSource.class, StAXSource.class }) {
+            context.getTypeConverterRegistry().addTypeConverter(type, 
Payload.class, converter);
+        }
+        context.start();
+        template = context.createProducerTemplate();
+        template.start();
+    }
+
+    @AfterEach
+    void tearDown() throws Exception {
+        if (template != null) {
+            template.stop();
+        }
+        if (context != null) {
+            context.stop();
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = { false, true })
+    void testImplicitDocumentElement(boolean genericSourceOnly) throws 
Exception {
+        Document document = parse(INPUT);
+        Document original = (Document) document.cloneNode(true);
+        Element element = document.getDocumentElement();
+        DOMSource source = new DOMSource(element, "file:/payload/request.xml");
+        Payload payload = new Payload(source, genericSourceOnly);
+        // Reuse the converter's cached DOMSource, as an Element-backed CXF 
payload does.
+        for (int i = 0; i < 3; i++) {
+            String result = template.requestBody(ENDPOINT, payload, 
String.class);
+            assertTrue(result.contains("<root"), result);
+            Document output = parse(result);
+            assertEquals("root", output.getDocumentElement().getNodeName());
+            assertEquals("test-user", 
output.getElementsByTagNameNS("urn:login", "UserId").item(0).getTextContent());
+            assertEquals("test-password", 
output.getElementsByTagNameNS("urn:login", 
"Password").item(0).getTextContent());
+            assertEquals(1, output.getElementsByTagNameNS("urn:login", 
"LogIn_Input").getLength());
+            assertFalse(result.contains("SOURCE_SYSTEM_VALUE"), result);
+        }
+        assertTrue(payload.conversions >= 3, "The test must use the registered 
DOM converter");
+        assertSame(element, source.getNode());
+        assertSame(document, element.getOwnerDocument());
+        assertSame(document, element.getParentNode());
+        assertTrue(original.isEqualNode(document), "Conversion must not modify 
the original DOM");
+        Source prepared = getSource(payload);
+        assertNotSame(source, prepared);
+        assertSame(document, ((DOMSource) prepared).getNode());
+        assertEquals(source.getSystemId(), prepared.getSystemId());
+        assertEquals("file:/payload/request.xml", source.getSystemId());
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = { false, true })
+    void testStaxLookupReturningDomSource(boolean sourceExpression) throws 
Exception {
+        Document document = parse(INPUT);
+        Document original = (Document) document.cloneNode(true);
+        DOMSource source = new DOMSource(document.getDocumentElement(), 
"file:/payload/request.xml");
+        GenericSourcePayload payload = new GenericSourcePayload(source);
+        TypeConverterSupport converter = new TypeConverterSupport() {
+            @Override
+            @SuppressWarnings("unchecked")
+            public <T> T convertTo(Class<T> type, Exchange exchange, Object 
value) {
+                GenericSourcePayload input = (GenericSourcePayload) value;
+                input.requestedType = type;
+                return (T) input.source;
+            }
+        };
+        context.getTypeConverterRegistry().addTypeConverter(Source.class, 
GenericSourcePayload.class, converter);
+        // Pin the generic Source converter selected by the registry for a 
StAX request, as with CXF payloads.
+        context.getTypeConverterRegistry().addTypeConverter(StAXSource.class, 
GenericSourcePayload.class, converter);
+        for (int i = 0; i < 3; i++) {
+            String result = sourceExpression
+                    ? template.requestBodyAndHeader(
+                            ENDPOINT + "?source=header:payloadSource", "unused 
body", "payloadSource", payload, String.class)
+                    : template.requestBody(ENDPOINT, payload, String.class);
+            assertSame(StAXSource.class, payload.requestedType, "Must exercise 
Saxon's non-null StAX conversion path");
+            assertEquals("root", 
parse(result).getDocumentElement().getNodeName(), result);
+            assertFalse(result.contains("SOURCE_SYSTEM_VALUE"), result);
+        }
+        Source prepared = getSource(payload);
+        assertSame(document, ((DOMSource) prepared).getNode());
+        assertNotSame(source, prepared);
+        assertEquals(source.getSystemId(), prepared.getSystemId());
+        assertSame(document.getDocumentElement(), source.getNode());
+        assertSame(document, source.getNode().getParentNode());
+        assertTrue(original.isEqualNode(document));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "element", "nested", "detached" })
+    void testExplicitElementSource(String kind) throws Exception {
+        DOMSource source = elementSource(kind);
+        assertSame(source, getSource(source));
+        assertElementContext(template.requestBody(ENDPOINT, source, 
String.class));
+    }
+
+    @Test
+    void testExplicitSourceExpression() throws Exception {
+        DOMSource source = elementSource("element");
+        Exchange exchange = new DefaultExchange(context);
+        exchange.getMessage().setBody(new Payload(source, false));
+        exchange.getMessage().setHeader("payloadSource", source);
+        Expression expression = 
ExpressionBuilder.headerExpression("payloadSource");
+        assertSame(source, new 
SaxonXmlSourceHandlerFactoryImpl().getSource(exchange, expression));
+        String result = template.requestBodyAndHeader(
+                ENDPOINT + "?source=header:payloadSource", new Payload(source, 
false), "payloadSource", source, String.class);
+        assertElementContext(result);
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "nested", "detached" })
+    void testImplicitSubtree(String kind) throws Exception {
+        DOMSource source = elementSource(kind);
+        Element element = (Element) source.getNode();
+        Document document = element.getOwnerDocument();
+        Document original = (Document) document.cloneNode(true);
+        var parent = element.getParentNode();
+        Payload payload = new Payload(source, false);
+        assertSame(source, getSource(payload));
+        assertElementContext(template.requestBody(ENDPOINT, payload, 
String.class));
+        assertSame(element, source.getNode());
+        assertSame(parent, element.getParentNode());
+        assertSame(document, element.getOwnerDocument());
+        assertTrue(original.isEqualNode(document));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "document", "sax", "stream", "stax" })
+    void testExplicitDocumentAndStreamingSources(String kind) throws Exception 
{
+        Source source = documentOrStreamingSource(kind);
+        assertSame(source, getSource(source));
+        String result = template.requestBody(ENDPOINT, source, String.class);
+        assertTrue(result.contains("<root"), result);
+        assertEquals("root", parse(result).getDocumentElement().getNodeName());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "document", "sax", "stream", "stax" })
+    void testImplicitDocumentAndStreamingSources(String kind) throws Exception 
{
+        Source source = documentOrStreamingSource(kind);
+        Payload payload = new Payload(source, false);
+        assertSame(source, getSource(payload));
+        String result = template.requestBody(ENDPOINT, payload, String.class);
+        assertEquals("root", parse(result).getDocumentElement().getNodeName());
+        assertTrue(payload.conversions >= 2);
+    }
+
+    private static Source documentOrStreamingSource(String kind) throws 
Exception {
+        return switch (kind) {
+            case "document" -> new DOMSource(parse(INPUT));
+            case "sax" -> new SAXSource(new InputSource(new 
StringReader(INPUT)));
+            case "stream" -> new StreamSource(new StringReader(INPUT));
+            case "stax" -> new 
StAXSource(XMLInputFactory.newFactory().createXMLStreamReader(new 
StringReader(INPUT)));
+            default -> throw new IllegalArgumentException(kind);
+        };
+    }
+
+    private Source getSource(Object body) throws Exception {
+        Exchange exchange = new DefaultExchange(context);
+        exchange.getMessage().setBody(body);
+        return new SaxonXmlSourceHandlerFactoryImpl().getSource(exchange, 
(Expression) null);
+    }
+
+    private static void assertElementContext(String result) throws Exception {
+        assertEquals("element-context", 
parse(result).getDocumentElement().getNodeName(), result);
+        assertFalse(result.contains("OUTSIDE_PAYLOAD"), result);
+        assertFalse(result.contains("UNRELATED"), result);
+    }
+
+    private static DOMSource elementSource(String kind) throws Exception {
+        Document document = parse(INPUT);
+        Element element = document.getDocumentElement();
+        if (kind.equals("nested")) {
+            document = parse("<envelope>OUTSIDE_PAYLOAD" + INPUT + 
"<other>UNRELATED</other></envelope>");
+            element = (Element) 
document.getDocumentElement().getElementsByTagNameNS("urn:request", 
"Request").item(0);
+        } else if (kind.equals("detached")) {
+            document.removeChild(element);
+        }
+        return new DOMSource(element);
+    }
+
+    private static Document parse(String xml) throws Exception {
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        factory.setNamespaceAware(true);
+        return factory.newDocumentBuilder().parse(new InputSource(new 
StringReader(xml)));
+    }
+
+    private static final class GenericSourcePayload {
+        private final DOMSource source;
+        private Class<?> requestedType;
+
+        private GenericSourcePayload(DOMSource source) {
+            this.source = source;
+        }
+    }
+
+    private static final class Payload {
+        private final Source source;
+        private final boolean genericSourceOnly;
+        private int conversions;
+
+        private Payload(Source source, boolean genericSourceOnly) {
+            this.source = source;
+            this.genericSourceOnly = genericSourceOnly;
+        }
+    }
+}
diff --git 
a/components/camel-xslt-saxon/src/test/resources/org/apache/camel/component/xslt/saxon/dom-source-root.xsl
 
b/components/camel-xslt-saxon/src/test/resources/org/apache/camel/component/xslt/saxon/dom-source-root.xsl
new file mode 100644
index 000000000000..935d5ed411bf
--- /dev/null
+++ 
b/components/camel-xslt-saxon/src/test/resources/org/apache/camel/component/xslt/saxon/dom-source-root.xsl
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform";
+                xmlns:req="urn:request" xmlns:inl="urn:login">
+    <xsl:template match="/">
+        <root>
+            <xsl:apply-templates select="//req:Authentication"/>
+        </root>
+    </xsl:template>
+    <xsl:template match="req:Request">
+        <element-context>
+            <xsl:apply-templates select="req:Authentication"/>
+        </element-context>
+    </xsl:template>
+    <xsl:template match="req:Authentication">
+        <inl:LogIn_Input>
+            <inl:UserId><xsl:value-of select="req:UserName"/></inl:UserId>
+            <inl:Password><xsl:value-of select="req:Password"/></inl:Password>
+        </inl:LogIn_Input>
+    </xsl:template>
+</xsl:stylesheet>
diff --git 
a/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XmlSourceHandlerFactoryImpl.java
 
b/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XmlSourceHandlerFactoryImpl.java
index b56a995a931d..d2fde011bedb 100644
--- 
a/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XmlSourceHandlerFactoryImpl.java
+++ 
b/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XmlSourceHandlerFactoryImpl.java
@@ -25,6 +25,8 @@ import javax.xml.transform.dom.DOMSource;
 import javax.xml.transform.sax.SAXSource;
 import javax.xml.transform.stream.StreamSource;
 
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
 import org.w3c.dom.Node;
 
 import org.apache.camel.Exchange;
@@ -58,18 +60,27 @@ public class XmlSourceHandlerFactoryImpl implements 
SourceHandlerFactory {
 
     @Override
     public Source getSource(Exchange exchange, Expression source) throws 
Exception {
+        Object body;
         if (source != null) {
-            Object body = source.evaluate(exchange, Object.class);
-            return getSource(exchange, body);
-        }
-        // only convert to input stream if really needed
-        if (isInputStreamNeeded(exchange)) {
-            InputStream is = exchange.getIn().getBody(InputStream.class);
-            return getSource(exchange, is);
+            body = source.evaluate(exchange, Object.class);
+        } else if (isInputStreamNeeded(exchange)) {
+            // only convert to input stream if really needed
+            body = exchange.getIn().getBody(InputStream.class);
         } else {
-            Object body = exchange.getMessage().getBody();
-            return getSource(exchange, body);
+            body = exchange.getMessage().getBody();
+        }
+
+        Source answer = getSource(exchange, body);
+        // Normalize after dispatch: a subclass may obtain a DOMSource while 
requesting another Source type.
+        // Explicit Sources, including source-expression results, keep their 
chosen context node.
+        if (!(body instanceof Source) && answer instanceof DOMSource domSource
+                && domSource.getNode() instanceof Element element) {
+            Document document = element.getOwnerDocument();
+            if (document != null && element == document.getDocumentElement()) {
+                return new DOMSource(document, domSource.getSystemId());
+            }
         }
+        return answer;
     }
 
     /**
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 7b807df8f8a2..0e5931d385aa 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -4048,6 +4048,21 @@ Previously it read the `SessionID` header when the route 
completed, so a route t
 which session received the reply. A route that changed the `SessionID` header 
to send the reply to another session
 must now send that message with a QuickFIX/J producer endpoint that sets the 
`sessionID` option.
 
+=== camel-xslt and camel-xslt-saxon - implicit DOM sources
+
+When a non-`Source` input is implicitly converted to a `DOMSource` pointing at 
its owner document's
+root element, the Saxon-backed XSLT transformer now starts at the document 
node. This preserves
+document-root (`match="/"`) template behavior when, for example, a CXF payload 
is converted to a DOM
+source. The default JDK XSLT transformer already starts at the document node; 
the change also applies
+to `xslt:` when configured with Saxon as its transformer factory.
+The original DOM and source system ID are preserved. Nested and detached 
elements are unchanged.
+Explicit `Source` inputs, including a `Source` returned by the endpoint's 
`source` expression,
+retain their selected context node, and streaming sources are unchanged.
+
+Custom implicit converters that intentionally select the document element as 
the XSLT context
+may observe different template selection. To retain element-context semantics, 
provide an explicit
+`DOMSource(element)` as the body or through the endpoint's `source` expression.
+
 === camel-xslt / camel-xslt-saxon - external document() access is denied by 
default
 
 Camel's default XSLT transformer factory (`XMLConverterHelper`) sets 
`ACCESS_EXTERNAL_DTD` and

Reply via email to