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 b3e290788209 CAMEL-25904: camel-beanio - BeanIOSplitter decodes a file
or stream with the charset of the exchange or its encoding (#28428)
b3e290788209 is described below
commit b3e2907882092c1cd7711e5a1b6f4dbfd31e620e
Author: allthingssecurity <[email protected]>
AuthorDate: Sun Oct 25 13:00:56 2026 +0530
CAMEL-25904: camel-beanio - BeanIOSplitter decodes a file or stream with
the charset of the exchange or its encoding (#28428)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../camel/dataformat/beanio/BeanIOSplitter.java | 38 ++++-
.../beanio/BeanIOSplitterEncodingTest.java | 156 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 10 ++
3 files changed, 198 insertions(+), 6 deletions(-)
diff --git
a/components/camel-beanio/src/main/java/org/apache/camel/dataformat/beanio/BeanIOSplitter.java
b/components/camel-beanio/src/main/java/org/apache/camel/dataformat/beanio/BeanIOSplitter.java
index 47787eff4e8b..1ae16b954f1d 100644
---
a/components/camel-beanio/src/main/java/org/apache/camel/dataformat/beanio/BeanIOSplitter.java
+++
b/components/camel-beanio/src/main/java/org/apache/camel/dataformat/beanio/BeanIOSplitter.java
@@ -17,8 +17,11 @@
package org.apache.camel.dataformat.beanio;
import java.io.File;
+import java.io.FileInputStream;
import java.io.InputStream;
+import java.io.InputStreamReader;
import java.io.Reader;
+import java.io.StringReader;
import java.nio.charset.Charset;
import java.util.Properties;
@@ -28,6 +31,7 @@ import org.apache.camel.Expression;
import org.apache.camel.Message;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.WrappedFile;
+import org.apache.camel.support.ExchangeHelper;
import org.apache.camel.support.ResourceHelper;
import org.apache.camel.util.IOHelper;
import org.apache.camel.util.ObjectHelper;
@@ -86,19 +90,41 @@ public class BeanIOSplitter implements Expression {
factory = createStreamFactory(exchange.getContext());
}
- BeanReader beanReader = null;
if (body instanceof WrappedFile wrappedfile) {
body = wrappedfile.getFile();
}
- if (body instanceof File file) {
- beanReader = factory.createReader(getStreamName(), file);
+ Reader reader;
+ if (body instanceof CharSequence text) {
+ // text is read as it is, without a round trip through bytes (also
a StringBuilder, which has no converter
+ // to a Reader)
+ reader = new StringReader(text.toString());
+ } else if (body instanceof Reader) {
+ reader = msg.getMandatoryBody(Reader.class);
+ } else {
+ // the bytes of a file or a stream are decoded with the charset of
the exchange (such as the charset of the
+ // file consumer), else with the encoding of this splitter
+ String charsetName = ExchangeHelper.getCharsetName(exchange,
false);
+ Charset charset = charsetName != null ?
Charset.forName(charsetName) : getEncoding();
+ InputStream is = body instanceof File file
+ ? new FileInputStream(file) :
msg.getMandatoryBody(InputStream.class);
+ reader = IOHelper.buffered(new InputStreamReader(is, charset));
}
- if (beanReader == null) {
- Reader reader = msg.getMandatoryBody(Reader.class);
+ BeanReader beanReader;
+ try {
beanReader = factory.createReader(getStreamName(), reader);
+ } catch (RuntimeException e) {
+ IOHelper.close(reader);
+ throw e;
}
- BeanIOIterator iterator = new BeanIOIterator(beanReader);
+ BeanIOIterator iterator;
+ try {
+ // reads the first record, which can fail
+ iterator = new BeanIOIterator(beanReader);
+ } catch (RuntimeException e) {
+ IOHelper.close(reader);
+ throw e;
+ }
BeanReaderErrorHandler errorHandler =
getOrCreateBeanReaderErrorHandler(configuration, exchange, null, iterator);
beanReader.setErrorHandler(errorHandler);
diff --git
a/components/camel-beanio/src/test/java/org/apache/camel/dataformat/beanio/BeanIOSplitterEncodingTest.java
b/components/camel-beanio/src/test/java/org/apache/camel/dataformat/beanio/BeanIOSplitterEncodingTest.java
new file mode 100644
index 000000000000..d1fc73355b13
--- /dev/null
+++
b/components/camel-beanio/src/test/java/org/apache/camel/dataformat/beanio/BeanIOSplitterEncodingTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.dataformat.beanio;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+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;
+
+/**
+ * The splitter decodes the bytes of a file or a stream with the charset of
the exchange (such as the charset option of
+ * the file consumer), else with its encoding, not with the default charset of
the JVM.
+ */
+class BeanIOSplitterEncodingTest extends CamelTestSupport {
+
+ private static final String DATA =
"J\u00f6rg,M\u00fcller,D\u00e9veloppeur,75000,10012009" + Constants.LS
+ +
"Ren\u00e9e,Stra\u00dfe,Architect,80000,01152008" + Constants.LS;
+
+ @TempDir
+ Path dir;
+
+ private static BeanIOSplitter splitter() throws Exception {
+ BeanIOSplitter splitter = new BeanIOSplitter();
+ splitter.setMapping("org/apache/camel/dataformat/beanio/mappings.xml");
+ splitter.setStreamName("employeeFile");
+ return splitter;
+ }
+
+ private static List<String> names(MockEndpoint mock) {
+ return mock.getReceivedExchanges().stream()
+ .map(e -> e.getIn().getBody(Employee.class))
+ .map(e -> e.getFirstName() + " " + e.getLastName() + " " +
e.getTitle())
+ .toList();
+ }
+
+ @Test
+ void fileIsReadWithTheEncodingOfTheSplitter() throws Exception {
+ Path file = Files.writeString(dir.resolve("employees.csv"), DATA,
StandardCharsets.ISO_8859_1);
+ BeanIOSplitter splitter = splitter();
+ splitter.setEncoding(StandardCharsets.ISO_8859_1);
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("direct:split").split(splitter).streaming().to("mock:split");
+ }
+ });
+ MockEndpoint mock = getMockEndpoint("mock:split");
+ mock.expectedMessageCount(2);
+
+ template.sendBody("direct:split", file.toFile());
+
+ mock.assertIsSatisfied();
+ assertEquals(List.of("J\u00f6rg M\u00fcller D\u00e9veloppeur",
"Ren\u00e9e Stra\u00dfe Architect"), names(mock));
+ }
+
+ @Test
+ void fileFromTheFileConsumerIsReadWithItsCharset() throws Exception {
+ Files.writeString(dir.resolve("employees.csv"), DATA,
StandardCharsets.ISO_8859_1);
+ BeanIOSplitter splitter = splitter();
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
fromF("file:%s?charset=ISO-8859-1&noop=true&initialDelay=0&delay=10", dir)
+ .split(splitter).streaming().to("mock:split");
+ }
+ });
+ MockEndpoint mock = getMockEndpoint("mock:split");
+ mock.expectedMessageCount(2);
+
+ mock.assertIsSatisfied();
+ assertEquals(List.of("J\u00f6rg M\u00fcller D\u00e9veloppeur",
"Ren\u00e9e Stra\u00dfe Architect"), names(mock));
+ }
+
+ @Test
+ void bytesAreReadWithTheCharsetOfTheExchange() throws Exception {
+ BeanIOSplitter splitter = splitter();
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("direct:split").split(splitter).streaming().to("mock:split");
+ }
+ });
+ MockEndpoint mock = getMockEndpoint("mock:split");
+ mock.expectedMessageCount(2);
+
+ template.send("direct:split", exchange -> {
+ exchange.setProperty(Exchange.CHARSET_NAME, "ISO-8859-1");
+
exchange.getIn().setBody(DATA.getBytes(StandardCharsets.ISO_8859_1));
+ });
+
+ mock.assertIsSatisfied();
+ assertEquals(List.of("J\u00f6rg M\u00fcller D\u00e9veloppeur",
"Ren\u00e9e Stra\u00dfe Architect"), names(mock));
+ }
+
+ @Test
+ void bytesAreReadWithTheEncodingOfTheSplitter() throws Exception {
+ BeanIOSplitter splitter = splitter();
+ splitter.setEncoding(StandardCharsets.ISO_8859_1);
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("direct:split").split(splitter).streaming().to("mock:split");
+ }
+ });
+ MockEndpoint mock = getMockEndpoint("mock:split");
+ mock.expectedMessageCount(2);
+
+ template.sendBody("direct:split",
DATA.getBytes(StandardCharsets.ISO_8859_1));
+
+ mock.assertIsSatisfied();
+ assertEquals(List.of("J\u00f6rg M\u00fcller D\u00e9veloppeur",
"Ren\u00e9e Stra\u00dfe Architect"), names(mock));
+ }
+
+ @Test
+ void textIsReadAsItIs() throws Exception {
+ BeanIOSplitter splitter = splitter();
+ // an encoding that differs from the default charset of the JVM must
not apply to text
+ splitter.setEncoding(StandardCharsets.UTF_16BE);
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("direct:split").split(splitter).streaming().to("mock:split");
+ }
+ });
+ MockEndpoint mock = getMockEndpoint("mock:split");
+ mock.expectedMessageCount(2);
+
+ template.sendBody("direct:split", new StringBuilder(DATA));
+
+ mock.assertIsSatisfied();
+ assertEquals(List.of("J\u00f6rg M\u00fcller D\u00e9veloppeur",
"Ren\u00e9e Stra\u00dfe Architect"), names(mock));
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 2d3c1d96e163..597f745d7393 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -1202,6 +1202,16 @@ The `aws2-ddbstream` consumer now reads a closed shard
to its end before it read
shard rotation the consumer started again from `streamIteratorType`: with
`FROM_START` it routed records of the stream
again, and with `FROM_LATEST` it skipped records already written to the new
shards.
+=== camel-beanio - BeanIOSplitter decodes files and streams with the charset
of the exchange or its encoding
+
+`BeanIOSplitter` read a file body with the default charset of the JVM, and a
stream or byte array body with the
+charset of the exchange or the default charset of the JVM; its `encoding`
option was not used. A file consumed with
+`charset=ISO-8859-1`, or a splitter with
`setEncoding(StandardCharsets.ISO_8859_1)`, had every non-ASCII character
+replaced. The splitter now decodes a file, stream or byte array body with the
charset of the exchange (the
+`CamelCharsetName` header or exchange property, as set by the `charset` option
of the file consumer), else with its
+`encoding` (default: the default charset of the JVM). A `String` or `Reader`
body is read as before, and other text
+(such as a `StringBuilder`, which failed before) is read as text.
+
=== camel-bindy - a number field without precision keeps all its decimals when
marshalled
A `BigDecimal`, `Double` or `Float` field whose `@DataField` (or
`@KeyValuePairField`) has no `precision` and no