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

davsclaus pushed a commit to branch CAMEL-26126-stax-body-converters
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 5e5f123687879e69531f240616bb2b48e7a96281
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 16:45:23 2026 +0100

    CAMEL-26126: camel-stax - accept a byte[], Path or DOM Document body
    
    The StAX producer read the body with 
getMandatoryBody(XMLStreamReader.class),
    and there is no converter to XMLStreamReader from a byte[], a Path or a DOM
    Document, so these bodies failed with NoTypeConversionAvailableException.
    
    When the body has no direct converter, the producer now reads it as an
    InputStream and turns that into an XMLStreamReader with the same secured
    StaxConverter factory (no DTDs, no external entities), and closes the stream
    it opened. The body types metadata and the component doc list the accepted
    bodies.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../org/apache/camel/catalog/components/stax.json  |   2 +-
 .../apache/camel/catalog/docs/stax-component.adoc  |   1 +
 .../org/apache/camel/component/stax/stax.json      |   2 +-
 .../camel-stax/src/main/docs/stax-component.adoc   |   1 +
 .../apache/camel/component/stax/StAXEndpoint.java  |   8 +-
 .../apache/camel/component/stax/StAXProcessor.java |  21 ++-
 .../camel/component/stax/StAXBodyTypesTest.java    | 153 +++++++++++++++++++++
 7 files changed, 179 insertions(+), 9 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/stax.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/stax.json
index 2d3590177b9e..b2b1c2adc7f1 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/stax.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/stax.json
@@ -28,7 +28,7 @@
     "autowiredEnabled": { "index": 1, "kind": "property", "displayName": 
"Autowired Enabled", "group": "advanced", "label": "advanced", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": true, "description": 
"Whether autowiring is enabled. This is used for automatic autowiring options 
(the option must be marked as autowired) by looking up in the registry to find 
if there is a single instance of matching t [...]
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "text", "stream", "file", "pojo" ], 
"format": "xml", "description": "The XML to parse: text, a stream, a file or a 
javax.xml.transform.Source. Bytes, a DOM Document, a Map or no body fail: 
convert bytes to a String first" },
+    "producerIn": { "carriers": [ "text", "bytes", "stream", "file", "pojo" ], 
"format": "xml", "description": "The XML to parse: text, bytes, a stream, a 
file or path, a javax.xml.transform.Source or a DOM Document. A Map or no body 
fail" },
     "producerOut": { "carriers": [ "pojo" ], "dependsOn": [ 
"contentHandlerClass" ], "description": "The ContentHandler after the parse: a 
new instance of contentHandlerClass per message, or the one bean of a #bean 
reference" }
   },
   "properties": {
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/stax-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/stax-component.adoc
index b3b56533d0f0..100ae3468e78 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/stax-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/stax-component.adoc
@@ -60,6 +60,7 @@ include::partial$component-endpoint-headers.adoc[]
 
 === Usage of a content handler as StAX parser
 
+The message body to parse can be text, bytes, a stream, a file or path, a 
`javax.xml.transform.Source` or a DOM `Document`.
 The message body after the handling is the handler itself.
 
 Here is an example:
diff --git 
a/components/camel-stax/src/generated/resources/META-INF/org/apache/camel/component/stax/stax.json
 
b/components/camel-stax/src/generated/resources/META-INF/org/apache/camel/component/stax/stax.json
index 2d3590177b9e..b2b1c2adc7f1 100644
--- 
a/components/camel-stax/src/generated/resources/META-INF/org/apache/camel/component/stax/stax.json
+++ 
b/components/camel-stax/src/generated/resources/META-INF/org/apache/camel/component/stax/stax.json
@@ -28,7 +28,7 @@
     "autowiredEnabled": { "index": 1, "kind": "property", "displayName": 
"Autowired Enabled", "group": "advanced", "label": "advanced", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": true, "description": 
"Whether autowiring is enabled. This is used for automatic autowiring options 
(the option must be marked as autowired) by looking up in the registry to find 
if there is a single instance of matching t [...]
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "text", "stream", "file", "pojo" ], 
"format": "xml", "description": "The XML to parse: text, a stream, a file or a 
javax.xml.transform.Source. Bytes, a DOM Document, a Map or no body fail: 
convert bytes to a String first" },
+    "producerIn": { "carriers": [ "text", "bytes", "stream", "file", "pojo" ], 
"format": "xml", "description": "The XML to parse: text, bytes, a stream, a 
file or path, a javax.xml.transform.Source or a DOM Document. A Map or no body 
fail" },
     "producerOut": { "carriers": [ "pojo" ], "dependsOn": [ 
"contentHandlerClass" ], "description": "The ContentHandler after the parse: a 
new instance of contentHandlerClass per message, or the one bean of a #bean 
reference" }
   },
   "properties": {
diff --git a/components/camel-stax/src/main/docs/stax-component.adoc 
b/components/camel-stax/src/main/docs/stax-component.adoc
index b3b56533d0f0..100ae3468e78 100644
--- a/components/camel-stax/src/main/docs/stax-component.adoc
+++ b/components/camel-stax/src/main/docs/stax-component.adoc
@@ -60,6 +60,7 @@ include::partial$component-endpoint-headers.adoc[]
 
 === Usage of a content handler as StAX parser
 
+The message body to parse can be text, bytes, a stream, a file or path, a 
`javax.xml.transform.Source` or a DOM `Document`.
 The message body after the handling is the handler itself.
 
 Here is an example:
diff --git 
a/components/camel-stax/src/main/java/org/apache/camel/component/stax/StAXEndpoint.java
 
b/components/camel-stax/src/main/java/org/apache/camel/component/stax/StAXEndpoint.java
index 2cd8f142cc94..f7619a88b88c 100644
--- 
a/components/camel-stax/src/main/java/org/apache/camel/component/stax/StAXEndpoint.java
+++ 
b/components/camel-stax/src/main/java/org/apache/camel/component/stax/StAXEndpoint.java
@@ -35,10 +35,10 @@ import org.apache.camel.support.ProcessorEndpoint;
  */
 @UriEndpoint(firstVersion = "2.9.0", scheme = "stax", title = "StAX", syntax = 
"stax:contentHandlerClass", producerOnly = true,
              remote = false, category = { Category.TRANSFORMATION })
-@BodyTypes(producerIn = @BodyType(value = { Carrier.TEXT, Carrier.STREAM, 
Carrier.FILE, Carrier.POJO }, format = "xml",
-                                  description = "The XML to parse: text, a 
stream, a file or a javax.xml.transform.Source."
-                                                + " Bytes, a DOM Document, a 
Map or no body fail: convert bytes to a String"
-                                                + " first"),
+@BodyTypes(producerIn = @BodyType(value = { Carrier.TEXT, Carrier.BYTES, 
Carrier.STREAM, Carrier.FILE, Carrier.POJO },
+                                  format = "xml",
+                                  description = "The XML to parse: text, 
bytes, a stream, a file or path, a"
+                                                + " javax.xml.transform.Source 
or a DOM Document. A Map or no body fail"),
            producerOut = @BodyType(value = Carrier.POJO, dependsOn = 
"contentHandlerClass",
                                    description = "The ContentHandler after the 
parse: a new instance of contentHandlerClass"
                                                  + " per message, or the one 
bean of a #bean reference"))
diff --git 
a/components/camel-stax/src/main/java/org/apache/camel/component/stax/StAXProcessor.java
 
b/components/camel-stax/src/main/java/org/apache/camel/component/stax/StAXProcessor.java
index 2fb8e0961a44..a9f588944a34 100644
--- 
a/components/camel-stax/src/main/java/org/apache/camel/component/stax/StAXProcessor.java
+++ 
b/components/camel-stax/src/main/java/org/apache/camel/component/stax/StAXProcessor.java
@@ -16,6 +16,8 @@
  */
 package org.apache.camel.component.stax;
 
+import java.io.InputStream;
+
 import javax.xml.stream.XMLStreamReader;
 
 import org.xml.sax.ContentHandler;
@@ -23,14 +25,17 @@ import org.xml.sax.InputSource;
 
 import com.ctc.wstx.sr.ValidatingStreamReader;
 import org.apache.camel.Exchange;
+import org.apache.camel.Message;
 import org.apache.camel.Processor;
 import org.apache.camel.support.ExchangeHelper;
+import org.apache.camel.util.IOHelper;
 
 /**
  * It uses SAX content handler to handle events.
  * <p/>
- * The {@link Exchange} is expected to contain a message body, that is 
convertable to both a {@link InputSource} and a
- * {@link XMLStreamReader} to support StAX streaming.
+ * The {@link Exchange} is expected to contain a message body that is 
convertable to a {@link XMLStreamReader} (such as
+ * text, a stream, a file or a {@link javax.xml.transform.Source}) or to an 
{@link InputStream} (such as bytes, a path
+ * or a DOM document) to support StAX streaming.
  */
 public class StAXProcessor implements Processor {
     private final Class<ContentHandler> contentHandlerClass;
@@ -49,8 +54,16 @@ public class StAXProcessor implements Processor {
     @Override
     public void process(Exchange exchange) throws Exception {
         XMLStreamReader stream = null;
+        InputStream is = null;
         try {
-            stream = exchange.getIn().getMandatoryBody(XMLStreamReader.class);
+            Message in = exchange.getIn();
+            stream = in.getBody(XMLStreamReader.class);
+            if (stream == null) {
+                // there is no direct converter from a byte[], a Path or a DOM 
Document, so read the body as a stream
+                // (the stream is parsed by the same secured XMLInputFactory 
as the other bodies)
+                is = in.getMandatoryBody(InputStream.class);
+                stream = 
exchange.getContext().getTypeConverter().mandatoryConvertTo(XMLStreamReader.class,
 exchange, is);
+            }
             StaxStreamXMLReader reader = new StaxStreamXMLReader(stream);
             ContentHandler handler;
             if (this.contentHandlerClass != null) {
@@ -76,6 +89,8 @@ public class StAXProcessor implements Processor {
                     validatingStreamReader.closeCompletely();
                 }
             }
+            // closing the reader does not close the stream we opened (such as 
on a Path)
+            IOHelper.close(is);
         }
     }
 
diff --git 
a/components/camel-stax/src/test/java/org/apache/camel/component/stax/StAXBodyTypesTest.java
 
b/components/camel-stax/src/test/java/org/apache/camel/component/stax/StAXBodyTypesTest.java
new file mode 100644
index 000000000000..2c4b207ff1f9
--- /dev/null
+++ 
b/components/camel-stax/src/test/java/org/apache/camel/component/stax/StAXBodyTypesTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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.stax;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+
+import javax.xml.transform.stream.StreamSource;
+
+import org.w3c.dom.Document;
+
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * The StAX producer accepts every body that can be read as XML: text, bytes, 
a stream, a file, a path, a
+ * {@link javax.xml.transform.Source} and a DOM {@link Document}.
+ */
+public class StAXBodyTypesTest extends CamelTestSupport {
+
+    private static final String XML = "<?xml version=\"1.0\" 
encoding=\"UTF-8\"?>"
+                                      + "<records><record 
key=\"1\">a</record><record key=\"2\">æøå</record></records>";
+
+    @TempDir
+    Path tempDir;
+
+    @Override
+    public RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:count").to("stax:" + 
CountingHandler.class.getName());
+                from("direct:text").to("stax:" + TextHandler.class.getName());
+            }
+        };
+    }
+
+    @Test
+    public void testString() {
+        assertCount(XML);
+    }
+
+    @Test
+    public void testBytes() {
+        assertCount(XML.getBytes(StandardCharsets.UTF_8));
+    }
+
+    @Test
+    public void testInputStream() {
+        assertCount(new 
ByteArrayInputStream(XML.getBytes(StandardCharsets.UTF_8)));
+    }
+
+    @Test
+    public void testFile() throws Exception {
+        assertCount(writeFile().toFile());
+    }
+
+    @Test
+    public void testPath() throws Exception {
+        assertCount(writeFile());
+    }
+
+    @Test
+    public void testSource() {
+        assertCount(new StreamSource(new 
ByteArrayInputStream(XML.getBytes(StandardCharsets.UTF_8))));
+    }
+
+    @Test
+    public void testDocument() {
+        Document document = 
context.getTypeConverter().convertTo(Document.class, XML);
+        assertCount(document);
+    }
+
+    @Test
+    public void testBytesKeepNonAsciiText() {
+        TextHandler handler
+                = template.requestBody("direct:text", 
XML.getBytes(StandardCharsets.UTF_8), TextHandler.class);
+        assertEquals("aæøå", handler.getText());
+    }
+
+    @Test
+    public void testBytesDoNotReadExternalEntities() throws Exception {
+        File secret = tempDir.resolve("secret.txt").toFile();
+        Files.writeString(secret.toPath(), "top-secret");
+        String xml = "<?xml version=\"1.0\"?><!DOCTYPE records [<!ENTITY xxe 
SYSTEM \"" + secret.toURI() + "\">]>"
+                     + "<records>&xxe;</records>";
+
+        // the bytes are parsed by the same secured factory as text: the 
external entity is not resolved
+        TextHandler handler = template.requestBody("direct:text", 
xml.getBytes(StandardCharsets.UTF_8), TextHandler.class);
+        assertFalse(handler.getText().contains("top-secret"));
+    }
+
+    @Test
+    public void testMapFails() {
+        CamelExecutionException e = assertThrows(CamelExecutionException.class,
+                () -> template.requestBody("direct:count", Map.of("records", 
"a")));
+        assertInstanceOf(InvalidPayloadException.class, e.getCause());
+    }
+
+    private Path writeFile() throws Exception {
+        Path file = tempDir.resolve("records.xml");
+        Files.writeString(file, XML, StandardCharsets.UTF_8);
+        return file;
+    }
+
+    private void assertCount(Object body) {
+        CountingHandler handler = template.requestBody("direct:count", body, 
CountingHandler.class);
+        assertEquals(3, handler.getNumber());
+    }
+
+    public static class TextHandler extends DefaultHandler {
+        private final StringBuilder text = new StringBuilder();
+
+        @Override
+        public void characters(char[] ch, int start, int length) throws 
SAXException {
+            text.append(ch, start, length);
+        }
+
+        public String getText() {
+            return text.toString();
+        }
+    }
+}

Reply via email to