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 b4a5f09e93ce CAMEL-26452: camel-beanio - BeanIOSplitter keeps the 
record after each record that the error handler handled (#28809)
b4a5f09e93ce is described below

commit b4a5f09e93cecbc79bbd8de9ed1fa5ec6e02edc2
Author: allthingssecurity <[email protected]>
AuthorDate: Mon Nov 2 12:00:48 2026 +0530

    CAMEL-26452: camel-beanio - BeanIOSplitter keeps the record after each 
record that the error handler handled (#28809)
    
    …h record that the error handler handled
    
    A custom BeanIOErrorHandler turns an invalid record into a result with
    handleErrorAndAddAsResult, which BeanIOSplitter hands to its BeanIOIterator
    with setNext. The error handler runs inside BeanReader.read(), and BeanIO 
then
    goes on and returns the next record from the same read. BeanIOIterator kept
    the result in a single field and put it in place of that record, so the
    valid record after each handled record was not split, and of two handled
    records in a row only the last result was kept. Only a single handled record
    at the end of the data worked.
    
    BeanIOIterator now queues the results of the error handler and returns them
    in their order, followed by the record that the read returned, which is the
    order the beanio data format uses when it unmarshals into a list.
    
    BeanIOSplitter also set the error handler on the reader only after the
    iterator had read the first record, so an invalid first record failed the
    split whatever the error handler or the ignore options, and the reader of 
the
    body was not closed when the error handler could not be created. It now sets
    the error handler first and then reads the first record, closing the reader
    when any of these steps fails.
    
    The 4.24 upgrade guide says so.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../camel/dataformat/beanio/BeanIOIterator.java    |  64 +++++++++----
 .../camel/dataformat/beanio/BeanIOSplitter.java    |  15 +--
 ...IOSplitterCustomBeanReaderErrorHandlerTest.java | 105 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  13 +++
 4 files changed, 174 insertions(+), 23 deletions(-)

diff --git 
a/components/camel-beanio/src/main/java/org/apache/camel/dataformat/beanio/BeanIOIterator.java
 
b/components/camel-beanio/src/main/java/org/apache/camel/dataformat/beanio/BeanIOIterator.java
index 6819b4ca67df..89eabfc65c62 100644
--- 
a/components/camel-beanio/src/main/java/org/apache/camel/dataformat/beanio/BeanIOIterator.java
+++ 
b/components/camel-beanio/src/main/java/org/apache/camel/dataformat/beanio/BeanIOIterator.java
@@ -18,6 +18,8 @@ package org.apache.camel.dataformat.beanio;
 
 import java.io.Closeable;
 import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.Deque;
 import java.util.Iterator;
 
 import org.beanio.BeanReader;
@@ -26,11 +28,31 @@ public class BeanIOIterator implements Iterator<Object>, 
Closeable {
 
     private BeanReader reader;
     private transient Object next;
-    private transient Object forceNext;
+    // the results that an error handler added (handleErrorAndAddAsResult) 
while the reader was reading, in their order:
+    // the reader goes on after a record it handled, so more than one can come 
before the record that read() returns
+    private final Deque<Object> forced = new ArrayDeque<>();
 
     public BeanIOIterator(BeanReader reader) {
+        this(reader, true);
+    }
+
+    /**
+     * @param reader    the reader
+     * @param readFirst whether to read the first record now, else {@link 
#readFirst()} must be called before the
+     *                  iterator is used (such as once the error handler is 
set on the reader)
+     */
+    BeanIOIterator(BeanReader reader, boolean readFirst) {
         this.reader = reader;
-        this.next = next();
+        if (readFirst) {
+            readFirst();
+        }
+    }
+
+    /**
+     * Reads the first record.
+     */
+    void readFirst() {
+        this.next = readNext();
     }
 
     @Override
@@ -50,19 +72,25 @@ public class BeanIOIterator implements Iterator<Object>, 
Closeable {
     public Object next() {
         Object answer = next;
         if (answer == null) {
-            answer = reader.read();
-            // after read we may force a next
-            if (forceNext != null) {
-                answer = forceNext;
-                forceNext = null;
-            }
+            answer = readNext();
         } else {
-            next = reader.read();
-            // after read we may force a next
-            if (forceNext != null) {
-                next = forceNext;
-                forceNext = null;
+            next = readNext();
+        }
+        return answer;
+    }
+
+    private Object readNext() {
+        if (!forced.isEmpty()) {
+            return forced.poll();
+        }
+        Object answer = reader.read();
+        if (!forced.isEmpty()) {
+            // the error handler added results for the records it handled 
during this read, which come before the
+            // record that the read returned (the same order as the data 
format, which adds them to its list)
+            if (answer != null) {
+                forced.add(answer);
             }
+            answer = forced.poll();
         }
         return answer;
     }
@@ -73,10 +101,14 @@ public class BeanIOIterator implements Iterator<Object>, 
Closeable {
     }
 
     /**
-     * Sets a custom object as the next, such as from a custom error handler
+     * Adds a custom object as the next result, such as from a custom error 
handler. Several objects added during one
+     * read are returned in the order they were added, before the record that 
the read returned. As the reader goes on
+     * after each record that the error handler handled, the results of 
handled records in a row are kept until the
+     * reader returns the next record. A {@code null} is ignored.
      */
     public void setNext(Object next) { // NOSONAR
-        // NOTE: this is forcefully setting forceNext object on purpose.
-        this.forceNext = next;
+        if (next != null) {
+            forced.add(next);
+        }
     }
 }
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 1ae16b954f1d..180748547855 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
@@ -117,18 +117,19 @@ public class BeanIOSplitter implements Expression {
             throw e;
         }
 
-        BeanIOIterator iterator;
+        BeanIOIterator iterator = new BeanIOIterator(beanReader, false);
         try {
-            // reads the first record, which can fail
-            iterator = new BeanIOIterator(beanReader);
-        } catch (RuntimeException e) {
+            BeanReaderErrorHandler errorHandler
+                    = getOrCreateBeanReaderErrorHandler(configuration, 
exchange, null, iterator);
+            beanReader.setErrorHandler(errorHandler);
+            // reads the first record, which can fail; the error handler is 
set first, so that it also handles an
+            // invalid first record
+            iterator.readFirst();
+        } catch (Exception e) {
             IOHelper.close(reader);
             throw e;
         }
 
-        BeanReaderErrorHandler errorHandler = 
getOrCreateBeanReaderErrorHandler(configuration, exchange, null, iterator);
-        beanReader.setErrorHandler(errorHandler);
-
         return iterator;
     }
 
diff --git 
a/components/camel-beanio/src/test/java/org/apache/camel/dataformat/beanio/BeanIOSplitterCustomBeanReaderErrorHandlerTest.java
 
b/components/camel-beanio/src/test/java/org/apache/camel/dataformat/beanio/BeanIOSplitterCustomBeanReaderErrorHandlerTest.java
index 24ff3ec22f89..03f7c1c269f5 100644
--- 
a/components/camel-beanio/src/test/java/org/apache/camel/dataformat/beanio/BeanIOSplitterCustomBeanReaderErrorHandlerTest.java
+++ 
b/components/camel-beanio/src/test/java/org/apache/camel/dataformat/beanio/BeanIOSplitterCustomBeanReaderErrorHandlerTest.java
@@ -16,17 +16,23 @@
  */
 package org.apache.camel.dataformat.beanio;
 
+import java.io.Reader;
+import java.io.StringReader;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
 
+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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class BeanIOSplitterCustomBeanReaderErrorHandlerTest extends 
CamelTestSupport {
 
@@ -40,6 +46,20 @@ public class BeanIOSplitterCustomBeanReaderErrorHandlerTest 
extends CamelTestSup
                                                   + 
"Jane,Doe,Architect,80000,01152008" + Constants.LS
                                                   + 
"Jon,Anderson,Manager,XXX,03182007" + Constants.LS;
 
+    private static final String FIXED_FAIL_MIDDLE_DATA = 
"Joe,Smith,Developer,75000,10012009" + Constants.LS
+                                                         + 
"Jane,Doe,Architect,XXX,01152008" + Constants.LS
+                                                         + 
"Jon,Anderson,Manager,85000,03182007" + Constants.LS;
+
+    private static final String FIXED_FAIL_TWICE_DATA = 
"Joe,Smith,Developer,75000,10012009" + Constants.LS
+                                                        + 
"Jane,Doe,Architect,XXX,01152008" + Constants.LS
+                                                        + 
"Jim,Doe,Tester,YYY,01152008" + Constants.LS
+                                                        + 
"Jon,Anderson,Manager,85000,03182007" + Constants.LS
+                                                        + 
"Jack,Black,Writer,70000,03182007" + Constants.LS;
+
+    private static final String FIXED_FAIL_FIRST_DATA = 
"Joe,Smith,Developer,XXX,10012009" + Constants.LS
+                                                        + 
"Jane,Doe,Architect,80000,01152008" + Constants.LS
+                                                        + 
"Jon,Anderson,Manager,85000,03182007" + Constants.LS;
+
     @Test
     void testSplit() throws Exception {
         List<Employee> employees = getEmployees();
@@ -68,6 +88,81 @@ public class BeanIOSplitterCustomBeanReaderErrorHandlerTest 
extends CamelTestSup
         assertEquals("employee", 
mock.getReceivedExchanges().get(2).getIn().getBody(MyErrorDto.class).getRecord());
     }
 
+    @Test
+    void testSplitFailInTheMiddle() throws Exception {
+        // the record after the failed record must be split too
+        MockEndpoint mock = getMockEndpoint("mock:beanio-unmarshal");
+        mock.expectedMessageCount(3);
+        mock.message(0).body().isInstanceOf(Employee.class);
+        mock.message(1).body().isInstanceOf(MyErrorDto.class);
+        mock.message(2).body().isInstanceOf(Employee.class);
+
+        template.sendBody("direct:unmarshal", FIXED_FAIL_MIDDLE_DATA);
+
+        mock.assertIsSatisfied();
+
+        assertEquals("Joe", 
mock.getReceivedExchanges().get(0).getIn().getBody(Employee.class).getFirstName());
+        assertEquals("Jon", 
mock.getReceivedExchanges().get(2).getIn().getBody(Employee.class).getFirstName());
+    }
+
+    @Test
+    void testSplitFailTwiceInARow() throws Exception {
+        // two failed records in a row each give a result, and the records 
after them are split too
+        MockEndpoint mock = getMockEndpoint("mock:beanio-unmarshal");
+        mock.expectedMessageCount(5);
+        mock.message(0).body().isInstanceOf(Employee.class);
+        mock.message(1).body().isInstanceOf(MyErrorDto.class);
+        mock.message(2).body().isInstanceOf(MyErrorDto.class);
+        mock.message(3).body().isInstanceOf(Employee.class);
+        mock.message(4).body().isInstanceOf(Employee.class);
+
+        template.sendBody("direct:unmarshal", FIXED_FAIL_TWICE_DATA);
+
+        mock.assertIsSatisfied();
+
+        assertEquals("Joe", 
mock.getReceivedExchanges().get(0).getIn().getBody(Employee.class).getFirstName());
+        // the results of the error handler come in the order of the failed 
records
+        
assertTrue(mock.getReceivedExchanges().get(1).getIn().getBody(MyErrorDto.class).getMessage().contains("line
 2"));
+        
assertTrue(mock.getReceivedExchanges().get(2).getIn().getBody(MyErrorDto.class).getMessage().contains("line
 3"));
+        assertEquals("Jon", 
mock.getReceivedExchanges().get(3).getIn().getBody(Employee.class).getFirstName());
+        assertEquals("Jack", 
mock.getReceivedExchanges().get(4).getIn().getBody(Employee.class).getFirstName());
+    }
+
+    @Test
+    void testSplitFailFirst() throws Exception {
+        // the error handler also handles a failed first record
+        MockEndpoint mock = getMockEndpoint("mock:beanio-unmarshal");
+        mock.expectedMessageCount(3);
+        mock.message(0).body().isInstanceOf(MyErrorDto.class);
+        mock.message(1).body().isInstanceOf(Employee.class);
+        mock.message(2).body().isInstanceOf(Employee.class);
+
+        template.sendBody("direct:unmarshal", FIXED_FAIL_FIRST_DATA);
+
+        mock.assertIsSatisfied();
+
+        
assertTrue(mock.getReceivedExchanges().get(0).getIn().getBody(MyErrorDto.class).getMessage().contains("line
 1"));
+        assertEquals("Jane", 
mock.getReceivedExchanges().get(1).getIn().getBody(Employee.class).getFirstName());
+    }
+
+    @Test
+    void testErrorHandlerThatCannotBeCreatedClosesTheReader() {
+        // the splitter fails before it returns its iterator, so it closes the 
reader of the body itself
+        AtomicBoolean closed = new AtomicBoolean();
+        Reader body = new StringReader(FIXED_DATA) {
+            @Override
+            public void close() {
+                closed.set(true);
+                super.close();
+            }
+        };
+
+        Exchange out = template.send("direct:unknownHandler", e -> 
e.getMessage().setBody(body));
+
+        assertInstanceOf(ClassNotFoundException.class, 
out.getException().getCause());
+        assertTrue(closed.get());
+    }
+
     @Override
     protected RouteBuilder createRouteBuilder() {
         return new RouteBuilder() {
@@ -88,6 +183,16 @@ public class BeanIOSplitterCustomBeanReaderErrorHandlerTest 
extends CamelTestSup
                         .to("log:line")
                         .to("mock:beanio-unmarshal");
                 // END SNIPPET: e1
+
+                BeanIOSplitter unknownHandler = new BeanIOSplitter();
+                
unknownHandler.setMapping("org/apache/camel/dataformat/beanio/mappings.xml");
+                unknownHandler.setStreamName("employeeFile");
+                
unknownHandler.setBeanReaderErrorHandlerType("org.apache.camel.dataformat.beanio.NoSuchErrorHandler");
+
+                // no stream caching, so that the splitter gets the reader of 
the body
+                from("direct:unknownHandler").streamCache("false")
+                        .split(unknownHandler).streaming()
+                        .to("mock:unknown");
             }
         };
     }
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 a90914d81fb9..cc485d8925c6 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
@@ -1694,6 +1694,19 @@ replaced. The splitter now decodes a file, stream or 
byte array body with the ch
 `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-beanio - BeanIOSplitter keeps the records after a record that the 
error handler handled
+
+When a custom `BeanIOErrorHandler` calls `handleErrorAndAddAsResult` for an 
invalid record, the BeanIO reader goes on
+and returns the next record in the same read. `BeanIOSplitter` replaced that 
next record with the result of the error
+handler, and of two records handled in a row it kept only the last result. 
Those records were not split, and nothing
+was logged. The splitter now returns the results of the error handler in their 
order, followed by the record that the
+reader returned, as the `beanio` data format does when it unmarshals into a 
list. A route that splits data with handled
+invalid records now gets every valid record.
+
+The splitter also sets its error handler before it reads the first record. 
Before, an invalid, unexpected or
+unidentified first record failed the split even with `ignoreInvalidRecords`, 
`ignoreUnexpectedRecords`,
+`ignoreUnidentifiedRecords` or a custom error handler; now the first record is 
handled as the others are.
+
 === 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