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

Reply via email to