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 0d151e827f Enhance Concat Fields to Support Skipping Null Values
(#6455)
0d151e827f is described below
commit 0d151e827fa9604718cc14d7940479c5fec62dcf
Author: lance <[email protected]>
AuthorDate: Mon Feb 2 20:42:20 2026 +0800
Enhance Concat Fields to Support Skipping Null Values (#6455)
Signed-off-by: lance <[email protected]>
---
.../transforms/concatfields/ConcatFields.java | 116 +---------
.../concatfields/ConcatFieldsDialog.java | 235 ++++++++++----------
.../transforms/concatfields/ConcatFieldsMeta.java | 7 +
.../concatfields/helper/ConcatFieldHelper.java | 171 +++++++++++++++
.../messages/messages_en_US.properties | 3 +-
.../helper/ConcatFieldHelperTests.java | 239 +++++++++++++++++++++
6 files changed, 549 insertions(+), 222 deletions(-)
diff --git
a/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/ConcatFields.java
b/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/ConcatFields.java
index 580b061dbf..7ac956e954 100644
---
a/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/ConcatFields.java
+++
b/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/ConcatFields.java
@@ -18,19 +18,15 @@
package org.apache.hop.pipeline.transforms.concatfields;
import java.util.ArrayList;
-import org.apache.hop.core.Const;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.exception.HopTransformException;
-import org.apache.hop.core.exception.HopValueException;
-import org.apache.hop.core.row.IValueMeta;
-import org.apache.hop.core.row.RowDataUtil;
-import org.apache.hop.core.row.value.ValueMetaBase;
import org.apache.hop.core.util.StringUtil;
import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.pipeline.Pipeline;
import org.apache.hop.pipeline.PipelineMeta;
import org.apache.hop.pipeline.transform.BaseTransform;
import org.apache.hop.pipeline.transform.TransformMeta;
+import
org.apache.hop.pipeline.transforms.concatfields.helper.ConcatFieldHelper;
public class ConcatFields extends BaseTransform<ConcatFieldsMeta,
ConcatFieldsData> {
@@ -43,13 +39,14 @@ public class ConcatFields extends
BaseTransform<ConcatFieldsMeta, ConcatFieldsDa
int copyNr,
PipelineMeta pipelineMeta,
Pipeline pipeline) {
- super(transformMeta, meta, data, copyNr, pipelineMeta, pipeline); //
allocate TextFileOutput
+ // allocate TextFileOutput
+ super(transformMeta, meta, data, copyNr, pipelineMeta, pipeline);
}
@Override
public synchronized boolean processRow() throws HopException {
-
- Object[] row = getRow(); // This also waits for a row to be finished.
+ // This also waits for a row to be finished.
+ Object[] row = getRow();
if (row == null) {
setOutputDone();
return false;
@@ -115,7 +112,7 @@ public class ConcatFields extends
BaseTransform<ConcatFieldsMeta, ConcatFieldsDa
}
}
- Object[] outputRowData = concatFields(row);
+ Object[] outputRowData = ConcatFieldHelper.concat(row, data, meta,
getInputRowMeta());
putRow(data.outputRowMeta, outputRowData);
if (isRowLevel()) {
@@ -132,102 +129,6 @@ public class ConcatFields extends
BaseTransform<ConcatFieldsMeta, ConcatFieldsDa
return true;
}
- /** Concat the field values and call putRow() */
- private Object[] concatFields(Object[] inputRowData) throws HopException {
- Object[] outputRowData = createOutputRow(inputRowData);
-
- StringBuilder targetString = new StringBuilder(data.targetFieldLength); //
use a good capacity
-
- for (int i = 0; i < data.inputFieldIndexes.size(); i++) {
- if (i > 0) {
- targetString.append(data.stringSeparator);
- }
-
- int inputRowIndex = data.inputFieldIndexes.get(i);
- IValueMeta valueMeta = getInputRowMeta().getValueMeta(inputRowIndex);
- Object valueData = inputRowData[inputRowIndex];
-
- String nullString;
- String trimType;
- if (meta.getOutputFields().isEmpty()) {
- // No specific null value defined
- nullString = "";
- trimType = ValueMetaBase.getTrimTypeCode(IValueMeta.TRIM_TYPE_NONE);
- } else {
- ConcatField field = meta.getOutputFields().get(i);
- nullString = Const.NVL(field.getNullString(), "");
- trimType = data.trimType[i];
- // Manage missing trim type. Leave the incoming value as it is
- if (trimType == null) {
- trimType = ValueMetaBase.getTrimTypeCode(IValueMeta.TRIM_TYPE_NONE);
- }
- }
-
- concatField(targetString, valueMeta, valueData, nullString, trimType);
- }
-
- outputRowData[data.outputRowMeta.size() - 1] = targetString.toString();
-
- return outputRowData;
- }
-
- private void concatField(
- StringBuilder targetField,
- IValueMeta valueMeta,
- Object valueData,
- String nullString,
- String trimType)
- throws HopValueException {
-
- if (meta.isForceEnclosure()) {
- targetField.append(meta.getEnclosure());
- }
- if (valueMeta.isNull(valueData)) {
- targetField.append(nullString);
- } else {
- if (trimType != null) {
- // Get the raw string value without applying the incoming field's trim
type
- // by temporarily setting trim type to NONE
- int originalTrimType = valueMeta.getTrimType();
- valueMeta.setTrimType(IValueMeta.TRIM_TYPE_NONE);
- String stringValue = valueMeta.getString(valueData);
- // Restore the original trim type
- valueMeta.setTrimType(originalTrimType);
-
- // Apply only the configured trim type from the transform
- targetField.append(
- Const.trimToType(stringValue,
ValueMetaBase.getTrimTypeByCode(trimType)));
- }
- }
- if (meta.isForceEnclosure()) {
- targetField.append(meta.getEnclosure());
- }
- }
-
- // reserve room for the target field and eventually re-map the fields
- private Object[] createOutputRow(Object[] inputRowData) {
- // Allocate a new empty row
- //
- Object[] outputRowData;
-
- if (meta.getExtraFields().isRemoveSelectedFields()) {
- outputRowData = RowDataUtil.allocateRowData(data.outputRowMeta.size());
- int outputRowIndex = 0;
-
- // Copy over only the fields we want from the input.
- //
- for (int inputRowIndex : data.outputFieldIndexes) {
- outputRowData[outputRowIndex++] = inputRowData[inputRowIndex];
- }
- } else {
- // Keep the current row and add a field
- //
- outputRowData = RowDataUtil.createResizedCopy(inputRowData,
data.outputRowMeta.size());
- }
-
- return outputRowData;
- }
-
@Override
public boolean init() {
@@ -262,9 +163,4 @@ public class ConcatFields extends
BaseTransform<ConcatFieldsMeta, ConcatFieldsDa
}
}
}
-
- @Override
- public void dispose() {
- super.dispose();
- }
}
diff --git
a/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/ConcatFieldsDialog.java
b/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/ConcatFieldsDialog.java
index 08a31f46f3..8c83f7d8b4 100644
---
a/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/ConcatFieldsDialog.java
+++
b/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/ConcatFieldsDialog.java
@@ -31,6 +31,7 @@ import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.pipeline.PipelineMeta;
import org.apache.hop.pipeline.transform.TransformMeta;
import org.apache.hop.ui.core.ConstUi;
+import org.apache.hop.ui.core.FormDataBuilder;
import org.apache.hop.ui.core.PropsUi;
import org.apache.hop.ui.core.dialog.BaseDialog;
import org.apache.hop.ui.core.dialog.ErrorDialog;
@@ -43,9 +44,9 @@ import
org.apache.hop.ui.pipeline.transform.ITableItemInsertListener;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.custom.CTabItem;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Point;
-import org.eclipse.swt.layout.FormAttachment;
-import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
@@ -72,8 +73,14 @@ public class ConcatFieldsDialog extends BaseTransformDialog {
private Button wRemove;
+ /** enclosure label / text */
+ private Label wlEnclosure;
+
private Button wForceEnclosure;
+ /** skip empty value(empty/null) */
+ private Button skipEmptyBtn;
+
private TableView wFields;
private final ConcatFieldsMeta input;
@@ -124,18 +131,16 @@ public class ConcatFieldsDialog extends
BaseTransformDialog {
wlTransformName.setText(BaseMessages.getString(PKG,
"System.TransformName.Label"));
wlTransformName.setToolTipText(BaseMessages.getString(PKG,
"System.TransformName.Tooltip"));
PropsUi.setLook(wlTransformName);
- fdlTransformName = new FormData();
- fdlTransformName.left = new FormAttachment(0, 0);
- fdlTransformName.top = new FormAttachment(0, margin);
- fdlTransformName.right = new FormAttachment(middle, -margin);
- wlTransformName.setLayoutData(fdlTransformName);
+ wlTransformName.setLayoutData(
+ FormDataBuilder.builder().top(0, margin).left().right(middle,
-margin).build());
wTransformName = new Text(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
PropsUi.setLook(wTransformName);
- fdTransformName = new FormData();
- fdTransformName.left = new FormAttachment(middle, 0);
- fdTransformName.top = new FormAttachment(wlTransformName, 0, SWT.CENTER);
- fdTransformName.right = new FormAttachment(100, 0);
- wTransformName.setLayoutData(fdTransformName);
+ wTransformName.setLayoutData(
+ FormDataBuilder.builder()
+ .top(wlTransformName, 0, SWT.CENTER)
+ .left(middle, 0)
+ .right(100, 0)
+ .build());
Control lastControl = wTransformName;
// TargetFieldName line
@@ -145,19 +150,16 @@ public class ConcatFieldsDialog extends
BaseTransformDialog {
wlTargetFieldName.setToolTipText(
BaseMessages.getString(PKG,
"ConcatFieldsDialog.TargetFieldName.Tooltip"));
PropsUi.setLook(wlTargetFieldName);
- FormData fdlTargetFieldName = new FormData();
- fdlTargetFieldName.left = new FormAttachment(0, 0);
- fdlTargetFieldName.top = new FormAttachment(lastControl, margin);
- fdlTargetFieldName.right = new FormAttachment(middle, -margin);
- wlTargetFieldName.setLayoutData(fdlTargetFieldName);
+ wlTargetFieldName.setLayoutData(
+ FormDataBuilder.builder().top(lastControl,
margin).left().right(middle, -margin).build());
wTargetFieldName = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT |
SWT.BORDER);
PropsUi.setLook(wTargetFieldName);
- FormData fdTargetFieldName = new FormData();
- fdTargetFieldName.left = new FormAttachment(middle, 0);
- fdTargetFieldName.top = new FormAttachment(wlTargetFieldName, 0,
SWT.CENTER);
- fdTargetFieldName.right = new FormAttachment(100, 0);
- wTargetFieldName.setLayoutData(fdTargetFieldName);
- lastControl = wTargetFieldName;
+ wTargetFieldName.setLayoutData(
+ FormDataBuilder.builder()
+ .top(wlTargetFieldName, 0, SWT.CENTER)
+ .left(middle, 0)
+ .right(100, 0)
+ .build());
// TargetFieldLength line
Label wlTargetFieldLength = new Label(shell, SWT.RIGHT);
@@ -166,101 +168,116 @@ public class ConcatFieldsDialog extends
BaseTransformDialog {
wlTargetFieldLength.setToolTipText(
BaseMessages.getString(PKG,
"ConcatFieldsDialog.TargetFieldLength.Tooltip"));
PropsUi.setLook(wlTargetFieldLength);
- FormData fdlTargetFieldLength = new FormData();
- fdlTargetFieldLength.left = new FormAttachment(0, 0);
- fdlTargetFieldLength.top = new FormAttachment(wTargetFieldName, margin);
- fdlTargetFieldLength.right = new FormAttachment(middle, -margin);
- wlTargetFieldLength.setLayoutData(fdlTargetFieldLength);
+ wlTargetFieldLength.setLayoutData(
+ FormDataBuilder.builder()
+ .top(wTargetFieldName, margin)
+ .left()
+ .right(middle, -margin)
+ .build());
wTargetFieldLength = new Text(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
PropsUi.setLook(wTargetFieldLength);
- FormData fdTargetFieldLength = new FormData();
- fdTargetFieldLength.left = new FormAttachment(middle, 0);
- fdTargetFieldLength.top = new FormAttachment(wlTargetFieldLength, 0,
SWT.CENTER);
- fdTargetFieldLength.right = new FormAttachment(100, 0);
- wTargetFieldLength.setLayoutData(fdTargetFieldLength);
+ wTargetFieldLength.setLayoutData(
+ FormDataBuilder.builder()
+ .top(wlTargetFieldLength, 0, SWT.CENTER)
+ .left(middle, 0)
+ .right(100, 0)
+ .build());
lastControl = wTargetFieldLength;
// Separator
Label wlSeparator = new Label(shell, SWT.RIGHT);
wlSeparator.setText(BaseMessages.getString(PKG,
"ConcatFieldsDialog.Separator.Label"));
PropsUi.setLook(wlSeparator);
- FormData fdlSeparator = new FormData();
- fdlSeparator.left = new FormAttachment(0, 0);
- fdlSeparator.top = new FormAttachment(lastControl, margin);
- fdlSeparator.right = new FormAttachment(middle, -margin);
- wlSeparator.setLayoutData(fdlSeparator);
+ wlSeparator.setLayoutData(
+ FormDataBuilder.builder().top(lastControl,
margin).left().right(middle, -margin).build());
Button wbSeparator = new Button(shell, SWT.PUSH | SWT.CENTER);
PropsUi.setLook(wbSeparator);
wbSeparator.setText(BaseMessages.getString(PKG,
"ConcatFieldsDialog.Separator.Button"));
- FormData fdbSeparator = new FormData();
- fdbSeparator.right = new FormAttachment(100, 0);
- fdbSeparator.top = new FormAttachment(wlSeparator, 0, SWT.CENTER);
- wbSeparator.setLayoutData(fdbSeparator);
+ wbSeparator.setLayoutData(
+ FormDataBuilder.builder().top(wlSeparator, 0, SWT.CENTER).right(100,
0).build());
wbSeparator.addListener(SWT.Selection, se ->
wSeparator.getTextWidget().insert("\t"));
wSeparator = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT |
SWT.BORDER);
PropsUi.setLook(wSeparator);
- FormData fdSeparator = new FormData();
- fdSeparator.left = new FormAttachment(middle, 0);
- fdSeparator.top = new FormAttachment(wlSeparator, 0, SWT.CENTER);
- fdSeparator.right = new FormAttachment(wbSeparator, -margin);
- wSeparator.setLayoutData(fdSeparator);
+ wSeparator.setLayoutData(
+ FormDataBuilder.builder()
+ .top(wlSeparator, 0, SWT.CENTER)
+ .left(middle, 0)
+ .right(wbSeparator, -margin)
+ .build());
lastControl = wbSeparator;
- // Enclosure line...
- Label wlEnclosure = new Label(shell, SWT.RIGHT);
- wlEnclosure.setText(BaseMessages.getString(PKG,
"ConcatFieldsDialog.Enclosure.Label"));
- PropsUi.setLook(wlEnclosure);
- FormData fdlEnclosure = new FormData();
- fdlEnclosure.left = new FormAttachment(0, 0);
- fdlEnclosure.top = new FormAttachment(lastControl, margin);
- fdlEnclosure.right = new FormAttachment(middle, -margin);
- wlEnclosure.setLayoutData(fdlEnclosure);
- wEnclosure = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT |
SWT.BORDER);
- PropsUi.setLook(wEnclosure);
- FormData fdEnclosure = new FormData();
- fdEnclosure.left = new FormAttachment(middle, 0);
- fdEnclosure.top = new FormAttachment(wlEnclosure, 0, SWT.CENTER);
- fdEnclosure.right = new FormAttachment(100, 0);
- wEnclosure.setLayoutData(fdEnclosure);
- lastControl = wEnclosure;
-
// Force enclosure
Label wlForceEnclosure = new Label(shell, SWT.RIGHT);
wlForceEnclosure.setText(
BaseMessages.getString(PKG,
"ConcatFieldsDialog.ForceEnclosure.Label"));
PropsUi.setLook(wlForceEnclosure);
- FormData fdlForceEnclosure = new FormData();
- fdlForceEnclosure.left = new FormAttachment(0, 0);
- fdlForceEnclosure.top = new FormAttachment(lastControl, margin);
- fdlForceEnclosure.right = new FormAttachment(middle, -margin);
- wlForceEnclosure.setLayoutData(fdlForceEnclosure);
+ wlForceEnclosure.setLayoutData(
+ FormDataBuilder.builder().left().top(lastControl,
margin).right(middle, -margin).build());
wForceEnclosure = new Button(shell, SWT.CHECK | SWT.LEFT);
PropsUi.setLook(wForceEnclosure);
- FormData fdForceEnclosure = new FormData();
- fdForceEnclosure.left = new FormAttachment(middle, 0);
- fdForceEnclosure.top = new FormAttachment(wlForceEnclosure, 0, SWT.CENTER);
- fdForceEnclosure.right = new FormAttachment(100, 0);
- wForceEnclosure.setLayoutData(fdForceEnclosure);
+ wForceEnclosure.setLayoutData(
+ FormDataBuilder.builder().left(middle, 0).top(wlForceEnclosure, 0,
SWT.CENTER).build());
lastControl = wForceEnclosure;
+ wForceEnclosure.addSelectionListener(
+ new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ input.setChanged();
+ activateEnclosure();
+ }
+ });
+
+ // Enclosure line...
+ wlEnclosure = new Label(shell, SWT.RIGHT);
+ wlEnclosure.setText(BaseMessages.getString(PKG,
"ConcatFieldsDialog.Enclosure.Label"));
+ PropsUi.setLook(wlEnclosure);
+ wlEnclosure.setLayoutData(
+ FormDataBuilder.builder()
+ .left(wForceEnclosure, margin)
+ .top(wSeparator, margin)
+ .width(180)
+ .build());
+ wEnclosure = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT |
SWT.BORDER);
+ PropsUi.setLook(wEnclosure);
+ wEnclosure.setLayoutData(
+ FormDataBuilder.builder()
+ .left(wlEnclosure, margin)
+ .top(wSeparator, margin)
+ .right(100, 0)
+ .build());
+
+ // skip null/empty
+ Label skipEmptyLabel = new Label(shell, SWT.RIGHT);
+ skipEmptyLabel.setText(BaseMessages.getString(PKG,
"ConcatFieldsDialog.SkipEmpty.Label"));
+ PropsUi.setLook(skipEmptyLabel);
+ skipEmptyLabel.setLayoutData(
+ FormDataBuilder.builder().left().top(lastControl,
margin).right(middle, -margin).build());
+ skipEmptyBtn = new Button(shell, SWT.CHECK | SWT.LEFT);
+ PropsUi.setLook(skipEmptyBtn);
+ skipEmptyBtn.setLayoutData(
+ FormDataBuilder.builder()
+ .left(middle, 0)
+ .top(skipEmptyLabel, 0, SWT.CENTER)
+ .right(100, 0)
+ .build());
+ lastControl = skipEmptyLabel;
// Remove concatenated fields from input...
Label wlRemove = new Label(shell, SWT.RIGHT);
wlRemove.setText(BaseMessages.getString(PKG,
"ConcatFieldsDialog.Remove.Label"));
PropsUi.setLook(wlRemove);
- FormData fdlRemove = new FormData();
- fdlRemove.left = new FormAttachment(0, 0);
- fdlRemove.top = new FormAttachment(lastControl, margin);
- fdlRemove.right = new FormAttachment(middle, -margin);
- wlRemove.setLayoutData(fdlRemove);
+ wlRemove.setLayoutData(
+ FormDataBuilder.builder().top(lastControl,
margin).left().right(middle, -margin).build());
wRemove = new Button(shell, SWT.CHECK | SWT.LEFT);
PropsUi.setLook(wRemove);
- FormData fdRemove = new FormData();
- fdRemove.left = new FormAttachment(middle, 0);
- fdRemove.top = new FormAttachment(wlRemove, 0, SWT.CENTER);
- fdRemove.right = new FormAttachment(100, 0);
- wRemove.setLayoutData(fdRemove);
+ wRemove.setLayoutData(
+ FormDataBuilder.builder()
+ .top(wlRemove, 0, SWT.CENTER)
+ .left(middle, 0)
+ .right(100, 0)
+ .build());
lastControl = wlRemove;
// ////////////////////////
@@ -305,7 +322,7 @@ public class ConcatFieldsDialog extends BaseTransformDialog
{
int totalSize = dats.length + nums.length;
String[] formats = new String[totalSize];
System.arraycopy(dats, 0, formats, 0, dats.length);
- System.arraycopy(nums, 0, formats, dats.length + 0, nums.length);
+ System.arraycopy(nums, 0, formats, dats.length, nums.length);
fieldColumns = new ColumnInfo[FieldsCols];
fieldColumns[0] =
@@ -370,13 +387,8 @@ public class ConcatFieldsDialog extends
BaseTransformDialog {
FieldsRows,
null,
props);
-
- FormData fdFields = new FormData();
- fdFields.left = new FormAttachment(0, 0);
- fdFields.top = new FormAttachment(0, 0);
- fdFields.right = new FormAttachment(100, 0);
- fdFields.bottom = new FormAttachment(wGet, -margin);
- wFields.setLayoutData(fdFields);
+ wFields.setLayoutData(
+ FormDataBuilder.builder().top().left().right(100, 0).bottom(wGet,
-margin).build());
//
// Search the fields in the background
@@ -399,23 +411,18 @@ public class ConcatFieldsDialog extends
BaseTransformDialog {
}
};
new Thread(runnable).start();
-
- FormData fdFieldsComp = new FormData();
- fdFieldsComp.left = new FormAttachment(0, 0);
- fdFieldsComp.top = new FormAttachment(0, 0);
- fdFieldsComp.right = new FormAttachment(100, 0);
- fdFieldsComp.bottom = new FormAttachment(100, 0);
- wFieldsComp.setLayoutData(fdFieldsComp);
+ wFieldsComp.setLayoutData(
+ FormDataBuilder.builder().top().left().right(100, 0).bottom(100,
0).build());
wFieldsComp.layout();
wFieldsTab.setControl(wFieldsComp);
-
- FormData fdTabFolder = new FormData();
- fdTabFolder.left = new FormAttachment(0, 0);
- fdTabFolder.top = new FormAttachment(lastControl, 2 * margin);
- fdTabFolder.right = new FormAttachment(100, 0);
- fdTabFolder.bottom = new FormAttachment(wOk, -2 * margin);
- wTabFolder.setLayoutData(fdTabFolder);
+ wTabFolder.setLayoutData(
+ FormDataBuilder.builder()
+ .top(lastControl, 2 * margin)
+ .left()
+ .right(100, 0)
+ .bottom(wOk, -2 * margin)
+ .build());
// Whenever something changes, set the tooltip to the expanded version:
wTargetFieldName.addModifyListener(
@@ -434,8 +441,8 @@ public class ConcatFieldsDialog extends BaseTransformDialog
{
getData();
+ activateEnclosure();
BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel());
-
return transformName;
}
@@ -456,6 +463,7 @@ public class ConcatFieldsDialog extends BaseTransformDialog
{
wEnclosure.setText(Const.NVL(input.getEnclosure(), ""));
wForceEnclosure.setSelection(input.isForceEnclosure());
wRemove.setSelection(input.getExtraFields().isRemoveSelectedFields());
+ skipEmptyBtn.setSelection(input.isSkipValueEmpty());
logDebug("getting fields info...");
@@ -497,6 +505,7 @@ public class ConcatFieldsDialog extends BaseTransformDialog
{
meta.setSeparator(wSeparator.getText());
meta.setEnclosure(wEnclosure.getText());
meta.setForceEnclosure(wForceEnclosure.getSelection());
+ meta.setSkipValueEmpty(skipEmptyBtn.getSelection());
meta.getExtraFields().setRemoveSelectedFields(wRemove.getSelection());
input.getOutputFields().clear();
@@ -522,12 +531,11 @@ public class ConcatFieldsDialog extends
BaseTransformDialog {
if (StringUtil.isEmpty(wTransformName.getText())) {
return;
}
-
- transformName = wTransformName.getText(); // return value
+ // return value
+ transformName = wTransformName.getText();
getInfo(input);
input.setChanged();
-
dispose();
}
@@ -597,4 +605,9 @@ public class ConcatFieldsDialog extends BaseTransformDialog
{
wFields.optWidth(true);
}
+
+ private void activateEnclosure() {
+ wlEnclosure.setEnabled(wForceEnclosure.getSelection());
+ wEnclosure.setEnabled(wForceEnclosure.getSelection());
+ }
}
diff --git
a/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/ConcatFieldsMeta.java
b/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/ConcatFieldsMeta.java
index 1676c6a4e9..3a75945044 100644
---
a/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/ConcatFieldsMeta.java
+++
b/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/ConcatFieldsMeta.java
@@ -74,6 +74,10 @@ public class ConcatFieldsMeta extends
BaseTransformMeta<ConcatFields, ConcatFiel
@HopMetadataProperty(key = "ConcatFields")
private ExtraFields extraFields;
+ /** ignore empty value */
+ @HopMetadataProperty(key = "skip_value_empty", injectionKey =
"SKIP_VALUE_EMPTY")
+ private boolean skipValueEmpty;
+
public ConcatFieldsMeta() {
super();
outputFields = new ArrayList<>();
@@ -100,6 +104,9 @@ public class ConcatFieldsMeta extends
BaseTransformMeta<ConcatFields, ConcatFiel
extraFields.setTargetFieldName("");
extraFields.setTargetFieldLength(0);
extraFields.setRemoveSelectedFields(false);
+
+ // default disable null/empty value
+ skipValueEmpty = false;
}
@Override
diff --git
a/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/helper/ConcatFieldHelper.java
b/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/helper/ConcatFieldHelper.java
new file mode 100644
index 0000000000..e7403ad7e9
--- /dev/null
+++
b/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/helper/ConcatFieldHelper.java
@@ -0,0 +1,171 @@
+/*
+ * 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.hop.pipeline.transforms.concatfields.helper;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import lombok.experimental.UtilityClass;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopValueException;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.RowDataUtil;
+import org.apache.hop.core.row.value.ValueMetaBase;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.pipeline.transforms.concatfields.ConcatField;
+import org.apache.hop.pipeline.transforms.concatfields.ConcatFieldsData;
+import org.apache.hop.pipeline.transforms.concatfields.ConcatFieldsMeta;
+
+/**
+ * Utility class used by the Concat Fields transform to concatenate multiple
input field values into
+ * a single string field.
+ *
+ * <p>The concatenation behavior supports:
+ *
+ * <ul>
+ * <li>Custom field separators
+ * <li>Null value replacement
+ * <li>Configurable trim types per field
+ * <li>Optional value enclosure
+ * <li>Skipping null and/or empty values
+ * </ul>
+ *
+ * <p>This helper is stateless and thread-safe.
+ */
+@UtilityClass
+public class ConcatFieldHelper {
+
+ /**
+ * Concatenates the values of the selected input fields into a single string
field.
+ *
+ * @param inputRowData the input row data
+ * @param data the runtime data for the Concat Fields transform
+ * @param meta the metadata configuration for the Concat Fields transform
+ * @param rowMeta the metadata describing the input row structure
+ * @return the output row data with the concatenated field appended
+ * @throws HopException if a conversion error occurs while processing field
values
+ */
+ public static Object[] concat(
+ Object[] inputRowData, ConcatFieldsData data, ConcatFieldsMeta meta,
IRowMeta rowMeta)
+ throws HopException {
+ Object[] outputRowData = createOutputRow(inputRowData, data, meta);
+ List<String> fields = new ArrayList<>(Math.max(data.targetFieldLength, 8));
+
+ for (int i = 0; i < data.inputFieldIndexes.size(); i++) {
+ int inputRowIndex = data.inputFieldIndexes.get(i);
+ IValueMeta valueMeta = rowMeta.getValueMeta(inputRowIndex);
+ Object valueData = inputRowData[inputRowIndex];
+
+ String nullString;
+ String trimType;
+ if (meta.getOutputFields().isEmpty()) {
+ // No specific null value defined
+ nullString = "";
+ trimType = ValueMetaBase.getTrimTypeCode(IValueMeta.TRIM_TYPE_NONE);
+ } else {
+ ConcatField field = meta.getOutputFields().get(i);
+ nullString = Const.NVL(field.getNullString(), "");
+ trimType = data.trimType[i];
+ // Manage missing trim type. Leave the incoming value as it is
+ if (trimType == null) {
+ trimType = ValueMetaBase.getTrimTypeCode(IValueMeta.TRIM_TYPE_NONE);
+ }
+ }
+
+ handlerField(fields, valueMeta, valueData, nullString, trimType);
+ }
+
+ // list -> string(support separator)
+ String separator = Const.NVL(data.stringSeparator, "");
+ String collect =
+ fields.stream()
+ .filter(c -> !(meta.isSkipValueEmpty() && Utils.isEmpty(c)))
+ .map(c -> meta.isForceEnclosure() ? meta.getEnclosure() + c +
meta.getEnclosure() : c)
+ .collect(Collectors.joining(separator));
+
+ outputRowData[data.outputRowMeta.size() - 1] = collect;
+ return outputRowData;
+ }
+
+ /**
+ * Converts a single field value to its string representation and adds it to
the provided list of
+ * field values.
+ *
+ * @param fields the list collecting converted field values
+ * @param valueMeta the value metadata describing the field type
+ * @param valueData the actual field value
+ * @param nullString the replacement value used when the field value is null
+ * @param trimType the trim type code to apply
+ * @throws HopValueException if the value cannot be converted to a string
+ */
+ private static void handlerField(
+ List<String> fields,
+ IValueMeta valueMeta,
+ Object valueData,
+ String nullString,
+ String trimType)
+ throws HopValueException {
+ if (valueMeta.isNull(valueData)) {
+ fields.add(nullString);
+ return;
+ }
+
+ if (trimType != null) {
+ // Get the raw string value without applying the incoming field's trim
type
+ // by temporarily setting trim type to NONE
+ int originalTrimType = valueMeta.getTrimType();
+ valueMeta.setTrimType(IValueMeta.TRIM_TYPE_NONE);
+ String stringValue = valueMeta.getString(valueData);
+ // Restore the original trim type
+ valueMeta.setTrimType(originalTrimType);
+
+ // Apply only the configured trim type from the transform
+ fields.add(Const.trimToType(stringValue,
ValueMetaBase.getTrimTypeByCode(trimType)));
+ }
+ }
+
+ /**
+ * Creates the output row based on the transform configuration.
+ *
+ * @param inputRowData the input row data
+ * @param data the runtime data for the transform
+ * @param meta the transform metadata
+ * @return a new output row array with the appropriate size
+ */
+ private static Object[] createOutputRow(
+ Object[] inputRowData, ConcatFieldsData data, ConcatFieldsMeta meta) {
+ // Allocate a new empty row
+ Object[] outputRowData;
+
+ if (meta.getExtraFields().isRemoveSelectedFields()) {
+ outputRowData = RowDataUtil.allocateRowData(data.outputRowMeta.size());
+ int outputRowIndex = 0;
+
+ // Copy over only the fields we want from the input.
+ for (int inputRowIndex : data.outputFieldIndexes) {
+ outputRowData[outputRowIndex++] = inputRowData[inputRowIndex];
+ }
+ } else {
+ // Keep the current row and add a field
+ outputRowData = RowDataUtil.createResizedCopy(inputRowData,
data.outputRowMeta.size());
+ }
+ return outputRowData;
+ }
+}
diff --git
a/plugins/transforms/concatfields/src/main/resources/org/apache/hop/pipeline/transforms/concatfields/messages/messages_en_US.properties
b/plugins/transforms/concatfields/src/main/resources/org/apache/hop/pipeline/transforms/concatfields/messages/messages_en_US.properties
index 8f6fcc0b4e..8f74a4bb03 100644
---
a/plugins/transforms/concatfields/src/main/resources/org/apache/hop/pipeline/transforms/concatfields/messages/messages_en_US.properties
+++
b/plugins/transforms/concatfields/src/main/resources/org/apache/hop/pipeline/transforms/concatfields/messages/messages_en_US.properties
@@ -51,4 +51,5 @@ ConcatFieldsMeta.CheckResult.FieldsReceived=Transform is
connected to previous o
ConcatFieldsMeta.CheckResult.TargetFieldLengthMissingFastDataDump=It is
recommended to specify the Length of Target Field when the Fast Data Dump
option is enabled.
ConcatFieldsMeta.CheckResult.TargetFieldNameMissing=The Target Field Name is
not specified.
ConcatFieldsMeta.keyword=concat,field
-ConcatFieldsDialog.ForceEnclosure.Label=Force enclosure?
\ No newline at end of file
+ConcatFieldsDialog.ForceEnclosure.Label=Force enclosure?
+ConcatFieldsDialog.SkipEmpty.Label=Skip empty
\ No newline at end of file
diff --git
a/plugins/transforms/concatfields/src/test/java/org/apache/hop/pipeline/transforms/concatfields/helper/ConcatFieldHelperTests.java
b/plugins/transforms/concatfields/src/test/java/org/apache/hop/pipeline/transforms/concatfields/helper/ConcatFieldHelperTests.java
new file mode 100644
index 0000000000..54d9a8ba15
--- /dev/null
+++
b/plugins/transforms/concatfields/src/test/java/org/apache/hop/pipeline/transforms/concatfields/helper/ConcatFieldHelperTests.java
@@ -0,0 +1,239 @@
+/*
+ * 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.hop.pipeline.transforms.concatfields.helper;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.stream.Stream;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaBase;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.pipeline.transforms.concatfields.ConcatField;
+import org.apache.hop.pipeline.transforms.concatfields.ConcatFieldsData;
+import org.apache.hop.pipeline.transforms.concatfields.ConcatFieldsMeta;
+import org.apache.hop.pipeline.transforms.concatfields.ExtraFields;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/** Unit test for {@link ConcatFieldHelper} */
+class ConcatFieldHelperTests {
+
+ @Test
+ void testConcatFieldsBasic() throws Exception {
+ // rowMeta
+ RowMeta rowMeta = new RowMeta();
+ rowMeta.addValueMeta(new ValueMetaString("f1"));
+ rowMeta.addValueMeta(new ValueMetaString("f2"));
+ // input row
+ Object[] inputRow = new Object[] {"hello", "world"};
+
+ // data
+ ConcatFieldsData data = new ConcatFieldsData();
+ data.inputFieldIndexes = List.of(0, 1);
+ data.outputFieldIndexes = List.of(0, 1);
+ data.stringSeparator = ",";
+ data.targetFieldLength = 50;
+ data.trimType =
+ new String[] {
+ ValueMetaBase.getTrimTypeCode(IValueMeta.TRIM_TYPE_NONE),
+ ValueMetaBase.getTrimTypeCode(IValueMeta.TRIM_TYPE_NONE)
+ };
+
+ RowMeta outputRowMeta = new RowMeta();
+ outputRowMeta.addValueMeta(new ValueMetaString("f1"));
+ outputRowMeta.addValueMeta(new ValueMetaString("f2"));
+ outputRowMeta.addValueMeta(new ValueMetaString("concat"));
+
+ data.outputRowMeta = outputRowMeta;
+
+ // meta
+ ConcatFieldsMeta meta = mock(ConcatFieldsMeta.class);
+ when(meta.isForceEnclosure()).thenReturn(false);
+ when(meta.getOutputFields()).thenReturn(List.of());
+ when(meta.getExtraFields()).thenReturn(new ExtraFields());
+
+ // result
+ Object[] result = ConcatFieldHelper.concat(inputRow, data, meta, rowMeta);
+
+ assertEquals("hello,world", result[2]);
+ }
+
+ @Test
+ void testConcatFieldsWithNullAndTrim() throws Exception {
+ RowMeta rowMeta = new RowMeta();
+ rowMeta.addValueMeta(new ValueMetaString("f1"));
+ rowMeta.addValueMeta(new ValueMetaString("f2"));
+
+ Object[] inputRow = new Object[] {" hello ", null};
+
+ ConcatFieldsData data = new ConcatFieldsData();
+ data.inputFieldIndexes = List.of(0, 1);
+ data.outputFieldIndexes = List.of(0, 1);
+ data.stringSeparator = "|";
+ data.targetFieldLength = 50;
+ data.trimType =
+ new String[] {
+ ValueMetaBase.getTrimTypeCode(IValueMeta.TRIM_TYPE_BOTH),
+ ValueMetaBase.getTrimTypeCode(IValueMeta.TRIM_TYPE_NONE)
+ };
+
+ RowMeta outputRowMeta = new RowMeta();
+ outputRowMeta.addValueMeta(new ValueMetaString("f1"));
+ outputRowMeta.addValueMeta(new ValueMetaString("f2"));
+ outputRowMeta.addValueMeta(new ValueMetaString("concat"));
+ data.outputRowMeta = outputRowMeta;
+
+ ConcatField field1 = new ConcatField();
+ field1.setNullString("");
+
+ ConcatField field2 = new ConcatField();
+ field2.setNullString("NULL");
+
+ ConcatFieldsMeta meta = mock(ConcatFieldsMeta.class);
+ when(meta.isForceEnclosure()).thenReturn(false);
+ when(meta.getOutputFields()).thenReturn(List.of(field1, field2));
+ when(meta.getExtraFields()).thenReturn(new ExtraFields());
+
+ Object[] result = ConcatFieldHelper.concat(inputRow, data, meta, rowMeta);
+
+ assertEquals("hello|NULL", result[2]);
+ }
+
+ @Test
+ void testConcatFieldsWithEnclosure() throws Exception {
+ RowMeta rowMeta = new RowMeta();
+ rowMeta.addValueMeta(new ValueMetaString("f1"));
+
+ Object[] inputRow = new Object[] {"abc"};
+
+ ConcatFieldsData data = new ConcatFieldsData();
+ data.inputFieldIndexes = List.of(0);
+ data.outputFieldIndexes = List.of(0, 1);
+ data.stringSeparator = "";
+ data.targetFieldLength = 10;
+ data.trimType = new String[]
{ValueMetaBase.getTrimTypeCode(IValueMeta.TRIM_TYPE_NONE)};
+
+ RowMeta outputRowMeta = new RowMeta();
+ outputRowMeta.addValueMeta(new ValueMetaString("f1"));
+ outputRowMeta.addValueMeta(new ValueMetaString("concat"));
+ data.outputRowMeta = outputRowMeta;
+
+ ConcatFieldsMeta meta = mock(ConcatFieldsMeta.class);
+ when(meta.isForceEnclosure()).thenReturn(true);
+ when(meta.getEnclosure()).thenReturn("\"");
+ when(meta.getOutputFields()).thenReturn(List.of());
+ when(meta.getExtraFields()).thenReturn(new ExtraFields());
+
+ Object[] result = ConcatFieldHelper.concat(inputRow, data, meta, rowMeta);
+
+ assertEquals("\"abc\"", result[1]);
+ }
+
+ @ParameterizedTest
+ @MethodSource("inputRowProvider")
+ void testConcatFieldsBasic(
+ Object[] inputRow, boolean isForceEnclosure, boolean skipEmpty, String
expected)
+ throws Exception {
+ // rowMeta
+ RowMeta rowMeta = new RowMeta();
+ rowMeta.addValueMeta(new ValueMetaString("col1"));
+ rowMeta.addValueMeta(new ValueMetaString("col2"));
+ rowMeta.addValueMeta(new ValueMetaString("col3"));
+
+ // data
+ ConcatFieldsData data = new ConcatFieldsData();
+ data.inputFieldIndexes = List.of(0, 1, 2);
+ data.outputFieldIndexes = List.of(0, 1, 2);
+ data.stringSeparator = ";";
+ data.targetFieldLength = 50;
+ data.trimType =
+ new String[] {
+ ValueMetaBase.getTrimTypeCode(IValueMeta.TRIM_TYPE_NONE),
+ ValueMetaBase.getTrimTypeCode(IValueMeta.TRIM_TYPE_NONE),
+ ValueMetaBase.getTrimTypeCode(IValueMeta.TRIM_TYPE_NONE)
+ };
+
+ RowMeta outputRowMeta = new RowMeta();
+ outputRowMeta.addValueMeta(new ValueMetaString("col1"));
+ outputRowMeta.addValueMeta(new ValueMetaString("col2"));
+ outputRowMeta.addValueMeta(new ValueMetaString("col3"));
+ outputRowMeta.addValueMeta(new ValueMetaString("concat"));
+
+ data.outputRowMeta = outputRowMeta;
+
+ // meta
+ ConcatFieldsMeta meta = mock(ConcatFieldsMeta.class);
+ when(meta.isForceEnclosure()).thenReturn(isForceEnclosure);
+ when(meta.getEnclosure()).thenReturn("\"");
+ when(meta.getOutputFields()).thenReturn(List.of());
+ when(meta.getExtraFields()).thenReturn(new ExtraFields());
+ when(meta.isSkipValueEmpty()).thenReturn(skipEmpty);
+
+ // result
+ Object[] result = ConcatFieldHelper.concat(inputRow, data, meta, rowMeta);
+ assertEquals(expected, result[3]);
+ }
+
+ static Stream<Arguments> inputRowProvider() {
+ return Stream.of(
+ Arguments.of(new Object[] {"a", "b", "c"}, false, false, "a;b;c"),
+ Arguments.of(new Object[] {"a", "b", null}, false, false, "a;b;"),
+ Arguments.of(new Object[] {"a", null, null}, false, false, "a;;"),
+ Arguments.of(new Object[] {null, "b", "c"}, false, false, ";b;c"),
+ Arguments.of(new Object[] {null, "b", null}, false, false, ";b;"),
+ Arguments.of(new Object[] {null, null, "c"}, false, false, ";;c"),
+ Arguments.of(new Object[] {null, null, null}, false, false, ";;"),
+ Arguments.of(new Object[] {"", "", ""}, false, false, ";;"),
+
+ // enable force enclosure.
+ Arguments.of(new Object[] {"a", "b", "c"}, true, false,
"\"a\";\"b\";\"c\""),
+ Arguments.of(new Object[] {"a", "b", null}, true, false,
"\"a\";\"b\";\"\""),
+ Arguments.of(new Object[] {"a", null, null}, true, false,
"\"a\";\"\";\"\""),
+ Arguments.of(new Object[] {null, "b", "c"}, true, false,
"\"\";\"b\";\"c\""),
+ Arguments.of(new Object[] {null, "b", null}, true, false,
"\"\";\"b\";\"\""),
+ Arguments.of(new Object[] {null, null, "c"}, true, false,
"\"\";\"\";\"c\""),
+ Arguments.of(new Object[] {null, null, null}, true, false,
"\"\";\"\";\"\""),
+ Arguments.of(new Object[] {"", "", ""}, true, false, "\"\";\"\";\"\""),
+
+ // skip empty(null/"")
+ Arguments.of(new Object[] {"a", "b", "c"}, false, true, "a;b;c"),
+ Arguments.of(new Object[] {"a", "b", null}, false, true, "a;b"),
+ Arguments.of(new Object[] {"a", null, null}, false, true, "a"),
+ Arguments.of(new Object[] {null, "b", "c"}, false, true, "b;c"),
+ Arguments.of(new Object[] {null, "b", null}, false, true, "b"),
+ Arguments.of(new Object[] {null, null, "c"}, false, true, "c"),
+ Arguments.of(new Object[] {null, null, null}, false, true, ""),
+ Arguments.of(new Object[] {"", "", ""}, false, true, ""),
+
+ // enable force enclosure, skip empty(null/"")
+ Arguments.of(new Object[] {"a", "b", "c"}, true, true,
"\"a\";\"b\";\"c\""),
+ Arguments.of(new Object[] {"a", "b", null}, true, true, "\"a\";\"b\""),
+ Arguments.of(new Object[] {"a", null, null}, true, true, "\"a\""),
+ Arguments.of(new Object[] {null, "b", "c"}, true, true, "\"b\";\"c\""),
+ Arguments.of(new Object[] {null, "b", null}, true, true, "\"b\""),
+ Arguments.of(new Object[] {null, null, "c"}, true, true, "\"c\""),
+ Arguments.of(new Object[] {null, null, null}, true, true, ""),
+ Arguments.of(new Object[] {"", "", ""}, true, true, ""));
+ }
+}