You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hop.apache.org by ha...@apache.org on 2026/08/15 08:24:36 UTC
(hop) branch main updated: issue #7953 : Add Multi Mapping transform (#7958)
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 9b8227aa18 issue #7953 : Add Multi Mapping transform (#7958)
9b8227aa18 is described below
commit 9b8227aa18a02dd6e1041455a5c153683b14e071
Author: Matt Casters <ma...@gmail.com>
AuthorDate: Sat Aug 15 10:24:30 2026 +0200
issue #7953 : Add Multi Mapping transform (#7958)
* issue #7953 : Add Multi Mapping transform
Restore the PDI Mapping contract as a new Multi Mapping transform with
0..N inputs and outputs, info and target hops, and row producers/listeners.
Simple Mapping is unchanged.
* issue #7953 : apache-rat
---
.../ROOT/assets/images/transforms/icons/MMAP.svg | 42 +
docs/hop-user-manual/modules/ROOT/nav.adoc | 1 +
.../modules/ROOT/pages/pipeline/transforms.adoc | 1 +
.../pages/pipeline/transforms/mapping-input.adoc | 3 +
.../pages/pipeline/transforms/mapping-output.adoc | 3 +
.../pages/pipeline/transforms/multi-mapping.adoc | 79 ++
.../pages/pipeline/transforms/simple-mapping.adoc | 2 +
.../pages/snippets/best-practices/mappings.adoc | 2 +
.../transforms/0108-multi-mapping-child-01.hpl | 155 ++++
.../transforms/0108-multi-mapping-parent-01.hpl | 188 +++++
.../datasets/golden-multi-mapping-01.csv | 5 +
.../metadata/dataset/golden-multi-mapping-01.json | 48 ++
.../0108-multi-mapping-parent-01 UNIT.json | 48 ++
.../transforms/mapping/MappingTransforms.java | 121 +++
.../pipeline/transforms/mapping/SimpleMapping.java | 24 +-
.../transforms/multimapping/MultiIOMappings.java | 93 +++
.../transforms/multimapping/MultiMapping.java | 470 +++++++++++
.../transforms/multimapping/MultiMappingData.java | 54 ++
.../multimapping/MultiMappingDialog.java | 901 +++++++++++++++++++++
.../multimapping/MultiMappingInputDefinition.java | 139 ++++
.../multimapping/MultiMappingInputRename.java | 68 ++
.../transforms/multimapping/MultiMappingMeta.java | 610 ++++++++++++++
.../multimapping/MultiMappingOutputDefinition.java | 132 +++
.../multimapping/MultiMappingOutputRename.java | 68 ++
.../transforms/mapping/src/main/resources/MMAP.svg | 42 +
.../messages/messages_en_US.properties | 112 +++
.../samples/transforms/multi-mapping-child.hpl | 165 ++++
.../samples/transforms/multi-mapping-parent.hpl | 218 +++++
.../transforms/mapping/MappingTransformsTest.java | 50 ++
.../MultiMappingMetaInjectionTest.java | 68 ++
.../multimapping/MultiMappingMetaTest.java | 239 ++++++
.../transforms/multimapping/MultiMappingTest.java | 114 +++
.../src/test/resources/multi-mapping-transform.xml | 86 ++
33 files changed, 4329 insertions(+), 22 deletions(-)
diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/MMAP.svg b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/MMAP.svg
new file mode 100644
index 0000000000..a986b31a31
--- /dev/null
+++ b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/MMAP.svg
@@ -0,0 +1,42 @@
+<?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.
+-->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
+ width="42px" height="42px" viewBox="0 0 42 42">
+ <g>
+ <rect x="14.753" y="8.232" fill="#0E3A5A" width="2.523" height="1.699"/>
+ <rect x="18.276" y="8.232" fill="#0E3A5A" width="2.524" height="1.699"/>
+ <rect x="28.85" y="8.232" fill="#0E3A5A" width="2.525" height="1.699"/>
+ <rect x="32.41" y="24.557" fill="#0E3A5A" width="1.699" height="2.396"/>
+ <rect x="25.325" y="8.232" fill="#0E3A5A" width="2.524" height="1.699"/>
+ <rect x="21.801" y="8.232" fill="#0E3A5A" width="2.524" height="1.699"/>
+ <rect x="32.41" y="17.767" fill="#0E3A5A" width="1.699" height="2.396"/>
+ <rect x="32.41" y="21.162" fill="#0E3A5A" width="1.699" height="2.395"/>
+ <rect x="32.41" y="10.977" fill="#0E3A5A" width="1.699" height="2.395"/>
+ <rect x="32.41" y="14.371" fill="#0E3A5A" width="1.699" height="2.396"/>
+ <polygon fill="#0E3A5A"
+ points="32.41,9.977 34.109,9.977 34.109,8.232 32.375,8.232 32.375,9.932 32.41,9.932"/>
+ <rect x="7.167" y="7.296" fill="#FFFFFF" width="3.666" height="3.666"/>
+ <rect x="7.167" y="31.037" fill="#FFFFFF" width="3.666" height="3.667"/>
+ <rect x="31.167" y="31.037" fill="#FFFFFF" width="3.666" height="3.667"/>
+ <rect x="31.167" y="7.296" fill="#FFFFFF" width="3.666" height="3.666"/>
+ <path fill="#0E3A5A" d="M34.109,29.338v-1.386H32.41v1.386h-2.942v3.172H12.532v-3.172h-2.7V12.661h2.7V9.932h1.221V8.232h-1.221
+ V5.597H5.468v7.064h2.665v16.677H5.468v7.065h7.064v-2.194h16.936v2.194h7.064v-7.065H34.109z M7.167,10.962V7.296h3.666v3.666
+ H7.167z M10.833,34.704H7.167v-3.667h3.666V34.704z M34.833,34.704h-3.666v-3.667h3.666V34.704z M34.833,10.962h-3.666V7.296h3.666
+ V10.962z"/>
+ </g>
+</svg>
diff --git a/docs/hop-user-manual/modules/ROOT/nav.adoc b/docs/hop-user-manual/modules/ROOT/nav.adoc
index a118fd3206..333fe3e94d 100644
--- a/docs/hop-user-manual/modules/ROOT/nav.adoc
+++ b/docs/hop-user-manual/modules/ROOT/nav.adoc
@@ -210,6 +210,7 @@ under the License.
*** xref:pipeline/transforms/mongodbdelete.adoc[MongoDB delete]
*** xref:pipeline/transforms/mongodbinput.adoc[MongoDB Input]
*** xref:pipeline/transforms/mongodboutput.adoc[MongoDB Output]
+*** xref:pipeline/transforms/multi-mapping.adoc[Multi Mapping]
*** xref:pipeline/transforms/multimerge.adoc[Multiway Merge Join]
*** xref:pipeline/transforms/mysqlbulkloader.adoc[MySQL bulk loader]
*** xref:pipeline/transforms/neo4j-cypher.adoc[Neo4j Cypher]
diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms.adoc
index 693269fb0d..52a2351ecf 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms.adoc
@@ -161,6 +161,7 @@ Those are explained once in xref:pipeline/formatting-values.adoc[Formatting numb
* xref:pipeline/transforms/mongodbdelete.adoc[MongoDB delete]
* xref:pipeline/transforms/mongodbinput.adoc[MongoDB Input]
* xref:pipeline/transforms/mongodboutput.adoc[MongoDB Output]
+* xref:pipeline/transforms/multi-mapping.adoc[Multi Mapping]
* xref:pipeline/transforms/multimerge.adoc[Multiway Merge Join]
* xref:pipeline/transforms/mysqlbulkloader.adoc[MySQL bulk loader]
* xref:pipeline/transforms/neo4j-cypher.adoc[Neo4j Cypher]
diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-input.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-input.adoc
index 364300d60b..9227792935 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-input.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-input.adoc
@@ -29,6 +29,9 @@ The Mapping input transform represents and describes the input for a Mapping pip
The data of this input will be provided by the parent pipeline (the pipeline that calls the mapping).
+A child used from xref:pipeline/transforms/simple-mapping.adoc[Simple Mapping] must contain exactly one Mapping input.
+A child used from xref:pipeline/transforms/multi-mapping.adoc[Multi Mapping] may contain zero or more Mapping input transforms.
+
|
== Supported Engines
[%noheader,cols="2,1a",frame=none, role="table-supported-engines"]
diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-output.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-output.adoc
index 731f27c9ee..77e38032b4 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-output.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-output.adoc
@@ -29,6 +29,9 @@ The Mapping output transform describes the output of the mapping pipeline to the
The data entering this transform will be streamed to the parent pipeline.
+A child used from xref:pipeline/transforms/simple-mapping.adoc[Simple Mapping] must contain exactly one Mapping output.
+A child used from xref:pipeline/transforms/multi-mapping.adoc[Multi Mapping] may contain zero or more Mapping output transforms.
+
|
== Supported Engines
[%noheader,cols="2,1a",frame=none, role="table-supported-engines"]
diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/multi-mapping.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/multi-mapping.adoc
new file mode 100644
index 0000000000..31ac2e84f9
--- /dev/null
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/multi-mapping.adoc
@@ -0,0 +1,79 @@
+////
+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: The Multi Mapping transform re-uses a child pipeline with zero or more inputs and outputs, including info and target streams.
+
+= image:transforms/icons/MMAP.svg[Multi Mapping transform Icon, role="image-doc-icon"] Multi Mapping
+
+[%noheader,cols="3a,1a", role="table-no-borders" ]
+|===
+|
+== Description
+
+The Multi Mapping transform re-uses a child pipeline (a mapping) with *zero or more* inputs and outputs.
+
+Unlike xref:pipeline/transforms/simple-mapping.adoc[Simple Mapping], which always has one xref:pipeline/transforms/mapping-input.adoc[Mapping input] and one xref:pipeline/transforms/mapping-output.adoc[Mapping output], Multi Mapping can:
+
+* start a child that generates its own data (0 inputs)
+* run a child that only writes data (0 outputs)
+* join or look up using several Mapping Input transforms
+* split rows to several Mapping Output transforms
+
+Non-main input mappings become *info hops* (the same idea as xref:pipeline/transforms/streamlookup.adoc[Stream Lookup]).
+Non-main output mappings become *target hops* (the same idea as xref:pipeline/transforms/switchcase.adoc[Switch / Case]).
+
+That lets you wrap a reusable Stream Lookup or Switch / Case style pipeline.
+
+The child pipeline is always executed with a local Hop pipeline run configuration so rows can be streamed in and out of the mapping.
+
+|
+== Supported Engines
+[%noheader,cols="2,1a",frame=none, role="table-supported-engines"]
+!===
+!Hop Engine! image:check_mark.svg[Supported, 24]
+!Single Threaded! image:cross.svg[Supported, 24]
+!Native Spark! image:cross.svg[Supported, 24]
+!Beam Spark! image:cross.svg[Supported, 24]
+!Beam Flink! image:cross.svg[Supported, 24]
+!Beam Dataflow! image:cross.svg[Supported, 24]
+!===
+|===
+
+== Options
+
+* Transform name: a unique name in your pipeline
+* Pipeline: the mapping (sub-) pipeline to run
+* Run configuration: local Hop run configuration only
+* Parameters tab: variables to pass into the child, plus inherit-all-variables
+* Input tabs (Add Input): one tab per input mapping
+** Input source transform: parent transform that sends rows (empty = auto-detect remaining hops)
+** Mapping Input transform: child Mapping Input that receives those rows
+** Main data path: checked for the primary stream; unchecked for an info hop
+** Update mapped field names downstream: reverse input renames on the way out
+** Field mapping table
+* Output tabs (Add Output): one tab per output mapping
+** Mapping Output transform: child Mapping Output that produces rows
+** Output target transform: parent transform that receives those rows (empty = auto-detect remaining hops)
+** Main data path: checked for the primary stream; unchecked for a target hop
+** Field mapping table
+
+You can close every input or output tab. A mapping with no inputs is a generator. A mapping with no outputs is a sink.
+
+== Sample
+
+The samples project contains parent/child examples under `transforms/` for a single stream, a generator, a join, an info lookup, and multiple target outputs.
diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/simple-mapping.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/simple-mapping.adoc
index 6d6b6ff4f5..d497c70fe3 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/simple-mapping.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/simple-mapping.adoc
@@ -44,6 +44,8 @@ In the Simple Mapping transform you can specify one xref:pipeline/transforms/map
Use this transform if you find yourself repeating the same logic multiple times over several different pipelines.
+If you need more than one input or output, or info / target hops (for example a reusable Stream Lookup or Switch / Case), use xref:pipeline/transforms/multi-mapping.adoc[Multi Mapping] instead.
+
== Options
The options are fairly self-explanatory:
diff --git a/docs/hop-user-manual/modules/ROOT/pages/snippets/best-practices/mappings.adoc b/docs/hop-user-manual/modules/ROOT/pages/snippets/best-practices/mappings.adoc
index f1389a0234..8667fae483 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/snippets/best-practices/mappings.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/snippets/best-practices/mappings.adoc
@@ -26,4 +26,6 @@ The Simple Mapping is a pipeline reading from a xref:pipeline/transforms/mapping
You can re-use the work in other pipelines using the xref:pipeline/transforms/simple-mapping.adoc[Simple Mapping] transform.
+When the reusable logic needs more than one input or output, or info / target hops, use xref:pipeline/transforms/multi-mapping.adoc[Multi Mapping] instead.
+
diff --git a/integration-tests/transforms/0108-multi-mapping-child-01.hpl b/integration-tests/transforms/0108-multi-mapping-child-01.hpl
new file mode 100644
index 0000000000..a74521e35d
--- /dev/null
+++ b/integration-tests/transforms/0108-multi-mapping-child-01.hpl
@@ -0,0 +1,155 @@
+<?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>
+ <pipeline_version/>
+ <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>
+ <pipeline_type>Normal</pipeline_type>
+ <pipeline_status>0</pipeline_status>
+ <parameters/>
+ <name>0101-multi-mapping-child-01</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <created_user>-</created_user>
+ <modified_user>-</modified_user>
+ <created_date>2022/11/08 21:16:00.171</created_date>
+ <modified_date>2022/11/08 21:16:00.171</modified_date>
+ </info>
+ <transform>
+ <type>MappingInput</type>
+ <name>fieldA, fieldB</name>
+ <fields>
+ <field>
+ <name>fieldA</name>
+ <type>Integer</type>
+ <length>-1</length>
+ <precision>-1</precision>
+ </field>
+ <field>
+ <name>fieldB</name>
+ <type>Integer</type>
+ <length>-1</length>
+ <precision>-1</precision>
+ </field>
+ </fields>
+ <distribute>Y</distribute>
+ <copies>1</copies>
+ <GUI>
+ <xloc>96</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ <description/>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ </transform>
+ <transform>
+ <type>MappingOutput</type>
+ <name>fieldSum</name>
+ <distribute>Y</distribute>
+ <copies>1</copies>
+ <GUI>
+ <xloc>512</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ <description/>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ </transform>
+ <transform>
+ <type>ScriptValueMod</type>
+ <name>fieldSum = fieldA+fieldB</name>
+ <jsScripts>
+ <jsScript>
+ <jsScript_type>0</jsScript_type>
+ <jsScript_name>Script 1</jsScript_name>
+ <jsScript_script>
+var fieldSum = fieldA + fieldB;
+
+var par1 = getVariable("PAR1", "");
+var par2 = getVariable("PAR2", "");
+</jsScript_script>
+ </jsScript>
+ </jsScripts>
+ <fields>
+ <field>
+ <name>fieldSum</name>
+ <rename>fieldSum</rename>
+ <type>Integer</type>
+ <length>-1</length>
+ <precision>-1</precision>
+ <replace>N</replace>
+ </field>
+ <field>
+ <name>par1</name>
+ <rename>par1</rename>
+ <type>String</type>
+ <length>-1</length>
+ <precision>-1</precision>
+ <replace>N</replace>
+ </field>
+ <field>
+ <name>par2</name>
+ <rename>par2</rename>
+ <type>String</type>
+ <length>-1</length>
+ <precision>-1</precision>
+ <replace>N</replace>
+ </field>
+ </fields>
+ <optimizationLevel>9</optimizationLevel>
+ <languageVersion>ES6</languageVersion>
+ <distribute>Y</distribute>
+ <copies>1</copies>
+ <GUI>
+ <xloc>320</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ <description/>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ </transform>
+ <order>
+ <hop>
+ <from>fieldSum = fieldA+fieldB</from>
+ <to>fieldSum</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>fieldA, fieldB</from>
+ <to>fieldSum = fieldA+fieldB</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <notepads/>
+ <attributes/>
+ <transform_error_handling/>
+</pipeline>
diff --git a/integration-tests/transforms/0108-multi-mapping-parent-01.hpl b/integration-tests/transforms/0108-multi-mapping-parent-01.hpl
new file mode 100644
index 0000000000..0ea29ba101
--- /dev/null
+++ b/integration-tests/transforms/0108-multi-mapping-parent-01.hpl
@@ -0,0 +1,188 @@
+<?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>
+ <pipeline_version/>
+ <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>
+ <pipeline_type>Normal</pipeline_type>
+ <pipeline_status>0</pipeline_status>
+ <parameters/>
+ <name>0101-multi-mapping-parent-01</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <created_user>-</created_user>
+ <modified_user>-</modified_user>
+ <created_date>2022/11/08 21:15:20.215</created_date>
+ <modified_date>2022/11/08 21:15:20.215</modified_date>
+ </info>
+ <transform>
+ <type>MultiMapping</type>
+ <name>0108-multi-mapping-child-01.hpl</name>
+ <runConfiguration>local</runConfiguration>
+ <mappings>
+ <input>
+ <mapping>
+ <input_transform/>
+ <output_transform/>
+ <description/>
+ <connector>
+ <parent>a</parent>
+ <child>fieldA</child>
+ </connector>
+ <connector>
+ <parent>b</parent>
+ <child>fieldB</child>
+ </connector>
+ <main_path>Y</main_path>
+ <rename_on_output>Y</rename_on_output>
+ </mapping>
+ </input>
+ <output>
+ <mapping>
+ <input_transform/>
+ <output_transform/>
+ <description/>
+ <connector>
+ <parent>fieldSum</parent>
+ <child>sum</child>
+ </connector>
+ <connector>
+ <parent>par1</parent>
+ <child>parameter1</child>
+ </connector>
+ <connector>
+ <parent>par2</parent>
+ <child>parameter2</child>
+ </connector>
+ <main_path>Y</main_path>
+ <rename_on_output>N</rename_on_output>
+ </mapping>
+ </output>
+ <parameters>
+ <variablemapping>
+ <variable>PAR1</variable>
+ <input>val1</input>
+ </variablemapping>
+ <variablemapping>
+ <variable>PAR2</variable>
+ <input>val2</input>
+ </variablemapping>
+ <inherit_all_vars>Y</inherit_all_vars>
+ </parameters>
+ </mappings>
+ <filename>${PROJECT_HOME}/0108-multi-mapping-child-01.hpl</filename>
+ <distribute>Y</distribute>
+ <copies>1</copies>
+ <GUI>
+ <xloc>304</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ <description/>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ </transform>
+ <transform>
+ <type>Dummy</type>
+ <name>Output</name>
+ <distribute>Y</distribute>
+ <copies>1</copies>
+ <GUI>
+ <xloc>512</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ <description/>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ </transform>
+ <transform>
+ <type>DataGrid</type>
+ <name>sample data</name>
+ <fields>
+ <field>
+ <name>a</name>
+ <type>Integer</type>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ <field>
+ <name>b</name>
+ <type>Integer</type>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ </fields>
+ <data>
+ <line>
+ <item>1</item>
+ <item>8</item>
+ </line>
+ <line>
+ <item>2</item>
+ <item>7</item>
+ </line>
+ <line>
+ <item>3</item>
+ <item>6</item>
+ </line>
+ <line>
+ <item>4</item>
+ <item>5</item>
+ </line>
+ </data>
+ <distribute>Y</distribute>
+ <copies>1</copies>
+ <GUI>
+ <xloc>96</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ <description/>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ </transform>
+ <order>
+ <hop>
+ <from>0108-multi-mapping-child-01.hpl</from>
+ <to>Output</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>sample data</from>
+ <to>0108-multi-mapping-child-01.hpl</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <notepads/>
+ <attributes/>
+ <transform_error_handling/>
+</pipeline>
diff --git a/integration-tests/transforms/datasets/golden-multi-mapping-01.csv b/integration-tests/transforms/datasets/golden-multi-mapping-01.csv
new file mode 100644
index 0000000000..b998ac6687
--- /dev/null
+++ b/integration-tests/transforms/datasets/golden-multi-mapping-01.csv
@@ -0,0 +1,5 @@
+a,b,sum,parameter1,parameter2
+1,8,9,val1,val2
+2,7,9,val1,val2
+3,6,9,val1,val2
+4,5,9,val1,val2
diff --git a/integration-tests/transforms/metadata/dataset/golden-multi-mapping-01.json b/integration-tests/transforms/metadata/dataset/golden-multi-mapping-01.json
new file mode 100644
index 0000000000..0013203dc9
--- /dev/null
+++ b/integration-tests/transforms/metadata/dataset/golden-multi-mapping-01.json
@@ -0,0 +1,48 @@
+{
+ "base_filename": "golden-multi-mapping-01.csv",
+ "name": "golden-multi-mapping-01",
+ "description": "",
+ "dataset_fields": [
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 5,
+ "field_precision": 0,
+ "field_name": "a",
+ "field_format": "####0;-####0"
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 5,
+ "field_precision": 0,
+ "field_name": "b",
+ "field_format": "####0;-####0"
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 5,
+ "field_precision": 0,
+ "field_name": "sum",
+ "field_format": "####0;-####0"
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "parameter1",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "parameter2",
+ "field_format": ""
+ }
+ ],
+ "folder_name": ""
+}
\ No newline at end of file
diff --git a/integration-tests/transforms/metadata/unit-test/0108-multi-mapping-parent-01 UNIT.json b/integration-tests/transforms/metadata/unit-test/0108-multi-mapping-parent-01 UNIT.json
new file mode 100644
index 0000000000..85e0aec256
--- /dev/null
+++ b/integration-tests/transforms/metadata/unit-test/0108-multi-mapping-parent-01 UNIT.json
@@ -0,0 +1,48 @@
+{
+ "database_replacements": [],
+ "autoOpening": true,
+ "description": "Multi Mapping 1-in-1-out parity with Simple Mapping",
+ "persist_filename": "",
+ "test_type": "UNIT_TEST",
+ "variableValues": [],
+ "basePath": "",
+ "golden_data_sets": [
+ {
+ "field_mappings": [
+ {
+ "transform_field": "a",
+ "data_set_field": "a"
+ },
+ {
+ "transform_field": "b",
+ "data_set_field": "b"
+ },
+ {
+ "transform_field": "sum",
+ "data_set_field": "sum"
+ },
+ {
+ "transform_field": "parameter1",
+ "data_set_field": "parameter1"
+ },
+ {
+ "transform_field": "parameter2",
+ "data_set_field": "parameter2"
+ }
+ ],
+ "field_order": [
+ "a",
+ "b",
+ "sum",
+ "parameter1",
+ "parameter2"
+ ],
+ "data_set_name": "golden-multi-mapping-01",
+ "transform_name": "Output"
+ }
+ ],
+ "input_data_sets": [],
+ "name": "0108-multi-mapping-parent-01 UNIT",
+ "trans_test_tweaks": [],
+ "pipeline_filename": "./0108-multi-mapping-parent-01.hpl"
+}
\ No newline at end of file
diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/MappingTransforms.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/MappingTransforms.java
new file mode 100644
index 0000000000..0e3e8ff245
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/MappingTransforms.java
@@ -0,0 +1,121 @@
+/*
+ * 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.mapping;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.pipeline.Pipeline;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.engine.IEngineComponent;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transforms.input.MappingInput;
+import org.apache.hop.pipeline.transforms.input.MappingInputMeta;
+import org.apache.hop.pipeline.transforms.output.MappingOutput;
+import org.apache.hop.pipeline.transforms.output.MappingOutputMeta;
+
+/** Shared helpers for locating Mapping Input / Output transforms in a child pipeline. */
+public final class MappingTransforms {
+
+ private MappingTransforms() {
+ // Utility class
+ }
+
+ public static boolean isInfoMapping(MappingIODefinition definition) {
+ return definition != null
+ && !definition.isMainDataPath()
+ && StringUtils.isNotEmpty(definition.getInputTransformName());
+ }
+
+ public static boolean isTargetMapping(MappingIODefinition definition) {
+ return definition != null
+ && !definition.isMainDataPath()
+ && StringUtils.isNotEmpty(definition.getOutputTransformName());
+ }
+
+ public static List<MappingInput> findMappingInputs(Pipeline mappingPipeline) {
+ List<MappingInput> list = new ArrayList<>();
+ if (mappingPipeline == null) {
+ return list;
+ }
+ for (IEngineComponent component : mappingPipeline.getComponents()) {
+ if (component instanceof MappingInput mappingInput) {
+ list.add(mappingInput);
+ }
+ }
+ return list;
+ }
+
+ public static List<MappingOutput> findMappingOutputs(Pipeline mappingPipeline) {
+ List<MappingOutput> list = new ArrayList<>();
+ if (mappingPipeline == null) {
+ return list;
+ }
+ for (IEngineComponent component : mappingPipeline.getComponents()) {
+ if (component instanceof MappingOutput mappingOutput) {
+ list.add(mappingOutput);
+ }
+ }
+ return list;
+ }
+
+ public static List<TransformMeta> findMappingInputMetas(PipelineMeta pipelineMeta) {
+ List<TransformMeta> list = new ArrayList<>();
+ if (pipelineMeta == null) {
+ return list;
+ }
+ for (TransformMeta transformMeta : pipelineMeta.getTransforms()) {
+ if (transformMeta.getTransform() instanceof MappingInputMeta) {
+ list.add(transformMeta);
+ }
+ }
+ return list;
+ }
+
+ public static List<TransformMeta> findMappingOutputMetas(PipelineMeta pipelineMeta) {
+ List<TransformMeta> list = new ArrayList<>();
+ if (pipelineMeta == null) {
+ return list;
+ }
+ for (TransformMeta transformMeta : pipelineMeta.getTransforms()) {
+ if (transformMeta.getTransform() instanceof MappingOutputMeta) {
+ list.add(transformMeta);
+ }
+ }
+ return list;
+ }
+
+ public static MappingInput findMappingInput(Pipeline mappingPipeline, String transformName) {
+ for (MappingInput mappingInput : findMappingInputs(mappingPipeline)) {
+ if (Utils.isEmpty(transformName) || transformName.equals(mappingInput.getTransformName())) {
+ return mappingInput;
+ }
+ }
+ return null;
+ }
+
+ public static MappingOutput findMappingOutput(Pipeline mappingPipeline, String transformName) {
+ for (MappingOutput mappingOutput : findMappingOutputs(mappingPipeline)) {
+ if (Utils.isEmpty(transformName) || transformName.equals(mappingOutput.getTransformName())) {
+ return mappingOutput;
+ }
+ }
+ return null;
+ }
+}
diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/SimpleMapping.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/SimpleMapping.java
index 53fcd4f548..97036a3a52 100644
--- a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/SimpleMapping.java
+++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/SimpleMapping.java
@@ -17,7 +17,6 @@
package org.apache.hop.pipeline.transforms.mapping;
-import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.hop.core.Const;
@@ -29,7 +28,6 @@ import org.apache.hop.pipeline.RowProducer;
import org.apache.hop.pipeline.SingleThreadedPipelineExecutor;
import org.apache.hop.pipeline.TransformWithMappingMeta;
import org.apache.hop.pipeline.config.PipelineRunConfiguration;
-import org.apache.hop.pipeline.engine.IEngineComponent;
import org.apache.hop.pipeline.engine.PipelineEngineFactory;
import org.apache.hop.pipeline.engines.local.LocalPipelineEngine;
import org.apache.hop.pipeline.engines.local.LocalPipelineRunConfiguration;
@@ -245,29 +243,11 @@ public class SimpleMapping extends BaseTransform<SimpleMappingMeta, SimpleMappin
}
public static List<MappingInput> findMappingInputs(Pipeline mappingPipeline) {
- List<MappingInput> list = new ArrayList<>();
-
- List<IEngineComponent> components = mappingPipeline.getComponents();
- for (IEngineComponent component : components) {
- if (component instanceof MappingInput mappingInput) {
- list.add(mappingInput);
- }
- }
-
- return list;
+ return MappingTransforms.findMappingInputs(mappingPipeline);
}
private List<MappingOutput> findMappingOutputs(Pipeline mappingPipeline) {
- List<MappingOutput> list = new ArrayList<>();
-
- List<IEngineComponent> components = mappingPipeline.getComponents();
- for (IEngineComponent component : components) {
- if (component instanceof MappingOutput mappingOutput) {
- list.add(mappingOutput);
- }
- }
-
- return list;
+ return MappingTransforms.findMappingOutputs(mappingPipeline);
}
@Override
diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiIOMappings.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiIOMappings.java
new file mode 100644
index 0000000000..76ab4cad8d
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiIOMappings.java
@@ -0,0 +1,93 @@
+/*
+ * 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.multimapping;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.pipeline.transforms.mapping.MappingIODefinition;
+import org.apache.hop.pipeline.transforms.mapping.MappingParameters;
+
+@Getter
+@Setter
+public class MultiIOMappings implements Cloneable {
+
+ @HopMetadataProperty(
+ key = "mapping",
+ groupKey = "input",
+ injectionGroupKey = "INPUT_MAPPINGS",
+ injectionGroupDescription = "MultiMappingMeta.Injection.INPUT_MAPPINGS")
+ private List<MultiMappingInputDefinition> inputMappings;
+
+ @HopMetadataProperty(
+ key = "mapping",
+ groupKey = "output",
+ injectionGroupKey = "OUTPUT_MAPPINGS",
+ injectionGroupDescription = "MultiMappingMeta.Injection.OUTPUT_MAPPINGS")
+ private List<MultiMappingOutputDefinition> outputMappings;
+
+ @HopMetadataProperty(key = "parameters")
+ private MappingParameters mappingParameters;
+
+ public MultiIOMappings() {
+ this.inputMappings = new ArrayList<>();
+ this.outputMappings = new ArrayList<>();
+ this.mappingParameters = new MappingParameters();
+ }
+
+ public MultiIOMappings(MultiIOMappings m) {
+ this();
+ if (m.inputMappings != null) {
+ for (MultiMappingInputDefinition definition : m.inputMappings) {
+ this.inputMappings.add(new MultiMappingInputDefinition(definition));
+ }
+ }
+ if (m.outputMappings != null) {
+ for (MultiMappingOutputDefinition definition : m.outputMappings) {
+ this.outputMappings.add(new MultiMappingOutputDefinition(definition));
+ }
+ }
+ this.mappingParameters =
+ m.mappingParameters != null
+ ? new MappingParameters(m.mappingParameters)
+ : new MappingParameters();
+ }
+
+ @Override
+ public MultiIOMappings clone() {
+ return new MultiIOMappings(this);
+ }
+
+ public List<MappingIODefinition> getInputIoDefinitions() {
+ List<MappingIODefinition> list = new ArrayList<>();
+ for (MultiMappingInputDefinition definition : inputMappings) {
+ list.add(definition.toIoDefinition());
+ }
+ return list;
+ }
+
+ public List<MappingIODefinition> getOutputIoDefinitions() {
+ List<MappingIODefinition> list = new ArrayList<>();
+ for (MultiMappingOutputDefinition definition : outputMappings) {
+ list.add(definition.toIoDefinition());
+ }
+ return list;
+ }
+}
diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMapping.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMapping.java
new file mode 100644
index 0000000000..aa271c94d1
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMapping.java
@@ -0,0 +1,470 @@
+/*
+ * 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.multimapping;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.IRowSet;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopTransformException;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.pipeline.Pipeline;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.RowProducer;
+import org.apache.hop.pipeline.TransformWithMappingMeta;
+import org.apache.hop.pipeline.config.PipelineRunConfiguration;
+import org.apache.hop.pipeline.engine.PipelineEngineFactory;
+import org.apache.hop.pipeline.engines.local.LocalPipelineEngine;
+import org.apache.hop.pipeline.engines.local.LocalPipelineRunConfiguration;
+import org.apache.hop.pipeline.transform.BaseTransform;
+import org.apache.hop.pipeline.transform.IRowListener;
+import org.apache.hop.pipeline.transform.ITransform;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transforms.input.MappingInput;
+import org.apache.hop.pipeline.transforms.mapping.MappingIODefinition;
+import org.apache.hop.pipeline.transforms.mapping.MappingTransforms;
+import org.apache.hop.pipeline.transforms.mapping.MappingValueRename;
+import org.apache.hop.pipeline.transforms.mapping.RowDataInputMapper;
+import org.apache.hop.pipeline.transforms.mapping.RowOutputDataMapper;
+import org.apache.hop.pipeline.transforms.output.MappingOutput;
+
+/** Execute a mapping with 0..N inputs and outputs using row producers and row listeners. */
+public class MultiMapping extends BaseTransform<MultiMappingMeta, MultiMappingData> {
+
+ private static final Class<?> PKG = MultiMappingMeta.class;
+
+ public MultiMapping(
+ TransformMeta transformMeta,
+ MultiMappingMeta meta,
+ MultiMappingData data,
+ int copyNr,
+ PipelineMeta pipelineMeta,
+ Pipeline pipeline) {
+ super(transformMeta, meta, data, copyNr, pipelineMeta, pipeline);
+ }
+
+ @Override
+ public boolean processRow() throws HopException {
+ try {
+ if (first) {
+ first = false;
+ data.wasStarted = true;
+ wireInputProducers();
+ wireOutputListeners();
+ data.mappingPipeline.startThreads();
+ drainInfoStreams();
+ data.infoDrained = true;
+ }
+
+ if (!data.mainRowSets.isEmpty()) {
+ Object[] row = nextMainRow();
+ if (row != null) {
+ return true;
+ }
+ }
+
+ finishProducers();
+ data.mappingPipeline.waitUntilFinished();
+ setOutputDone();
+ return false;
+ } catch (Exception e) {
+ if (data.mappingPipeline != null) {
+ data.mappingPipeline.stopAll();
+ }
+ throw new HopException(e);
+ }
+ }
+
+ private void wireInputProducers() throws HopException {
+ List<MappingInput> mappingInputs = MappingTransforms.findMappingInputs(data.mappingPipeline);
+ Set<String> referencedInputs = new HashSet<>();
+
+ List<MappingIODefinition> inputDefinitions = new ArrayList<>();
+ for (MultiMappingInputDefinition definition : meta.getInputMappings()) {
+ inputDefinitions.add(definition.toIoDefinition());
+ }
+
+ if (inputDefinitions.isEmpty() && mappingInputs.size() == 1) {
+ MappingIODefinition auto =
+ new MappingIODefinition(null, mappingInputs.get(0).getTransformName());
+ auto.setMainDataPath(true);
+ inputDefinitions.add(auto);
+ }
+
+ for (MappingIODefinition definition : inputDefinitions) {
+ MappingInput mappingInput =
+ MappingTransforms.findMappingInput(
+ data.mappingPipeline, definition.getOutputTransformName());
+ if (mappingInput == null) {
+ throw new HopException(
+ BaseMessages.getString(
+ PKG,
+ "MultiMapping.Exception.MappingInputNotFound",
+ Const.NVL(definition.getOutputTransformName(), "")));
+ }
+ referencedInputs.add(mappingInput.getTransformName());
+
+ RowProducer rowProducer =
+ data.mappingPipeline.addRowProducer(mappingInput.getTransformName(), 0);
+ RowDataInputMapper mapper = new RowDataInputMapper(definition, rowProducer);
+ data.allInputMappers.add(mapper);
+
+ if (MappingTransforms.isInfoMapping(definition)) {
+ IRowSet rowSet = findInputRowSet(definition.getInputTransformName());
+ if (rowSet == null) {
+ throw new HopException(
+ BaseMessages.getString(
+ PKG,
+ "MultiMapping.Exception.InfoRowSetNotFound",
+ definition.getInputTransformName()));
+ }
+ data.infoRowSets.add(rowSet);
+ data.rowSetMappers.put(rowSet, mapper);
+ } else if (StringUtils.isNotEmpty(definition.getInputTransformName())) {
+ IRowSet rowSet = findInputRowSet(definition.getInputTransformName());
+ if (rowSet == null) {
+ throw new HopException(
+ BaseMessages.getString(
+ PKG,
+ "MultiMapping.Exception.InputRowSetNotFound",
+ definition.getInputTransformName()));
+ }
+ data.mainRowSets.add(rowSet);
+ data.rowSetMappers.put(rowSet, mapper);
+ } else {
+ for (IRowSet rowSet : getInputRowSets()) {
+ if (!isInfoRowSet(rowSet) && !data.rowSetMappers.containsKey(rowSet)) {
+ data.mainRowSets.add(rowSet);
+ data.rowSetMappers.put(rowSet, mapper);
+ }
+ }
+ }
+ }
+
+ for (MappingInput mappingInput : mappingInputs) {
+ if (!referencedInputs.contains(mappingInput.getTransformName())) {
+ RowProducer rowProducer =
+ data.mappingPipeline.addRowProducer(mappingInput.getTransformName(), 0);
+ rowProducer.finished();
+ }
+ }
+ }
+
+ private boolean isInfoRowSet(IRowSet rowSet) {
+ for (IRowSet infoRowSet : data.infoRowSets) {
+ if (infoRowSet == rowSet) {
+ return true;
+ }
+ }
+ String origin = rowSet.getOriginTransformName();
+ for (MultiMappingInputDefinition definition : meta.getInputMappings()) {
+ if (MappingTransforms.isInfoMapping(definition.toIoDefinition())
+ && origin != null
+ && origin.equalsIgnoreCase(definition.getInputTransformName())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void wireOutputListeners() throws HopException {
+ List<MappingOutput> mappingOutputs = MappingTransforms.findMappingOutputs(data.mappingPipeline);
+ List<MappingValueRename> inputRenames = new ArrayList<>();
+ for (MultiMappingInputDefinition inputDefinition : meta.getInputMappings()) {
+ if (inputDefinition.isRenamingOnOutput()) {
+ MultiMappingMeta.addInputRenames(
+ inputRenames, inputDefinition.toIoDefinition().getValueRenames());
+ }
+ }
+ MappingIODefinition combinedInputRenames = new MappingIODefinition();
+ combinedInputRenames.setRenamingOnOutput(!inputRenames.isEmpty());
+ combinedInputRenames.setValueRenames(inputRenames);
+
+ Set<String> claimedTargets = new HashSet<>();
+ for (MultiMappingOutputDefinition definition : meta.getOutputMappings()) {
+ if (MappingTransforms.isTargetMapping(definition.toIoDefinition())) {
+ claimedTargets.add(definition.getOutputTransformName());
+ }
+ }
+
+ List<MappingIODefinition> outputDefinitions = new ArrayList<>();
+ for (MultiMappingOutputDefinition definition : meta.getOutputMappings()) {
+ outputDefinitions.add(definition.toIoDefinition());
+ }
+ if (outputDefinitions.isEmpty() && mappingOutputs.size() == 1) {
+ MappingIODefinition auto =
+ new MappingIODefinition(mappingOutputs.get(0).getTransformName(), null);
+ auto.setMainDataPath(true);
+ outputDefinitions.add(auto);
+ }
+
+ Set<String> referencedOutputs = new HashSet<>();
+ for (MappingIODefinition definition : outputDefinitions) {
+ MappingOutput mappingOutput =
+ MappingTransforms.findMappingOutput(
+ data.mappingPipeline, definition.getInputTransformName());
+ if (mappingOutput == null) {
+ throw new HopException(
+ BaseMessages.getString(
+ PKG,
+ "MultiMapping.Exception.MappingOutputNotFound",
+ Const.NVL(definition.getInputTransformName(), "")));
+ }
+ referencedOutputs.add(mappingOutput.getTransformName());
+
+ List<IRowSet> targetRowSets = resolveOutputRowSets(definition, claimedTargets);
+ RowOutputDataMapper outputDataMapper =
+ new RowOutputDataMapper(
+ combinedInputRenames,
+ definition,
+ (rowMeta, row) -> putRowsTo(rowMeta, row, targetRowSets));
+ mappingOutput.addRowListener(outputDataMapper);
+ }
+ }
+
+ private List<IRowSet> resolveOutputRowSets(
+ MappingIODefinition definition, Set<String> claimedTargets) throws HopTransformException {
+ List<IRowSet> rowSets = new ArrayList<>();
+ if (StringUtils.isNotEmpty(definition.getOutputTransformName())) {
+ IRowSet rowSet = findOutputRowSet(definition.getOutputTransformName());
+ if (rowSet != null) {
+ rowSets.add(rowSet);
+ }
+ return rowSets;
+ }
+ for (IRowSet rowSet : getOutputRowSets()) {
+ String destination = rowSet.getDestinationTransformName();
+ if (destination == null || !claimedTargets.contains(destination)) {
+ rowSets.add(rowSet);
+ }
+ }
+ return rowSets;
+ }
+
+ private void putRowsTo(IRowMeta rowMeta, Object[] row, List<IRowSet> rowSets)
+ throws HopTransformException {
+ if (rowSets.isEmpty()) {
+ return;
+ }
+ for (IRowSet rowSet : rowSets) {
+ putRowTo(rowMeta, row, rowSet);
+ }
+ }
+
+ private void drainInfoStreams() throws HopException {
+ for (IRowSet rowSet : data.infoRowSets) {
+ RowDataInputMapper mapper = data.rowSetMappers.get(rowSet);
+ Object[] row = getRowFrom(rowSet);
+ while (row != null && !isStopped() && !data.mappingPipeline.isFinishedOrStopped()) {
+ IRowMeta rowMeta = rowSet.getRowMeta();
+ boolean put = false;
+ while (!(data.mappingPipeline.isFinishedOrStopped() || put)) {
+ put = mapper.putRow(rowMeta, row);
+ }
+ if (!put) {
+ break;
+ }
+ row = getRowFrom(rowSet);
+ }
+ mapper.finished();
+ data.finishedRowSets.add(rowSet);
+ }
+ }
+
+ private Object[] nextMainRow() throws HopException {
+ int attempts = data.mainRowSets.size();
+ for (int i = 0; i < attempts; i++) {
+ if (data.mainRowSetIndex >= data.mainRowSets.size()) {
+ data.mainRowSetIndex = 0;
+ }
+ IRowSet rowSet = data.mainRowSets.get(data.mainRowSetIndex);
+ data.mainRowSetIndex++;
+ if (data.finishedRowSets.contains(rowSet)) {
+ continue;
+ }
+ Object[] row = getRowFrom(rowSet);
+ if (row == null) {
+ RowDataInputMapper mapper = data.rowSetMappers.get(rowSet);
+ if (mapper != null) {
+ mapper.finished();
+ }
+ data.finishedRowSets.add(rowSet);
+ continue;
+ }
+ RowDataInputMapper mapper = data.rowSetMappers.get(rowSet);
+ IRowMeta rowMeta = rowSet.getRowMeta();
+ boolean put = false;
+ while (!(data.mappingPipeline.isFinishedOrStopped() || put)) {
+ put = mapper.putRow(rowMeta, row);
+ }
+ if (!put) {
+ return null;
+ }
+ return row;
+ }
+ return null;
+ }
+
+ private void finishProducers() {
+ if (data.producersFinished) {
+ return;
+ }
+ data.producersFinished = true;
+ for (RowDataInputMapper mapper : data.allInputMappers) {
+ mapper.finished();
+ }
+ }
+
+ public void prepareMappingExecution() throws HopException {
+ if (data.mappingPipelineMeta == null) {
+ data.mappingPipelineMeta =
+ new PipelineMeta(variables.resolve(meta.getFilename()), metadataProvider, variables);
+ data.mappingPipelineMeta.clearChanged();
+ }
+
+ String runConfigName = resolve(meta.getRunConfigurationName());
+ if (StringUtils.isEmpty(runConfigName)) {
+ data.mappingPipeline = new LocalPipelineEngine(data.mappingPipelineMeta, this, this);
+ } else {
+ PipelineRunConfiguration runConfig =
+ metadataProvider.getSerializer(PipelineRunConfiguration.class).load(runConfigName);
+ if (runConfig == null) {
+ throw new HopException("Unable to find run configuration with name " + runConfigName);
+ }
+ if (!(runConfig.getEngineRunConfiguration() instanceof LocalPipelineRunConfiguration)) {
+ throw new HopException(
+ "Apache Hop can only run multi mappings with a local pipeline engine, not with run configuration "
+ + runConfigName);
+ }
+ data.mappingPipeline =
+ (LocalPipelineEngine)
+ PipelineEngineFactory.createPipelineEngine(
+ getPipeline(), runConfigName, metadataProvider, data.mappingPipelineMeta);
+ }
+
+ data.mappingPipeline.copyParametersFromDefinitions(data.mappingPipelineMeta);
+ TransformWithMappingMeta.activateParams(
+ data.mappingPipeline,
+ data.mappingPipeline,
+ this,
+ data.mappingPipelineMeta.listParameters(),
+ meta.getMappingParameters().getVariables(),
+ meta.getMappingParameters().getInputs(),
+ meta.getMappingParameters().isInheritingAllVariables());
+
+ data.mappingPipeline.setParentPipeline(getPipeline());
+ data.mappingPipeline.setParent(this);
+ data.mappingPipeline.setSafeModeEnabled(getPipeline().isSafeModeEnabled());
+ data.mappingPipeline.setGatheringMetrics(getPipeline().isGatheringMetrics());
+
+ try {
+ data.mappingPipeline.prepareExecution();
+ } catch (HopException e) {
+ throw new HopException(
+ BaseMessages.getString(PKG, "MultiMapping.Exception.UnableToPrepareExecutionOfMapping"),
+ e);
+ }
+
+ getPipeline().addActiveSubPipeline(getTransformName(), data.mappingPipeline);
+ }
+
+ @Override
+ public boolean init() {
+ if (super.init()) {
+ try {
+ data.mappingPipelineMeta =
+ TransformWithMappingMeta.loadMappingMeta(meta, getMetadataProvider(), this);
+ if (data.mappingPipelineMeta != null) {
+ prepareMappingExecution();
+ return true;
+ } else {
+ logError("No valid mapping was specified!");
+ return false;
+ }
+ } catch (Exception e) {
+ logError(
+ "Unable to load the mapping pipeline '"
+ + resolve(meta.getFilename())
+ + "' (PROJECT_HOME="
+ + getVariable("PROJECT_HOME")
+ + "): "
+ + e);
+ logError(Const.getStackTracker(e));
+ setErrors(1);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void dispose() {
+ try {
+ if (data.mappingPipeline != null) {
+ if (data.wasStarted && !data.mappingPipeline.isFinished()) {
+ data.mappingPipeline.waitUntilFinished();
+ }
+ if (data.mappingPipeline.getErrors() > 0) {
+ logError(BaseMessages.getString(PKG, "MultiMapping.Log.ErrorOccurredInSubPipeline"));
+ setErrors(1);
+ }
+ }
+ } finally {
+ super.dispose();
+ }
+ }
+
+ @Override
+ public void stopRunning() {
+ if (data.mappingPipeline != null) {
+ data.mappingPipeline.stopAll();
+ }
+ }
+
+ @Override
+ public void stopAll() {
+ if (data.mappingPipeline != null) {
+ data.mappingPipeline.stopAll();
+ }
+ super.stopAll();
+ }
+
+ @Override
+ public void addRowListener(IRowListener rowListener) {
+ super.addRowListener(rowListener);
+ if (data.mappingPipeline == null) {
+ return;
+ }
+ for (MappingOutput mappingOutput : MappingTransforms.findMappingOutputs(data.mappingPipeline)) {
+ mappingOutput.addRowListener(rowListener);
+ }
+ }
+
+ public Pipeline getMappingPipeline() {
+ return data.mappingPipeline;
+ }
+
+ ITransform getMappingInputTransform() {
+ List<MappingInput> inputs = MappingTransforms.findMappingInputs(data.mappingPipeline);
+ return inputs.isEmpty() ? null : inputs.get(0);
+ }
+}
diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingData.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingData.java
new file mode 100644
index 0000000000..f15350e684
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingData.java
@@ -0,0 +1,54 @@
+/*
+ * 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.multimapping;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.hop.core.IRowSet;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.engines.local.LocalPipelineEngine;
+import org.apache.hop.pipeline.transform.BaseTransformData;
+import org.apache.hop.pipeline.transform.ITransformData;
+import org.apache.hop.pipeline.transforms.mapping.RowDataInputMapper;
+
+@SuppressWarnings("java:S1104")
+public class MultiMappingData extends BaseTransformData implements ITransformData {
+
+ public LocalPipelineEngine mappingPipeline;
+ public PipelineMeta mappingPipelineMeta;
+ public boolean wasStarted;
+ public boolean infoDrained;
+ public boolean producersFinished;
+
+ public List<RowDataInputMapper> allInputMappers = new ArrayList<>();
+ public List<IRowSet> infoRowSets = new ArrayList<>();
+ public List<IRowSet> mainRowSets = new ArrayList<>();
+ public Map<IRowSet, RowDataInputMapper> rowSetMappers = new HashMap<>();
+ public Set<IRowSet> finishedRowSets = new HashSet<>();
+ public int mainRowSetIndex;
+
+ public MultiMappingData() {
+ super();
+ mappingPipeline = null;
+ wasStarted = false;
+ }
+}
diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingDialog.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingDialog.java
new file mode 100644
index 0000000000..fdd81867ce
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingDialog.java
@@ -0,0 +1,901 @@
+/*
+ * 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.multimapping;
+
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.Props;
+import org.apache.hop.core.SourceToTargetMapping;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.logging.LogChannel;
+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.core.vfs.HopVfs;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.config.PipelineRunConfiguration;
+import org.apache.hop.pipeline.engines.local.LocalPipelineRunConfiguration;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transforms.mapping.MappingParameters;
+import org.apache.hop.pipeline.transforms.mapping.MappingTransforms;
+import org.apache.hop.pipeline.transforms.mapping.MappingVariableMapping;
+import org.apache.hop.pipeline.transforms.mapping.SimpleMappingMeta;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.dialog.BaseDialog;
+import org.apache.hop.ui.core.dialog.EnterMappingDialog;
+import org.apache.hop.ui.core.dialog.ErrorDialog;
+import org.apache.hop.ui.core.dialog.MessageBox;
+import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.core.widget.ColumnInfo;
+import org.apache.hop.ui.core.widget.ColumnsResizer;
+import org.apache.hop.ui.core.widget.ComboVar;
+import org.apache.hop.ui.core.widget.TableView;
+import org.apache.hop.ui.core.widget.TextVar;
+import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.file.pipeline.HopPipelineFileType;
+import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CCombo;
+import org.eclipse.swt.custom.CTabFolder;
+import org.eclipse.swt.custom.CTabFolder2Adapter;
+import org.eclipse.swt.custom.CTabFolderEvent;
+import org.eclipse.swt.custom.CTabItem;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.TableItem;
+
+public class MultiMappingDialog extends BaseTransformDialog {
+ private static final Class<?> PKG = MultiMappingMeta.class;
+
+ private final MultiMappingMeta mappingMeta;
+ private final MultiIOMappings workingMappings;
+
+ private TextVar wPath;
+ private ComboVar wRunConfig;
+ private CTabFolder wTabFolder;
+ private Button wbAddInput;
+ private Button wbAddOutput;
+
+ private PipelineMeta mappingPipelineMeta;
+ private ModifyListener lsMod;
+ private final List<Runnable> changeList = new ArrayList<>();
+
+ public MultiMappingDialog(
+ Shell parent,
+ IVariables variables,
+ MultiMappingMeta transformMeta,
+ PipelineMeta pipelineMeta) {
+ super(parent, variables, transformMeta, pipelineMeta);
+ this.mappingMeta = transformMeta;
+ this.workingMappings = new MultiIOMappings(transformMeta.getIoMappings());
+ }
+
+ @Override
+ public String open() {
+ createShell(BaseMessages.getString(PKG, "MultiMappingDialog.Shell.Title"));
+ buildButtonBar().ok(e -> ok()).cancel(e -> cancel()).build();
+
+ lsMod = e -> mappingMeta.setChanged();
+ changed = mappingMeta.hasChanged();
+
+ Label wlPath = new Label(shell, SWT.RIGHT);
+ PropsUi.setLook(wlPath);
+ wlPath.setText(BaseMessages.getString(PKG, "MultiMappingDialog.Pipeline.Label"));
+ FormData fdlPath = new FormData();
+ fdlPath.left = new FormAttachment(0, 0);
+ fdlPath.top = new FormAttachment(wSpacer, margin);
+ fdlPath.right = new FormAttachment(middle, -margin);
+ wlPath.setLayoutData(fdlPath);
+
+ Button wbEdit = new Button(shell, SWT.PUSH);
+ PropsUi.setLook(wbEdit);
+ wbEdit.setText(BaseMessages.getString(PKG, "MultiMappingDialog.Edit.Label"));
+ FormData fdEdit = new FormData();
+ fdEdit.right = new FormAttachment(100, 0);
+ fdEdit.top = new FormAttachment(wlPath, 0, SWT.CENTER);
+ wbEdit.setLayoutData(fdEdit);
+ wbEdit.addListener(SWT.Selection, e -> editPipeline());
+
+ Button wbNew = new Button(shell, SWT.PUSH);
+ PropsUi.setLook(wbNew);
+ wbNew.setText(BaseMessages.getString(PKG, "MultiMappingDialog.New.Label"));
+ FormData fdNew = new FormData();
+ fdNew.right = new FormAttachment(wbEdit, -margin);
+ fdNew.top = new FormAttachment(wlPath, 0, SWT.CENTER);
+ wbNew.setLayoutData(fdNew);
+ wbNew.addListener(SWT.Selection, e -> newPipeline());
+
+ Button wbBrowse = new Button(shell, SWT.PUSH);
+ PropsUi.setLook(wbBrowse);
+ wbBrowse.setText(BaseMessages.getString(PKG, "MultiMappingDialog.Browse.Label"));
+ FormData fdBrowse = new FormData();
+ fdBrowse.right = new FormAttachment(wbNew, -margin);
+ fdBrowse.top = new FormAttachment(wlPath, 0, SWT.CENTER);
+ wbBrowse.setLayoutData(fdBrowse);
+ wbBrowse.addListener(SWT.Selection, e -> selectFilePipeline());
+
+ wPath = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+ PropsUi.setLook(wPath);
+ FormData fdPath = new FormData();
+ fdPath.left = new FormAttachment(wlPath, margin);
+ fdPath.top = new FormAttachment(wlPath, 0, SWT.CENTER);
+ fdPath.right = new FormAttachment(wbBrowse, -margin);
+ wPath.setLayoutData(fdPath);
+
+ Label wlRunConfig = new Label(shell, SWT.RIGHT);
+ PropsUi.setLook(wlRunConfig);
+ wlRunConfig.setText(BaseMessages.getString(PKG, "MultiMappingDialog.RunConfig.Label"));
+ FormData fdlRunConfig = new FormData();
+ fdlRunConfig.left = new FormAttachment(0, 0);
+ fdlRunConfig.top = new FormAttachment(wPath, margin);
+ fdlRunConfig.right = new FormAttachment(middle, -margin);
+ wlRunConfig.setLayoutData(fdlRunConfig);
+ wRunConfig = new ComboVar(variables, shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+ PropsUi.setLook(wRunConfig);
+ FormData fdRunConfig = new FormData();
+ fdRunConfig.left = new FormAttachment(wlRunConfig, margin);
+ fdRunConfig.top = new FormAttachment(wlRunConfig, 0, SWT.CENTER);
+ fdRunConfig.right = new FormAttachment(wbBrowse, -margin);
+ wRunConfig.setLayoutData(fdRunConfig);
+
+ wbAddOutput = new Button(shell, SWT.PUSH);
+ PropsUi.setLook(wbAddOutput);
+ wbAddOutput.setText(BaseMessages.getString(PKG, "MultiMappingDialog.button.AddOutput"));
+ FormData fdAddOutput = new FormData();
+ fdAddOutput.right = new FormAttachment(100, 0);
+ fdAddOutput.top = new FormAttachment(wRunConfig, margin);
+ wbAddOutput.setLayoutData(fdAddOutput);
+ wbAddOutput.addListener(
+ SWT.Selection, e -> addOutputTab(new MultiMappingOutputDefinition(), true));
+
+ wbAddInput = new Button(shell, SWT.PUSH);
+ PropsUi.setLook(wbAddInput);
+ wbAddInput.setText(BaseMessages.getString(PKG, "MultiMappingDialog.button.AddInput"));
+ FormData fdAddInput = new FormData();
+ fdAddInput.right = new FormAttachment(wbAddOutput, -margin);
+ fdAddInput.top = new FormAttachment(wRunConfig, margin);
+ wbAddInput.setLayoutData(fdAddInput);
+ wbAddInput.addListener(
+ SWT.Selection, e -> addInputTab(new MultiMappingInputDefinition(), true));
+
+ wTabFolder = new CTabFolder(shell, SWT.BORDER);
+ PropsUi.setLook(wTabFolder, Props.WIDGET_STYLE_TAB);
+ wTabFolder.setUnselectedCloseVisible(true);
+ FormData fdTabFolder = new FormData();
+ fdTabFolder.left = new FormAttachment(0, 0);
+ fdTabFolder.top = new FormAttachment(wbAddInput, margin);
+ fdTabFolder.right = new FormAttachment(100, 0);
+ fdTabFolder.bottom = new FormAttachment(wOk, -margin);
+ wTabFolder.setLayoutData(fdTabFolder);
+ wTabFolder.addCTabFolder2Listener(
+ new CTabFolder2Adapter() {
+ @Override
+ public void close(CTabFolderEvent event) {
+ CTabItem item = (CTabItem) event.item;
+ if (item.getData() == null) {
+ event.doit = false;
+ return;
+ }
+ MessageBox box = new MessageBox(shell, SWT.YES | SWT.NO | SWT.ICON_QUESTION);
+ box.setText(
+ BaseMessages.getString(
+ PKG, "MultiMappingDialog.CloseDefinitionTabAreYouSure.Title"));
+ box.setMessage(
+ BaseMessages.getString(
+ PKG, "MultiMappingDialog.CloseDefinitionTabAreYouSure.Message"));
+ if (box.open() != SWT.YES) {
+ event.doit = false;
+ return;
+ }
+ Object data = item.getData();
+ if (data instanceof MultiMappingInputDefinition definition) {
+ workingMappings.getInputMappings().remove(definition);
+ } else if (data instanceof MultiMappingOutputDefinition definition) {
+ workingMappings.getOutputMappings().remove(definition);
+ }
+ mappingMeta.setChanged();
+ }
+ });
+
+ getData();
+ mappingMeta.setChanged(changed);
+ wTabFolder.setSelection(0);
+ focusTransformName();
+ BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel());
+ return transformName;
+ }
+
+ private void selectFilePipeline() {
+ try {
+ HopPipelineFileType fileType = new HopPipelineFileType();
+ String filename =
+ BaseDialog.presentFileDialog(
+ false, shell, fileType.getFilterExtensions(), fileType.getFilterNames(), true);
+ if (filename != null) {
+ loadPipelineFile(filename);
+ wPath.setText(filename);
+ }
+ } catch (HopException ex) {
+ new ErrorDialog(
+ shell,
+ BaseMessages.getString(PKG, "MultiMappingDialog.ErrorLoadingPipeline.DialogTitle"),
+ BaseMessages.getString(PKG, "MultiMappingDialog.ErrorLoadingPipeline.DialogMessage"),
+ ex);
+ }
+ }
+
+ private void newPipeline() {
+ try {
+ HopPipelineFileType fileType = new HopPipelineFileType();
+ String filename =
+ BaseDialog.presentFileDialog(
+ false, shell, fileType.getFilterExtensions(), fileType.getFilterNames(), true);
+ if (Utils.isEmpty(filename)) {
+ return;
+ }
+ if (!filename.endsWith(".hpl")) {
+ filename = filename + ".hpl";
+ }
+ PipelineMeta newMeta = new PipelineMeta();
+ newMeta.setFilename(filename);
+ try (OutputStream outputStream = HopVfs.getOutputStream(filename, false)) {
+ outputStream.write(newMeta.getXml(variables).getBytes(StandardCharsets.UTF_8));
+ }
+ loadPipelineFile(filename);
+ wPath.setText(filename);
+ mappingMeta.setChanged();
+ } catch (Exception ex) {
+ new ErrorDialog(
+ shell,
+ BaseMessages.getString(PKG, "MultiMappingDialog.ErrorLoadingPipeline.DialogTitle"),
+ BaseMessages.getString(PKG, "MultiMappingDialog.ErrorLoadingPipeline.DialogMessage"),
+ ex);
+ }
+ }
+
+ private void editPipeline() {
+ try {
+ if (Utils.isEmpty(wPath.getText())) {
+ return;
+ }
+ HopGui.getInstance().fileDelegate.fileOpen(variables.resolve(wPath.getText()));
+ } catch (Exception ex) {
+ new ErrorDialog(
+ shell,
+ BaseMessages.getString(PKG, "MultiMappingDialog.ErrorLoadingPipeline.DialogTitle"),
+ BaseMessages.getString(PKG, "MultiMappingDialog.ErrorLoadingPipeline.DialogMessage"),
+ ex);
+ }
+ }
+
+ private void loadPipelineFile(String fname) throws HopException {
+ mappingPipelineMeta = new PipelineMeta(variables.resolve(fname), metadataProvider, variables);
+ mappingPipelineMeta.clearChanged();
+ }
+
+ void loadPipeline() throws HopException {
+ String filename = wPath.getText();
+ if (Utils.isEmpty(filename)) {
+ return;
+ }
+ if (!filename.endsWith(".hpl")) {
+ filename = filename + ".hpl";
+ wPath.setText(filename);
+ }
+ loadPipelineFile(filename);
+ }
+
+ public void getData() {
+ wPath.setText(Const.NVL(mappingMeta.getFilename(), ""));
+ addParametersTab(workingMappings.getMappingParameters());
+ for (MultiMappingInputDefinition definition : workingMappings.getInputMappings()) {
+ addInputTab(definition, false);
+ }
+ for (MultiMappingOutputDefinition definition : workingMappings.getOutputMappings()) {
+ addOutputTab(definition, false);
+ }
+ wTabFolder.setSelection(0);
+
+ Pattern p = Pattern.compile("^[/\\w]*(\\$\\{\\w+})[/.\\w]*");
+ Matcher m = p.matcher(Const.NVL(mappingMeta.getFilename(), ""));
+ if (!m.lookingAt()) {
+ try {
+ loadPipeline();
+ } catch (Exception e) {
+ // Ignore errors while populating the dialog
+ }
+ }
+ try {
+ List<PipelineRunConfiguration> runConfigs =
+ metadataProvider.getSerializer(PipelineRunConfiguration.class).loadAll();
+ for (PipelineRunConfiguration runConfig : runConfigs) {
+ if (runConfig.getEngineRunConfiguration() instanceof LocalPipelineRunConfiguration) {
+ wRunConfig.add(runConfig.getName());
+ }
+ }
+ wRunConfig.setText(Const.NVL(mappingMeta.getRunConfigurationName(), ""));
+ } catch (Exception e) {
+ LogChannel.UI.logError("Error loading pipeline run configurations", e);
+ }
+ }
+
+ private void addParametersTab(MappingParameters parameters) {
+ CTabItem wParametersTab = new CTabItem(wTabFolder, SWT.NONE);
+ wParametersTab.setFont(GuiResource.getInstance().getFontDefault());
+ wParametersTab.setText(BaseMessages.getString(PKG, "MultiMappingDialog.Parameters.Title"));
+ wParametersTab.setToolTipText(
+ BaseMessages.getString(PKG, "MultiMappingDialog.Parameters.Tooltip"));
+
+ Composite composite = new Composite(wTabFolder, SWT.NONE);
+ PropsUi.setLook(composite);
+ FormLayout layout = new FormLayout();
+ layout.marginWidth = 15;
+ layout.marginHeight = 15;
+ composite.setLayout(layout);
+
+ Button wInheritAll = new Button(composite, SWT.CHECK);
+ PropsUi.setLook(wInheritAll);
+ wInheritAll.setText(BaseMessages.getString(PKG, "MultiMappingDialog.Parameters.InheritAll"));
+ FormData fdInherit = new FormData();
+ fdInherit.left = new FormAttachment(0, 0);
+ fdInherit.bottom = new FormAttachment(100, 0);
+ wInheritAll.setLayoutData(fdInherit);
+ wInheritAll.setSelection(parameters.isInheritingAllVariables());
+
+ ColumnInfo[] colinfo =
+ new ColumnInfo[] {
+ new ColumnInfo(
+ BaseMessages.getString(PKG, "MultiMappingDialog.Parameters.column.Variable"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false,
+ false),
+ new ColumnInfo(
+ BaseMessages.getString(PKG, "MultiMappingDialog.Parameters.column.ValueOrField"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false,
+ false),
+ };
+ colinfo[1].setUsingVariables(true);
+ TableView wMappingParameters =
+ new TableView(
+ variables,
+ composite,
+ SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER,
+ colinfo,
+ Math.max(parameters.getVariableMappings().size(), 1),
+ false,
+ lsMod,
+ props,
+ false);
+ PropsUi.setLook(wMappingParameters);
+ FormData fdMappings = new FormData();
+ fdMappings.left = new FormAttachment(0, 0);
+ fdMappings.right = new FormAttachment(100, 0);
+ fdMappings.top = new FormAttachment(0, 0);
+ fdMappings.bottom = new FormAttachment(wInheritAll, -10);
+ wMappingParameters.setLayoutData(fdMappings);
+ wMappingParameters.getTable().addListener(SWT.Resize, new ColumnsResizer(0, 50, 50));
+
+ for (int i = 0; i < parameters.getVariableMappings().size(); i++) {
+ MappingVariableMapping mapping = parameters.getVariableMappings().get(i);
+ TableItem tableItem = wMappingParameters.table.getItem(i);
+ tableItem.setText(1, Const.NVL(mapping.getName(), ""));
+ tableItem.setText(2, Const.NVL(mapping.getValue(), ""));
+ }
+ wMappingParameters.setRowNums();
+ wMappingParameters.optWidth(true);
+
+ wParametersTab.setControl(composite);
+ changeList.add(
+ () -> {
+ parameters.getVariableMappings().clear();
+ for (TableItem item : wMappingParameters.getNonEmptyItems()) {
+ parameters
+ .getVariableMappings()
+ .add(new MappingVariableMapping(item.getText(1), item.getText(2)));
+ }
+ parameters.setInheritingAllVariables(wInheritAll.getSelection());
+ });
+ }
+
+ private void addInputTab(MultiMappingInputDefinition definition, boolean addToModel) {
+ if (addToModel) {
+ workingMappings.getInputMappings().add(definition);
+ mappingMeta.setChanged();
+ }
+ CTabItem tab = new CTabItem(wTabFolder, SWT.CLOSE);
+ tab.setFont(GuiResource.getInstance().getFontDefault());
+ tab.setData(definition);
+ tab.setText(
+ tabTitle(
+ BaseMessages.getString(PKG, "MultiMappingDialog.InputTab.Title"),
+ definition.getInputTransformName()));
+
+ Composite composite = new Composite(wTabFolder, SWT.NONE);
+ PropsUi.setLook(composite);
+ FormLayout layout = new FormLayout();
+ layout.marginWidth = 15;
+ layout.marginHeight = 15;
+ composite.setLayout(layout);
+
+ Label wlSource = new Label(composite, SWT.RIGHT);
+ PropsUi.setLook(wlSource);
+ wlSource.setText(
+ BaseMessages.getString(PKG, "MultiMappingDialog.InputTab.label.InputSourceTransformName"));
+ FormData fdlSource = new FormData();
+ fdlSource.left = new FormAttachment(0, 0);
+ fdlSource.top = new FormAttachment(0, 0);
+ fdlSource.right = new FormAttachment(middle, -margin);
+ wlSource.setLayoutData(fdlSource);
+ CCombo wSource = new CCombo(composite, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+ PropsUi.setLook(wSource);
+ wSource.setItems(pipelineMeta.getPrevTransformNames(transformMeta));
+ wSource.setText(Const.NVL(definition.getInputTransformName(), ""));
+ FormData fdSource = new FormData();
+ fdSource.left = new FormAttachment(wlSource, margin);
+ fdSource.top = new FormAttachment(wlSource, 0, SWT.CENTER);
+ fdSource.right = new FormAttachment(100, 0);
+ wSource.setLayoutData(fdSource);
+
+ Label wlTarget = new Label(composite, SWT.RIGHT);
+ PropsUi.setLook(wlTarget);
+ wlTarget.setText(
+ BaseMessages.getString(PKG, "MultiMappingDialog.InputTab.label.OutputTargetTransformName"));
+ FormData fdlTarget = new FormData();
+ fdlTarget.left = new FormAttachment(0, 0);
+ fdlTarget.top = new FormAttachment(wSource, margin);
+ fdlTarget.right = new FormAttachment(middle, -margin);
+ wlTarget.setLayoutData(fdlTarget);
+ CCombo wTarget = new CCombo(composite, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+ PropsUi.setLook(wTarget);
+ wTarget.setItems(childTransformNames(true));
+ wTarget.setText(Const.NVL(definition.getOutputTransformName(), ""));
+ FormData fdTarget = new FormData();
+ fdTarget.left = new FormAttachment(wlTarget, margin);
+ fdTarget.top = new FormAttachment(wlTarget, 0, SWT.CENTER);
+ fdTarget.right = new FormAttachment(100, 0);
+ wTarget.setLayoutData(fdTarget);
+
+ Label wlDesc = new Label(composite, SWT.RIGHT);
+ PropsUi.setLook(wlDesc);
+ wlDesc.setText(BaseMessages.getString(PKG, "MultiMappingDialog.InputTab.label.Description"));
+ FormData fdlDesc = new FormData();
+ fdlDesc.left = new FormAttachment(0, 0);
+ fdlDesc.top = new FormAttachment(wTarget, margin);
+ fdlDesc.right = new FormAttachment(middle, -margin);
+ wlDesc.setLayoutData(fdlDesc);
+ org.eclipse.swt.widgets.Text wDesc =
+ new org.eclipse.swt.widgets.Text(composite, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+ PropsUi.setLook(wDesc);
+ wDesc.setText(Const.NVL(definition.getDescription(), ""));
+ FormData fdDesc = new FormData();
+ fdDesc.left = new FormAttachment(wlDesc, margin);
+ fdDesc.top = new FormAttachment(wlDesc, 0, SWT.CENTER);
+ fdDesc.right = new FormAttachment(100, 0);
+ wDesc.setLayoutData(fdDesc);
+
+ Button wMain = new Button(composite, SWT.CHECK);
+ PropsUi.setLook(wMain);
+ wMain.setText(BaseMessages.getString(PKG, "MultiMappingDialog.input.MainDataPath"));
+ wMain.setSelection(definition.isMainDataPath());
+ FormData fdMain = new FormData();
+ fdMain.left = new FormAttachment(middle, margin);
+ fdMain.top = new FormAttachment(wDesc, margin);
+ wMain.setLayoutData(fdMain);
+
+ Button wRename = new Button(composite, SWT.CHECK);
+ PropsUi.setLook(wRename);
+ wRename.setText(BaseMessages.getString(PKG, "MultiMappingDialog.input.RenamingOnOutput"));
+ wRename.setSelection(definition.isRenamingOnOutput());
+ FormData fdRename = new FormData();
+ fdRename.left = new FormAttachment(middle, margin);
+ fdRename.top = new FormAttachment(wMain, margin);
+ wRename.setLayoutData(fdRename);
+
+ Button wbEnterMapping = new Button(composite, SWT.PUSH);
+ PropsUi.setLook(wbEnterMapping);
+ wbEnterMapping.setText(BaseMessages.getString(PKG, "MultiMappingDialog.button.EnterMapping"));
+ FormData fdbEnter = new FormData();
+ fdbEnter.bottom = new FormAttachment(100);
+ fdbEnter.right = new FormAttachment(100);
+ wbEnterMapping.setLayoutData(fdbEnter);
+
+ ColumnInfo[] colinfo =
+ new ColumnInfo[] {
+ new ColumnInfo(
+ BaseMessages.getString(PKG, "MultiMappingDialog.InputTab.column.SourceField"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false,
+ false),
+ new ColumnInfo(
+ BaseMessages.getString(PKG, "MultiMappingDialog.InputTab.column.TargetField"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false,
+ false),
+ };
+ TableView wFieldMappings =
+ new TableView(
+ variables,
+ composite,
+ SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER,
+ colinfo,
+ 1,
+ false,
+ lsMod,
+ props,
+ false);
+ FormData fdFields = new FormData();
+ fdFields.left = new FormAttachment(0, 0);
+ fdFields.right = new FormAttachment(100, 0);
+ fdFields.top = new FormAttachment(wRename, margin);
+ fdFields.bottom = new FormAttachment(wbEnterMapping, -10);
+ wFieldMappings.setLayoutData(fdFields);
+ populateRenames(wFieldMappings, definition.getValueRenames());
+
+ wbEnterMapping.addListener(
+ SWT.Selection,
+ e -> {
+ try {
+ loadPipeline();
+ IRowMeta sourceRowMeta =
+ Utils.isEmpty(wSource.getText())
+ ? pipelineMeta.getPrevTransformFields(variables, transformMeta)
+ : pipelineMeta.getTransformFields(
+ variables, pipelineMeta.findTransform(wSource.getText()));
+ TransformMeta mappingInput =
+ SimpleMappingMeta.findMappingInputTransform(mappingPipelineMeta, wTarget.getText());
+ IRowMeta targetRowMeta =
+ mappingPipelineMeta.getTransformFields(variables, mappingInput);
+ EnterMappingDialog dialog =
+ new EnterMappingDialog(
+ shell, sourceRowMeta.getFieldNames(), targetRowMeta.getFieldNames());
+ List<SourceToTargetMapping> mappings = dialog.open();
+ if (mappings != null) {
+ wFieldMappings.clearAll(false);
+ for (SourceToTargetMapping mapping : mappings) {
+ TableItem item = new TableItem(wFieldMappings.table, SWT.NONE);
+ item.setText(1, mapping.getSourceString(sourceRowMeta.getFieldNames()));
+ item.setText(2, mapping.getTargetString(targetRowMeta.getFieldNames()));
+ }
+ wFieldMappings.removeEmptyRows();
+ wFieldMappings.setRowNums();
+ wFieldMappings.optWidth(true);
+ }
+ } catch (Exception ex) {
+ new ErrorDialog(
+ shell,
+ BaseMessages.getString(PKG, "System.Dialog.Error.Title"),
+ BaseMessages.getString(
+ PKG,
+ "MultiMappingDialog.Exception.ErrorGettingMappingSourceAndTargetFields",
+ ex.toString()),
+ ex);
+ }
+ });
+
+ tab.setControl(composite);
+ wTabFolder.setSelection(tab);
+ changeList.add(
+ () -> {
+ definition.setInputTransformName(wSource.getText());
+ definition.setOutputTransformName(wTarget.getText());
+ definition.setDescription(wDesc.getText());
+ definition.setMainDataPath(wMain.getSelection());
+ definition.setRenamingOnOutput(wRename.getSelection());
+ definition.getValueRenames().clear();
+ for (TableItem item : wFieldMappings.getNonEmptyItems()) {
+ definition
+ .getValueRenames()
+ .add(new MultiMappingInputRename(item.getText(1), item.getText(2)));
+ }
+ });
+ }
+
+ private void addOutputTab(MultiMappingOutputDefinition definition, boolean addToModel) {
+ if (addToModel) {
+ workingMappings.getOutputMappings().add(definition);
+ mappingMeta.setChanged();
+ }
+ CTabItem tab = new CTabItem(wTabFolder, SWT.CLOSE);
+ tab.setFont(GuiResource.getInstance().getFontDefault());
+ tab.setData(definition);
+ tab.setText(
+ tabTitle(
+ BaseMessages.getString(PKG, "MultiMappingDialog.OutputTab.Title"),
+ definition.getOutputTransformName()));
+
+ Composite composite = new Composite(wTabFolder, SWT.NONE);
+ PropsUi.setLook(composite);
+ FormLayout layout = new FormLayout();
+ layout.marginWidth = 15;
+ layout.marginHeight = 15;
+ composite.setLayout(layout);
+
+ Label wlSource = new Label(composite, SWT.RIGHT);
+ PropsUi.setLook(wlSource);
+ wlSource.setText(
+ BaseMessages.getString(PKG, "MultiMappingDialog.OutputTab.label.InputSourceTransformName"));
+ FormData fdlSource = new FormData();
+ fdlSource.left = new FormAttachment(0, 0);
+ fdlSource.top = new FormAttachment(0, 0);
+ fdlSource.right = new FormAttachment(middle, -margin);
+ wlSource.setLayoutData(fdlSource);
+ CCombo wSource = new CCombo(composite, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+ PropsUi.setLook(wSource);
+ wSource.setItems(childTransformNames(false));
+ wSource.setText(Const.NVL(definition.getInputTransformName(), ""));
+ FormData fdSource = new FormData();
+ fdSource.left = new FormAttachment(wlSource, margin);
+ fdSource.top = new FormAttachment(wlSource, 0, SWT.CENTER);
+ fdSource.right = new FormAttachment(100, 0);
+ wSource.setLayoutData(fdSource);
+
+ Label wlTarget = new Label(composite, SWT.RIGHT);
+ PropsUi.setLook(wlTarget);
+ wlTarget.setText(
+ BaseMessages.getString(
+ PKG, "MultiMappingDialog.OutputTab.label.OutputTargetTransformName"));
+ FormData fdlTarget = new FormData();
+ fdlTarget.left = new FormAttachment(0, 0);
+ fdlTarget.top = new FormAttachment(wSource, margin);
+ fdlTarget.right = new FormAttachment(middle, -margin);
+ wlTarget.setLayoutData(fdlTarget);
+ CCombo wTarget = new CCombo(composite, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+ PropsUi.setLook(wTarget);
+ wTarget.setItems(pipelineMeta.getNextTransformNames(transformMeta));
+ wTarget.setText(Const.NVL(definition.getOutputTransformName(), ""));
+ FormData fdTarget = new FormData();
+ fdTarget.left = new FormAttachment(wlTarget, margin);
+ fdTarget.top = new FormAttachment(wlTarget, 0, SWT.CENTER);
+ fdTarget.right = new FormAttachment(100, 0);
+ wTarget.setLayoutData(fdTarget);
+
+ Label wlDesc = new Label(composite, SWT.RIGHT);
+ PropsUi.setLook(wlDesc);
+ wlDesc.setText(BaseMessages.getString(PKG, "MultiMappingDialog.OutputTab.label.Description"));
+ FormData fdlDesc = new FormData();
+ fdlDesc.left = new FormAttachment(0, 0);
+ fdlDesc.top = new FormAttachment(wTarget, margin);
+ fdlDesc.right = new FormAttachment(middle, -margin);
+ wlDesc.setLayoutData(fdlDesc);
+ org.eclipse.swt.widgets.Text wDesc =
+ new org.eclipse.swt.widgets.Text(composite, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+ PropsUi.setLook(wDesc);
+ wDesc.setText(Const.NVL(definition.getDescription(), ""));
+ FormData fdDesc = new FormData();
+ fdDesc.left = new FormAttachment(wlDesc, margin);
+ fdDesc.top = new FormAttachment(wlDesc, 0, SWT.CENTER);
+ fdDesc.right = new FormAttachment(100, 0);
+ wDesc.setLayoutData(fdDesc);
+
+ Button wMain = new Button(composite, SWT.CHECK);
+ PropsUi.setLook(wMain);
+ wMain.setText(BaseMessages.getString(PKG, "MultiMappingDialog.input.MainDataPath"));
+ wMain.setSelection(definition.isMainDataPath());
+ FormData fdMain = new FormData();
+ fdMain.left = new FormAttachment(middle, margin);
+ fdMain.top = new FormAttachment(wDesc, margin);
+ wMain.setLayoutData(fdMain);
+
+ Button wbGetFields = new Button(composite, SWT.PUSH);
+ PropsUi.setLook(wbGetFields);
+ wbGetFields.setText(BaseMessages.getString(PKG, "MultiMappingDialog.button.GetFields"));
+ FormData fdbGet = new FormData();
+ fdbGet.bottom = new FormAttachment(100);
+ fdbGet.right = new FormAttachment(100);
+ wbGetFields.setLayoutData(fdbGet);
+
+ ColumnInfo[] colinfo =
+ new ColumnInfo[] {
+ new ColumnInfo(
+ BaseMessages.getString(PKG, "MultiMappingDialog.OutputTab.column.SourceField"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false,
+ false),
+ new ColumnInfo(
+ BaseMessages.getString(PKG, "MultiMappingDialog.OutputTab.column.TargetField"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false,
+ false),
+ };
+ TableView wFieldMappings =
+ new TableView(
+ variables,
+ composite,
+ SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER,
+ colinfo,
+ 1,
+ false,
+ lsMod,
+ props,
+ false);
+ FormData fdFields = new FormData();
+ fdFields.left = new FormAttachment(0, 0);
+ fdFields.right = new FormAttachment(100, 0);
+ fdFields.top = new FormAttachment(wMain, margin);
+ fdFields.bottom = new FormAttachment(wbGetFields, -10);
+ wFieldMappings.setLayoutData(fdFields);
+ populateOutputRenames(wFieldMappings, definition.getValueRenames());
+
+ wbGetFields.addListener(
+ SWT.Selection,
+ e -> {
+ try {
+ loadPipeline();
+ TransformMeta mappingOutput =
+ SimpleMappingMeta.findMappingOutputTransform(
+ mappingPipelineMeta, wSource.getText());
+ IRowMeta sourceRowMeta =
+ mappingPipelineMeta.getTransformFields(variables, mappingOutput);
+ BaseTransformDialog.getFieldsFromPrevious(
+ sourceRowMeta,
+ wFieldMappings,
+ 1,
+ new int[] {1},
+ new int[] {},
+ -1,
+ -1,
+ (tableItem, v) -> {
+ tableItem.setText(2, tableItem.getText(1));
+ return true;
+ });
+ } catch (Exception ex) {
+ new ErrorDialog(
+ shell,
+ BaseMessages.getString(PKG, "System.Dialog.Error.Title"),
+ BaseMessages.getString(
+ PKG,
+ "MultiMappingDialog.Exception.ErrorGettingMappingSourceAndTargetFields",
+ ex.toString()),
+ ex);
+ }
+ });
+
+ tab.setControl(composite);
+ wTabFolder.setSelection(tab);
+ changeList.add(
+ () -> {
+ definition.setInputTransformName(wSource.getText());
+ definition.setOutputTransformName(wTarget.getText());
+ definition.setDescription(wDesc.getText());
+ definition.setMainDataPath(wMain.getSelection());
+ definition.getValueRenames().clear();
+ for (TableItem item : wFieldMappings.getNonEmptyItems()) {
+ definition
+ .getValueRenames()
+ .add(new MultiMappingOutputRename(item.getText(1), item.getText(2)));
+ }
+ });
+ }
+
+ private void populateRenames(TableView view, List<MultiMappingInputRename> renames) {
+ for (MultiMappingInputRename rename : renames) {
+ TableItem item = new TableItem(view.table, SWT.NONE);
+ item.setText(1, Const.NVL(rename.getSourceValueName(), ""));
+ item.setText(2, Const.NVL(rename.getTargetValueName(), ""));
+ }
+ view.removeEmptyRows();
+ view.setRowNums();
+ view.optWidth(true);
+ }
+
+ private void populateOutputRenames(TableView view, List<MultiMappingOutputRename> renames) {
+ for (MultiMappingOutputRename rename : renames) {
+ TableItem item = new TableItem(view.table, SWT.NONE);
+ item.setText(1, Const.NVL(rename.getSourceValueName(), ""));
+ item.setText(2, Const.NVL(rename.getTargetValueName(), ""));
+ }
+ view.removeEmptyRows();
+ view.setRowNums();
+ view.optWidth(true);
+ }
+
+ private String[] childTransformNames(boolean mappingInput) {
+ if (mappingPipelineMeta == null) {
+ try {
+ loadPipeline();
+ } catch (Exception e) {
+ return new String[0];
+ }
+ }
+ if (mappingPipelineMeta == null) {
+ return new String[0];
+ }
+ List<TransformMeta> transforms =
+ mappingInput
+ ? MappingTransforms.findMappingInputMetas(mappingPipelineMeta)
+ : MappingTransforms.findMappingOutputMetas(mappingPipelineMeta);
+ return transforms.stream().map(TransformMeta::getName).toArray(String[]::new);
+ }
+
+ private String tabTitle(String base, String transformName) {
+ if (Utils.isEmpty(transformName)) {
+ return base;
+ }
+ return base + " : " + transformName;
+ }
+
+ private void cancel() {
+ transformName = null;
+ mappingMeta.setChanged(changed);
+ dispose();
+ }
+
+ private void ok() {
+ if (Utils.isEmpty(wTransformName.getText())) {
+ return;
+ }
+ if (Utils.isEmpty(wPath.getText())) {
+ MessageBox mb = new MessageBox(shell, SWT.OK | SWT.ICON_ERROR);
+ mb.setText(BaseMessages.getString(PKG, "MultiMappingDialog.FilenameMissing.Header"));
+ mb.setMessage(BaseMessages.getString(PKG, "MultiMappingDialog.FilenameMissing.Message"));
+ mb.open();
+ return;
+ }
+ if (isSelfReferencing()) {
+ MessageBox mb = new MessageBox(shell, SWT.OK | SWT.ICON_ERROR);
+ mb.setText(BaseMessages.getString(PKG, "MultiMappingDialog.SelfReference.Header"));
+ mb.setMessage(BaseMessages.getString(PKG, "MultiMappingDialog.SelfReference.Message"));
+ mb.open();
+ return;
+ }
+
+ transformName = wTransformName.getText();
+ mappingMeta.setFilename(wPath.getText());
+ mappingMeta.setRunConfigurationName(wRunConfig.getText());
+
+ Pattern p = Pattern.compile("^[/\\w]*(\\$\\{\\w+})[/.\\w]*");
+ Matcher m = p.matcher(mappingMeta.getFilename());
+ if (!m.lookingAt()) {
+ try {
+ loadPipeline();
+ } catch (HopException e) {
+ new ErrorDialog(
+ shell,
+ BaseMessages.getString(PKG, "MultiMappingDialog.ErrorLoadingSpecifiedPipeline.Title"),
+ BaseMessages.getString(PKG, "MultiMappingDialog.ErrorLoadingSpecifiedPipeline.Message"),
+ e);
+ return;
+ }
+ }
+
+ for (Runnable applyChanges : changeList) {
+ applyChanges.run();
+ }
+ mappingMeta.setIoMappings(workingMappings);
+ mappingMeta.resetTransformIoMeta();
+ mappingMeta.setChanged(true);
+ dispose();
+ }
+
+ private boolean isSelfReferencing() {
+ return variables.resolve(wPath.getText()).equals(variables.resolve(pipelineMeta.getFilename()));
+ }
+}
diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingInputDefinition.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingInputDefinition.java
new file mode 100644
index 0000000000..2ebf6269c9
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingInputDefinition.java
@@ -0,0 +1,139 @@
+/*
+ * 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.multimapping;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transforms.mapping.MappingIODefinition;
+import org.apache.hop.pipeline.transforms.mapping.MappingValueRename;
+
+/** Parent source → child Mapping Input. */
+@Getter
+@Setter
+public class MultiMappingInputDefinition implements Cloneable {
+
+ @SuppressWarnings("java:S2065")
+ private transient TransformMeta inputTransform;
+
+ @HopMetadataProperty(
+ key = "input_transform",
+ injectionKey = "INPUT_SOURCE_TRANSFORM",
+ injectionKeyDescription = "MultiMappingMeta.Injection.INPUT_SOURCE_TRANSFORM")
+ private String inputTransformName;
+
+ @HopMetadataProperty(
+ key = "output_transform",
+ injectionKey = "INPUT_MAPPING_TRANSFORM",
+ injectionKeyDescription = "MultiMappingMeta.Injection.INPUT_MAPPING_TRANSFORM")
+ private String outputTransformName;
+
+ @HopMetadataProperty(
+ key = "description",
+ injectionKey = "INPUT_DESCRIPTION",
+ injectionKeyDescription = "MultiMappingMeta.Injection.INPUT_DESCRIPTION")
+ private String description;
+
+ @HopMetadataProperty(
+ key = "connector",
+ injectionGroupKey = "INPUT_RENAMES",
+ injectionGroupDescription = "MultiMappingMeta.Injection.INPUT_RENAMES")
+ private List<MultiMappingInputRename> valueRenames;
+
+ @HopMetadataProperty(
+ key = "main_path",
+ injectionKey = "INPUT_MAIN_PATH",
+ injectionKeyDescription = "MultiMappingMeta.Injection.INPUT_MAIN_PATH")
+ private boolean mainDataPath;
+
+ @HopMetadataProperty(
+ key = "rename_on_output",
+ injectionKey = "INPUT_RENAME_ON_OUTPUT",
+ injectionKeyDescription = "MultiMappingMeta.Injection.INPUT_RENAME_ON_OUTPUT")
+ private boolean renamingOnOutput;
+
+ public MultiMappingInputDefinition() {
+ this.valueRenames = new ArrayList<>();
+ }
+
+ public MultiMappingInputDefinition(String inputTransformName, String outputTransformName) {
+ this();
+ this.inputTransformName = inputTransformName;
+ this.outputTransformName = outputTransformName;
+ }
+
+ public MultiMappingInputDefinition(MultiMappingInputDefinition d) {
+ this();
+ copyFrom(d);
+ }
+
+ public void copyFrom(MultiMappingInputDefinition d) {
+ this.inputTransformName = d.inputTransformName;
+ this.outputTransformName = d.outputTransformName;
+ this.description = d.description;
+ this.mainDataPath = d.mainDataPath;
+ this.renamingOnOutput = d.renamingOnOutput;
+ this.inputTransform = d.inputTransform;
+ this.valueRenames.clear();
+ for (MultiMappingInputRename rename : d.valueRenames) {
+ this.valueRenames.add(new MultiMappingInputRename(rename));
+ }
+ }
+
+ @Override
+ public MultiMappingInputDefinition clone() {
+ return new MultiMappingInputDefinition(this);
+ }
+
+ public MappingIODefinition toIoDefinition() {
+ MappingIODefinition definition =
+ new MappingIODefinition(inputTransformName, outputTransformName);
+ definition.setDescription(description);
+ definition.setMainDataPath(mainDataPath);
+ definition.setRenamingOnOutput(renamingOnOutput);
+ definition.setInputTransform(inputTransform);
+ List<MappingValueRename> renames = new ArrayList<>();
+ for (MultiMappingInputRename rename : valueRenames) {
+ renames.add(rename.toValueRename());
+ }
+ definition.setValueRenames(renames);
+ return definition;
+ }
+
+ public static MultiMappingInputDefinition fromIoDefinition(MappingIODefinition d) {
+ MultiMappingInputDefinition definition = new MultiMappingInputDefinition();
+ if (d == null) {
+ return definition;
+ }
+ definition.inputTransformName = d.getInputTransformName();
+ definition.outputTransformName = d.getOutputTransformName();
+ definition.description = d.getDescription();
+ definition.mainDataPath = d.isMainDataPath();
+ definition.renamingOnOutput = d.isRenamingOnOutput();
+ definition.inputTransform = d.getInputTransform();
+ if (d.getValueRenames() != null) {
+ for (MappingValueRename rename : d.getValueRenames()) {
+ definition.valueRenames.add(MultiMappingInputRename.fromValueRename(rename));
+ }
+ }
+ return definition;
+ }
+}
diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingInputRename.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingInputRename.java
new file mode 100644
index 0000000000..9156a8443c
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingInputRename.java
@@ -0,0 +1,68 @@
+/*
+ * 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.multimapping;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.pipeline.transforms.mapping.MappingValueRename;
+
+@Getter
+@Setter
+public class MultiMappingInputRename implements Cloneable {
+
+ @HopMetadataProperty(
+ key = "parent",
+ injectionKey = "INPUT_RENAME_SOURCE",
+ injectionKeyDescription = "MultiMappingMeta.Injection.INPUT_RENAME_SOURCE")
+ private String sourceValueName;
+
+ @HopMetadataProperty(
+ key = "child",
+ injectionKey = "INPUT_RENAME_TARGET",
+ injectionKeyDescription = "MultiMappingMeta.Injection.INPUT_RENAME_TARGET")
+ private String targetValueName;
+
+ public MultiMappingInputRename() {}
+
+ public MultiMappingInputRename(String sourceValueName, String targetValueName) {
+ this.sourceValueName = sourceValueName == null ? "" : sourceValueName;
+ this.targetValueName = targetValueName == null ? "" : targetValueName;
+ }
+
+ public MultiMappingInputRename(MultiMappingInputRename rename) {
+ this.sourceValueName = rename.sourceValueName;
+ this.targetValueName = rename.targetValueName;
+ }
+
+ @Override
+ public MultiMappingInputRename clone() {
+ return new MultiMappingInputRename(this);
+ }
+
+ public MappingValueRename toValueRename() {
+ return new MappingValueRename(sourceValueName, targetValueName);
+ }
+
+ public static MultiMappingInputRename fromValueRename(MappingValueRename rename) {
+ if (rename == null) {
+ return new MultiMappingInputRename();
+ }
+ return new MultiMappingInputRename(rename.getSourceValueName(), rename.getTargetValueName());
+ }
+}
diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingMeta.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingMeta.java
new file mode 100644
index 0000000000..a2b921f584
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingMeta.java
@@ -0,0 +1,610 @@
+/*
+ * 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.multimapping;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.CheckResult;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.ICheckResult;
+import org.apache.hop.core.annotations.ActionTransformType;
+import org.apache.hop.core.annotations.Transform;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopTransformException;
+import org.apache.hop.core.file.IHasFilename;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.util.Utils;
+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.HopMetadataPropertyType;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.pipeline.ISubPipelineAwareMeta;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.PipelineMeta.PipelineType;
+import org.apache.hop.pipeline.TransformWithMappingMeta;
+import org.apache.hop.pipeline.transform.ITransformIOMeta;
+import org.apache.hop.pipeline.transform.TransformIOMeta;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transform.stream.IStream;
+import org.apache.hop.pipeline.transform.stream.IStream.StreamType;
+import org.apache.hop.pipeline.transform.stream.Stream;
+import org.apache.hop.pipeline.transform.stream.StreamIcon;
+import org.apache.hop.pipeline.transforms.input.MappingInputMeta;
+import org.apache.hop.pipeline.transforms.mapping.MappingIODefinition;
+import org.apache.hop.pipeline.transforms.mapping.MappingParameters;
+import org.apache.hop.pipeline.transforms.mapping.MappingTransforms;
+import org.apache.hop.pipeline.transforms.mapping.MappingValueRename;
+import org.apache.hop.pipeline.transforms.mapping.SimpleMappingMeta;
+import org.apache.hop.resource.ResourceEntry;
+import org.apache.hop.resource.ResourceEntry.ResourceType;
+import org.apache.hop.resource.ResourceReference;
+
+/** Meta-data for the Multi Mapping transform: 0..N inputs and outputs to a child pipeline. */
+@Transform(
+ id = "MultiMapping",
+ name = "i18n::MultiMapping.Name",
+ description = "i18n::MultiMapping.Description",
+ image = "MMAP.svg",
+ categoryDescription = "i18n:org.apache.hop.pipeline.transform:BaseTransform.Category.Mapping",
+ keywords = "i18n::MultiMappingMeta.keyword",
+ documentationUrl = "/pipeline/transforms/multi-mapping.html",
+ actionTransformTypes = {ActionTransformType.HOP_FILE, ActionTransformType.HOP_PIPELINE})
+@Getter
+@Setter
+public class MultiMappingMeta extends TransformWithMappingMeta<MultiMapping, MultiMappingData>
+ implements ISubPipelineAwareMeta {
+
+ private static final Class<?> PKG = MultiMappingMeta.class;
+
+ private static final IStream NEW_INFO_STREAM =
+ new Stream(
+ StreamType.INFO,
+ null,
+ BaseMessages.getString(PKG, "MultiMappingMeta.InfoStream.New.Description"),
+ StreamIcon.INFO,
+ null);
+
+ private static final IStream NEW_TARGET_STREAM =
+ new Stream(
+ StreamType.TARGET,
+ null,
+ BaseMessages.getString(PKG, "MultiMappingMeta.TargetStream.New.Description"),
+ StreamIcon.TARGET,
+ null);
+
+ @HopMetadataProperty(
+ key = "runConfiguration",
+ hopMetadataPropertyType = HopMetadataPropertyType.PIPELINE_RUN_CONFIG,
+ injectionKey = "RUN_CONFIGURATION",
+ injectionKeyDescription = "MultiMappingMeta.Injection.RUN_CONFIGURATION")
+ private String runConfigurationName;
+
+ @HopMetadataProperty(key = "mappings")
+ private MultiIOMappings ioMappings;
+
+ public MultiMappingMeta() {
+ super();
+ ioMappings = new MultiIOMappings();
+ }
+
+ public MultiMappingMeta(MultiMappingMeta m) {
+ super(m);
+ this.runConfigurationName = m.runConfigurationName;
+ this.ioMappings =
+ m.ioMappings != null ? new MultiIOMappings(m.ioMappings) : new MultiIOMappings();
+ }
+
+ @Override
+ public MultiMappingMeta clone() {
+ return new MultiMappingMeta(this);
+ }
+
+ @Override
+ public void setDefault() {
+ MultiMappingInputDefinition inputDefinition = new MultiMappingInputDefinition(null, null);
+ inputDefinition.setMainDataPath(true);
+ inputDefinition.setRenamingOnOutput(true);
+ ioMappings.getInputMappings().add(inputDefinition);
+
+ MultiMappingOutputDefinition outputDefinition = new MultiMappingOutputDefinition(null, null);
+ outputDefinition.setMainDataPath(true);
+ ioMappings.getOutputMappings().add(outputDefinition);
+ }
+
+ public MappingParameters getMappingParameters() {
+ return ioMappings.getMappingParameters();
+ }
+
+ public void setMappingParameters(MappingParameters mappingParameters) {
+ ioMappings.setMappingParameters(mappingParameters);
+ }
+
+ public List<MultiMappingInputDefinition> getInputMappings() {
+ return ioMappings.getInputMappings();
+ }
+
+ public List<MultiMappingOutputDefinition> getOutputMappings() {
+ return ioMappings.getOutputMappings();
+ }
+
+ @Override
+ public void getFields(
+ IRowMeta row,
+ String origin,
+ IRowMeta[] info,
+ TransformMeta nextTransform,
+ IVariables variables,
+ IHopMetadataProvider metadataProvider)
+ throws HopTransformException {
+ PipelineMeta mappingPipelineMeta;
+ try {
+ mappingPipelineMeta = loadMappingMeta(this, metadataProvider, variables);
+ } catch (HopException e) {
+ throw new HopTransformException(
+ BaseMessages.getString(PKG, "MultiMappingMeta.Exception.UnableToLoadMappingPipeline"), e);
+ }
+
+ List<MappingValueRename> inputRenameList = new ArrayList<>();
+ String[] infoTransforms = getInfoTransforms();
+
+ for (MultiMappingInputDefinition inputDefinition : ioMappings.getInputMappings()) {
+ MappingIODefinition definition = inputDefinition.toIoDefinition();
+ IRowMeta inputRowMeta = resolveInputRowMeta(row, info, infoTransforms, definition);
+
+ TransformMeta mappingInputTransform =
+ SimpleMappingMeta.findMappingInputTransform(
+ mappingPipelineMeta, definition.getOutputTransformName());
+ MappingInputMeta mappingInputMeta = (MappingInputMeta) mappingInputTransform.getTransform();
+ mappingInputMeta.setInputRowMeta(inputRowMeta);
+
+ if (definition.isRenamingOnOutput()) {
+ addInputRenames(inputRenameList, definition.getValueRenames());
+ }
+ }
+
+ MultiMappingOutputDefinition outputDefinition = findOutputDefinition(nextTransform);
+ if (outputDefinition == null) {
+ // No output mappings: this transform produces no fields.
+ row.clear();
+ return;
+ }
+
+ TransformMeta mappingOutputTransform =
+ SimpleMappingMeta.findMappingOutputTransform(
+ mappingPipelineMeta, outputDefinition.getInputTransformName());
+ IRowMeta mappingOutputRowMeta =
+ mappingPipelineMeta.getTransformFields(variables, mappingOutputTransform);
+
+ if (!inputRenameList.isEmpty()) {
+ for (MappingValueRename rename : inputRenameList) {
+ IValueMeta valueMeta = mappingOutputRowMeta.searchValueMeta(rename.getTargetValueName());
+ if (valueMeta != null) {
+ valueMeta.setName(rename.getSourceValueName());
+ }
+ }
+ }
+ for (MappingValueRename rename : outputDefinition.toIoDefinition().getValueRenames()) {
+ IValueMeta valueMeta = mappingOutputRowMeta.searchValueMeta(rename.getSourceValueName());
+ if (valueMeta != null) {
+ valueMeta.setName(rename.getTargetValueName());
+ }
+ }
+
+ row.clear();
+ row.addRowMeta(mappingOutputRowMeta);
+ }
+
+ private IRowMeta resolveInputRowMeta(
+ IRowMeta row, IRowMeta[] info, String[] infoTransforms, MappingIODefinition definition)
+ throws HopTransformException {
+ if (definition.isMainDataPath() || Utils.isEmpty(definition.getInputTransformName())) {
+ IRowMeta inputRowMeta = row.clone();
+ applyRenames(inputRowMeta, definition.getValueRenames());
+ return inputRowMeta;
+ }
+
+ int infoIndex =
+ infoTransforms == null
+ ? -1
+ : Const.indexOfString(definition.getInputTransformName(), infoTransforms);
+ if (infoIndex < 0) {
+ throw new HopTransformException(
+ BaseMessages.getString(
+ PKG,
+ "MultiMappingMeta.Exception.UnableToFindMetadataInfo",
+ definition.getInputTransformName()));
+ }
+ if (info != null && infoIndex < info.length && info[infoIndex] != null) {
+ IRowMeta inputRowMeta = info[infoIndex].clone();
+ applyRenames(inputRowMeta, definition.getValueRenames());
+ return inputRowMeta;
+ }
+ return null;
+ }
+
+ private static void applyRenames(IRowMeta inputRowMeta, List<MappingValueRename> valueRenames)
+ throws HopTransformException {
+ if (inputRowMeta == null || inputRowMeta.isEmpty() || valueRenames == null) {
+ return;
+ }
+ for (MappingValueRename valueRename : valueRenames) {
+ IValueMeta valueMeta = inputRowMeta.searchValueMeta(valueRename.getSourceValueName());
+ if (valueMeta == null) {
+ throw new HopTransformException(
+ BaseMessages.getString(
+ PKG,
+ "MultiMappingMeta.Exception.UnableToFindField",
+ valueRename.getSourceValueName()));
+ }
+ valueMeta.setName(valueRename.getTargetValueName());
+ }
+ }
+
+ MultiMappingOutputDefinition findOutputDefinition(TransformMeta nextTransform) {
+ if (nextTransform != null) {
+ for (MultiMappingOutputDefinition definition : ioMappings.getOutputMappings()) {
+ if (nextTransform.getName().equals(definition.getOutputTransformName())) {
+ return definition;
+ }
+ }
+ }
+ MultiMappingOutputDefinition main = null;
+ for (MultiMappingOutputDefinition definition : ioMappings.getOutputMappings()) {
+ if (definition.isMainDataPath() || Utils.isEmpty(definition.getOutputTransformName())) {
+ main = definition;
+ }
+ }
+ return main;
+ }
+
+ static void addInputRenames(
+ List<MappingValueRename> renameList, List<MappingValueRename> addRenameList) {
+ if (addRenameList == null) {
+ return;
+ }
+ for (MappingValueRename rename : addRenameList) {
+ if (renameList.indexOf(rename) < 0) {
+ renameList.add(rename);
+ }
+ }
+ }
+
+ public String[] getInfoTransforms() {
+ List<String> names = new ArrayList<>();
+ for (MultiMappingInputDefinition definition : ioMappings.getInputMappings()) {
+ if (MappingTransforms.isInfoMapping(definition.toIoDefinition())) {
+ names.add(definition.getInputTransformName());
+ }
+ }
+ return names.isEmpty() ? null : names.toArray(String[]::new);
+ }
+
+ public String[] getTargetTransforms() {
+ List<String> names = new ArrayList<>();
+ for (MultiMappingOutputDefinition definition : ioMappings.getOutputMappings()) {
+ if (MappingTransforms.isTargetMapping(definition.toIoDefinition())) {
+ names.add(definition.getOutputTransformName());
+ }
+ }
+ return names.isEmpty() ? null : names.toArray(String[]::new);
+ }
+
+ @Override
+ public void check(
+ List<ICheckResult> remarks,
+ PipelineMeta pipelineMeta,
+ TransformMeta transformMeta,
+ IRowMeta prev,
+ String[] input,
+ String[] output,
+ IRowMeta info,
+ IVariables variables,
+ IHopMetadataProvider metadataProvider) {
+ if (StringUtils.isEmpty(filename)) {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_ERROR,
+ BaseMessages.getString(PKG, "MultiMappingMeta.CheckResult.NoMappingSpecified"),
+ transformMeta));
+ return;
+ }
+
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_OK,
+ BaseMessages.getString(PKG, "MultiMappingMeta.CheckResult.MappingPipelineSpecified"),
+ transformMeta));
+
+ if (prev == null || prev.isEmpty()) {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_WARNING,
+ BaseMessages.getString(PKG, "MultiMappingMeta.CheckResult.NotReceivingAnyFields"),
+ transformMeta));
+ } else {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_OK,
+ BaseMessages.getString(
+ PKG, "MultiMappingMeta.CheckResult.TransformReceivingFields", prev.size() + ""),
+ transformMeta));
+ }
+
+ if (input.length > 0) {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_OK,
+ BaseMessages.getString(
+ PKG, "MultiMappingMeta.CheckResult.TransformReceivingFieldsFromOtherTransforms"),
+ transformMeta));
+ } else {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_WARNING,
+ BaseMessages.getString(PKG, "MultiMappingMeta.CheckResult.NoInputReceived"),
+ transformMeta));
+ }
+
+ try {
+ PipelineMeta mappingPipelineMeta = loadMappingMeta(this, metadataProvider, variables);
+ for (MultiMappingInputDefinition definition : ioMappings.getInputMappings()) {
+ if (StringUtils.isNotEmpty(definition.getInputTransformName())
+ && pipelineMeta.findTransform(definition.getInputTransformName()) == null) {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_ERROR,
+ BaseMessages.getString(
+ PKG,
+ "MultiMappingMeta.CheckResult.ParentTransformNotFound",
+ definition.getInputTransformName()),
+ transformMeta));
+ }
+ if (MappingTransforms.isInfoMapping(definition.toIoDefinition())
+ && Utils.isEmpty(definition.getInputTransformName())) {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_WARNING,
+ BaseMessages.getString(
+ PKG, "MultiMappingMeta.CheckResult.InfoMappingMissingSource"),
+ transformMeta));
+ }
+ SimpleMappingMeta.findMappingInputTransform(
+ mappingPipelineMeta, definition.getOutputTransformName());
+ }
+ for (MultiMappingOutputDefinition definition : ioMappings.getOutputMappings()) {
+ if (StringUtils.isNotEmpty(definition.getOutputTransformName())
+ && pipelineMeta.findTransform(definition.getOutputTransformName()) == null) {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_ERROR,
+ BaseMessages.getString(
+ PKG,
+ "MultiMappingMeta.CheckResult.ParentTransformNotFound",
+ definition.getOutputTransformName()),
+ transformMeta));
+ }
+ SimpleMappingMeta.findMappingOutputTransform(
+ mappingPipelineMeta, definition.getInputTransformName());
+ }
+ } catch (HopException e) {
+ remarks.add(
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_ERROR,
+ BaseMessages.getString(
+ PKG, "MultiMappingMeta.CheckResult.UnableToLoadMappingPipeline")
+ + " : "
+ + e.getMessage(),
+ transformMeta));
+ }
+ }
+
+ @Override
+ public List<ResourceReference> getResourceDependencies(
+ IVariables variables, TransformMeta transformMeta) {
+ List<ResourceReference> references = new ArrayList<>(5);
+ String realFilename = variables.resolve(filename);
+ ResourceReference reference = new ResourceReference(transformMeta);
+ references.add(reference);
+ if (StringUtils.isNotEmpty(realFilename)) {
+ reference.getEntries().add(new ResourceEntry(realFilename, ResourceType.ACTIONFILE));
+ }
+ return references;
+ }
+
+ @Override
+ public ITransformIOMeta getTransformIOMeta() {
+ ITransformIOMeta ioMeta = super.getTransformIOMeta(false);
+ if (ioMeta == null) {
+ ioMeta = new TransformIOMeta(true, true, true, false, true, true);
+ for (MultiMappingInputDefinition definition : ioMappings.getInputMappings()) {
+ MappingIODefinition ioDefinition = definition.toIoDefinition();
+ if (MappingTransforms.isInfoMapping(ioDefinition)) {
+ ioMeta.addStream(
+ new Stream(
+ StreamType.INFO,
+ definition.getInputTransform(),
+ BaseMessages.getString(PKG, "MultiMappingMeta.InfoStream.Description"),
+ StreamIcon.INFO,
+ definition.getInputTransformName()));
+ }
+ }
+ for (MultiMappingOutputDefinition definition : ioMappings.getOutputMappings()) {
+ MappingIODefinition ioDefinition = definition.toIoDefinition();
+ if (MappingTransforms.isTargetMapping(ioDefinition)) {
+ ioMeta.addStream(
+ new Stream(
+ StreamType.TARGET,
+ null,
+ BaseMessages.getString(
+ PKG,
+ "MultiMappingMeta.TargetStream.Description",
+ Const.NVL(definition.getOutputTransformName(), "")),
+ StreamIcon.TARGET,
+ definition.getOutputTransformName()));
+ }
+ }
+ setTransformIOMeta(ioMeta);
+ }
+ return ioMeta;
+ }
+
+ @Override
+ public void searchInfoAndTargetTransforms(List<TransformMeta> transforms) {
+ for (MultiMappingInputDefinition definition : ioMappings.getInputMappings()) {
+ if (MappingTransforms.isInfoMapping(definition.toIoDefinition())) {
+ definition.setInputTransform(
+ TransformMeta.findTransform(transforms, definition.getInputTransformName()));
+ }
+ }
+ List<IStream> infoStreams = getTransformIOMeta().getInfoStreams();
+ int infoIndex = 0;
+ for (MultiMappingInputDefinition definition : ioMappings.getInputMappings()) {
+ if (MappingTransforms.isInfoMapping(definition.toIoDefinition())
+ && infoIndex < infoStreams.size()) {
+ infoStreams.get(infoIndex++).setTransformMeta(definition.getInputTransform());
+ }
+ }
+ List<IStream> targetStreams = getTransformIOMeta().getTargetStreams();
+ int targetIndex = 0;
+ for (MultiMappingOutputDefinition definition : ioMappings.getOutputMappings()) {
+ if (MappingTransforms.isTargetMapping(definition.toIoDefinition())
+ && targetIndex < targetStreams.size()) {
+ targetStreams
+ .get(targetIndex++)
+ .setTransformMeta(
+ TransformMeta.findTransform(transforms, definition.getOutputTransformName()));
+ }
+ }
+ }
+
+ @Override
+ public List<IStream> getOptionalStreams() {
+ List<IStream> list = new ArrayList<>();
+ list.add(NEW_INFO_STREAM);
+ list.add(NEW_TARGET_STREAM);
+ return list;
+ }
+
+ @Override
+ public void handleStreamSelection(IStream stream) {
+ if (stream == NEW_INFO_STREAM) {
+ MultiMappingInputDefinition definition = new MultiMappingInputDefinition();
+ definition.setMainDataPath(false);
+ if (stream.getTransformMeta() != null) {
+ definition.setInputTransformName(stream.getTransformMeta().getName());
+ definition.setInputTransform(stream.getTransformMeta());
+ }
+ ioMappings.getInputMappings().add(definition);
+ resetTransformIoMeta();
+ } else if (stream == NEW_TARGET_STREAM) {
+ MultiMappingOutputDefinition definition = new MultiMappingOutputDefinition();
+ definition.setMainDataPath(false);
+ if (stream.getTransformMeta() != null) {
+ definition.setOutputTransformName(stream.getTransformMeta().getName());
+ }
+ ioMappings.getOutputMappings().add(definition);
+ resetTransformIoMeta();
+ }
+ }
+
+ @Override
+ public boolean cleanAfterHopFromRemove(TransformMeta toTransform) {
+ if (toTransform == null) {
+ return false;
+ }
+ boolean changed = false;
+ String toName = toTransform.getName();
+ for (MultiMappingOutputDefinition definition : ioMappings.getOutputMappings()) {
+ if (toName.equals(definition.getOutputTransformName())) {
+ definition.setOutputTransformName(null);
+ changed = true;
+ }
+ }
+ if (changed) {
+ resetTransformIoMeta();
+ }
+ return changed;
+ }
+
+ @Override
+ public boolean cleanAfterHopToRemove(TransformMeta fromTransform) {
+ if (fromTransform == null) {
+ return false;
+ }
+ boolean changed = false;
+ String fromName = fromTransform.getName();
+ for (MultiMappingInputDefinition definition : ioMappings.getInputMappings()) {
+ if (fromName.equals(definition.getInputTransformName())) {
+ definition.setInputTransformName(null);
+ definition.setInputTransform(null);
+ changed = true;
+ }
+ }
+ if (changed) {
+ resetTransformIoMeta();
+ }
+ return changed;
+ }
+
+ @Override
+ public boolean excludeFromRowLayoutVerification() {
+ return true;
+ }
+
+ @Override
+ public boolean excludeFromCopyDistributeVerification() {
+ return true;
+ }
+
+ @Override
+ public PipelineType[] getSupportedPipelineTypes() {
+ return new PipelineType[] {PipelineType.Normal};
+ }
+
+ @Override
+ public String[] getReferencedObjectDescriptions() {
+ return new String[] {
+ BaseMessages.getString(PKG, "MultiMappingMeta.ReferencedObject.Description"),
+ };
+ }
+
+ private boolean isMappingDefined() {
+ return StringUtils.isNotEmpty(filename);
+ }
+
+ @Override
+ public boolean[] isReferencedObjectEnabled() {
+ return new boolean[] {isMappingDefined()};
+ }
+
+ @Override
+ public IHasFilename loadReferencedObject(
+ int index, IHopMetadataProvider metadataProvider, IVariables variables) throws HopException {
+ return loadMappingMeta(this, metadataProvider, variables);
+ }
+
+ @Override
+ public boolean supportsDrillDown() {
+ return true;
+ }
+}
diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingOutputDefinition.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingOutputDefinition.java
new file mode 100644
index 0000000000..21e6a53d6b
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingOutputDefinition.java
@@ -0,0 +1,132 @@
+/*
+ * 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.multimapping;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.pipeline.transforms.mapping.MappingIODefinition;
+import org.apache.hop.pipeline.transforms.mapping.MappingValueRename;
+
+/** Child Mapping Output → parent target. */
+@Getter
+@Setter
+public class MultiMappingOutputDefinition implements Cloneable {
+
+ @HopMetadataProperty(
+ key = "input_transform",
+ injectionKey = "OUTPUT_MAPPING_TRANSFORM",
+ injectionKeyDescription = "MultiMappingMeta.Injection.OUTPUT_MAPPING_TRANSFORM")
+ private String inputTransformName;
+
+ @HopMetadataProperty(
+ key = "output_transform",
+ injectionKey = "OUTPUT_TARGET_TRANSFORM",
+ injectionKeyDescription = "MultiMappingMeta.Injection.OUTPUT_TARGET_TRANSFORM")
+ private String outputTransformName;
+
+ @HopMetadataProperty(
+ key = "description",
+ injectionKey = "OUTPUT_DESCRIPTION",
+ injectionKeyDescription = "MultiMappingMeta.Injection.OUTPUT_DESCRIPTION")
+ private String description;
+
+ @HopMetadataProperty(
+ key = "connector",
+ injectionGroupKey = "OUTPUT_RENAMES",
+ injectionGroupDescription = "MultiMappingMeta.Injection.OUTPUT_RENAMES")
+ private List<MultiMappingOutputRename> valueRenames;
+
+ @HopMetadataProperty(
+ key = "main_path",
+ injectionKey = "OUTPUT_MAIN_PATH",
+ injectionKeyDescription = "MultiMappingMeta.Injection.OUTPUT_MAIN_PATH")
+ private boolean mainDataPath;
+
+ @HopMetadataProperty(
+ key = "rename_on_output",
+ injectionKey = "OUTPUT_RENAME_ON_OUTPUT",
+ injectionKeyDescription = "MultiMappingMeta.Injection.OUTPUT_RENAME_ON_OUTPUT")
+ private boolean renamingOnOutput;
+
+ public MultiMappingOutputDefinition() {
+ this.valueRenames = new ArrayList<>();
+ }
+
+ public MultiMappingOutputDefinition(String inputTransformName, String outputTransformName) {
+ this();
+ this.inputTransformName = inputTransformName;
+ this.outputTransformName = outputTransformName;
+ }
+
+ public MultiMappingOutputDefinition(MultiMappingOutputDefinition d) {
+ this();
+ copyFrom(d);
+ }
+
+ public void copyFrom(MultiMappingOutputDefinition d) {
+ this.inputTransformName = d.inputTransformName;
+ this.outputTransformName = d.outputTransformName;
+ this.description = d.description;
+ this.mainDataPath = d.mainDataPath;
+ this.renamingOnOutput = d.renamingOnOutput;
+ this.valueRenames.clear();
+ for (MultiMappingOutputRename rename : d.valueRenames) {
+ this.valueRenames.add(new MultiMappingOutputRename(rename));
+ }
+ }
+
+ @Override
+ public MultiMappingOutputDefinition clone() {
+ return new MultiMappingOutputDefinition(this);
+ }
+
+ public MappingIODefinition toIoDefinition() {
+ MappingIODefinition definition =
+ new MappingIODefinition(inputTransformName, outputTransformName);
+ definition.setDescription(description);
+ definition.setMainDataPath(mainDataPath);
+ definition.setRenamingOnOutput(renamingOnOutput);
+ List<MappingValueRename> renames = new ArrayList<>();
+ for (MultiMappingOutputRename rename : valueRenames) {
+ renames.add(rename.toValueRename());
+ }
+ definition.setValueRenames(renames);
+ return definition;
+ }
+
+ public static MultiMappingOutputDefinition fromIoDefinition(MappingIODefinition d) {
+ MultiMappingOutputDefinition definition = new MultiMappingOutputDefinition();
+ if (d == null) {
+ return definition;
+ }
+ definition.inputTransformName = d.getInputTransformName();
+ definition.outputTransformName = d.getOutputTransformName();
+ definition.description = d.getDescription();
+ definition.mainDataPath = d.isMainDataPath();
+ definition.renamingOnOutput = d.isRenamingOnOutput();
+ if (d.getValueRenames() != null) {
+ for (MappingValueRename rename : d.getValueRenames()) {
+ definition.valueRenames.add(MultiMappingOutputRename.fromValueRename(rename));
+ }
+ }
+ return definition;
+ }
+}
diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingOutputRename.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingOutputRename.java
new file mode 100644
index 0000000000..a23b587cc1
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingOutputRename.java
@@ -0,0 +1,68 @@
+/*
+ * 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.multimapping;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.pipeline.transforms.mapping.MappingValueRename;
+
+@Getter
+@Setter
+public class MultiMappingOutputRename implements Cloneable {
+
+ @HopMetadataProperty(
+ key = "parent",
+ injectionKey = "OUTPUT_RENAME_SOURCE",
+ injectionKeyDescription = "MultiMappingMeta.Injection.OUTPUT_RENAME_SOURCE")
+ private String sourceValueName;
+
+ @HopMetadataProperty(
+ key = "child",
+ injectionKey = "OUTPUT_RENAME_TARGET",
+ injectionKeyDescription = "MultiMappingMeta.Injection.OUTPUT_RENAME_TARGET")
+ private String targetValueName;
+
+ public MultiMappingOutputRename() {}
+
+ public MultiMappingOutputRename(String sourceValueName, String targetValueName) {
+ this.sourceValueName = sourceValueName == null ? "" : sourceValueName;
+ this.targetValueName = targetValueName == null ? "" : targetValueName;
+ }
+
+ public MultiMappingOutputRename(MultiMappingOutputRename rename) {
+ this.sourceValueName = rename.sourceValueName;
+ this.targetValueName = rename.targetValueName;
+ }
+
+ @Override
+ public MultiMappingOutputRename clone() {
+ return new MultiMappingOutputRename(this);
+ }
+
+ public MappingValueRename toValueRename() {
+ return new MappingValueRename(sourceValueName, targetValueName);
+ }
+
+ public static MultiMappingOutputRename fromValueRename(MappingValueRename rename) {
+ if (rename == null) {
+ return new MultiMappingOutputRename();
+ }
+ return new MultiMappingOutputRename(rename.getSourceValueName(), rename.getTargetValueName());
+ }
+}
diff --git a/plugins/transforms/mapping/src/main/resources/MMAP.svg b/plugins/transforms/mapping/src/main/resources/MMAP.svg
new file mode 100644
index 0000000000..a986b31a31
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/resources/MMAP.svg
@@ -0,0 +1,42 @@
+<?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.
+-->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
+ width="42px" height="42px" viewBox="0 0 42 42">
+ <g>
+ <rect x="14.753" y="8.232" fill="#0E3A5A" width="2.523" height="1.699"/>
+ <rect x="18.276" y="8.232" fill="#0E3A5A" width="2.524" height="1.699"/>
+ <rect x="28.85" y="8.232" fill="#0E3A5A" width="2.525" height="1.699"/>
+ <rect x="32.41" y="24.557" fill="#0E3A5A" width="1.699" height="2.396"/>
+ <rect x="25.325" y="8.232" fill="#0E3A5A" width="2.524" height="1.699"/>
+ <rect x="21.801" y="8.232" fill="#0E3A5A" width="2.524" height="1.699"/>
+ <rect x="32.41" y="17.767" fill="#0E3A5A" width="1.699" height="2.396"/>
+ <rect x="32.41" y="21.162" fill="#0E3A5A" width="1.699" height="2.395"/>
+ <rect x="32.41" y="10.977" fill="#0E3A5A" width="1.699" height="2.395"/>
+ <rect x="32.41" y="14.371" fill="#0E3A5A" width="1.699" height="2.396"/>
+ <polygon fill="#0E3A5A"
+ points="32.41,9.977 34.109,9.977 34.109,8.232 32.375,8.232 32.375,9.932 32.41,9.932"/>
+ <rect x="7.167" y="7.296" fill="#FFFFFF" width="3.666" height="3.666"/>
+ <rect x="7.167" y="31.037" fill="#FFFFFF" width="3.666" height="3.667"/>
+ <rect x="31.167" y="31.037" fill="#FFFFFF" width="3.666" height="3.667"/>
+ <rect x="31.167" y="7.296" fill="#FFFFFF" width="3.666" height="3.666"/>
+ <path fill="#0E3A5A" d="M34.109,29.338v-1.386H32.41v1.386h-2.942v3.172H12.532v-3.172h-2.7V12.661h2.7V9.932h1.221V8.232h-1.221
+ V5.597H5.468v7.064h2.665v16.677H5.468v7.065h7.064v-2.194h16.936v2.194h7.064v-7.065H34.109z M7.167,10.962V7.296h3.666v3.666
+ H7.167z M10.833,34.704H7.167v-3.667h3.666V34.704z M34.833,34.704h-3.666v-3.667h3.666V34.704z M34.833,10.962h-3.666V7.296h3.666
+ V10.962z"/>
+ </g>
+</svg>
diff --git a/plugins/transforms/mapping/src/main/resources/org/apache/hop/pipeline/transforms/multimapping/messages/messages_en_US.properties b/plugins/transforms/mapping/src/main/resources/org/apache/hop/pipeline/transforms/multimapping/messages/messages_en_US.properties
new file mode 100644
index 0000000000..14b36b7420
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/resources/org/apache/hop/pipeline/transforms/multimapping/messages/messages_en_US.properties
@@ -0,0 +1,112 @@
+#
+#
+# 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.
+#
+
+MultiMapping.Name=Multi Mapping
+MultiMapping.Description=Re-uses a child pipeline with zero or more inputs and outputs, including info and target streams
+MultiMappingMeta.keyword=mapping,multi,subpipeline,reusable,info,target
+MultiMappingMeta.ReferencedObject.Description=Mapping (sub-pipeline)
+MultiMappingMeta.InfoStream.Description=These rows are used as an info stream for the mapping
+MultiMappingMeta.InfoStream.New.Description=New info input
+MultiMappingMeta.TargetStream.Description=Target output: {0}
+MultiMappingMeta.TargetStream.New.Description=New target output
+MultiMappingMeta.Exception.UnableToLoadMappingPipeline=Unable to load the mapping pipeline
+MultiMappingMeta.Exception.UnableToFindField=Unable to find field : {0}
+MultiMappingMeta.Exception.UnableToFindMetadataInfo=Unable to find transform ''{0}'' to determine the row metadata for the mapping input.
+MultiMappingMeta.CheckResult.NoMappingSpecified=There is no mapping specified\!
+MultiMappingMeta.CheckResult.MappingPipelineSpecified=A mapping (pipeline) is specified.
+MultiMappingMeta.CheckResult.NotReceivingAnyFields=Not receiving any fields from previous transforms.
+MultiMappingMeta.CheckResult.TransformReceivingFields=Transform is connected to previous one, receiving {0} fields
+MultiMappingMeta.CheckResult.TransformReceivingFieldsFromOtherTransforms=Transform is receiving info from other transforms.
+MultiMappingMeta.CheckResult.NoInputReceived=No input received from other transforms (allowed for generator mappings).
+MultiMappingMeta.CheckResult.ParentTransformNotFound=Parent transform ''{0}'' could not be found.
+MultiMappingMeta.CheckResult.InfoMappingMissingSource=An info mapping has no source transform name.
+MultiMappingMeta.CheckResult.UnableToLoadMappingPipeline=Unable to load the mapping pipeline
+MultiMappingMeta.Injection.RUN_CONFIGURATION=Pipeline run configuration (local only)
+MultiMappingMeta.Injection.INPUT_MAPPINGS=Input mappings
+MultiMappingMeta.Injection.OUTPUT_MAPPINGS=Output mappings
+MultiMappingMeta.Injection.INPUT_SOURCE_TRANSFORM=Parent source transform for an input mapping
+MultiMappingMeta.Injection.INPUT_MAPPING_TRANSFORM=Child Mapping Input transform
+MultiMappingMeta.Injection.INPUT_DESCRIPTION=Input mapping description
+MultiMappingMeta.Injection.INPUT_MAIN_PATH=Input mapping is the main data path
+MultiMappingMeta.Injection.INPUT_RENAME_ON_OUTPUT=Rename input fields back on output
+MultiMappingMeta.Injection.INPUT_RENAMES=Input field renames
+MultiMappingMeta.Injection.INPUT_RENAME_SOURCE=Input rename source field
+MultiMappingMeta.Injection.INPUT_RENAME_TARGET=Input rename target field
+MultiMappingMeta.Injection.OUTPUT_MAPPING_TRANSFORM=Child Mapping Output transform
+MultiMappingMeta.Injection.OUTPUT_TARGET_TRANSFORM=Parent target transform for an output mapping
+MultiMappingMeta.Injection.OUTPUT_DESCRIPTION=Output mapping description
+MultiMappingMeta.Injection.OUTPUT_MAIN_PATH=Output mapping is the main data path
+MultiMappingMeta.Injection.OUTPUT_RENAME_ON_OUTPUT=Output rename on output flag
+MultiMappingMeta.Injection.OUTPUT_RENAMES=Output field renames
+MultiMappingMeta.Injection.OUTPUT_RENAME_SOURCE=Output rename source field
+MultiMappingMeta.Injection.OUTPUT_RENAME_TARGET=Output rename target field
+MultiMapping.Exception.UnableToPrepareExecutionOfMapping=Unable to prepare (allocate, initialize) the execution of the mapping (sub-pipeline)
+MultiMapping.Exception.MappingInputNotFound=Unable to find Mapping Input transform ''{0}'' in the mapping pipeline
+MultiMapping.Exception.MappingOutputNotFound=Unable to find Mapping Output transform ''{0}'' in the mapping pipeline
+MultiMapping.Exception.InfoRowSetNotFound=Unable to find the info row set from transform ''{0}''
+MultiMapping.Exception.InputRowSetNotFound=Unable to find the input row set from transform ''{0}''
+MultiMapping.Log.ErrorOccurredInSubPipeline=An error occurred in the sub-pipeline, halting processing
+MultiMappingDialog.Shell.Title=Multi Mapping
+MultiMappingDialog.Pipeline.Label=Pipeline
+MultiMappingDialog.Browse.Label=Browse...
+MultiMappingDialog.New.Label=New
+MultiMappingDialog.Edit.Label=Edit
+MultiMappingDialog.RunConfig.Label=Run configuration (local only)
+MultiMappingDialog.Parameters.Title=Parameters
+MultiMappingDialog.Parameters.Tooltip=You can specify the parameters (variables) to set for this mapping (sub-pipeline)
+MultiMappingDialog.Parameters.column.Variable=Variable name
+MultiMappingDialog.Parameters.column.ValueOrField=String value (can include variable expressions like '${VARIABLE}')
+MultiMappingDialog.Parameters.InheritAll=Inherit all variables from the parent pipeline
+MultiMappingDialog.InputTab.Title=Input
+MultiMappingDialog.InputTab.Tooltip=Defines an input stream for this mapping
+MultiMappingDialog.OutputTab.Title=Output
+MultiMappingDialog.OutputTab.Tooltip=Defines an output stream for this mapping
+MultiMappingDialog.button.AddInput=Add Input
+MultiMappingDialog.button.AddOutput=Add Output
+MultiMappingDialog.button.EnterMapping=Mapping...
+MultiMappingDialog.button.GetFields=Get Fields
+MultiMappingDialog.InputTab.label.InputSourceTransformName=Input source transform name
+MultiMappingDialog.InputTab.label.OutputTargetTransformName=Mapping Input transform name
+MultiMappingDialog.InputTab.label.Description=Description
+MultiMappingDialog.InputTab.column.SourceField=Fieldname from source transform
+MultiMappingDialog.InputTab.column.TargetField=Fieldname to mapping input transform
+MultiMappingDialog.OutputTab.label.InputSourceTransformName=Mapping Output transform name
+MultiMappingDialog.OutputTab.label.OutputTargetTransformName=Output target transform name
+MultiMappingDialog.OutputTab.label.Description=Description
+MultiMappingDialog.OutputTab.column.SourceField=Fieldname from mapping transform
+MultiMappingDialog.OutputTab.column.TargetField=Fieldname to target transform
+MultiMappingDialog.input.MainDataPath=Is this the main data path?
+MultiMappingDialog.input.RenamingOnOutput=Update mapped field names downstream
+MultiMappingDialog.CloseDefinitionTabAreYouSure.Title=Delete mapping definition
+MultiMappingDialog.CloseDefinitionTabAreYouSure.Message=Are you sure you want to delete this mapping definition?
+MultiMappingDialog.FilenameMissing.Header=Missing filename
+MultiMappingDialog.FilenameMissing.Message=Please specify a mapping pipeline filename.
+MultiMappingDialog.SelfReference.Header=Self reference
+MultiMappingDialog.SelfReference.Message=The mapping pipeline cannot be the same as the parent pipeline.
+MultiMappingDialog.ErrorLoadingPipeline.DialogTitle=Error loading pipeline
+MultiMappingDialog.ErrorLoadingPipeline.DialogMessage=There was an error loading the pipeline from file:
+MultiMappingDialog.ErrorLoadingSpecifiedPipeline.Title=Error
+MultiMappingDialog.ErrorLoadingSpecifiedPipeline.Message=There was an error during the loading and verification of the selected pipeline.
+MultiMappingDialog.Exception.NoMappingSpecified=No mapping (sub-pipeline) was specified or it was not a valid one.
+MultiMappingDialog.Exception.ErrorGettingMappingSourceAndTargetFields=There was an error getting the source and target fields : {0}
+MultiMappingDialog.SelectPipelineTransform.Title=Select a transform
+MultiMappingDialog.SelectPipelineTransform.Message=Select a transform from the parent pipeline:
+MultiMappingDialog.SelectMappingTransform.Title=Select a mapping transform
+MultiMappingDialog.SelectMappingTransform.Message=Select a transform from the mapping sub-pipeline:
+MultiMappingDialog.ErrorGettingPreviousFields.DialogTitle=Error getting previous fields
+MultiMappingDialog.ErrorGettingPreviousFields.DialogMessage=There was an error getting the fields entering this transform
diff --git a/plugins/transforms/mapping/src/main/samples/transforms/multi-mapping-child.hpl b/plugins/transforms/mapping/src/main/samples/transforms/multi-mapping-child.hpl
new file mode 100644
index 0000000000..51106875a4
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/samples/transforms/multi-mapping-child.hpl
@@ -0,0 +1,165 @@
+<?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>multi-mapping-child</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>2020/09/09 16:13:22.075</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2020/09/09 16:13:22.075</modified_date>
+ <key_for_session_key>H4sIAAAAAAAAAAMAAAAAAAAAAAA=</key_for_session_key>
+ <is_key_private>N</is_key_private>
+ </info>
+ <notepads>
+ <notepad>
+ <note>This mapping pipeline accepts a first and last name field and
+concatenates it with a space in between to output the full name.
+It can be re-used by other pipelines in a generic way.</note>
+ <xloc>128</xloc>
+ <yloc>160</yloc>
+ <width>339</width>
+ <heigth>58</heigth>
+ <fontname>Noto Sans</fontname>
+ <fontsize>11</fontsize>
+ <fontbold>N</fontbold>
+ <fontitalic>N</fontitalic>
+ <fontcolorred>14</fontcolorred>
+ <fontcolorgreen>58</fontcolorgreen>
+ <fontcolorblue>90</fontcolorblue>
+ <backgroundcolorred>201</backgroundcolorred>
+ <backgroundcolorgreen>232</backgroundcolorgreen>
+ <backgroundcolorblue>251</backgroundcolorblue>
+ <bordercolorred>14</bordercolorred>
+ <bordercolorgreen>58</bordercolorgreen>
+ <bordercolorblue>90</bordercolorblue>
+ </notepad>
+ </notepads>
+ <order>
+ <hop>
+ <from>Mapping Input</from>
+ <to>calculate name</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>calculate name</from>
+ <to>Mapping Output</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>Mapping Input</name>
+ <type>MappingInput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <name>firstName</name>
+ <type>String</type>
+ <length>-1</length>
+ <precision>-1</precision>
+ </field>
+ <field>
+ <name>lastName</name>
+ <type>String</type>
+ <length>-1</length>
+ <precision>-1</precision>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>160</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Mapping Output</name>
+ <type>MappingOutput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>512</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>calculate name</name>
+ <type>ScriptValueMod</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <optimizationLevel>9</optimizationLevel>
+ <jsScripts>
+ <jsScript>
+ <jsScript_type>0</jsScript_type>
+ <jsScript_name>Script 1</jsScript_name>
+ <jsScript_script>
+var name = firstName + " " + lastName;
+
+</jsScript_script>
+ </jsScript>
+ </jsScripts>
+ <fields>
+ <field>
+ <name>name</name>
+ <rename>name</rename>
+ <type>String</type>
+ <length>-1</length>
+ <precision>-1</precision>
+ <replace>N</replace>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>336</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/plugins/transforms/mapping/src/main/samples/transforms/multi-mapping-parent.hpl b/plugins/transforms/mapping/src/main/samples/transforms/multi-mapping-parent.hpl
new file mode 100644
index 0000000000..8e71c8e509
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/samples/transforms/multi-mapping-parent.hpl
@@ -0,0 +1,218 @@
+<?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>multi-mapping-parent</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>2020/09/09 16:18:45.749</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2020/09/09 16:18:45.749</modified_date>
+ <key_for_session_key>H4sIAAAAAAAAAAMAAAAAAAAAAAA=</key_for_session_key>
+ <is_key_private>N</is_key_private>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>Data grid</from>
+ <to>id</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>Multi Mapping</from>
+ <to>Results</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>id</from>
+ <to>Multi Mapping</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>
+ <fields>
+ <field>
+ <name>first</name>
+ <type>String</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ <field>
+ <name>last</name>
+ <type>String</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ </fields>
+ <data>
+ <line>
+ <item>Johnny</item>
+ <item>Walker</item>
+ </line>
+ <line>
+ <item>Captain</item>
+ <item>Morgan</item>
+ </line>
+ <line>
+ <item>Jim</item>
+ <item>Bean</item>
+ </line>
+ <line>
+ <item>Evan</item>
+ <item>Williams</item>
+ </line>
+ </data>
+ <attributes/>
+ <GUI>
+ <xloc>160</xloc>
+ <yloc>96</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>608</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Multi Mapping</name>
+ <type>MultiMapping</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <filename>${PROJECT_HOME}/transforms/multi-mapping-child.hpl</filename>
+ <mappings>
+ <input>
+ <mapping>
+ <input_transform/>
+ <output_transform/>
+ <main_path>Y</main_path>
+ <rename_on_output>Y</rename_on_output>
+ <description/>
+ <connector>
+ <parent>first</parent>
+ <child>firstName</child>
+ </connector>
+ <connector>
+ <parent>last</parent>
+ <child>lastName</child>
+ </connector>
+ </mapping>
+ </input>
+ <output>
+ <mapping>
+ <input_transform/>
+ <output_transform/>
+ <main_path>Y</main_path>
+ <rename_on_output>N</rename_on_output>
+ <description/>
+ <connector>
+ <parent>name</parent>
+ <child>resultName</child>
+ </connector>
+ </mapping>
+ </output>
+ <parameters>
+ <inherit_all_vars>Y</inherit_all_vars>
+ </parameters>
+ </mappings>
+ <attributes/>
+ <GUI>
+ <xloc>432</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>id</name>
+ <type>Sequence</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <valuename>id</valuename>
+ <use_database>N</use_database>
+ <connection/>
+ <schema/>
+ <seqname>SEQ_</seqname>
+ <use_counter>Y</use_counter>
+ <counter_name/>
+ <start_at>1</start_at>
+ <increment_by>1</increment_by>
+ <max_value>999999999</max_value>
+ <attributes/>
+ <GUI>
+ <xloc>288</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/mapping/MappingTransformsTest.java b/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/mapping/MappingTransformsTest.java
new file mode 100644
index 0000000000..2f6b396b5f
--- /dev/null
+++ b/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/mapping/MappingTransformsTest.java
@@ -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.
+ */
+package org.apache.hop.pipeline.transforms.mapping;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+class MappingTransformsTest {
+
+ @Test
+ void infoMappingRequiresNonMainAndParentName() {
+ MappingIODefinition definition = new MappingIODefinition("Lookup", "Lookup input");
+ definition.setMainDataPath(false);
+ assertTrue(MappingTransforms.isInfoMapping(definition));
+ definition.setMainDataPath(true);
+ assertFalse(MappingTransforms.isInfoMapping(definition));
+ definition.setMainDataPath(false);
+ definition.setInputTransformName(null);
+ assertFalse(MappingTransforms.isInfoMapping(definition));
+ assertFalse(MappingTransforms.isInfoMapping(null));
+ }
+
+ @Test
+ void targetMappingRequiresNonMainAndParentName() {
+ MappingIODefinition definition = new MappingIODefinition("Case out", "Handle A");
+ definition.setMainDataPath(false);
+ assertTrue(MappingTransforms.isTargetMapping(definition));
+ definition.setMainDataPath(true);
+ assertFalse(MappingTransforms.isTargetMapping(definition));
+ definition.setOutputTransformName(null);
+ definition.setMainDataPath(false);
+ assertFalse(MappingTransforms.isTargetMapping(definition));
+ }
+}
diff --git a/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingMetaInjectionTest.java b/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingMetaInjectionTest.java
new file mode 100644
index 0000000000..eca9c35fa4
--- /dev/null
+++ b/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingMetaInjectionTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.multimapping;
+
+import org.apache.hop.core.injection.BaseMetadataInjectionTestJunit5;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class MultiMappingMetaInjectionTest extends BaseMetadataInjectionTestJunit5<MultiMappingMeta> {
+
+ @RegisterExtension
+ static RestoreHopEngineEnvironmentExtension env = new RestoreHopEngineEnvironmentExtension();
+
+ @BeforeEach
+ void setup() throws Exception {
+ MultiMappingMeta meta = new MultiMappingMeta();
+ meta.getInputMappings().add(new MultiMappingInputDefinition());
+ meta.getInputMappings().get(0).getValueRenames().add(new MultiMappingInputRename());
+ meta.getOutputMappings().add(new MultiMappingOutputDefinition());
+ meta.getOutputMappings().get(0).getValueRenames().add(new MultiMappingOutputRename());
+ setup(meta);
+ }
+
+ @Test
+ void test() throws Exception {
+ check("filename", () -> meta.getFilename());
+ check("RUN_CONFIGURATION", () -> meta.getRunConfigurationName());
+ check("INPUT_SOURCE_TRANSFORM", () -> meta.getInputMappings().get(0).getInputTransformName());
+ check("INPUT_MAPPING_TRANSFORM", () -> meta.getInputMappings().get(0).getOutputTransformName());
+ check("INPUT_DESCRIPTION", () -> meta.getInputMappings().get(0).getDescription());
+ check("INPUT_MAIN_PATH", () -> meta.getInputMappings().get(0).isMainDataPath());
+ check("INPUT_RENAME_ON_OUTPUT", () -> meta.getInputMappings().get(0).isRenamingOnOutput());
+ check(
+ "OUTPUT_MAPPING_TRANSFORM", () -> meta.getOutputMappings().get(0).getInputTransformName());
+ check(
+ "OUTPUT_TARGET_TRANSFORM", () -> meta.getOutputMappings().get(0).getOutputTransformName());
+ check("OUTPUT_DESCRIPTION", () -> meta.getOutputMappings().get(0).getDescription());
+ check("OUTPUT_MAIN_PATH", () -> meta.getOutputMappings().get(0).isMainDataPath());
+ check("OUTPUT_RENAME_ON_OUTPUT", () -> meta.getOutputMappings().get(0).isRenamingOnOutput());
+ check("inherit_all_vars", () -> meta.getMappingParameters().isInheritingAllVariables());
+ skipPropertyTest("variable");
+ skipPropertyTest("input");
+ skipPropertyTest("INPUT_RENAME_SOURCE");
+ skipPropertyTest("INPUT_RENAME_TARGET");
+ skipPropertyTest("OUTPUT_RENAME_SOURCE");
+ skipPropertyTest("OUTPUT_RENAME_TARGET");
+ skipPropertyTest("mappings");
+ skipPropertyTest("parameters");
+ skipPropertyTest("mapping");
+ skipPropertyTest("connector");
+ }
+}
diff --git a/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingMetaTest.java b/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingMetaTest.java
new file mode 100644
index 0000000000..e47f28b7b5
--- /dev/null
+++ b/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingMetaTest.java
@@ -0,0 +1,239 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hop.pipeline.transforms.multimapping;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.ICheckResult;
+import org.apache.hop.core.plugins.PluginRegistry;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transform.TransformSerializationTestUtil;
+import org.apache.hop.pipeline.transform.stream.IStream;
+import org.apache.hop.pipeline.transform.stream.IStream.StreamType;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class MultiMappingMetaTest {
+
+ @BeforeEach
+ void setUp() throws Exception {
+ HopEnvironment.init();
+ PluginRegistry.init();
+ }
+
+ @Test
+ void testSerialization() throws Exception {
+ MultiMappingMeta meta =
+ TransformSerializationTestUtil.testSerialization(
+ "/multi-mapping-transform.xml", MultiMappingMeta.class);
+ assertEquals("${PROJECT_HOME}/multi-mapping-child.hpl", meta.getFilename());
+ assertEquals("local", meta.getRunConfigurationName());
+ assertEquals(2, meta.getInputMappings().size());
+ assertEquals(2, meta.getOutputMappings().size());
+ assertFalse(meta.getInputMappings().get(0).isMainDataPath());
+ assertEquals("Lookup source", meta.getInputMappings().get(0).getInputTransformName());
+ assertTrue(meta.getInputMappings().get(1).isMainDataPath());
+ assertTrue(meta.getInputMappings().get(1).isRenamingOnOutput());
+ assertEquals("Handle A", meta.getOutputMappings().get(1).getOutputTransformName());
+ assertTrue(meta.getMappingParameters().isInheritingAllVariables());
+ assertEquals(1, meta.getMappingParameters().getVariableMappings().size());
+ assertEquals("PARAM_X", meta.getMappingParameters().getVariableMappings().get(0).getName());
+ }
+
+ @Test
+ void testCloneAndDefault() {
+ MultiMappingMeta meta = new MultiMappingMeta();
+ meta.setDefault();
+ assertEquals(1, meta.getInputMappings().size());
+ assertEquals(1, meta.getOutputMappings().size());
+ assertTrue(meta.getInputMappings().get(0).isMainDataPath());
+ assertTrue(meta.getInputMappings().get(0).isRenamingOnOutput());
+
+ MultiMappingMeta copy = meta.clone();
+ assertEquals(1, copy.getInputMappings().size());
+ copy.getInputMappings().get(0).setInputTransformName("changed");
+ assertTrue(
+ meta.getInputMappings().get(0).getInputTransformName() == null
+ || !meta.getInputMappings().get(0).getInputTransformName().equals("changed"));
+ }
+
+ @Test
+ void testTransformIoMetaExposesInfoAndTargetStreams() {
+ MultiMappingMeta meta = new MultiMappingMeta();
+ meta.setDefault();
+
+ MultiMappingInputDefinition info = new MultiMappingInputDefinition("Lookup", "Lookup input");
+ info.setMainDataPath(false);
+ meta.getInputMappings().add(info);
+
+ MultiMappingOutputDefinition target = new MultiMappingOutputDefinition("Case out", "Handle A");
+ target.setMainDataPath(false);
+ meta.getOutputMappings().add(target);
+ meta.resetTransformIoMeta();
+
+ List<IStream> infoStreams = meta.getTransformIOMeta().getInfoStreams();
+ List<IStream> targetStreams = meta.getTransformIOMeta().getTargetStreams();
+ assertEquals(1, infoStreams.size());
+ assertEquals(StreamType.INFO, infoStreams.get(0).getStreamType());
+ assertEquals(1, targetStreams.size());
+ assertEquals(StreamType.TARGET, targetStreams.get(0).getStreamType());
+ assertNotNull(meta.getInfoTransforms());
+ assertEquals("Lookup", meta.getInfoTransforms()[0]);
+ assertNotNull(meta.getTargetTransforms());
+ assertEquals("Handle A", meta.getTargetTransforms()[0]);
+ }
+
+ @Test
+ void testSearchInfoAndTargetTransforms() {
+ MultiMappingMeta meta = new MultiMappingMeta();
+ MultiMappingInputDefinition info = new MultiMappingInputDefinition("Lookup", "Lookup input");
+ info.setMainDataPath(false);
+ meta.getInputMappings().add(info);
+ MultiMappingOutputDefinition target = new MultiMappingOutputDefinition("Case out", "Handle A");
+ target.setMainDataPath(false);
+ meta.getOutputMappings().add(target);
+ meta.resetTransformIoMeta();
+
+ TransformMeta lookup = new TransformMeta();
+ lookup.setName("Lookup");
+ TransformMeta handleA = new TransformMeta();
+ handleA.setName("Handle A");
+ meta.searchInfoAndTargetTransforms(List.of(lookup, handleA));
+ assertEquals(lookup, info.getInputTransform());
+ assertEquals(lookup, meta.getTransformIOMeta().getInfoStreams().get(0).getTransformMeta());
+ assertEquals(handleA, meta.getTransformIOMeta().getTargetStreams().get(0).getTransformMeta());
+ }
+
+ @Test
+ void testCheckWithoutFilenameIsError() {
+ MultiMappingMeta meta = new MultiMappingMeta();
+ List<ICheckResult> remarks = new ArrayList<>();
+ meta.check(
+ remarks,
+ new PipelineMeta(),
+ new TransformMeta(),
+ new RowMeta(),
+ new String[0],
+ new String[0],
+ new RowMeta(),
+ new Variables(),
+ new MemoryMetadataProvider());
+ assertTrue(remarks.stream().anyMatch(r -> r.getType() == ICheckResult.TYPE_RESULT_ERROR));
+ }
+
+ @Test
+ void testCheckZeroInputIsWarningNotError() {
+ MultiMappingMeta meta = new MultiMappingMeta();
+ meta.setFilename("dummy.hpl");
+ List<ICheckResult> remarks = new ArrayList<>();
+ meta.check(
+ remarks,
+ new PipelineMeta(),
+ new TransformMeta(),
+ new RowMeta(),
+ new String[0],
+ new String[0],
+ new RowMeta(),
+ new Variables(),
+ new MemoryMetadataProvider());
+ assertTrue(
+ remarks.stream()
+ .anyMatch(
+ r ->
+ r.getType() == ICheckResult.TYPE_RESULT_WARNING
+ && r.getText().contains("No input")));
+ assertFalse(
+ remarks.stream()
+ .anyMatch(
+ r ->
+ r.getType() == ICheckResult.TYPE_RESULT_ERROR
+ && r.getText().contains("No input")));
+ }
+
+ @Test
+ void testFindOutputDefinitionPrefersNamedTarget() {
+ MultiMappingMeta meta = new MultiMappingMeta();
+ MultiMappingOutputDefinition main = new MultiMappingOutputDefinition("Main out", null);
+ main.setMainDataPath(true);
+ MultiMappingOutputDefinition target = new MultiMappingOutputDefinition("Case out", "Handle A");
+ target.setMainDataPath(false);
+ meta.getOutputMappings().add(main);
+ meta.getOutputMappings().add(target);
+
+ TransformMeta next = new TransformMeta();
+ next.setName("Handle A");
+ assertEquals(target, meta.findOutputDefinition(next));
+ assertEquals(main, meta.findOutputDefinition(null));
+ }
+
+ @Test
+ void testOptionalStreamsAddDefinitions() {
+ MultiMappingMeta meta = new MultiMappingMeta();
+ List<IStream> optional = meta.getOptionalStreams();
+ assertEquals(2, optional.size());
+
+ TransformMeta lookup = new TransformMeta();
+ lookup.setName("Lookup");
+ optional.get(0).setTransformMeta(lookup);
+ meta.handleStreamSelection(optional.get(0));
+ assertEquals(1, meta.getInputMappings().size());
+ assertFalse(meta.getInputMappings().get(0).isMainDataPath());
+ assertEquals("Lookup", meta.getInputMappings().get(0).getInputTransformName());
+
+ TransformMeta handleA = new TransformMeta();
+ handleA.setName("Handle A");
+ optional.get(1).setTransformMeta(handleA);
+ meta.handleStreamSelection(optional.get(1));
+ assertEquals(1, meta.getOutputMappings().size());
+ assertEquals("Handle A", meta.getOutputMappings().get(0).getOutputTransformName());
+ }
+
+ @Test
+ void testCleanAfterHopRemove() {
+ MultiMappingMeta meta = new MultiMappingMeta();
+ MultiMappingInputDefinition info = new MultiMappingInputDefinition("Lookup", "Lookup input");
+ info.setMainDataPath(false);
+ meta.getInputMappings().add(info);
+ MultiMappingOutputDefinition target = new MultiMappingOutputDefinition("Case out", "Handle A");
+ target.setMainDataPath(false);
+ meta.getOutputMappings().add(target);
+
+ TransformMeta lookup = new TransformMeta();
+ lookup.setName("Lookup");
+ assertTrue(meta.cleanAfterHopToRemove(lookup));
+ assertTrue(
+ meta.getInputMappings().get(0).getInputTransformName() == null
+ || meta.getInputMappings().get(0).getInputTransformName().isEmpty());
+
+ TransformMeta handleA = new TransformMeta();
+ handleA.setName("Handle A");
+ assertTrue(meta.cleanAfterHopFromRemove(handleA));
+ assertTrue(
+ meta.getOutputMappings().get(0).getOutputTransformName() == null
+ || meta.getOutputMappings().get(0).getOutputTransformName().isEmpty());
+ }
+}
diff --git a/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingTest.java b/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingTest.java
new file mode 100644
index 0000000000..36d28f83e1
--- /dev/null
+++ b/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingTest.java
@@ -0,0 +1,114 @@
+/*
+ * 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.multimapping;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import org.apache.hop.core.logging.ILoggingObject;
+import org.apache.hop.pipeline.engines.local.LocalPipelineEngine;
+import org.apache.hop.pipeline.transforms.mock.TransformMockHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class MultiMappingTest {
+
+ private TransformMockHelper<MultiMappingMeta, MultiMappingData> transformMockHelper;
+
+ @BeforeEach
+ void setup() {
+ transformMockHelper =
+ new TransformMockHelper<>(
+ "MULTI_MAPPING_TEST", MultiMappingMeta.class, MultiMappingData.class);
+ when(transformMockHelper.logChannelFactory.create(any(), any(ILoggingObject.class)))
+ .thenReturn(transformMockHelper.iLogChannel);
+ when(transformMockHelper.pipeline.isRunning()).thenReturn(true);
+ }
+
+ @AfterEach
+ void tearDown() {
+ transformMockHelper.cleanUp();
+ }
+
+ @Test
+ void disposeDoesNotNpeWhenInitFailed() {
+ MultiMappingData data = new MultiMappingData();
+ MultiMapping mapping =
+ new MultiMapping(
+ transformMockHelper.transformMeta,
+ transformMockHelper.iTransformMeta,
+ data,
+ 0,
+ transformMockHelper.pipelineMeta,
+ transformMockHelper.pipeline);
+ assertDoesNotThrow(mapping::dispose);
+ }
+
+ @Test
+ void stopAllIsSafeWhenChildMissing() {
+ MultiMappingData data = new MultiMappingData();
+ MultiMapping mapping =
+ new MultiMapping(
+ transformMockHelper.transformMeta,
+ transformMockHelper.iTransformMeta,
+ data,
+ 0,
+ transformMockHelper.pipelineMeta,
+ transformMockHelper.pipeline);
+ assertDoesNotThrow(mapping::stopAll);
+ assertDoesNotThrow(mapping::stopRunning);
+ }
+
+ @Test
+ void initFailsWithoutFilename() {
+ MultiMappingMeta meta = new MultiMappingMeta();
+ MultiMappingData data = new MultiMappingData();
+ when(transformMockHelper.iTransformMeta.getFilename()).thenReturn(null);
+ MultiMapping mapping =
+ new MultiMapping(
+ transformMockHelper.transformMeta,
+ meta,
+ data,
+ 0,
+ transformMockHelper.pipelineMeta,
+ transformMockHelper.pipeline);
+ mapping.setMetadataProvider(transformMockHelper.pipeline.getMetadataProvider());
+ assertFalse(mapping.init());
+ }
+
+ @Test
+ void disposeFlagsErrorWhenChildHadErrors() {
+ MultiMappingData data = new MultiMappingData();
+ data.wasStarted = true;
+ LocalPipelineEngine child = org.mockito.Mockito.mock(LocalPipelineEngine.class);
+ when(child.isFinished()).thenReturn(true);
+ when(child.getErrors()).thenReturn(1);
+ data.mappingPipeline = child;
+ MultiMapping mapping =
+ new MultiMapping(
+ transformMockHelper.transformMeta,
+ transformMockHelper.iTransformMeta,
+ data,
+ 0,
+ transformMockHelper.pipelineMeta,
+ transformMockHelper.pipeline);
+ mapping.dispose();
+ }
+}
diff --git a/plugins/transforms/mapping/src/test/resources/multi-mapping-transform.xml b/plugins/transforms/mapping/src/test/resources/multi-mapping-transform.xml
new file mode 100644
index 0000000000..37fd59d759
--- /dev/null
+++ b/plugins/transforms/mapping/src/test/resources/multi-mapping-transform.xml
@@ -0,0 +1,86 @@
+<!--
+ ~ 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.
+ ~
+ -->
+
+<transform>
+ <name>Multi Mapping</name>
+ <type>MultiMapping</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <filename>${PROJECT_HOME}/multi-mapping-child.hpl</filename>
+ <runConfiguration>local</runConfiguration>
+ <mappings>
+ <input>
+ <mapping>
+ <input_transform>Lookup source</input_transform>
+ <output_transform>Lookup input</output_transform>
+ <main_path>N</main_path>
+ <rename_on_output>N</rename_on_output>
+ <description>info hop</description>
+ <connector>
+ <parent>id</parent>
+ <child>lookup_id</child>
+ </connector>
+ </mapping>
+ <mapping>
+ <input_transform/>
+ <output_transform>Stream input</output_transform>
+ <main_path>Y</main_path>
+ <rename_on_output>Y</rename_on_output>
+ <description>main</description>
+ </mapping>
+ </input>
+ <output>
+ <mapping>
+ <input_transform>Main output</input_transform>
+ <output_transform/>
+ <main_path>Y</main_path>
+ <rename_on_output>N</rename_on_output>
+ <description>main out</description>
+ <connector>
+ <parent>result</parent>
+ <child>out</child>
+ </connector>
+ </mapping>
+ <mapping>
+ <input_transform>Case A output</input_transform>
+ <output_transform>Handle A</output_transform>
+ <main_path>N</main_path>
+ <rename_on_output>N</rename_on_output>
+ <description>target</description>
+ </mapping>
+ </output>
+ <parameters>
+ <inherit_all_vars>Y</inherit_all_vars>
+ <variablemapping>
+ <variable>PARAM_X</variable>
+ <input>'${Y}'</input>
+ </variablemapping>
+ </parameters>
+ </mappings>
+ <attributes/>
+ <GUI>
+ <xloc>288</xloc>
+ <yloc>112</yloc>
+ </GUI>
+</transform>