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(); + } + } +}
