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