This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 03accc4245 fix error handling for get data from XML, fixes #5921
(#7463)
03accc4245 is described below
commit 03accc4245b5f986723ad9d29e3103b6e062763b
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Wed Jul 8 20:05:32 2026 +0200
fix error handling for get data from XML, fixes #5921 (#7463)
---
.../transforms/xml/getxmldata/GetXmlData.java | 260 +++++++++++----------
.../transforms/xml/getxmldata/GetXMLDataTest.java | 116 +++++++++
2 files changed, 259 insertions(+), 117 deletions(-)
diff --git
a/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/getxmldata/GetXmlData.java
b/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/getxmldata/GetXmlData.java
index 204cfcdd95..6353b07fda 100644
---
a/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/getxmldata/GetXmlData.java
+++
b/plugins/transforms/xml/src/main/java/org/apache/hop/pipeline/transforms/xml/getxmldata/GetXmlData.java
@@ -443,152 +443,178 @@ public class GetXmlData extends
BaseTransform<GetXmlDataMeta, GetXmlDataData> {
private boolean ReadNextString() {
- try {
- // Grab another row ...
- data.readrow = getRow();
+ // Loop so that, when error handling is enabled, we can skip an offending
input row and
+ // immediately continue with the next one without recursing.
+ while (true) {
+ try {
+ // Grab another row ...
+ data.readrow = getRow();
- if (data.readrow == null) {
- // finished processing!
+ if (data.readrow == null) {
+ // finished processing!
- if (isDetailed()) {
- logDetailed(BaseMessages.getString(PKG,
"GetXMLData.Log.FinishedProcessing"));
+ if (isDetailed()) {
+ logDetailed(BaseMessages.getString(PKG,
"GetXMLData.Log.FinishedProcessing"));
+ }
+ return false;
}
- return false;
- }
- if (first) {
- first = false;
+ if (first) {
+ first = false;
- data.nrReadRow = getInputRowMeta().size();
- data.inputRowMeta = getInputRowMeta();
- data.outputRowMeta = data.inputRowMeta.clone();
- meta.getFields(data.outputRowMeta, getTransformName(), null, null,
this, metadataProvider);
+ data.nrReadRow = getInputRowMeta().size();
+ data.inputRowMeta = getInputRowMeta();
+ data.outputRowMeta = data.inputRowMeta.clone();
+ meta.getFields(
+ data.outputRowMeta, getTransformName(), null, null, this,
metadataProvider);
- // Get total previous fields
- data.totalpreviousfields = data.inputRowMeta.size();
+ // Get total previous fields
+ data.totalpreviousfields = data.inputRowMeta.size();
- // Create convert meta-data objects that will contain Date & Number
formatters
- data.convertRowMeta = new RowMeta();
- for (IValueMeta valueMeta : data.convertRowMeta.getValueMetaList()) {
- data.convertRowMeta.addValueMeta(
- ValueMetaFactory.cloneValueMeta(valueMeta,
IValueMeta.TYPE_STRING));
- }
+ // Create convert meta-data objects that will contain Date & Number
formatters
+ data.convertRowMeta = new RowMeta();
+ for (IValueMeta valueMeta : data.convertRowMeta.getValueMetaList()) {
+ data.convertRowMeta.addValueMeta(
+ ValueMetaFactory.cloneValueMeta(valueMeta,
IValueMeta.TYPE_STRING));
+ }
- // For String to <type> conversions, we allocate a conversion meta
data row as well...
- //
- data.convertRowMeta =
data.outputRowMeta.cloneToType(IValueMeta.TYPE_STRING);
+ // For String to <type> conversions, we allocate a conversion meta
data row as well...
+ //
+ data.convertRowMeta =
data.outputRowMeta.cloneToType(IValueMeta.TYPE_STRING);
- // Check is XML field is provided
- if (Utils.isEmpty(meta.getXmlField())) {
- logError(BaseMessages.getString(PKG, "GetXMLData.Log.NoField"));
- throw new HopException(BaseMessages.getString(PKG,
"GetXMLData.Log.NoField"));
- }
+ // Check is XML field is provided
+ if (Utils.isEmpty(meta.getXmlField())) {
+ logError(BaseMessages.getString(PKG, "GetXMLData.Log.NoField"));
+ throw new HopException(BaseMessages.getString(PKG,
"GetXMLData.Log.NoField"));
+ }
- // cache the position of the field
- if (data.indexOfXmlField < 0) {
- data.indexOfXmlField =
getInputRowMeta().indexOfValue(meta.getXmlField());
+ // cache the position of the field
if (data.indexOfXmlField < 0) {
- // The field is unreachable !
- logError(
- BaseMessages.getString(
- PKG, "GetXMLData.Log.ErrorFindingField",
meta.getXmlField()));
- throw new HopException(
- BaseMessages.getString(
- PKG, "GetXMLData.Exception.CouldnotFindField",
meta.getXmlField()));
+ data.indexOfXmlField =
getInputRowMeta().indexOfValue(meta.getXmlField());
+ if (data.indexOfXmlField < 0) {
+ // The field is unreachable !
+ logError(
+ BaseMessages.getString(
+ PKG, "GetXMLData.Log.ErrorFindingField",
meta.getXmlField()));
+ throw new HopException(
+ BaseMessages.getString(
+ PKG, "GetXMLData.Exception.CouldnotFindField",
meta.getXmlField()));
+ }
}
}
- }
- if (meta.isInFields()) {
- // get XML field value
- String fieldvalue = getInputRowMeta().getString(data.readrow,
data.indexOfXmlField);
+ if (meta.isInFields()) {
+ // get XML field value
+ String fieldvalue = getInputRowMeta().getString(data.readrow,
data.indexOfXmlField);
- if (isDetailed()) {
- logDetailed(
- BaseMessages.getString(
- PKG, "GetXMLData.Log.XMLStream", meta.getXmlField(),
fieldvalue));
- }
+ if (isDetailed()) {
+ logDetailed(
+ BaseMessages.getString(
+ PKG, "GetXMLData.Log.XMLStream", meta.getXmlField(),
fieldvalue));
+ }
- if (meta.isAFile()) {
- FileObject file = null;
try {
- // XML source is a file.
- file = HopVfs.getFileObject(resolve(fieldvalue), variables);
-
- if (meta.isIgnoreEmptyFile() && file.getContent().getSize() == 0) {
- logBasic(
- BaseMessages.getString(
- PKG, "GetXMLData.Error.FileSizeZero", "" +
file.getName()));
- return ReadNextString();
- }
+ if (meta.isAFile()) {
+ FileObject file = null;
+ try {
+ // XML source is a file.
+ file = HopVfs.getFileObject(resolve(fieldvalue), variables);
+
+ if (meta.isIgnoreEmptyFile() && file.getContent().getSize() ==
0) {
+ logBasic(
+ BaseMessages.getString(
+ PKG, "GetXMLData.Error.FileSizeZero", "" +
file.getName()));
+ continue;
+ }
- // Open the XML document
- if (!setDocument(null, file, false, false)) {
- throw new HopException(
- BaseMessages.getString(PKG,
CONST_GET_XMLDATA_LOG_UNABLE_CREATE_DOCUMENT));
- }
+ // Open the XML document
+ if (!setDocument(null, file, false, false)) {
+ throw new HopException(
+ BaseMessages.getString(PKG,
CONST_GET_XMLDATA_LOG_UNABLE_CREATE_DOCUMENT));
+ }
- if (!applyXPath()) {
- throw new HopException(
- BaseMessages.getString(PKG,
CONST_GET_XMLDATA_LOG_UNABLE_APPLY_XPATH));
- }
+ if (!applyXPath()) {
+ throw new HopException(
+ BaseMessages.getString(PKG,
CONST_GET_XMLDATA_LOG_UNABLE_APPLY_XPATH));
+ }
- addFileToResultFilesname(file);
+ addFileToResultFilesname(file);
- if (isDetailed()) {
- logDetailed(
- BaseMessages.getString(
- PKG,
- CONST_GET_XMLDATA_LOG_LOOP_FILE_OCCURENCES,
- "" + data.nodesize,
- file.getName().getBaseName()));
- }
+ if (isDetailed()) {
+ logDetailed(
+ BaseMessages.getString(
+ PKG,
+ CONST_GET_XMLDATA_LOG_LOOP_FILE_OCCURENCES,
+ "" + data.nodesize,
+ file.getName().getBaseName()));
+ }
- } catch (Exception e) {
- throw new HopException(e);
- } finally {
- try {
- if (file != null) {
- file.close();
+ } catch (Exception e) {
+ throw new HopException(e);
+ } finally {
+ try {
+ if (file != null) {
+ file.close();
+ }
+ } catch (Exception e) {
+ // Ignore close errors
+ }
+ }
+ } else {
+ boolean url = false;
+ boolean xmltring = true;
+ if (meta.isReadUrl()) {
+ url = true;
+ xmltring = false;
}
- } catch (Exception e) {
- // Ignore close errors
- }
- }
- } else {
- boolean url = false;
- boolean xmltring = true;
- if (meta.isReadUrl()) {
- url = true;
- xmltring = false;
- }
- // Open the XML document
- if (!setDocument(fieldvalue, null, xmltring, url)) {
- throw new HopException(
- BaseMessages.getString(PKG,
CONST_GET_XMLDATA_LOG_UNABLE_CREATE_DOCUMENT));
- }
+ // Open the XML document
+ if (!setDocument(fieldvalue, null, xmltring, url)) {
+ throw new HopException(
+ BaseMessages.getString(PKG,
CONST_GET_XMLDATA_LOG_UNABLE_CREATE_DOCUMENT));
+ }
- // Apply XPath and set node list
- if (!applyXPath()) {
- throw new HopException(
- BaseMessages.getString(PKG,
CONST_GET_XMLDATA_LOG_UNABLE_APPLY_XPATH));
- }
- if (isDetailed()) {
- logDetailed(
- BaseMessages.getString(
- PKG, CONST_GET_XMLDATA_LOG_LOOP_FILE_OCCURENCES, "" +
data.nodesize));
+ // Apply XPath and set node list
+ if (!applyXPath()) {
+ throw new HopException(
+ BaseMessages.getString(PKG,
CONST_GET_XMLDATA_LOG_UNABLE_APPLY_XPATH));
+ }
+ if (isDetailed()) {
+ logDetailed(
+ BaseMessages.getString(
+ PKG, CONST_GET_XMLDATA_LOG_LOOP_FILE_OCCURENCES, "" +
data.nodesize));
+ }
+ }
+ } catch (HopException e) {
+ // A problem while reading or parsing the XML coming from the
input field is a
+ // per-row data error (e.g. the field is null or the content is
not valid XML).
+ // When error handling is enabled, divert the offending input row
to the error
+ // stream and continue with the next one instead of aborting the
whole transform.
+ if (getTransformMeta().isDoingErrorHandling()) {
+ // Keep the same row structure processPutRow() uses so both
error paths align.
+ Object[] errorRowData =
+ RowDataUtil.createResizedCopy(data.readrow,
data.outputRowMeta.size());
+ putError(
+ data.outputRowMeta,
+ errorRowData,
+ 1,
+ e.toString(),
+ meta.getXmlField(),
+ "GetXMLData002");
+ continue;
+ }
+ throw e;
}
}
+ return true;
+ } catch (Exception e) {
+ logError(BaseMessages.getString(PKG, "GetXMLData.Log.UnexpectedError",
e.toString()));
+ stopAll();
+ logError(Const.getStackTracker(e));
+ setErrors(1);
+ return false;
}
- } catch (Exception e) {
- logError(BaseMessages.getString(PKG, "GetXMLData.Log.UnexpectedError",
e.toString()));
- stopAll();
- logError(Const.getStackTracker(e));
- setErrors(1);
- return false;
}
- return true;
}
private void addFileToResultFilesname(FileObject file) {
diff --git
a/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/getxmldata/GetXMLDataTest.java
b/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/getxmldata/GetXMLDataTest.java
index 549da286d5..51016a0db0 100644
---
a/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/getxmldata/GetXMLDataTest.java
+++
b/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/getxmldata/GetXMLDataTest.java
@@ -41,6 +41,7 @@ import org.apache.hop.pipeline.PipelineMeta;
import org.apache.hop.pipeline.RowProducer;
import org.apache.hop.pipeline.engines.local.LocalPipelineEngine;
import org.apache.hop.pipeline.transform.ITransform;
+import org.apache.hop.pipeline.transform.TransformErrorMeta;
import org.apache.hop.pipeline.transform.TransformMeta;
import org.apache.hop.pipeline.transforms.dummy.DummyMeta;
import org.apache.hop.pipeline.transforms.injector.InjectorMeta;
@@ -453,4 +454,119 @@ class GetXMLDataTest {
assertEquals("${xml_path}", gxdm.getInputFields().get(0).getXPath());
assertEquals("data/owner",
gxdm.getInputFields().get(0).getResolvedXPath());
}
+
+ /** Build the five string fields used by the "in fields" test pipelines. */
+ private GetXmlDataField[] createXmlDataFields() {
+ String[] names = {"objectid", "sapident", "quantity", "merkmalname",
"merkmalswert"};
+ String[] xpaths = {"ObjectID", "SAPIDENT", "Quantity", "Merkmalname",
"Merkmalswert"};
+
+ GetXmlDataField[] fields = new GetXmlDataField[names.length];
+ for (int idx = 0; idx < fields.length; idx++) {
+ GetXmlDataField field = new GetXmlDataField();
+ field.setName(names[idx]);
+ field.setXPath(xpaths[idx]);
+
field.setElementType(GetXmlDataField.getElementTypeCode(GetXmlDataField.ELEMENT_TYPE_NODE));
+ field.setType(ValueMetaFactory.getValueMetaName(IValueMeta.TYPE_STRING));
+ field.setFormat("");
+ field.setLength(-1);
+ field.setPrecision(-1);
+ field.setCurrencySymbol("");
+ field.setDecimalSymbol("");
+ field.setGroupSymbol("");
+
field.setTrimType(GetXmlDataField.getTrimTypeCode(GetXmlDataField.TYPE_TRIM_NONE));
+ fields[idx] = field;
+ }
+ return fields;
+ }
+
+ /**
+ * With error handling enabled and XML coming from an input field, a row
whose XML field is null
+ * (or otherwise unparseable) must be diverted to the error stream while the
transform keeps
+ * processing the remaining rows, instead of aborting the whole transform.
Regression test for the
+ * "transform still stops when it should continue" bug.
+ */
+ @Test
+ void testErrorHandlingContinuesOnBadXml() throws Exception {
+ PipelineMeta pipelineMeta = new PipelineMeta();
+ pipelineMeta.setName("getxmldata-errorhandling");
+
+ PluginRegistry registry = PluginRegistry.getInstance();
+
+ // Injector
+ String injectorTransformName = "injector transform";
+ InjectorMeta im = new InjectorMeta();
+ String injectorPid = registry.getPluginId(TransformPluginType.class, im);
+ TransformMeta injectorTransform = new TransformMeta(injectorPid,
injectorTransformName, im);
+ pipelineMeta.addTransform(injectorTransform);
+
+ // Get XML Data, reading the XML from the incoming "field1"
+ String getXMLDataName = "get xml data transform";
+ GetXmlDataMeta gxdm = new GetXmlDataMeta();
+ gxdm.setEncoding(Const.UTF_8);
+ gxdm.setAFile(false);
+ gxdm.setInFields(true);
+ gxdm.setLoopXPath("Level1/Level2/Props");
+ gxdm.setXmlField("field1");
+ gxdm.setInputFields(java.util.Arrays.asList(createXmlDataFields()));
+ String getXMLDataPid = registry.getPluginId(TransformPluginType.class,
gxdm);
+ TransformMeta getXMLDataTransform = new TransformMeta(getXMLDataPid,
getXMLDataName, gxdm);
+ pipelineMeta.addTransform(getXMLDataTransform);
+ pipelineMeta.addPipelineHop(new PipelineHopMeta(injectorTransform,
getXMLDataTransform));
+
+ // Main output
+ String dummyMainName = "dummy main";
+ DummyMeta dmMain = new DummyMeta();
+ String dummyMainPid = registry.getPluginId(TransformPluginType.class,
dmMain);
+ TransformMeta dummyMain = new TransformMeta(dummyMainPid, dummyMainName,
dmMain);
+ pipelineMeta.addTransform(dummyMain);
+ pipelineMeta.addPipelineHop(new PipelineHopMeta(getXMLDataTransform,
dummyMain));
+
+ // Error output
+ String dummyErrorName = "dummy error";
+ DummyMeta dmError = new DummyMeta();
+ String dummyErrorPid = registry.getPluginId(TransformPluginType.class,
dmError);
+ TransformMeta dummyError = new TransformMeta(dummyErrorPid,
dummyErrorName, dmError);
+ pipelineMeta.addTransform(dummyError);
+ pipelineMeta.addPipelineHop(new PipelineHopMeta(getXMLDataTransform,
dummyError));
+
+ // Enable error handling on Get XML Data, routing error rows to the error
dummy
+ TransformErrorMeta errorMeta = new TransformErrorMeta(getXMLDataTransform,
dummyError);
+ errorMeta.setEnabled(true);
+ getXMLDataTransform.setTransformErrorMeta(errorMeta);
+
+ Pipeline pipeline = new LocalPipelineEngine(pipelineMeta);
+ pipeline.prepareExecution();
+
+ // Capture the error rows emitted by the Get XML Data transform itself...
+ RowTransformCollector errorCollector = new RowTransformCollector();
+ pipeline.getTransform(getXMLDataName, 0).addRowListener(errorCollector);
+ // ...and the good rows arriving at the main output.
+ RowTransformCollector mainCollector = new RowTransformCollector();
+ pipeline.getTransform(dummyMainName, 0).addRowListener(mainCollector);
+
+ RowProducer rp = pipeline.addRowProducer(injectorTransformName, 0);
+ pipeline.startThreads();
+
+ // A good row, a row whose XML field is null (the bug trigger), then
another good row.
+ IRowMeta rm = createRowMetaInterface();
+ rp.putRow(rm, new Object[] {getXML1()});
+ rp.putRow(rm, new Object[] {null});
+ rp.putRow(rm, new Object[] {getXML2()});
+ rp.finished();
+
+ pipeline.waitUntilFinished();
+
+ // The transform must keep going: no errors reported, so the pipeline did
not abort.
+ assertEquals(0, pipeline.getResult().getNrErrors(), "transform should not
report errors");
+
+ // Both good rows still produce their output (2 from XML1 + 1 from XML2).
+ assertEquals(3, mainCollector.getRowsWritten().size(), "good rows still
flow through");
+
+ // The single bad row is diverted to error handling and carries the
original (null) field.
+ assertEquals(1, errorCollector.getRowsError().size(), "bad row goes to
error handling");
+ assertEquals(
+ null,
+ errorCollector.getRowsError().getFirst().getData()[0],
+ "error row keeps the offending input value");
+ }
}