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 2e7dfb0759 Issue #6078 (Repeat Fields) (#6139)
2e7dfb0759 is described below
commit 2e7dfb0759aca44b2a428a34411bce4404039998
Author: Matt Casters <[email protected]>
AuthorDate: Tue Dec 9 16:02:11 2025 +0100
Issue #6078 (Repeat Fields) (#6139)
* Issue #6078 (Repeat Fields)
* Issue #6078 (Repeat Fields)
* Issue #6078 (Repeat Fields - test renamed to avoid collision with others)
* Issue #6078 (Repeat Fields - test renamed to avoid collision with others)
* Issue #6078 (Repeat Fields - Typo fix in test)
* rebase + add doc page to nav.adoc
---------
Co-authored-by: Matt Casters <[email protected]>
Co-authored-by: Hans Van Akelyen <[email protected]>
---
.../images/transforms/icons/repeatfields.svg | 4 +
docs/hop-user-manual/modules/ROOT/nav.adoc | 1 +
.../pages/pipeline/transforms/repeatfields.adoc | 126 +++++++
.../0082-repeat-fields-current-when-indicated.hpl | 333 ++++++++++++++++++
.../transforms/0082-repeat-fields-mdi-main.hpl | 319 +++++++++++++++++
.../transforms/0082-repeat-fields-mdi.hpl | 327 +++++++++++++++++
.../0082-repeat-fields-previous-when-null.hpl | 192 ++++++++++
.../transforms/0082-repeat-fields-previous.hpl | 192 ++++++++++
...golden-repeat-fields-current-when-indicated.csv | 7 +
.../datasets/golden-repeat-fields-previous.csv | 10 +
.../datasets/golden-repeat-fields-when-null.csv | 10 +
.../transforms/main-0082-repeat-fields.hwf | 89 +++++
...olden-repeat-fields-current-when-indicated.json | 72 ++++
.../dataset/golden-repeat-fields-previous.json | 48 +++
.../dataset/golden-repeat-fields-when-null.json | 48 +++
...-repeat-fields-current-when-indicated UNIT.json | 63 ++++
.../0082-repeat-fields-mdi-main UNIT.json | 58 ++++
.../0082-repeat-fields-previous UNIT.json | 48 +++
...0082-repeat-fields-previous-when-null UNIT.json | 48 +++
plugins/transforms/pom.xml | 1 +
plugins/transforms/repeatfields/pom.xml | 45 +++
.../repeatfields/src/assembly/assembly.xml | 50 +++
.../pipeline/transforms/repeatfields/Repeat.java | 53 +++
.../transforms/repeatfields/RepeatFields.java | 186 ++++++++++
.../transforms/repeatfields/RepeatFieldsData.java | 32 ++
.../repeatfields/RepeatFieldsDialog.java | 385 +++++++++++++++++++++
.../transforms/repeatfields/RepeatFieldsMeta.java | 157 +++++++++
.../messages/messages_en_US.properties | 44 +++
.../src/main/resources/repeatfields.svg | 4 +
.../repeatfields/src/main/resources/version.xml | 20 ++
30 files changed, 2972 insertions(+)
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/repeatfields.svg
b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/repeatfields.svg
new file mode 100644
index 0000000000..daa970dc75
--- /dev/null
+++
b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/repeatfields.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">
+ <path fill="#0e3a5a" d="M11,17H4A2,2 0 0,1 2,15V3A2,2 0 0,1
4,1H16V3H4V15H11V13L15,16L11,19V17M19,21V7H8V13H6V7A2,2 0 0,1 8,5H19A2,2 0 0,1
21,7V21A2,2 0 0,1 19,23H8A2,2 0 0,1 6,21V19H8V21H19Z"/>
+</svg>
diff --git a/docs/hop-user-manual/modules/ROOT/nav.adoc
b/docs/hop-user-manual/modules/ROOT/nav.adoc
index a622235839..b0de982604 100644
--- a/docs/hop-user-manual/modules/ROOT/nav.adoc
+++ b/docs/hop-user-manual/modules/ROOT/nav.adoc
@@ -217,6 +217,7 @@ under the License.
*** xref:pipeline/transforms/propertyoutput.adoc[Properties file Output]
*** xref:pipeline/transforms/redshift-bulkloader.adoc[Redshift Bulk Loader]
*** xref:pipeline/transforms/regexeval.adoc[Regex Evaluation]
+*** xref:pipeline/transforms/repeatfields.adoc[Repeat Fields]
*** xref:pipeline/transforms/replacestring.adoc[Replace in String]
*** xref:pipeline/transforms/reservoirsampling.adoc[Reservoir Sampling]
*** xref:pipeline/transforms/rest.adoc[REST Client]
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/repeatfields.adoc
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/repeatfields.adoc
new file mode 100644
index 0000000000..eb7b634890
--- /dev/null
+++
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/repeatfields.adoc
@@ -0,0 +1,126 @@
+////
+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.
+////
+:documentationPath: /pipeline/transforms/
+:language: en_US
+:description: Repeat a field value from a previous row
+
+:openvar: {
+:closevar: }
+
+= image:transforms/icons/repeatfields.svg[Repeat Fields Icon,
role="image-doc-icon"] Repeat Fields
+
+[%noheader,cols="3a,1a", role="table-no-borders" ]
+|===
+|
+== Description
+
+The Repeat Fields transform is solving a few common problems:
+
+* Repeat a field value from the previous row, if the value is null on the
current row: this commonly happens when
+reading data from merged cells in a spreadsheet. In that scenario only the
first row is filled in for the merged cell
+and we need to repeat the data from the first row of the cell.
+* Take a field value from the current row when an indicator matches, otherwise
take the value from the previous row.
+This can be used to combine data coming from multiple sources when you have a
common key to group on.
+
+IMPORTANT: If the incoming data is not sorted on the specified group field(s),
+the output results may not be correct. We recommend sorting the incoming data
within the pipeline,
+or in a source database.
+
+CAUTION: This transform isn't supported in data processing engines that can't
guarantee a row order in the given group.
+Even on the native Hop engine you need to make sure to use partitioning on the
group field(s) to process data in
+parallel.
+
+|
+== Supported Engines
+[%noheader,cols="2,1a",frame=none, role="table-supported-engines"]
+!===
+!Hop Engine! image:check_mark.svg[Supported, 24]
+!Spark! image:cross.svg[Not Supported, 24]
+!Flink! image:cross.svg[Not Supported, 24]
+!Dataflow! image:cross.svg[Not Supported, 24]
+!===
+|===
+
+== Group fields
+
+In the Group fields section you can specify the group in which to operate.
+Every new group starts without a previous row.
+
+== The fields to repeat
+
+[options="header"]
+|===
+|Option|Description
+|Repeat type
+|
+**Previous row**: repeat the field value from the previous row, regardless of
its current value.
+If there is no previous row, take the value from the current row. (ETL
Metadata Injection type = `previous`)
+
+**Previous when field is null**: copy the field value from the previous row if
the current source value is null. (ETL Metadata Injection type =
`previous_when_null`)
+
+**Current when indicated**: take the value from the current source field if
the given indicator matches the value in the indicator field. (ETL Metadata
Injection type = `current_when_indicated`)
+
+|Source field
+|The source field to repeat
+
+|Target field
+|Specify the name of the target field (mandatory)
+
+|Indicator field name
+|When using the "Current when indicated" type, this is the name of the field
that contains the indicator value.
+
+|Indicator value
+|When using the "Current when indicated" type, this is the indicator value to
match with.
+
+|===
+
+== Current when indicated
+
+When we're dealing with source data coming from multiple sources, for example
information about a customer,
+we want to assemble a record containing all the different fields from the
different sources.
+In the example of our customer, source system A might contain personal data
like name, first name, birthdate, and so on.
+Another source system B might contain the status of the customer.
+Finally, source system C contains a flag indicating financial status.
+Each of the sources get changed at a different point in time. In a slowly
changing dimension (see:
xref:pipeline/transforms/dimensionlookup.adoc[Dimension Lookup/Update] ) we
want to see the changes when they occur in the different sources.
+We also want to process the data from the sources as they arrive.
+
+[options="header"]
+|===
+|source|customer_id|timestamp|lastname|firstname|birthdate|status|indicator
+|A|1|2025/01/01 12:00:00|Mouse|Mickey|1928/11/18|null|null
+|B|1|2025/01/01 13:00:00|null|null|null|active|null
+|C|1|2025/01/01 14:00:00|null|null|null|null|positive
+|===
+
+What we want to do with this transform is take the appropriate fields from the
source data taking into account the
+source field as an indicator. We want to end up with this result:
+
+[options="header"]
+|===
+|source|customer_id|timestamp|lastname|firstname|birthdate|status|indicator
+|A|1|2025/01/01 12:00:00|Mouse|Mickey|1928/11/18|null|null
+|B|1|2025/01/01 13:00:00|Mouse|Mickey|1928/11/18|active|null
+|C|1|2025/01/01 14:00:00|Mouse|Mickey|1928/11/18|active|positive
+|===
+
+These 3 rows can then be used to create a detailed and correct timeline using
the xref:pipeline/transforms/dimensionlookup.adoc[Dimension Lookup/Update]
transform.
+
+TIP: Add the last record from your target slowly changing dimension to modify
existing records.
+
+IMPORTANT: As mentioned above, but worth repeating, make sure your data is
sorted on the group key
+(the customer ID in this example) and **ALSO** on the timestamp to get a
correct timeline result.
+
diff --git
a/integration-tests/transforms/0082-repeat-fields-current-when-indicated.hpl
b/integration-tests/transforms/0082-repeat-fields-current-when-indicated.hpl
new file mode 100644
index 0000000000..1444cacb99
--- /dev/null
+++ b/integration-tests/transforms/0082-repeat-fields-current-when-indicated.hpl
@@ -0,0 +1,333 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+ <info>
+ <name>0082-repeat-fields-current-when-indicated</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2025/12/07 14:20:41.310</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2025/12/07 14:20:41.310</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>Data grid</from>
+ <to>Repeat Fields</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>Repeat Fields</from>
+ <to>grab results</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>grab results</from>
+ <to>result</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>Data grid</name>
+ <type>DataGrid</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <data>
+ <line>
+ <item>A</item>
+ <item>1</item>
+ <item>2025/01/01 12:00:00</item>
+ <item>Mouse</item>
+ <item>Mickey</item>
+ <item>1928/11/18</item>
+ <item/>
+ <item/>
+ </line>
+ <line>
+ <item>B</item>
+ <item>1</item>
+ <item>2025/01/01 13:00:00</item>
+ <item/>
+ <item/>
+ <item/>
+ <item>active</item>
+ <item/>
+ </line>
+ <line>
+ <item>C</item>
+ <item>1</item>
+ <item>2025/01/01 14:00:00</item>
+ <item/>
+ <item/>
+ <item/>
+ <item/>
+ <item>positive</item>
+ </line>
+ <line>
+ <item>A</item>
+ <item>2</item>
+ <item>2025/01/01 08:00:00</item>
+ <item>Duck</item>
+ <item>Daffy</item>
+ <item>1937/04/17</item>
+ <item/>
+ <item/>
+ </line>
+ <line>
+ <item>B</item>
+ <item>2</item>
+ <item>2025/01/01 09:00:00</item>
+ <item/>
+ <item/>
+ <item/>
+ <item>inactive</item>
+ <item/>
+ </line>
+ <line>
+ <item>C</item>
+ <item>2</item>
+ <item>2025/01/01 10:00:00</item>
+ <item/>
+ <item/>
+ <item/>
+ <item/>
+ <item>negative</item>
+ </line>
+ </data>
+ <fields>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>source</name>
+ <type>String</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>id</name>
+ <type>Integer</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>timestamp</name>
+ <format>yyyy/MM/dd HH:mm:ss</format>
+ <type>Date</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>lastname</name>
+ <type>String</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>firstname</name>
+ <type>String</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>birthdate</name>
+ <format>yyyy/MM/dd</format>
+ <type>Date</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>status</name>
+ <type>String</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>indicator</name>
+ <type>String</type>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>96</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Repeat Fields</name>
+ <type>RepeatFields</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <group_fields>
+ <group_field>id</group_field>
+ </group_fields>
+ <repeats>
+ <field>
+ <indicator_field_name>source</indicator_field_name>
+ <indicator_value>A</indicator_value>
+ <source_field>lastname</source_field>
+ <target_field>rLast</target_field>
+ <type>current_when_indicated</type>
+ </field>
+ <field>
+ <indicator_field_name>source</indicator_field_name>
+ <indicator_value>A</indicator_value>
+ <source_field>firstname</source_field>
+ <target_field>rFirst</target_field>
+ <type>current_when_indicated</type>
+ </field>
+ <field>
+ <indicator_field_name>source</indicator_field_name>
+ <indicator_value>A</indicator_value>
+ <source_field>birthdate</source_field>
+ <target_field>rBirthdate</target_field>
+ <type>current_when_indicated</type>
+ </field>
+ <field>
+ <indicator_field_name>source</indicator_field_name>
+ <indicator_value>B</indicator_value>
+ <source_field>status</source_field>
+ <target_field>rStatus</target_field>
+ <type>current_when_indicated</type>
+ </field>
+ <field>
+ <indicator_field_name>source</indicator_field_name>
+ <indicator_value>C</indicator_value>
+ <source_field>indicator</source_field>
+ <target_field>rIndicator</target_field>
+ <type>current_when_indicated</type>
+ </field>
+ </repeats>
+ <attributes/>
+ <GUI>
+ <xloc>208</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>grab results</name>
+ <type>SelectValues</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <length>-2</length>
+ <name>source</name>
+ <precision>-2</precision>
+ </field>
+ <field>
+ <length>-2</length>
+ <name>id</name>
+ <precision>-2</precision>
+ </field>
+ <field>
+ <length>-2</length>
+ <name>timestamp</name>
+ <precision>-2</precision>
+ </field>
+ <field>
+ <length>-2</length>
+ <name>rLast</name>
+ <precision>-2</precision>
+ </field>
+ <field>
+ <length>-2</length>
+ <name>rFirst</name>
+ <precision>-2</precision>
+ </field>
+ <field>
+ <length>-2</length>
+ <name>rBirthdate</name>
+ <precision>-2</precision>
+ </field>
+ <field>
+ <length>-2</length>
+ <name>rStatus</name>
+ <precision>-2</precision>
+ </field>
+ <field>
+ <length>-2</length>
+ <name>rIndicator</name>
+ <precision>-2</precision>
+ </field>
+ <select_unspecified>N</select_unspecified>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>320</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>result</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>432</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/integration-tests/transforms/0082-repeat-fields-mdi-main.hpl
b/integration-tests/transforms/0082-repeat-fields-mdi-main.hpl
new file mode 100644
index 0000000000..72ab0ecddb
--- /dev/null
+++ b/integration-tests/transforms/0082-repeat-fields-mdi-main.hpl
@@ -0,0 +1,319 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+ <info>
+ <name>0082-repeat-fields-mdi-main</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2025/12/07 14:55:57.333</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2025/12/07 14:55:57.333</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>groups</from>
+ <to>ETL metadata injection</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>repeats</from>
+ <to>ETL metadata injection</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>ETL metadata injection</from>
+ <to>Results</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>ETL metadata injection</name>
+ <type>MetaInject</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <filename>${PROJECT_HOME}/0082-repeat-fields-mdi.hpl</filename>
+ <run_configuration>local</run_configuration>
+ <source_transform>result</source_transform>
+ <source_output_fields>
+ <source_output_field>
+ <source_output_field_name>source</source_output_field_name>
+ <source_output_field_type>String</source_output_field_type>
+ <source_output_field_length>-1</source_output_field_length>
+ <source_output_field_precision>-1</source_output_field_precision>
+ </source_output_field>
+ <source_output_field>
+ <source_output_field_name>id</source_output_field_name>
+ <source_output_field_type>Integer</source_output_field_type>
+ <source_output_field_length>-1</source_output_field_length>
+ <source_output_field_precision>-1</source_output_field_precision>
+ </source_output_field>
+ <source_output_field>
+ <source_output_field_name>timestamp</source_output_field_name>
+ <source_output_field_type>Date</source_output_field_type>
+ <source_output_field_length>-1</source_output_field_length>
+ <source_output_field_precision>-1</source_output_field_precision>
+ </source_output_field>
+ <source_output_field>
+ <source_output_field_name>rLast</source_output_field_name>
+ <source_output_field_type>String</source_output_field_type>
+ <source_output_field_length>-1</source_output_field_length>
+ <source_output_field_precision>-1</source_output_field_precision>
+ </source_output_field>
+ <source_output_field>
+ <source_output_field_name>rFirst</source_output_field_name>
+ <source_output_field_type>String</source_output_field_type>
+ <source_output_field_length>-1</source_output_field_length>
+ <source_output_field_precision>-1</source_output_field_precision>
+ </source_output_field>
+ <source_output_field>
+ <source_output_field_name>rBirthdate</source_output_field_name>
+ <source_output_field_type>Date</source_output_field_type>
+ <source_output_field_length>-1</source_output_field_length>
+ <source_output_field_precision>-1</source_output_field_precision>
+ </source_output_field>
+ <source_output_field>
+ <source_output_field_name>rStatus</source_output_field_name>
+ <source_output_field_type>String</source_output_field_type>
+ <source_output_field_length>-1</source_output_field_length>
+ <source_output_field_precision>-1</source_output_field_precision>
+ </source_output_field>
+ <source_output_field>
+ <source_output_field_name>rIndicator</source_output_field_name>
+ <source_output_field_type>String</source_output_field_type>
+ <source_output_field_length>-1</source_output_field_length>
+ <source_output_field_precision>-1</source_output_field_precision>
+ </source_output_field>
+ </source_output_fields>
+ <target_file/>
+ <create_parent_folder>Y</create_parent_folder>
+ <no_execution>N</no_execution>
+ <allow_empty_stream_on_execution>N</allow_empty_stream_on_execution>
+ <stream_source_transform/>
+ <stream_target_transform/>
+ <mappings>
+ <mapping>
+ <target_transform_name>Repeat Fields</target_transform_name>
+ <target_attribute_key>source_field</target_attribute_key>
+ <target_detail>Y</target_detail>
+ <source_transform>repeats</source_transform>
+ <source_field>source</source_field>
+ </mapping>
+ <mapping>
+ <target_transform_name>Repeat Fields</target_transform_name>
+ <target_attribute_key>indicator_field_name</target_attribute_key>
+ <target_detail>Y</target_detail>
+ <source_transform>repeats</source_transform>
+ <source_field>indicatorField</source_field>
+ </mapping>
+ <mapping>
+ <target_transform_name>Repeat Fields</target_transform_name>
+ <target_attribute_key>GROUP_FIELD</target_attribute_key>
+ <target_detail>Y</target_detail>
+ <source_transform>groups</source_transform>
+ <source_field>group</source_field>
+ </mapping>
+ <mapping>
+ <target_transform_name>Repeat Fields</target_transform_name>
+ <target_attribute_key>target_field</target_attribute_key>
+ <target_detail>Y</target_detail>
+ <source_transform>repeats</source_transform>
+ <source_field>target</source_field>
+ </mapping>
+ <mapping>
+ <target_transform_name>Repeat Fields</target_transform_name>
+ <target_attribute_key>type</target_attribute_key>
+ <target_detail>Y</target_detail>
+ <source_transform>repeats</source_transform>
+ <source_field>type</source_field>
+ </mapping>
+ <mapping>
+ <target_transform_name>Repeat Fields</target_transform_name>
+ <target_attribute_key>indicator_value</target_attribute_key>
+ <target_detail>Y</target_detail>
+ <source_transform>repeats</source_transform>
+ <source_field>indicatorValue</source_field>
+ </mapping>
+ </mappings>
+ <attributes/>
+ <GUI>
+ <xloc>240</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Results</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>400</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>groups</name>
+ <type>DataGrid</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <data>
+ <line>
+ <item>id</item>
+ </line>
+ </data>
+ <fields>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>group</name>
+ <type>String</type>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>96</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>repeats</name>
+ <type>DataGrid</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <data>
+ <line>
+ <item>current_when_indicated</item>
+ <item>lastname</item>
+ <item>rLast</item>
+ <item>source</item>
+ <item>A</item>
+ </line>
+ <line>
+ <item>current_when_indicated</item>
+ <item>firstname</item>
+ <item>rFirst</item>
+ <item>source</item>
+ <item>A</item>
+ </line>
+ <line>
+ <item>current_when_indicated</item>
+ <item>birthdate</item>
+ <item>rBirthdate</item>
+ <item>source</item>
+ <item>A</item>
+ </line>
+ <line>
+ <item>current_when_indicated</item>
+ <item>status</item>
+ <item>rStatus</item>
+ <item>source</item>
+ <item>B</item>
+ </line>
+ <line>
+ <item>current_when_indicated</item>
+ <item>indicator</item>
+ <item>rIndicator</item>
+ <item>source</item>
+ <item>C</item>
+ </line>
+ </data>
+ <fields>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>type</name>
+ <type>String</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>source</name>
+ <type>String</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>target</name>
+ <type>String</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>indicatorField</name>
+ <type>String</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>indicatorValue</name>
+ <type>String</type>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>96</xloc>
+ <yloc>208</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/integration-tests/transforms/0082-repeat-fields-mdi.hpl
b/integration-tests/transforms/0082-repeat-fields-mdi.hpl
new file mode 100644
index 0000000000..d54495453c
--- /dev/null
+++ b/integration-tests/transforms/0082-repeat-fields-mdi.hpl
@@ -0,0 +1,327 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+ <info>
+ <name>0082-repeat-fields-mdi</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2025/12/07 14:20:41.310</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2025/12/07 14:20:41.310</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>Data grid</from>
+ <to>Repeat Fields</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>Repeat Fields</from>
+ <to>grab results</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>grab results</from>
+ <to>result</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>Data grid</name>
+ <type>DataGrid</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <data>
+ <line>
+ <item>A</item>
+ <item>1</item>
+ <item>2025/01/01 12:00:00</item>
+ <item>Mouse</item>
+ <item>Mickey</item>
+ <item>1928/11/18</item>
+ <item/>
+ <item/>
+ </line>
+ <line>
+ <item>B</item>
+ <item>1</item>
+ <item>2025/01/01 13:00:00</item>
+ <item/>
+ <item/>
+ <item/>
+ <item>active</item>
+ <item/>
+ </line>
+ <line>
+ <item>C</item>
+ <item>1</item>
+ <item>2025/01/01 14:00:00</item>
+ <item/>
+ <item/>
+ <item/>
+ <item/>
+ <item>positive</item>
+ </line>
+ <line>
+ <item>A</item>
+ <item>2</item>
+ <item>2025/01/01 08:00:00</item>
+ <item>Duck</item>
+ <item>Daffy</item>
+ <item>1937/04/17</item>
+ <item/>
+ <item/>
+ </line>
+ <line>
+ <item>B</item>
+ <item>2</item>
+ <item>2025/01/01 09:00:00</item>
+ <item/>
+ <item/>
+ <item/>
+ <item>inactive</item>
+ <item/>
+ </line>
+ <line>
+ <item>C</item>
+ <item>2</item>
+ <item>2025/01/01 10:00:00</item>
+ <item/>
+ <item/>
+ <item/>
+ <item/>
+ <item>negative</item>
+ </line>
+ </data>
+ <fields>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <currency/>
+ <set_empty_string>N</set_empty_string>
+ <name>source</name>
+ <format/>
+ <group/>
+ <decimal/>
+ <type>String</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <currency/>
+ <set_empty_string>N</set_empty_string>
+ <name>id</name>
+ <format/>
+ <group/>
+ <decimal/>
+ <type>Integer</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <currency/>
+ <set_empty_string>N</set_empty_string>
+ <name>timestamp</name>
+ <format>yyyy/MM/dd HH:mm:ss</format>
+ <group/>
+ <decimal/>
+ <type>Date</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <currency/>
+ <set_empty_string>N</set_empty_string>
+ <name>lastname</name>
+ <format/>
+ <group/>
+ <decimal/>
+ <type>String</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <currency/>
+ <set_empty_string>N</set_empty_string>
+ <name>firstname</name>
+ <format/>
+ <group/>
+ <decimal/>
+ <type>String</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <currency/>
+ <set_empty_string>N</set_empty_string>
+ <name>birthdate</name>
+ <format>yyyy/MM/dd</format>
+ <group/>
+ <decimal/>
+ <type>Date</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <currency/>
+ <set_empty_string>N</set_empty_string>
+ <name>status</name>
+ <format/>
+ <group/>
+ <decimal/>
+ <type>String</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <currency/>
+ <set_empty_string>N</set_empty_string>
+ <name>indicator</name>
+ <format/>
+ <group/>
+ <decimal/>
+ <type>String</type>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>96</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Repeat Fields</name>
+ <type>RepeatFields</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <group_fields>
+</group_fields>
+ <repeats>
+</repeats>
+ <attributes/>
+ <GUI>
+ <xloc>208</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>grab results</name>
+ <type>SelectValues</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <length>-2</length>
+ <name>source</name>
+ <precision>-2</precision>
+ </field>
+ <field>
+ <length>-2</length>
+ <name>id</name>
+ <precision>-2</precision>
+ </field>
+ <field>
+ <length>-2</length>
+ <name>timestamp</name>
+ <precision>-2</precision>
+ </field>
+ <field>
+ <length>-2</length>
+ <name>rLast</name>
+ <precision>-2</precision>
+ </field>
+ <field>
+ <length>-2</length>
+ <name>rFirst</name>
+ <precision>-2</precision>
+ </field>
+ <field>
+ <length>-2</length>
+ <name>rBirthdate</name>
+ <precision>-2</precision>
+ </field>
+ <field>
+ <length>-2</length>
+ <name>rStatus</name>
+ <precision>-2</precision>
+ </field>
+ <field>
+ <length>-2</length>
+ <name>rIndicator</name>
+ <precision>-2</precision>
+ </field>
+ <select_unspecified>N</select_unspecified>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>320</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>result</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>432</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git
a/integration-tests/transforms/0082-repeat-fields-previous-when-null.hpl
b/integration-tests/transforms/0082-repeat-fields-previous-when-null.hpl
new file mode 100644
index 0000000000..608e004669
--- /dev/null
+++ b/integration-tests/transforms/0082-repeat-fields-previous-when-null.hpl
@@ -0,0 +1,192 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+ <info>
+ <name>0081-repeat-fields-previous-when-null</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2025/12/07 14:17:55.746</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2025/12/07 14:17:55.746</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>Repeat Fields</from>
+ <to>Result</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>source</from>
+ <to>Repeat Fields</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>Repeat Fields</name>
+ <type>RepeatFields</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <group_fields>
+ <group_field>id</group_field>
+ </group_fields>
+ <repeats>
+ <field>
+ <source_field>value1</source_field>
+ <target_field>v1</target_field>
+ <type>previous_when_null</type>
+ </field>
+ <field>
+ <source_field>value2</source_field>
+ <target_field>v2</target_field>
+ <type>previous_when_null</type>
+ </field>
+ </repeats>
+ <attributes/>
+ <GUI>
+ <xloc>208</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Result</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>336</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>source</name>
+ <type>DataGrid</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <data>
+ <line>
+ <item>1</item>
+ <item>v111</item>
+ <item>v121</item>
+ </line>
+ <line>
+ <item>1</item>
+ <item/>
+ <item>v122</item>
+ </line>
+ <line>
+ <item>1</item>
+ <item>v111</item>
+ <item/>
+ </line>
+ <line>
+ <item>2</item>
+ <item>v212</item>
+ <item>v222</item>
+ </line>
+ <line>
+ <item>2</item>
+ <item/>
+ <item/>
+ </line>
+ <line>
+ <item>2</item>
+ <item/>
+ <item>v222</item>
+ </line>
+ <line>
+ <item>3</item>
+ <item>v313</item>
+ <item>v323</item>
+ </line>
+ <line>
+ <item>3</item>
+ <item/>
+ <item/>
+ </line>
+ <line>
+ <item>3</item>
+ <item>v313</item>
+ <item/>
+ </line>
+ </data>
+ <fields>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>id</name>
+ <format>0</format>
+ <type>Integer</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>value1</name>
+ <type>String</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>value2</name>
+ <type>String</type>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>96</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/integration-tests/transforms/0082-repeat-fields-previous.hpl
b/integration-tests/transforms/0082-repeat-fields-previous.hpl
new file mode 100644
index 0000000000..e13c115053
--- /dev/null
+++ b/integration-tests/transforms/0082-repeat-fields-previous.hpl
@@ -0,0 +1,192 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+ <info>
+ <name>0081-repeat-fields-previous</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2025/12/07 14:10:54.470</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2025/12/07 14:10:54.470</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>source</from>
+ <to>Repeat Fields</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>Repeat Fields</from>
+ <to>Result</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>Repeat Fields</name>
+ <type>RepeatFields</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <group_fields>
+ <group_field>id</group_field>
+ </group_fields>
+ <repeats>
+ <field>
+ <source_field>value1</source_field>
+ <target_field>v1</target_field>
+ <type>previous</type>
+ </field>
+ <field>
+ <source_field>value2</source_field>
+ <target_field>v2</target_field>
+ <type>previous</type>
+ </field>
+ </repeats>
+ <attributes/>
+ <GUI>
+ <xloc>208</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Result</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>336</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>source</name>
+ <type>DataGrid</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <data>
+ <line>
+ <item>1</item>
+ <item>v111</item>
+ <item>v121</item>
+ </line>
+ <line>
+ <item>1</item>
+ <item>v112</item>
+ <item>v122</item>
+ </line>
+ <line>
+ <item>1</item>
+ <item>v111</item>
+ <item>v121</item>
+ </line>
+ <line>
+ <item>2</item>
+ <item>v212</item>
+ <item>v222</item>
+ </line>
+ <line>
+ <item>2</item>
+ <item>v213</item>
+ <item>v223</item>
+ </line>
+ <line>
+ <item>2</item>
+ <item>v212</item>
+ <item>v222</item>
+ </line>
+ <line>
+ <item>3</item>
+ <item>v313</item>
+ <item>v323</item>
+ </line>
+ <line>
+ <item>3</item>
+ <item>v313</item>
+ <item>v323</item>
+ </line>
+ <line>
+ <item>3</item>
+ <item>v313</item>
+ <item>v323</item>
+ </line>
+ </data>
+ <fields>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>id</name>
+ <format>0</format>
+ <type>Integer</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>value1</name>
+ <type>String</type>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>value2</name>
+ <type>String</type>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>96</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git
a/integration-tests/transforms/datasets/golden-repeat-fields-current-when-indicated.csv
b/integration-tests/transforms/datasets/golden-repeat-fields-current-when-indicated.csv
new file mode 100644
index 0000000000..b4c85d78ab
--- /dev/null
+++
b/integration-tests/transforms/datasets/golden-repeat-fields-current-when-indicated.csv
@@ -0,0 +1,7 @@
+source,id,timestamp,rLast,rFirst,rBirthdate,rStatus,rIndicator
+A,1,2025/01/01 12:00:00,Mouse,Mickey,1928/11/18,,
+B,1,2025/01/01 13:00:00,Mouse,Mickey,1928/11/18,active,
+C,1,2025/01/01 14:00:00,Mouse,Mickey,1928/11/18,active,positive
+A,2,2025/01/01 08:00:00,Duck,Daffy,1937/04/17,,
+B,2,2025/01/01 09:00:00,Duck,Daffy,1937/04/17,inactive,
+C,2,2025/01/01 10:00:00,Duck,Daffy,1937/04/17,inactive,negative
diff --git
a/integration-tests/transforms/datasets/golden-repeat-fields-previous.csv
b/integration-tests/transforms/datasets/golden-repeat-fields-previous.csv
new file mode 100644
index 0000000000..775e1fed96
--- /dev/null
+++ b/integration-tests/transforms/datasets/golden-repeat-fields-previous.csv
@@ -0,0 +1,10 @@
+id,value1,value2,v1,v2
+1,v111,v121,v111,v121
+1,v112,v122,v111,v121
+1,v111,v121,v111,v121
+2,v212,v222,v212,v222
+2,v213,v223,v212,v222
+2,v212,v222,v212,v222
+3,v313,v323,v313,v323
+3,v313,v323,v313,v323
+3,v313,v323,v313,v323
diff --git
a/integration-tests/transforms/datasets/golden-repeat-fields-when-null.csv
b/integration-tests/transforms/datasets/golden-repeat-fields-when-null.csv
new file mode 100644
index 0000000000..075d533020
--- /dev/null
+++ b/integration-tests/transforms/datasets/golden-repeat-fields-when-null.csv
@@ -0,0 +1,10 @@
+id,value1,value2,v1,v2
+1,v111,v121,v111,v121
+1,,v122,v111,v122
+1,v111,,v111,v122
+2,v212,v222,v212,v222
+2,,,v212,v222
+2,,v222,v212,v222
+3,v313,v323,v313,v323
+3,,,v313,v323
+3,v313,,v313,v323
diff --git a/integration-tests/transforms/main-0082-repeat-fields.hwf
b/integration-tests/transforms/main-0082-repeat-fields.hwf
new file mode 100644
index 0000000000..7a1006d2dc
--- /dev/null
+++ b/integration-tests/transforms/main-0082-repeat-fields.hwf
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<workflow>
+ <name>main-0082-repeat-fields</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <workflow_version/>
+ <created_user>-</created_user>
+ <created_date>2025/12/07 14:10:31.342</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2025/12/07 14:10:31.342</modified_date>
+ <parameters>
+ </parameters>
+ <actions>
+ <action>
+ <name>Start</name>
+ <description/>
+ <type>SPECIAL</type>
+ <attributes/>
+ <DayOfMonth>1</DayOfMonth>
+ <doNotWaitOnFirstExecution>N</doNotWaitOnFirstExecution>
+ <hour>12</hour>
+ <intervalMinutes>60</intervalMinutes>
+ <intervalSeconds>0</intervalSeconds>
+ <minutes>0</minutes>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <weekDay>1</weekDay>
+ <parallel>N</parallel>
+ <xloc>80</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>Run 0082 tests</name>
+ <description/>
+ <type>RunPipelineTests</type>
+ <attributes/>
+ <test_names>
+ <test_name>
+ <name>0082-repeat-fields-current-when-indicated UNIT</name>
+ </test_name>
+ <test_name>
+ <name>0082-repeat-fields-previous UNIT</name>
+ </test_name>
+ <test_name>
+ <name>0082-repeat-fields-previous-when-null UNIT</name>
+ </test_name>
+ <test_name>
+ <name>0082-repeat-fields-mdi-main UNIT</name>
+ </test_name>
+ </test_names>
+ <parallel>N</parallel>
+ <xloc>256</xloc>
+ <yloc>64</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>Run 0082 tests</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
diff --git
a/integration-tests/transforms/metadata/dataset/golden-repeat-fields-current-when-indicated.json
b/integration-tests/transforms/metadata/dataset/golden-repeat-fields-current-when-indicated.json
new file mode 100644
index 0000000000..d11028a787
--- /dev/null
+++
b/integration-tests/transforms/metadata/dataset/golden-repeat-fields-current-when-indicated.json
@@ -0,0 +1,72 @@
+{
+ "base_filename": "golden-repeat-fields-current-when-indicated.csv",
+ "name": "golden-repeat-fields-current-when-indicated",
+ "description": "",
+ "dataset_fields": [
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "source",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 5,
+ "field_precision": 0,
+ "field_name": "id",
+ "field_format": "####0;-####0"
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 3,
+ "field_precision": -1,
+ "field_name": "timestamp",
+ "field_format": "yyyy/MM/dd HH:mm:ss"
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "rLast",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "rFirst",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 3,
+ "field_precision": -1,
+ "field_name": "rBirthdate",
+ "field_format": "yyyy/MM/dd"
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "rStatus",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "rIndicator",
+ "field_format": ""
+ }
+ ],
+ "folder_name": ""
+}
\ No newline at end of file
diff --git
a/integration-tests/transforms/metadata/dataset/golden-repeat-fields-previous.json
b/integration-tests/transforms/metadata/dataset/golden-repeat-fields-previous.json
new file mode 100644
index 0000000000..b73ace9b32
--- /dev/null
+++
b/integration-tests/transforms/metadata/dataset/golden-repeat-fields-previous.json
@@ -0,0 +1,48 @@
+{
+ "base_filename": "golden-repeat-fields-previous.csv",
+ "name": "golden-repeat-fields-previous",
+ "description": "",
+ "dataset_fields": [
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 5,
+ "field_precision": 0,
+ "field_name": "id",
+ "field_format": "0"
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "value1",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "value2",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "v1",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "v2",
+ "field_format": ""
+ }
+ ],
+ "folder_name": ""
+}
\ No newline at end of file
diff --git
a/integration-tests/transforms/metadata/dataset/golden-repeat-fields-when-null.json
b/integration-tests/transforms/metadata/dataset/golden-repeat-fields-when-null.json
new file mode 100644
index 0000000000..070f91877f
--- /dev/null
+++
b/integration-tests/transforms/metadata/dataset/golden-repeat-fields-when-null.json
@@ -0,0 +1,48 @@
+{
+ "base_filename": "golden-repeat-fields-when-null.csv",
+ "name": "golden-repeat-fields-when-null",
+ "description": "",
+ "dataset_fields": [
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 5,
+ "field_precision": 0,
+ "field_name": "id",
+ "field_format": "0"
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "value1",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "value2",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "v1",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "v2",
+ "field_format": ""
+ }
+ ],
+ "folder_name": ""
+}
\ No newline at end of file
diff --git
a/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-current-when-indicated
UNIT.json
b/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-current-when-indicated
UNIT.json
new file mode 100644
index 0000000000..06b991f497
--- /dev/null
+++
b/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-current-when-indicated
UNIT.json
@@ -0,0 +1,63 @@
+{
+ "database_replacements": [],
+ "autoOpening": true,
+ "description": "",
+ "persist_filename": "",
+ "test_type": "UNIT_TEST",
+ "variableValues": [],
+ "basePath": "",
+ "golden_data_sets": [
+ {
+ "field_mappings": [
+ {
+ "transform_field": "source",
+ "data_set_field": "source"
+ },
+ {
+ "transform_field": "id",
+ "data_set_field": "id"
+ },
+ {
+ "transform_field": "timestamp",
+ "data_set_field": "timestamp"
+ },
+ {
+ "transform_field": "rLast",
+ "data_set_field": "rLast"
+ },
+ {
+ "transform_field": "rFirst",
+ "data_set_field": "rFirst"
+ },
+ {
+ "transform_field": "rBirthdate",
+ "data_set_field": "rBirthdate"
+ },
+ {
+ "transform_field": "rStatus",
+ "data_set_field": "rStatus"
+ },
+ {
+ "transform_field": "rIndicator",
+ "data_set_field": "rIndicator"
+ }
+ ],
+ "field_order": [
+ "source",
+ "id",
+ "timestamp",
+ "rLast",
+ "rFirst",
+ "rBirthdate",
+ "rStatus",
+ "rIndicator"
+ ],
+ "data_set_name": "golden-repeat-fields-current-when-indicated",
+ "transform_name": "result"
+ }
+ ],
+ "input_data_sets": [],
+ "name": "0082-repeat-fields-current-when-indicated UNIT",
+ "trans_test_tweaks": [],
+ "pipeline_filename": "./0082-repeat-fields-current-when-indicated.hpl"
+}
\ No newline at end of file
diff --git
a/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-mdi-main
UNIT.json
b/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-mdi-main
UNIT.json
new file mode 100644
index 0000000000..9df5505843
--- /dev/null
+++
b/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-mdi-main
UNIT.json
@@ -0,0 +1,58 @@
+{
+ "database_replacements": [],
+ "autoOpening": true,
+ "description": "",
+ "persist_filename": "",
+ "test_type": "UNIT_TEST",
+ "variableValues": [],
+ "basePath": "",
+ "golden_data_sets": [
+ {
+ "field_mappings": [
+ {
+ "transform_field": "source",
+ "data_set_field": "source"
+ },
+ {
+ "transform_field": "id",
+ "data_set_field": "id"
+ },
+ {
+ "transform_field": "timestamp",
+ "data_set_field": "timestamp"
+ },
+ {
+ "transform_field": "rLast",
+ "data_set_field": "rLast"
+ },
+ {
+ "transform_field": "rFirst",
+ "data_set_field": "rFirst"
+ },
+ {
+ "transform_field": "rBirthdate",
+ "data_set_field": "rBirthdate"
+ },
+ {
+ "transform_field": "rStatus",
+ "data_set_field": "rStatus"
+ },
+ {
+ "transform_field": "rIndicator",
+ "data_set_field": "rIndicator"
+ }
+ ],
+ "field_order": [
+ "id",
+ "timestamp",
+ "source"
+ ],
+ "data_set_name": "golden-repeat-fields-current-when-indicated",
+ "transform_name": "Results"
+ }
+ ],
+ "input_data_sets": [],
+ "name": "0082-repeat-fields-mdi-main UNIT",
+ "trans_test_tweaks": [],
+ "pipeline_filename": "./0082-repeat-fields-mdi-main.hpl"
+}
\ No newline at end of file
diff --git
a/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-previous
UNIT.json
b/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-previous
UNIT.json
new file mode 100644
index 0000000000..bf5dab4bb6
--- /dev/null
+++
b/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-previous
UNIT.json
@@ -0,0 +1,48 @@
+{
+ "database_replacements": [],
+ "autoOpening": true,
+ "description": "",
+ "persist_filename": "",
+ "test_type": "UNIT_TEST",
+ "variableValues": [],
+ "basePath": "",
+ "golden_data_sets": [
+ {
+ "field_mappings": [
+ {
+ "transform_field": "id",
+ "data_set_field": "id"
+ },
+ {
+ "transform_field": "value1",
+ "data_set_field": "value1"
+ },
+ {
+ "transform_field": "value2",
+ "data_set_field": "value2"
+ },
+ {
+ "transform_field": "v1",
+ "data_set_field": "v1"
+ },
+ {
+ "transform_field": "v2",
+ "data_set_field": "v2"
+ }
+ ],
+ "field_order": [
+ "id",
+ "value1",
+ "value2",
+ "v1",
+ "v2"
+ ],
+ "data_set_name": "golden-repeat-fields-previous",
+ "transform_name": "Result"
+ }
+ ],
+ "input_data_sets": [],
+ "name": "0082-repeat-fields-previous UNIT",
+ "trans_test_tweaks": [],
+ "pipeline_filename": "./0082-repeat-fields-previous.hpl"
+}
\ No newline at end of file
diff --git
a/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-previous-when-null
UNIT.json
b/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-previous-when-null
UNIT.json
new file mode 100644
index 0000000000..e074f4150e
--- /dev/null
+++
b/integration-tests/transforms/metadata/unit-test/0082-repeat-fields-previous-when-null
UNIT.json
@@ -0,0 +1,48 @@
+{
+ "database_replacements": [],
+ "autoOpening": true,
+ "description": "",
+ "persist_filename": "",
+ "test_type": "UNIT_TEST",
+ "variableValues": [],
+ "basePath": "",
+ "golden_data_sets": [
+ {
+ "field_mappings": [
+ {
+ "transform_field": "id",
+ "data_set_field": "id"
+ },
+ {
+ "transform_field": "value1",
+ "data_set_field": "value1"
+ },
+ {
+ "transform_field": "value2",
+ "data_set_field": "value2"
+ },
+ {
+ "transform_field": "v1",
+ "data_set_field": "v1"
+ },
+ {
+ "transform_field": "v2",
+ "data_set_field": "v2"
+ }
+ ],
+ "field_order": [
+ "id",
+ "value1",
+ "value2",
+ "v1",
+ "v2"
+ ],
+ "data_set_name": "golden-repeat-fields-when-null",
+ "transform_name": "Result"
+ }
+ ],
+ "input_data_sets": [],
+ "name": "0082-repeat-fields-previous-when-null UNIT",
+ "trans_test_tweaks": [],
+ "pipeline_filename": "./0082-repeat-fields-previous-when-null.hpl"
+}
\ No newline at end of file
diff --git a/plugins/transforms/pom.xml b/plugins/transforms/pom.xml
index 6682aa5496..68cf25b8a4 100644
--- a/plugins/transforms/pom.xml
+++ b/plugins/transforms/pom.xml
@@ -124,6 +124,7 @@
<module>propertyoutput</module>
<module>randomvalue</module>
<module>regexeval</module>
+ <module>repeatfields</module>
<module>replacestring</module>
<module>reservoirsampling</module>
<module>rest</module>
diff --git a/plugins/transforms/repeatfields/pom.xml
b/plugins/transforms/repeatfields/pom.xml
new file mode 100644
index 0000000000..2ee361d3d5
--- /dev/null
+++ b/plugins/transforms/repeatfields/pom.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ ~
+ -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.hop</groupId>
+ <artifactId>hop-plugins-transforms</artifactId>
+ <version>2.17.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>hop-transform-repeatfields</artifactId>
+ <packaging>jar</packaging>
+ <name>Hop Plugins Transforms Repeat Fields</name>
+
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.hop</groupId>
+ <artifactId>hop-libs</artifactId>
+ <version>${project.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
+ <dependencies></dependencies>
+</project>
diff --git a/plugins/transforms/repeatfields/src/assembly/assembly.xml
b/plugins/transforms/repeatfields/src/assembly/assembly.xml
new file mode 100644
index 0000000000..cf4445966b
--- /dev/null
+++ b/plugins/transforms/repeatfields/src/assembly/assembly.xml
@@ -0,0 +1,50 @@
+<!--
+ ~ 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.
+ ~
+ -->
+
+<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0
http://maven.apache.org/xsd/assembly-2.2.0.xsd">
+ <id>hop-transform-repeatfields</id>
+ <formats>
+ <format>zip</format>
+ </formats>
+ <baseDirectory>.</baseDirectory>
+ <files>
+ <file>
+ <source>${project.basedir}/src/main/resources/version.xml</source>
+ <outputDirectory>plugins/transforms/repeatfields</outputDirectory>
+ <filtered>true</filtered>
+ </file>
+ </files>
+
+ <fileSets>
+ <fileSet>
+ <directory>${project.basedir}/src/main/samples</directory>
+ <outputDirectory>config/projects/samples/</outputDirectory>
+ </fileSet>
+ </fileSets>
+
+ <dependencySets>
+ <dependencySet>
+ <includes>
+
<include>org.apache.hop:hop-transform-repeatfields:jar</include>
+ </includes>
+ <outputDirectory>plugins/transforms/repeatfields</outputDirectory>
+ </dependencySet>
+ </dependencySets>
+</assembly>
\ No newline at end of file
diff --git
a/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/Repeat.java
b/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/Repeat.java
new file mode 100644
index 0000000000..66d6b3f54c
--- /dev/null
+++
b/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/Repeat.java
@@ -0,0 +1,53 @@
+/*
+ * 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.repeatfields;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import
org.apache.hop.pipeline.transforms.repeatfields.RepeatFieldsMeta.RepeatType;
+
+@Getter
+@Setter
+public class Repeat {
+ @HopMetadataProperty(storeWithCode = true)
+ private RepeatType type;
+
+ @HopMetadataProperty(key = "source_field")
+ private String sourceField;
+
+ @HopMetadataProperty(key = "target_field")
+ private String targetField;
+
+ @HopMetadataProperty(key = "indicator_field_name")
+ private String indicatorFieldName;
+
+ @HopMetadataProperty(key = "indicator_value")
+ private String indicatorValue;
+
+ public Repeat() {}
+
+ public Repeat(Repeat r) {
+ this.type = r.type;
+ this.sourceField = r.sourceField;
+ this.targetField = r.targetField;
+ this.indicatorFieldName = r.indicatorFieldName;
+ this.indicatorValue = r.indicatorValue;
+ }
+}
diff --git
a/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFields.java
b/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFields.java
new file mode 100644
index 0000000000..a333182bd6
--- /dev/null
+++
b/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFields.java
@@ -0,0 +1,186 @@
+/*
+ * 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.repeatfields;
+
+import com.google.common.primitives.Ints;
+import java.util.ArrayList;
+import org.apache.commons.lang.StringUtils;
+import org.apache.hop.core.exception.HopException;
+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.pipeline.Pipeline;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.BaseTransform;
+import org.apache.hop.pipeline.transform.TransformMeta;
+
+public class RepeatFields extends BaseTransform<RepeatFieldsMeta,
RepeatFieldsData> {
+ public RepeatFields(
+ TransformMeta transformMeta,
+ RepeatFieldsMeta meta,
+ RepeatFieldsData data,
+ int copyNr,
+ PipelineMeta pipelineMeta,
+ Pipeline pipeline) {
+ super(transformMeta, meta, data, copyNr, pipelineMeta, pipeline);
+ }
+
+ @Override
+ public boolean processRow() throws HopException {
+ Object[] r = getRow();
+ if (r == null) {
+ setOutputDone();
+ return false;
+ }
+
+ if (first) {
+ first = false;
+
+ // The output row metadata
+ data.outputRowMeta = getInputRowMeta().clone();
+ meta.getFields(data.outputRowMeta, getTransformName(), null, null, this,
metadataProvider);
+
+ data.groupIndexes = new ArrayList<>();
+ for (String groupField : meta.getGroupFields()) {
+ int index = getInputRowMeta().indexOfValue(groupField);
+ if (index < 0) {
+ throw new HopException("Unable to find group field: " + groupField);
+ }
+ data.groupIndexes.add(index);
+ }
+ data.sourceIndexes = new ArrayList<>();
+ data.indicatorIndexes = new ArrayList<>();
+ for (Repeat repeat : meta.getRepeats()) {
+ String sourceFieldName = resolve(repeat.getSourceField());
+ int index = getInputRowMeta().indexOfValue(sourceFieldName);
+ if (index < 0) {
+ throw new HopException("Unable to find source field to repeat: " +
sourceFieldName);
+ }
+ data.sourceIndexes.add(index);
+
+ int indicatorIndex = -1;
+ String indicatorFieldName = resolve(repeat.getIndicatorFieldName());
+ if (StringUtils.isNotEmpty(indicatorFieldName)) {
+ indicatorIndex = getInputRowMeta().indexOfValue(indicatorFieldName);
+ if (indicatorIndex < 0) {
+ throw new HopException("Unable to find indicator field: " +
indicatorFieldName);
+ }
+ }
+ data.indicatorIndexes.add(indicatorIndex);
+ }
+ }
+
+ // Compare the current row with the previous one. If there's a change,
start a new group
+ //
+ if (isNewGroup(r, data.previousRow)) {
+ startNewGroup();
+ }
+
+ Object[] outputRow = RowDataUtil.resizeArray(r, data.outputRowMeta.size());
+ int targetIndex = getInputRowMeta().size();
+ for (int i = 0; i < meta.getRepeats().size(); i++) {
+ Repeat repeat = meta.getRepeats().get(i);
+ int sourceIndex = data.sourceIndexes.get(i);
+ IValueMeta sourceValueMeta = getInputRowMeta().getValueMeta(sourceIndex);
+ Object sourceValue = r[sourceIndex];
+ Object targetValue = sourceValue;
+ // What do we need to do?
+ switch (repeat.getType()) {
+ case Previous -> targetValue = getPreviousValue(sourceValue,
targetIndex);
+ case PreviousWhenNull -> {
+ // If the source value is null, take the previous value.
+ if (sourceValueMeta.isNull(sourceValue)) {
+ targetValue = getPreviousValue(null, targetIndex);
+ }
+ }
+ case CurrentWhenIndicated ->
+ targetValue = getCurrentValueWhenIndicated(i, repeat, r,
sourceValue, targetIndex);
+ }
+ outputRow[targetIndex] = targetValue;
+ targetIndex++;
+ }
+
+ // Make a copy of the current row to prevent next transforms from
modifying it.
+ //
+ data.previousRow = data.outputRowMeta.cloneRow(outputRow);
+
+ // Send the output to the next transforms.
+ //
+ putRow(data.outputRowMeta, outputRow);
+
+ return true;
+ }
+
+ private Object getCurrentValueWhenIndicated(
+ int i, Repeat repeat, Object[] r, Object sourceValue, int targetIndex)
throws HopException {
+ Object targetValue;
+ int indicatorIndex = data.indicatorIndexes.get(i);
+ if (indicatorIndex < 0) {
+ throw new HopException("Unable to find indicator field: " +
repeat.getIndicatorFieldName());
+ }
+ String indicator = getInputRowMeta().getString(r, indicatorIndex);
+ if (StringUtils.isEmpty(indicator)) {
+ throw new HopException(
+ "No indicator value was found in field " +
repeat.getIndicatorFieldName());
+ }
+ if (indicator.equals(repeat.getIndicatorValue())) {
+ targetValue = sourceValue;
+ } else {
+ if (data.previousRow == null) {
+ // The first row in a data set is often the last version of an SCD2
dimension.
+ // We'll simply keep this value.
+ targetValue = sourceValue;
+ } else {
+ // We take the target value from the previous row
+ targetValue = data.previousRow[targetIndex];
+ }
+ }
+ return targetValue;
+ }
+
+ /**
+ * Take the value from the previous row target field (if there is one). The
default value (if
+ * there's no previous row) is the source value of the first row.
+ *
+ * @param sourceValue The source value (default when no previous row)
+ * @param targetIndex The target field index to read from
+ * @return The value for the previous row (or the given current source value)
+ */
+ private Object getPreviousValue(Object sourceValue, int targetIndex) {
+ Object value = sourceValue;
+ if (data.previousRow != null) {
+ value = data.previousRow[targetIndex];
+ }
+ return value;
+ }
+
+ private void startNewGroup() {
+ // There is no previous row
+ data.previousRow = null;
+ }
+
+ private boolean isNewGroup(Object[] currentRow, Object[] previousRow) throws
HopValueException {
+ if (previousRow == null) {
+ return true;
+ }
+ int compare =
+ getInputRowMeta().compare(currentRow, previousRow,
Ints.toArray(data.groupIndexes));
+ return compare != 0;
+ }
+}
diff --git
a/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFieldsData.java
b/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFieldsData.java
new file mode 100644
index 0000000000..214ec8cfd9
--- /dev/null
+++
b/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFieldsData.java
@@ -0,0 +1,32 @@
+/*
+ * 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.repeatfields;
+
+import java.util.List;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.pipeline.transform.BaseTransformData;
+import org.apache.hop.pipeline.transform.ITransformData;
+
+public class RepeatFieldsData extends BaseTransformData implements
ITransformData {
+ public List<Integer> groupIndexes;
+ public List<Integer> sourceIndexes;
+ public List<Integer> indicatorIndexes;
+ public IRowMeta outputRowMeta;
+ public Object[] previousRow;
+}
diff --git
a/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFieldsDialog.java
b/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFieldsDialog.java
new file mode 100644
index 0000000000..71d6ab2712
--- /dev/null
+++
b/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFieldsDialog.java
@@ -0,0 +1,385 @@
+/*
+ * 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.repeatfields;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.core.variables.IVariables;
+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.PropsUi;
+import org.apache.hop.ui.core.dialog.BaseDialog;
+import org.apache.hop.ui.core.dialog.ErrorDialog;
+import org.apache.hop.ui.core.dialog.MessageDialogWithToggle;
+import org.apache.hop.ui.core.widget.ColumnInfo;
+import org.apache.hop.ui.core.widget.TableView;
+import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
+import org.eclipse.swt.SWT;
+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.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.TableItem;
+import org.eclipse.swt.widgets.Text;
+
+public class RepeatFieldsDialog extends BaseTransformDialog {
+ private static final Class<?> PKG = RepeatFields.class;
+
+ public static final String STRING_SORT_WARNING_PARAMETER =
"RepeatFieldsWarning";
+
+ private ColumnInfo[] ciGroup;
+ private TableView wGroup;
+
+ private ColumnInfo[] ciRepeats;
+ private TableView wRepeats;
+
+ private final RepeatFieldsMeta input;
+ private final List<String> inputFields = new ArrayList<>();
+
+ public RepeatFieldsDialog(
+ Shell parent,
+ IVariables variables,
+ RepeatFieldsMeta transformMeta,
+ PipelineMeta pipelineMeta) {
+ super(parent, variables, transformMeta, pipelineMeta);
+ input = transformMeta;
+ }
+
+ @Override
+ public String open() {
+ Shell parent = getParent();
+
+ shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.RESIZE | SWT.MAX |
SWT.MIN);
+ PropsUi.setLook(shell);
+ setShellImage(shell, input);
+
+ FormLayout formLayout = new FormLayout();
+ formLayout.marginWidth = PropsUi.getFormMargin();
+ formLayout.marginHeight = PropsUi.getFormMargin();
+
+ shell.setLayout(formLayout);
+ shell.setText(BaseMessages.getString(PKG,
"RepeatFieldsDialog.Shell.Title"));
+
+ int middle = props.getMiddlePct();
+ int margin = PropsUi.getMargin();
+
+ // The buttons go at the bottom
+ //
+ // THE BUTTONS
+ wOk = new Button(shell, SWT.PUSH);
+ wOk.setText(BaseMessages.getString(PKG, "System.Button.OK"));
+ wOk.addListener(SWT.Selection, e -> ok());
+ wCancel = new Button(shell, SWT.PUSH);
+ wCancel.setText(BaseMessages.getString(PKG, "System.Button.Cancel"));
+ wCancel.addListener(SWT.Selection, e -> cancel());
+ setButtonPositions(new Button[] {wOk, wCancel}, margin, null);
+
+ // TransformName line
+ wlTransformName = new Label(shell, SWT.RIGHT);
+ wlTransformName.setText(BaseMessages.getString(PKG,
"RepeatFieldsDialog.TransformName.Label"));
+ PropsUi.setLook(wlTransformName);
+ fdlTransformName = new FormData();
+ fdlTransformName.left = new FormAttachment(0, 0);
+ fdlTransformName.right = new FormAttachment(middle, -margin);
+ fdlTransformName.top = new FormAttachment(0, margin);
+ wlTransformName.setLayoutData(fdlTransformName);
+ wTransformName = new Text(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+ wTransformName.setText(transformName);
+ PropsUi.setLook(wTransformName);
+ fdTransformName = new FormData();
+ fdTransformName.left = new FormAttachment(middle, 0);
+ fdTransformName.top = new FormAttachment(0, margin);
+ fdTransformName.right = new FormAttachment(100, 0);
+ wTransformName.setLayoutData(fdTransformName);
+
+ Label wlGroup = new Label(shell, SWT.NONE);
+ wlGroup.setText(BaseMessages.getString(PKG,
"RepeatFieldsDialog.Group.Label"));
+ PropsUi.setLook(wlGroup);
+ FormData fdlGroup = new FormData();
+ fdlGroup.left = new FormAttachment(0, 0);
+ fdlGroup.top = new FormAttachment(wlTransformName, 2 * margin);
+ wlGroup.setLayoutData(fdlGroup);
+
+ // The group fields
+ //
+ int groupRows = input.getGroupFields().size();
+ ciGroup =
+ new ColumnInfo[] {
+ new ColumnInfo(
+ BaseMessages.getString(PKG,
"RepeatFieldsDialog.ColumnInfo.GroupField"),
+ ColumnInfo.COLUMN_TYPE_CCOMBO,
+ new String[] {""},
+ false)
+ };
+
+ wGroup =
+ new TableView(
+ variables,
+ shell,
+ SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI | SWT.V_SCROLL |
SWT.H_SCROLL,
+ ciGroup,
+ groupRows,
+ null,
+ props);
+
+ Button wGet = new Button(shell, SWT.PUSH);
+ wGet.setText(BaseMessages.getString(PKG,
"RepeatFieldsDialog.GetFields.Button"));
+ FormData fdGet = new FormData();
+ fdGet.top = new FormAttachment(wlGroup, margin);
+ fdGet.right = new FormAttachment(100, 0);
+ wGet.setLayoutData(fdGet);
+ wGet.addListener(SWT.Selection, e -> getGroups());
+
+ FormData fdGroup = new FormData();
+ fdGroup.left = new FormAttachment(0, 0);
+ fdGroup.top = new FormAttachment(wlGroup, margin);
+ fdGroup.right = new FormAttachment(wGet, -margin);
+ fdGroup.bottom = new FormAttachment(30, 0); // 35% from the top
+ wGroup.setLayoutData(fdGroup);
+
+ // The repeat fields section
+ //
+ Label wlRepeats = new Label(shell, SWT.NONE);
+ wlRepeats.setText(BaseMessages.getString(PKG,
"RepeatFieldsDialog.RepeatFields.Label"));
+ PropsUi.setLook(wlRepeats);
+ FormData fdlRepeats = new FormData();
+ fdlRepeats.left = new FormAttachment(0, 0);
+ fdlRepeats.top = new FormAttachment(wGroup, margin);
+ wlRepeats.setLayoutData(fdlRepeats);
+
+ ciRepeats =
+ new ColumnInfo[] {
+ new ColumnInfo(
+ BaseMessages.getString(PKG,
"RepeatFieldsDialog.ColumnInfo.RepeatType"),
+ ColumnInfo.COLUMN_TYPE_CCOMBO,
+ new String[] {""},
+ false),
+ new ColumnInfo(
+ BaseMessages.getString(PKG,
"RepeatFieldsDialog.ColumnInfo.SourceFieldName"),
+ ColumnInfo.COLUMN_TYPE_CCOMBO,
+ new String[] {""},
+ false),
+ new ColumnInfo(
+ BaseMessages.getString(PKG,
"RepeatFieldsDialog.ColumnInfo.TargetFieldName"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false),
+ new ColumnInfo(
+ BaseMessages.getString(PKG,
"RepeatFieldsDialog.ColumnInfo.IndicatorFieldName"),
+ ColumnInfo.COLUMN_TYPE_CCOMBO,
+ new String[] {""},
+ false),
+ new ColumnInfo(
+ BaseMessages.getString(PKG,
"RepeatFieldsDialog.ColumnInfo.IndicatorValue"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false),
+ };
+
+ ciRepeats[0].setComboValues(RepeatFieldsMeta.RepeatType.getDescriptions());
+ ciRepeats[1].setUsingVariables(true);
+ ciRepeats[2].setUsingVariables(true);
+ ciRepeats[3].setUsingVariables(true);
+
+ wRepeats =
+ new TableView(
+ variables,
+ shell,
+ SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI | SWT.V_SCROLL |
SWT.H_SCROLL,
+ ciRepeats,
+ input.getRepeats().size(),
+ null,
+ props);
+
+ Button wGetRepeats = new Button(shell, SWT.PUSH);
+ wGetRepeats.setText(BaseMessages.getString(PKG,
"RepeatFieldsDialog.GetRepeatFields.Button"));
+ FormData fdGetRepeats = new FormData();
+ fdGetRepeats.top = new FormAttachment(wlRepeats, margin);
+ fdGetRepeats.right = new FormAttachment(100, 0);
+ wGetRepeats.setLayoutData(fdGetRepeats);
+ wGetRepeats.addListener(SWT.Selection, e -> getRepeats());
+
+ FormData fdRepeats = new FormData();
+ fdRepeats.left = new FormAttachment(0, 0);
+ fdRepeats.top = new FormAttachment(wlRepeats, margin);
+ fdRepeats.right = new FormAttachment(wGetRepeats, -margin);
+ fdRepeats.bottom = new FormAttachment(wOk, -2 * margin);
+ wRepeats.setLayoutData(fdRepeats);
+
+ final Runnable runnable =
+ () -> {
+ TransformMeta transformMeta =
pipelineMeta.findTransform(transformName);
+ if (transformMeta != null) {
+ try {
+ IRowMeta row = pipelineMeta.getPrevTransformFields(variables,
transformMeta);
+
+ // Remember these fields...
+ for (int i = 0; i < row.size(); i++) {
+ inputFields.add(row.getValueMeta(i).getName());
+ }
+ setComboBoxes();
+ } catch (HopException e) {
+ logError(BaseMessages.getString(PKG,
"System.Dialog.GetFieldsFailed.Message"));
+ }
+ }
+ };
+ new Thread(runnable).start();
+
+ getData();
+
+ BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel());
+
+ return transformName;
+ }
+
+ protected void setComboBoxes() {
+ // Something was changed in the row.
+ //
+ String[] fieldNames = ConstUi.sortFieldNames(inputFields);
+ ciGroup[0].setComboValues(fieldNames);
+ ciRepeats[1].setComboValues(fieldNames);
+ ciRepeats[3].setComboValues(fieldNames);
+ }
+
+ /** Copy information from the meta-data input to the dialog fields. */
+ public void getData() {
+ for (int i = 0; i < input.getGroupFields().size(); i++) {
+ TableItem item = wGroup.table.getItem(i);
+ item.setText(1, Const.NVL(input.getGroupFields().get(i), ""));
+ }
+
+ for (int i = 0; i < input.getRepeats().size(); i++) {
+ Repeat repeat = input.getRepeats().get(i);
+ TableItem item = wRepeats.table.getItem(i);
+ item.setText(1, repeat.getType() != null ?
repeat.getType().getDescription() : "");
+ item.setText(2, Const.NVL(repeat.getSourceField(), ""));
+ item.setText(3, Const.NVL(repeat.getTargetField(), ""));
+ item.setText(4, Const.NVL(repeat.getIndicatorFieldName(), ""));
+ item.setText(5, Const.NVL(repeat.getIndicatorValue(), ""));
+ }
+
+ wGroup.setRowNums();
+ wGroup.optWidth(true);
+ wRepeats.setRowNums();
+ wRepeats.optWidth(true);
+
+ wTransformName.selectAll();
+ wTransformName.setFocus();
+ }
+
+ private void cancel() {
+ transformName = null;
+ dispose();
+ }
+
+ private void ok() {
+ if (Utils.isEmpty(wTransformName.getText())) {
+ return;
+ }
+
+ transformName = wTransformName.getText();
+
+ getInfo(input);
+
+ showSortWarning();
+
+ dispose();
+ }
+
+ private void showSortWarning() {
+ if (!input.getGroupFields().isEmpty()
+ &&
"Y".equalsIgnoreCase(props.getCustomParameter(STRING_SORT_WARNING_PARAMETER,
"Y"))) {
+ MessageDialogWithToggle md =
+ new MessageDialogWithToggle(
+ shell,
+ BaseMessages.getString(PKG,
"RepeatFieldsDialog.SortWarningDialog.DialogTitle"),
+ BaseMessages.getString(
+ PKG,
"RepeatFieldsDialog.SortWarningDialog.DialogMessage", Const.CR)
+ + Const.CR,
+ SWT.ICON_WARNING,
+ new String[] {
+ BaseMessages.getString(PKG,
"RepeatFieldsDialog.SortWarningDialog.Option1")
+ },
+ BaseMessages.getString(PKG,
"RepeatFieldsDialog.SortWarningDialog.Option2"),
+
"N".equalsIgnoreCase(props.getCustomParameter(STRING_SORT_WARNING_PARAMETER,
"Y")));
+ md.open();
+ props.setCustomParameter(STRING_SORT_WARNING_PARAMETER,
md.getToggleState() ? "N" : "Y");
+ }
+ }
+
+ /** Copy the information in the widgets into the metadata */
+ private void getInfo(RepeatFieldsMeta meta) {
+ // The group
+ meta.getGroupFields().clear();
+ for (TableItem item : wGroup.getNonEmptyItems()) {
+ input.getGroupFields().add(item.getText(1));
+ }
+
+ // The fields to repeat
+ //
+ meta.getRepeats().clear();
+ for (TableItem item : wRepeats.getNonEmptyItems()) {
+ Repeat repeat = new Repeat();
+
repeat.setType(RepeatFieldsMeta.RepeatType.lookupDescription(item.getText(1)));
+ repeat.setSourceField(item.getText(2));
+ repeat.setTargetField(item.getText(3));
+ repeat.setIndicatorFieldName(item.getText(4));
+ repeat.setIndicatorValue(item.getText(5));
+ meta.getRepeats().add(repeat);
+ }
+ }
+
+ private void getGroups() {
+ try {
+ IRowMeta r = pipelineMeta.getPrevTransformFields(variables,
transformName);
+ if (r != null && !r.isEmpty()) {
+ BaseTransformDialog.getFieldsFromPrevious(
+ r, wGroup, 1, new int[] {1}, new int[] {}, -1, -1, null);
+ }
+ } catch (HopException ke) {
+ new ErrorDialog(
+ shell,
+ BaseMessages.getString(PKG, "System.Dialog.GetFieldsFailed.Title"),
+ BaseMessages.getString(PKG, "System.Dialog.GetFieldsFailed.Message"),
+ ke);
+ }
+ }
+
+ private void getRepeats() {
+ try {
+ IRowMeta r = pipelineMeta.getPrevTransformFields(variables,
transformName);
+ if (r != null && !r.isEmpty()) {
+ BaseTransformDialog.getFieldsFromPrevious(
+ r, wRepeats, 2, new int[] {2}, new int[] {}, -1, -1, null);
+ }
+ } catch (HopException ke) {
+ new ErrorDialog(
+ shell,
+ BaseMessages.getString(PKG, "System.Dialog.GetFieldsFailed.Title"),
+ BaseMessages.getString(PKG, "System.Dialog.GetFieldsFailed.Message"),
+ ke);
+ }
+ }
+}
diff --git
a/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFieldsMeta.java
b/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFieldsMeta.java
new file mode 100644
index 0000000000..52809d68f2
--- /dev/null
+++
b/plugins/transforms/repeatfields/src/main/java/org/apache/hop/pipeline/transforms/repeatfields/RepeatFieldsMeta.java
@@ -0,0 +1,157 @@
+/*
+ * 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.repeatfields;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.commons.lang.StringUtils;
+import org.apache.hop.core.annotations.Transform;
+import org.apache.hop.core.exception.HopTransformException;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.metadata.api.IEnumHasCodeAndDescription;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.pipeline.transform.BaseTransformMeta;
+import org.apache.hop.pipeline.transform.TransformMeta;
+
+@Transform(
+ id = "RepeatFields",
+ image = "repeatfields.svg",
+ name = "i18n::RepeatFields.Name",
+ description = "i18n::RepeatFields.Description",
+ categoryDescription =
"i18n:org.apache.hop.pipeline.transform:BaseTransform.Category.Utility",
+ keywords = "i18n::RepeatFields.keyword",
+ documentationUrl = "/pipeline/transforms/repeatfields.html")
+@Getter
+@Setter
+public class RepeatFieldsMeta extends BaseTransformMeta<RepeatFields,
RepeatFieldsData> {
+ private static final Class<?> PKG = RepeatFields.class;
+
+ @HopMetadataProperty(
+ key = "group_field",
+ groupKey = "group_fields",
+ injectionGroupKey = "GROUP_FIELDS",
+ injectionGroupDescription = "RepeatFields.Injection.GroupFields",
+ injectionKey = "GROUP_FIELD",
+ injectionKeyDescription = "RepeatFields.Injection.GroupField")
+ private List<String> groupFields;
+
+ @HopMetadataProperty(
+ key = "field",
+ groupKey = "repeats",
+ injectionGroupKey = "REPEATS",
+ injectionGroupDescription = "RepeatFields.Injection.Repeats",
+ injectionKey = "REPEAT",
+ injectionKeyDescription = "RepeatFields.Injection.Repeat")
+ private List<Repeat> repeats;
+
+ public RepeatFieldsMeta() {
+ super();
+ this.groupFields = new ArrayList<>();
+ this.repeats = new ArrayList<>();
+ }
+
+ public RepeatFieldsMeta(RepeatFieldsMeta m) {
+ this();
+ this.groupFields.addAll(m.groupFields);
+ for (Repeat repeat : m.repeats) {
+ this.repeats.add(new Repeat(repeat));
+ }
+ }
+
+ @Override
+ public Object clone() {
+ return new RepeatFieldsMeta(this);
+ }
+
+ /**
+ * We only add an optional target field containing a corrected copy of the
source field.
+ *
+ * @param inputRowMeta the input row meta that is modified in this method to
reflect the output
+ * row metadata of the transform
+ * @param name Name of the transform to use as input for the origin field in
the values
+ * @param info Fields used as extra lookup information
+ * @param nextTransform the next transform that is targeted
+ * @param variables the variables The variable variables to use to replace
variables
+ * @param metadataProvider the MetaStore to use to load additional external
data or metadata
+ * impacting the output fields
+ * @throws HopTransformException in case there is a missing source field
+ */
+ @Override
+ public void getFields(
+ IRowMeta inputRowMeta,
+ String name,
+ IRowMeta[] info,
+ TransformMeta nextTransform,
+ IVariables variables,
+ IHopMetadataProvider metadataProvider)
+ throws HopTransformException {
+ for (Repeat repeat : repeats) {
+ String targetFieldName = variables.resolve(repeat.getTargetField());
+ if (StringUtils.isNotEmpty(targetFieldName)) {
+ // We need to add a new field with the new name and the same data type
as the source.
+ String sourceFieldName = variables.resolve(repeat.getSourceField());
+ IValueMeta sourceFieldValueMeta =
inputRowMeta.searchValueMeta(sourceFieldName);
+ if (sourceFieldValueMeta == null) {
+ throw new HopTransformException(
+ "Unable to find source field "
+ + sourceFieldName
+ + " in the input of transform "
+ + name);
+ }
+ IValueMeta targetFieldValueMeta = sourceFieldValueMeta.clone();
+ targetFieldValueMeta.setName(targetFieldName);
+ targetFieldValueMeta.setOrigin(name);
+ inputRowMeta.addValueMeta(targetFieldValueMeta);
+ }
+ }
+ }
+
+ @Getter
+ public enum RepeatType implements IEnumHasCodeAndDescription {
+ Previous("previous", BaseMessages.getString(PKG,
"RepeatFields.Previous.Description")),
+ PreviousWhenNull(
+ "previous_when_null",
+ BaseMessages.getString(PKG,
"RepeatFields.PreviousWhenNull.Description")),
+ CurrentWhenIndicated(
+ "current_when_indicated",
+ BaseMessages.getString(PKG,
"RepeatFields.CurrentWhenIndicated.Description"));
+
+ private final String code;
+ private final String description;
+
+ RepeatType(String code, String description) {
+ this.code = code;
+ this.description = description;
+ }
+
+ public static RepeatType lookupDescription(String description) {
+ return IEnumHasCodeAndDescription.lookupDescription(RepeatType.class,
description, null);
+ }
+
+ public static String[] getDescriptions() {
+ return IEnumHasCodeAndDescription.getDescriptions(RepeatType.class);
+ }
+ }
+}
diff --git
a/plugins/transforms/repeatfields/src/main/resources/org/apache/hop/pipeline/transforms/repeatfields/messages/messages_en_US.properties
b/plugins/transforms/repeatfields/src/main/resources/org/apache/hop/pipeline/transforms/repeatfields/messages/messages_en_US.properties
new file mode 100644
index 0000000000..84643c30cf
--- /dev/null
+++
b/plugins/transforms/repeatfields/src/main/resources/org/apache/hop/pipeline/transforms/repeatfields/messages/messages_en_US.properties
@@ -0,0 +1,44 @@
+#
+# 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.
+#
+RepeatFields.Name=Repeat Fields
+RepeatFields.Description=Repeat field values from the previous row
+RepeatFields.keyword=Repeat,MDM
+RepeatFields.Injection.GroupFields=Group fields
+RepeatFields.Injection.GroupField=Group field
+RepeatFields.Injection.Repeats=Repeats
+RepeatFields.Injection.Repeat=Repeat
+RepeatFields.Previous.Description=Previous row
+RepeatFields.PreviousWhenNull.Description=Previous when field is null
+RepeatFields.CurrentWhenIndicated.Description=Current when indicated
+RepeatFieldsDialog.Shell.Title=Repeat Fields
+RepeatFieldsDialog.TransformName.Label=Transform name
+RepeatFieldsDialog.Group.Label=Group fields
+RepeatFieldsDialog.ColumnInfo.GroupField=Group field
+RepeatFieldsDialog.GetFields.Button=Get group fields
+RepeatFieldsDialog.RepeatFields.Label=The fields to repeat
+RepeatFieldsDialog.ColumnInfo.RepeatType=Repeat type
+RepeatFieldsDialog.ColumnInfo.SourceFieldName=Source field
+RepeatFieldsDialog.ColumnInfo.TargetFieldName=Target field
+RepeatFieldsDialog.ColumnInfo.IndicatorFieldName=Indicator field name
+RepeatFieldsDialog.ColumnInfo.IndicatorValue=Indicator value
+RepeatFieldsDialog.GetRepeatFields.Button=Get repeat fields
+RepeatFieldsDialog.SortWarningDialog.DialogMessage=If the incoming data is not
sorted on the specified group field(s), \
+ the output results may not be correct.\nWe recommend sorting the incoming
data within the pipeline, \
+ or in a source database.
+RepeatFieldsDialog.SortWarningDialog.DialogTitle=Notice
+RepeatFieldsDialog.SortWarningDialog.Option1=Close
+RepeatFieldsDialog.SortWarningDialog.Option2=Don''t show this message again.
diff --git
a/plugins/transforms/repeatfields/src/main/resources/repeatfields.svg
b/plugins/transforms/repeatfields/src/main/resources/repeatfields.svg
new file mode 100644
index 0000000000..daa970dc75
--- /dev/null
+++ b/plugins/transforms/repeatfields/src/main/resources/repeatfields.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">
+ <path fill="#0e3a5a" d="M11,17H4A2,2 0 0,1 2,15V3A2,2 0 0,1
4,1H16V3H4V15H11V13L15,16L11,19V17M19,21V7H8V13H6V7A2,2 0 0,1 8,5H19A2,2 0 0,1
21,7V21A2,2 0 0,1 19,23H8A2,2 0 0,1 6,21V19H8V21H19Z"/>
+</svg>
diff --git a/plugins/transforms/repeatfields/src/main/resources/version.xml
b/plugins/transforms/repeatfields/src/main/resources/version.xml
new file mode 100644
index 0000000000..6be576acae
--- /dev/null
+++ b/plugins/transforms/repeatfields/src/main/resources/version.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ ~
+ -->
+
+<version>${project.version}</version>
\ No newline at end of file