This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/camel-4.22.x by this push:
     new 0e825c1540cc CAMEL-25224: camel-xslt - preserve document context for 
implicitly converted sources (#27286)
0e825c1540cc is described below

commit 0e825c1540ccfd71afdaf37be715fa3a5eaa4bde
Author: Salvatore Mongiardo <[email protected]>
AuthorDate: Sat Oct 3 10:17:14 2026 +0200

    CAMEL-25224: camel-xslt - preserve document context for implicitly 
converted sources (#27286)
    
    Backport of #27154 to camel-4.22.x. When a DOMSource or SAXSource was 
implicitly converted before being handed to the XSLT processor, the document 
context was lost, so relative references in stylesheets (xsl:import, 
document()) could resolve incorrectly.
---
 .../xslt/saxon/XsltSaxonDomSourceTest.java         | 290 +++++++++++++++++++++
 .../camel/component/xslt/saxon/dom-source-root.xsl |  38 +++
 .../xslt/XmlSourceHandlerFactoryImpl.java          |  29 ++-
 3 files changed, 348 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 6e254d57f25a..cb2a3c7b497c 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;
     }
 
     /**

Reply via email to