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 e14560239d value mapper fixes and tests, fixes #7005 (#7008)
e14560239d is described below
commit e14560239dfc3076d64447ed3ae208ddc419dfce
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Thu Apr 16 13:57:49 2026 +0200
value mapper fixes and tests, fixes #7005 (#7008)
---
...per.hpl => 0045-value-mapper-default-value.hpl} | 136 +++---
...pper.hpl => 0045-value-mapper-empty-string.hpl} | 136 +++---
integration-tests/transforms/0045-value-mapper.hpl | 140 ++++---
...r.csv => golden-value-mapper-default-value.csv} | 19 +-
...er.csv => golden-value-mapper-empty-string.csv} | 19 +-
.../transforms/datasets/golden-value-mapper.csv | 19 +-
.../transforms/main-0045-value-mapper.hwf | 88 ++--
.../dataset/golden-value-mapper-default-value.json | 40 ++
.../dataset/golden-value-mapper-empty-string.json | 40 ++
.../0045-value-mapper-default-value UNIT.json | 14 +
.../0045-value-mapper-empty-string UNIT.json | 43 ++
.../transforms/valuemapper/ValueMapper.java | 85 ++--
.../transforms/valuemapper/ValueMapperData.java | 3 +
.../transforms/valuemapper/ValueMapperDialog.java | 12 +-
.../transforms/valuemapper/ValueMapperMeta.java | 124 ++----
.../pipeline/transforms/valuemapper/Values.java | 52 +--
.../valuemapper/messages/messages_en_US.properties | 2 +
.../valuemapper/ValueMapperMetaInjectionTest.java | 3 +
.../valuemapper/ValueMapperMetaTest.java | 135 +++++-
.../transforms/valuemapper/ValueMapperTest.java | 464 +++++++++++++++++++++
.../hopgui/file/workflow/HopGuiWorkflowGraph.java | 5 +-
21 files changed, 1131 insertions(+), 448 deletions(-)
diff --git a/integration-tests/transforms/0045-value-mapper.hpl
b/integration-tests/transforms/0045-value-mapper-default-value.hpl
similarity index 86%
copy from integration-tests/transforms/0045-value-mapper.hpl
copy to integration-tests/transforms/0045-value-mapper-default-value.hpl
index 67639a340e..e39fc37f11 100644
--- a/integration-tests/transforms/0045-value-mapper.hpl
+++ b/integration-tests/transforms/0045-value-mapper-default-value.hpl
@@ -19,81 +19,52 @@ limitations under the License.
-->
<pipeline>
<info>
- <name>0045-value-mapper</name>
- <name_sync_with_filename>Y</name_sync_with_filename>
- <description/>
- <extended_description/>
- <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>
<parameter>
<name>NOT_DEFINED</name>
<default_value>Not defined</default_value>
- <description/>
</parameter>
<parameter>
<name>NOT_FOUND</name>
<default_value>Not found</default_value>
- <description/>
</parameter>
</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>
+ <name>0045-value-mapper</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
<created_user>-</created_user>
- <created_date>2022/06/30 15:35:29.264</created_date>
<modified_user>-</modified_user>
+ <created_date>2022/06/30 15:35:29.264</created_date>
<modified_date>2022/06/30 15:35:29.264</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>Value mapper</to>
- <enabled>Y</enabled>
- </hop>
- <hop>
- <from>Value mapper</from>
- <to>Dummy</to>
- <enabled>Y</enabled>
- </hop>
- </order>
<transform>
- <name>Data grid</name>
<type>DataGrid</type>
- <description/>
- <distribute>N</distribute>
- <custom_distribution/>
- <copies>1</copies>
- <partitioning>
- <method>none</method>
- <schema_name/>
- </partitioning>
+ <name>Data grid</name>
<fields>
<field>
- <set_empty_string>N</set_empty_string>
- <length>3</length>
<name>IATA_Code</name>
- <precision>-1</precision>
<type>String</type>
+ <length>3</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
</field>
<field>
- <set_empty_string>N</set_empty_string>
- <length>50</length>
<name>Airport_Name</name>
- <precision>-1</precision>
<type>String</type>
+ <length>50</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
</field>
<field>
- <set_empty_string>N</set_empty_string>
- <length>2</length>
<name>Country_Code</name>
- <precision>-1</precision>
<type>String</type>
+ <length>2</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
</field>
</fields>
<data>
@@ -137,81 +108,112 @@ limitations under the License.
<item>Hurghada</item>
<item>EG</item>
</line>
+ <line>
+ <item>LIS</item>
+ <item>Humberto Delgado Airport</item>
+ <item>PT</item>
+ </line>
</data>
- <attributes/>
+ <distribute>N</distribute>
+ <copies>1</copies>
<GUI>
<xloc>96</xloc>
<yloc>64</yloc>
</GUI>
- </transform>
- <transform>
- <name>Dummy</name>
- <type>Dummy</type>
- <description/>
- <distribute>Y</distribute>
- <custom_distribution/>
- <copies>1</copies>
<partitioning>
<method>none</method>
<schema_name/>
</partitioning>
<attributes/>
- <GUI>
- <xloc>368</xloc>
- <yloc>64</yloc>
- </GUI>
</transform>
<transform>
- <name>Value mapper</name>
- <type>ValueMapper</type>
- <description/>
+ <type>Dummy</type>
+ <name>Dummy</name>
<distribute>Y</distribute>
- <custom_distribution/>
<copies>1</copies>
+ <GUI>
+ <xloc>368</xloc>
+ <yloc>64</yloc>
+ </GUI>
<partitioning>
<method>none</method>
<schema_name/>
</partitioning>
+ <attributes/>
+ </transform>
+ <transform>
+ <type>ValueMapper</type>
+ <name>Value mapper</name>
<field_to_use>Country_Code</field_to_use>
<target_field>Country_Name</target_field>
- <non_match_default>[${NOT_FOUND}]</non_match_default>
+ <non_match_default>[${NOT_DEFINED}]</non_match_default>
+ <target_type/>
<fields>
<field>
- <source_value/>
<target_value>[${NOT_DEFINED}]</target_value>
+ <empty_string_equals_null>Y</empty_string_equals_null>
</field>
<field>
<source_value>BE</source_value>
<target_value>Belgium</target_value>
+ <empty_string_equals_null>Y</empty_string_equals_null>
</field>
<field>
<source_value>CA</source_value>
<target_value>Canada</target_value>
+ <empty_string_equals_null>Y</empty_string_equals_null>
</field>
<field>
<source_value>FR</source_value>
<target_value>France</target_value>
+ <empty_string_equals_null>Y</empty_string_equals_null>
</field>
<field>
<source_value>UK</source_value>
<target_value>United Kingdom</target_value>
+ <empty_string_equals_null>Y</empty_string_equals_null>
</field>
<field>
<source_value>US</source_value>
<target_value>United States</target_value>
+ <empty_string_equals_null>Y</empty_string_equals_null>
</field>
<field>
<source_value>DE</source_value>
<target_value>German</target_value>
+ <empty_string_equals_null>Y</empty_string_equals_null>
+ </field>
+ <field>
+ <source_value>PT</source_value>
+ <target_value/>
+ <empty_string_equals_null>Y</empty_string_equals_null>
</field>
</fields>
- <attributes/>
+ <distribute>Y</distribute>
+ <copies>1</copies>
<GUI>
<xloc>240</xloc>
<yloc>64</yloc>
</GUI>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
</transform>
- <transform_error_handling>
- </transform_error_handling>
+ <order>
+ <hop>
+ <from>Data grid</from>
+ <to>Value mapper</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>Value mapper</from>
+ <to>Dummy</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <notepads/>
<attributes/>
+ <transform_error_handling/>
</pipeline>
diff --git a/integration-tests/transforms/0045-value-mapper.hpl
b/integration-tests/transforms/0045-value-mapper-empty-string.hpl
similarity index 87%
copy from integration-tests/transforms/0045-value-mapper.hpl
copy to integration-tests/transforms/0045-value-mapper-empty-string.hpl
index 67639a340e..c00024b083 100644
--- a/integration-tests/transforms/0045-value-mapper.hpl
+++ b/integration-tests/transforms/0045-value-mapper-empty-string.hpl
@@ -19,81 +19,52 @@ limitations under the License.
-->
<pipeline>
<info>
- <name>0045-value-mapper</name>
- <name_sync_with_filename>Y</name_sync_with_filename>
- <description/>
- <extended_description/>
- <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>
<parameter>
<name>NOT_DEFINED</name>
<default_value>Not defined</default_value>
- <description/>
</parameter>
<parameter>
<name>NOT_FOUND</name>
<default_value>Not found</default_value>
- <description/>
</parameter>
</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>
+ <name>0045-value-mapper</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
<created_user>-</created_user>
- <created_date>2022/06/30 15:35:29.264</created_date>
<modified_user>-</modified_user>
+ <created_date>2022/06/30 15:35:29.264</created_date>
<modified_date>2022/06/30 15:35:29.264</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>Value mapper</to>
- <enabled>Y</enabled>
- </hop>
- <hop>
- <from>Value mapper</from>
- <to>Dummy</to>
- <enabled>Y</enabled>
- </hop>
- </order>
<transform>
- <name>Data grid</name>
<type>DataGrid</type>
- <description/>
- <distribute>N</distribute>
- <custom_distribution/>
- <copies>1</copies>
- <partitioning>
- <method>none</method>
- <schema_name/>
- </partitioning>
+ <name>Data grid</name>
<fields>
<field>
- <set_empty_string>N</set_empty_string>
- <length>3</length>
<name>IATA_Code</name>
- <precision>-1</precision>
<type>String</type>
+ <length>3</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
</field>
<field>
- <set_empty_string>N</set_empty_string>
- <length>50</length>
<name>Airport_Name</name>
- <precision>-1</precision>
<type>String</type>
+ <length>50</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
</field>
<field>
- <set_empty_string>N</set_empty_string>
- <length>2</length>
<name>Country_Code</name>
- <precision>-1</precision>
<type>String</type>
+ <length>2</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
</field>
</fields>
<data>
@@ -137,81 +108,112 @@ limitations under the License.
<item>Hurghada</item>
<item>EG</item>
</line>
+ <line>
+ <item>LIS</item>
+ <item>Humberto Delgado Airport</item>
+ <item>PT</item>
+ </line>
</data>
- <attributes/>
+ <distribute>N</distribute>
+ <copies>1</copies>
<GUI>
<xloc>96</xloc>
<yloc>64</yloc>
</GUI>
- </transform>
- <transform>
- <name>Dummy</name>
- <type>Dummy</type>
- <description/>
- <distribute>Y</distribute>
- <custom_distribution/>
- <copies>1</copies>
<partitioning>
<method>none</method>
<schema_name/>
</partitioning>
<attributes/>
- <GUI>
- <xloc>368</xloc>
- <yloc>64</yloc>
- </GUI>
</transform>
<transform>
- <name>Value mapper</name>
- <type>ValueMapper</type>
- <description/>
+ <type>Dummy</type>
+ <name>Dummy</name>
<distribute>Y</distribute>
- <custom_distribution/>
<copies>1</copies>
+ <GUI>
+ <xloc>368</xloc>
+ <yloc>64</yloc>
+ </GUI>
<partitioning>
<method>none</method>
<schema_name/>
</partitioning>
+ <attributes/>
+ </transform>
+ <transform>
+ <type>ValueMapper</type>
+ <name>Value mapper</name>
<field_to_use>Country_Code</field_to_use>
<target_field>Country_Name</target_field>
- <non_match_default>[${NOT_FOUND}]</non_match_default>
+ <non_match_default/>
+ <target_type/>
<fields>
<field>
- <source_value/>
<target_value>[${NOT_DEFINED}]</target_value>
+ <empty_string_equals_null>Y</empty_string_equals_null>
</field>
<field>
<source_value>BE</source_value>
<target_value>Belgium</target_value>
+ <empty_string_equals_null>Y</empty_string_equals_null>
</field>
<field>
<source_value>CA</source_value>
<target_value>Canada</target_value>
+ <empty_string_equals_null>Y</empty_string_equals_null>
</field>
<field>
<source_value>FR</source_value>
<target_value>France</target_value>
+ <empty_string_equals_null>Y</empty_string_equals_null>
</field>
<field>
<source_value>UK</source_value>
<target_value>United Kingdom</target_value>
+ <empty_string_equals_null>Y</empty_string_equals_null>
</field>
<field>
<source_value>US</source_value>
<target_value>United States</target_value>
+ <empty_string_equals_null>Y</empty_string_equals_null>
</field>
<field>
<source_value>DE</source_value>
<target_value>German</target_value>
+ <empty_string_equals_null>Y</empty_string_equals_null>
+ </field>
+ <field>
+ <source_value>PT</source_value>
+ <target_value/>
+ <empty_string_equals_null>N</empty_string_equals_null>
</field>
</fields>
- <attributes/>
+ <distribute>Y</distribute>
+ <copies>1</copies>
<GUI>
<xloc>240</xloc>
<yloc>64</yloc>
</GUI>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
</transform>
- <transform_error_handling>
- </transform_error_handling>
+ <order>
+ <hop>
+ <from>Data grid</from>
+ <to>Value mapper</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>Value mapper</from>
+ <to>Dummy</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <notepads/>
<attributes/>
+ <transform_error_handling/>
</pipeline>
diff --git a/integration-tests/transforms/0045-value-mapper.hpl
b/integration-tests/transforms/0045-value-mapper.hpl
index 67639a340e..7001a17580 100644
--- a/integration-tests/transforms/0045-value-mapper.hpl
+++ b/integration-tests/transforms/0045-value-mapper.hpl
@@ -19,81 +19,64 @@ limitations under the License.
-->
<pipeline>
<info>
- <name>0045-value-mapper</name>
- <name_sync_with_filename>Y</name_sync_with_filename>
- <description/>
- <extended_description/>
- <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>
<parameter>
<name>NOT_DEFINED</name>
<default_value>Not defined</default_value>
- <description/>
</parameter>
<parameter>
<name>NOT_FOUND</name>
<default_value>Not found</default_value>
- <description/>
</parameter>
</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>
+ <name>0045-value-mapper</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
<created_user>-</created_user>
- <created_date>2022/06/30 15:35:29.264</created_date>
<modified_user>-</modified_user>
+ <created_date>2022/06/30 15:35:29.264</created_date>
<modified_date>2022/06/30 15:35:29.264</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>Value mapper</to>
- <enabled>Y</enabled>
- </hop>
- <hop>
- <from>Value mapper</from>
- <to>Dummy</to>
- <enabled>Y</enabled>
- </hop>
- </order>
<transform>
- <name>Data grid</name>
<type>DataGrid</type>
- <description/>
- <distribute>N</distribute>
- <custom_distribution/>
- <copies>1</copies>
- <partitioning>
- <method>none</method>
- <schema_name/>
- </partitioning>
+ <name>Data grid</name>
<fields>
<field>
- <set_empty_string>N</set_empty_string>
- <length>3</length>
+ <currency/>
+ <decimal/>
+ <group/>
<name>IATA_Code</name>
- <precision>-1</precision>
<type>String</type>
+ <format/>
+ <length>3</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
</field>
<field>
- <set_empty_string>N</set_empty_string>
- <length>50</length>
+ <currency/>
+ <decimal/>
+ <group/>
<name>Airport_Name</name>
- <precision>-1</precision>
<type>String</type>
+ <format/>
+ <length>50</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
</field>
<field>
- <set_empty_string>N</set_empty_string>
- <length>2</length>
+ <currency/>
+ <decimal/>
+ <group/>
<name>Country_Code</name>
- <precision>-1</precision>
<type>String</type>
+ <format/>
+ <length>2</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
</field>
</fields>
<data>
@@ -137,47 +120,48 @@ limitations under the License.
<item>Hurghada</item>
<item>EG</item>
</line>
+ <line>
+ <item>LIS</item>
+ <item>Humberto Delgado Airport</item>
+ <item>PT</item>
+ </line>
</data>
- <attributes/>
+ <distribute>N</distribute>
+ <copies>1</copies>
<GUI>
<xloc>96</xloc>
<yloc>64</yloc>
</GUI>
- </transform>
- <transform>
- <name>Dummy</name>
- <type>Dummy</type>
- <description/>
- <distribute>Y</distribute>
- <custom_distribution/>
- <copies>1</copies>
<partitioning>
<method>none</method>
<schema_name/>
</partitioning>
<attributes/>
- <GUI>
- <xloc>368</xloc>
- <yloc>64</yloc>
- </GUI>
</transform>
<transform>
- <name>Value mapper</name>
- <type>ValueMapper</type>
- <description/>
+ <type>Dummy</type>
+ <name>Dummy</name>
<distribute>Y</distribute>
- <custom_distribution/>
<copies>1</copies>
+ <GUI>
+ <xloc>368</xloc>
+ <yloc>64</yloc>
+ </GUI>
<partitioning>
<method>none</method>
<schema_name/>
</partitioning>
+ <attributes/>
+ </transform>
+ <transform>
+ <type>ValueMapper</type>
+ <name>Value mapper</name>
<field_to_use>Country_Code</field_to_use>
<target_field>Country_Name</target_field>
- <non_match_default>[${NOT_FOUND}]</non_match_default>
+ <non_match_default/>
+ <target_type/>
<fields>
<field>
- <source_value/>
<target_value>[${NOT_DEFINED}]</target_value>
</field>
<field>
@@ -204,14 +188,36 @@ limitations under the License.
<source_value>DE</source_value>
<target_value>German</target_value>
</field>
+ <field>
+ <source_value>PT</source_value>
+ <target_value/>
+ </field>
</fields>
- <attributes/>
+ <distribute>Y</distribute>
+ <copies>1</copies>
<GUI>
<xloc>240</xloc>
<yloc>64</yloc>
</GUI>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
</transform>
- <transform_error_handling>
- </transform_error_handling>
+ <order>
+ <hop>
+ <from>Data grid</from>
+ <to>Value mapper</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>Value mapper</from>
+ <to>Dummy</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <notepads/>
<attributes/>
+ <transform_error_handling/>
</pipeline>
diff --git a/integration-tests/transforms/datasets/golden-value-mapper.csv
b/integration-tests/transforms/datasets/golden-value-mapper-default-value.csv
similarity index 83%
copy from integration-tests/transforms/datasets/golden-value-mapper.csv
copy to
integration-tests/transforms/datasets/golden-value-mapper-default-value.csv
index 23edc538fc..9887eb65b2 100644
--- a/integration-tests/transforms/datasets/golden-value-mapper.csv
+++
b/integration-tests/transforms/datasets/golden-value-mapper-default-value.csv
@@ -1,9 +1,10 @@
-IATA_Code,Airport_Name,Country_Code,Country_Name
-JFK,John F. Kennedy International Airport,US,United States
-LAX,Los Angeles International Airport,,[Not defined]
-ORD,O'Hare International Airport,US,United States
-YYZ,Lester B. Pearson International Airport,CA,Canada
-LHR,Heathrow Airport,UK,United Kingdom
-BRU,Brussels Airport,BE,Belgium
-CDG,Paris Charles de Gaulle Airport,FR,France
-HRG,Hurghada,EG,[Not found]
+IATA_Code,Airport_Name,Country_Code,Country_Name
+JFK,John F. Kennedy International Airport,US,United States
+LAX,Los Angeles International Airport,,[Not defined]
+ORD,O'Hare International Airport,US,United States
+YYZ,Lester B. Pearson International Airport,CA,Canada
+LHR,Heathrow Airport,UK,United Kingdom
+BRU,Brussels Airport,BE,Belgium
+CDG,Paris Charles de Gaulle Airport,FR,France
+HRG,Hurghada,EG,[Not defined]
+LIS,Humberto Delgado Airport,PT,
diff --git a/integration-tests/transforms/datasets/golden-value-mapper.csv
b/integration-tests/transforms/datasets/golden-value-mapper-empty-string.csv
similarity index 86%
copy from integration-tests/transforms/datasets/golden-value-mapper.csv
copy to
integration-tests/transforms/datasets/golden-value-mapper-empty-string.csv
index 23edc538fc..eec98c8322 100644
--- a/integration-tests/transforms/datasets/golden-value-mapper.csv
+++ b/integration-tests/transforms/datasets/golden-value-mapper-empty-string.csv
@@ -1,9 +1,10 @@
-IATA_Code,Airport_Name,Country_Code,Country_Name
-JFK,John F. Kennedy International Airport,US,United States
-LAX,Los Angeles International Airport,,[Not defined]
-ORD,O'Hare International Airport,US,United States
-YYZ,Lester B. Pearson International Airport,CA,Canada
-LHR,Heathrow Airport,UK,United Kingdom
-BRU,Brussels Airport,BE,Belgium
-CDG,Paris Charles de Gaulle Airport,FR,France
-HRG,Hurghada,EG,[Not found]
+IATA_Code,Airport_Name,Country_Code,Country_Name
+JFK,John F. Kennedy International Airport,US,United States
+LAX,Los Angeles International Airport,,[Not defined]
+ORD,O'Hare International Airport,US,United States
+YYZ,Lester B. Pearson International Airport,CA,Canada
+LHR,Heathrow Airport,UK,United Kingdom
+BRU,Brussels Airport,BE,Belgium
+CDG,Paris Charles de Gaulle Airport,FR,France
+HRG,Hurghada,EG,
+LIS,Humberto Delgado Airport,PT,
diff --git a/integration-tests/transforms/datasets/golden-value-mapper.csv
b/integration-tests/transforms/datasets/golden-value-mapper.csv
index 23edc538fc..eec98c8322 100644
--- a/integration-tests/transforms/datasets/golden-value-mapper.csv
+++ b/integration-tests/transforms/datasets/golden-value-mapper.csv
@@ -1,9 +1,10 @@
-IATA_Code,Airport_Name,Country_Code,Country_Name
-JFK,John F. Kennedy International Airport,US,United States
-LAX,Los Angeles International Airport,,[Not defined]
-ORD,O'Hare International Airport,US,United States
-YYZ,Lester B. Pearson International Airport,CA,Canada
-LHR,Heathrow Airport,UK,United Kingdom
-BRU,Brussels Airport,BE,Belgium
-CDG,Paris Charles de Gaulle Airport,FR,France
-HRG,Hurghada,EG,[Not found]
+IATA_Code,Airport_Name,Country_Code,Country_Name
+JFK,John F. Kennedy International Airport,US,United States
+LAX,Los Angeles International Airport,,[Not defined]
+ORD,O'Hare International Airport,US,United States
+YYZ,Lester B. Pearson International Airport,CA,Canada
+LHR,Heathrow Airport,UK,United Kingdom
+BRU,Brussels Airport,BE,Belgium
+CDG,Paris Charles de Gaulle Airport,FR,France
+HRG,Hurghada,EG,
+LIS,Humberto Delgado Airport,PT,
diff --git a/integration-tests/transforms/main-0045-value-mapper.hwf
b/integration-tests/transforms/main-0045-value-mapper.hwf
index 73b34d7a01..dcb2c3927e 100644
--- a/integration-tests/transforms/main-0045-value-mapper.hwf
+++ b/integration-tests/transforms/main-0045-value-mapper.hwf
@@ -20,88 +20,87 @@ limitations under the License.
<workflow>
<name>main-0045-value-mapper</name>
<name_sync_with_filename>Y</name_sync_with_filename>
- <description/>
- <extended_description/>
- <workflow_version/>
<created_user>-</created_user>
- <created_date>2022/04/29 10:42:16.470</created_date>
<modified_user>-</modified_user>
+ <created_date>2022/04/29 10:42:16.470</created_date>
<modified_date>2022/04/29 10:42:16.470</modified_date>
- <parameters>
- </parameters>
+ <parameters/>
<actions>
<action>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <intervalSeconds>0</intervalSeconds>
+ <intervalMinutes>60</intervalMinutes>
+ <DayOfMonth>1</DayOfMonth>
+ <weekDay>1</weekDay>
+ <minutes>0</minutes>
+ <hour>12</hour>
+ <doNotWaitOnFirstExecution>N</doNotWaitOnFirstExecution>
<name>Start</name>
<description/>
<type>SPECIAL</type>
<attributes/>
- <DayOfMonth>1</DayOfMonth>
- <doNotWaitOnFirstExecution>N</doNotWaitOnFirstExecution>
- <hour>12</hour>
- <intervalMinutes>60</intervalMinutes>
- <intervalSeconds>0</intervalSeconds>
- <minutes>0</minutes>
- <repeat>N</repeat>
- <schedulerType>0</schedulerType>
- <weekDay>1</weekDay>
- <parallel>N</parallel>
<xloc>50</xloc>
<yloc>50</yloc>
+ <parallel>N</parallel>
<attributes_hac/>
</action>
<action>
- <name>Run ValueMapper Unit Test</name>
- <description/>
- <type>RunPipelineTests</type>
- <attributes/>
<test_names>
<test_name>
<name>0045-value-mapper UNIT</name>
</test_name>
+ <test_name>
+ <name>0045-value-mapper-default-value UNIT</name>
+ </test_name>
+ <test_name>
+ <name>0045-value-mapper-empty-string UNIT</name>
+ </test_name>
</test_names>
- <parallel>N</parallel>
+ <name>Run ValueMapper Unit Test</name>
+ <description/>
+ <type>RunPipelineTests</type>
+ <attributes/>
<xloc>448</xloc>
<yloc>48</yloc>
+ <parallel>N</parallel>
<attributes_hac/>
</action>
<action>
- <name>0045-value-mapper-output-type.hpl</name>
- <description/>
- <type>PIPELINE</type>
- <attributes/>
- <add_date>N</add_date>
- <add_time>N</add_time>
- <clear_files>N</clear_files>
+ <filename>${PROJECT_HOME}/0045-value-mapper-output-type.hpl</filename>
+ <params_from_previous>N</params_from_previous>
+ <exec_per_row>N</exec_per_row>
<clear_rows>N</clear_rows>
+ <clear_files>N</clear_files>
<create_parent_folder>N</create_parent_folder>
- <exec_per_row>N</exec_per_row>
- <filename>${PROJECT_HOME}/0045-value-mapper-output-type.hpl</filename>
- <logext/>
- <logfile/>
+ <set_logfile>N</set_logfile>
+ <set_append_logfile>N</set_append_logfile>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
<loglevel>Basic</loglevel>
+ <wait_until_finished>Y</wait_until_finished>
<parameters>
<pass_all_parameters>Y</pass_all_parameters>
</parameters>
- <params_from_previous>N</params_from_previous>
<run_configuration>local</run_configuration>
- <set_append_logfile>N</set_append_logfile>
- <set_logfile>N</set_logfile>
- <wait_until_finished>Y</wait_until_finished>
- <parallel>N</parallel>
+ <name>0045-value-mapper-output-type.hpl</name>
+ <type>PIPELINE</type>
+ <attributes/>
<xloc>224</xloc>
<yloc>48</yloc>
+ <parallel>N</parallel>
<attributes_hac/>
</action>
<action>
+ <message>Something wrong happened!</message>
+ <always_log_rows>N</always_log_rows>
<name>Error</name>
<description/>
<type>ABORT</type>
<attributes/>
- <always_log_rows>N</always_log_rows>
- <message>Something wrong happened!</message>
- <parallel>N</parallel>
<xloc>224</xloc>
<yloc>176</yloc>
+ <parallel>N</parallel>
<attributes_hac/>
</action>
</actions>
@@ -109,26 +108,25 @@ limitations under the License.
<hop>
<from>Start</from>
<to>0045-value-mapper-output-type.hpl</to>
- <enabled>Y</enabled>
<evaluation>Y</evaluation>
<unconditional>Y</unconditional>
+ <enabled>Y</enabled>
</hop>
<hop>
<from>0045-value-mapper-output-type.hpl</from>
<to>Run ValueMapper Unit Test</to>
- <enabled>Y</enabled>
<evaluation>Y</evaluation>
<unconditional>N</unconditional>
+ <enabled>Y</enabled>
</hop>
<hop>
<from>0045-value-mapper-output-type.hpl</from>
<to>Error</to>
- <enabled>Y</enabled>
<evaluation>N</evaluation>
<unconditional>N</unconditional>
+ <enabled>Y</enabled>
</hop>
</hops>
- <notepads>
- </notepads>
+ <notepads/>
<attributes/>
</workflow>
diff --git
a/integration-tests/transforms/metadata/dataset/golden-value-mapper-default-value.json
b/integration-tests/transforms/metadata/dataset/golden-value-mapper-default-value.json
new file mode 100644
index 0000000000..aa4281363f
--- /dev/null
+++
b/integration-tests/transforms/metadata/dataset/golden-value-mapper-default-value.json
@@ -0,0 +1,40 @@
+{
+ "base_filename": "golden-value-mapper-default-value.csv",
+ "name": "golden-value-mapper-default-value",
+ "description": "",
+ "dataset_fields": [
+ {
+ "field_comment": "",
+ "field_length": 3,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "IATA_Code",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": 50,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "Airport_Name",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": 2,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "Country_Code",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": 16,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "Country_Name",
+ "field_format": ""
+ }
+ ],
+ "folder_name": ""
+}
\ No newline at end of file
diff --git
a/integration-tests/transforms/metadata/dataset/golden-value-mapper-empty-string.json
b/integration-tests/transforms/metadata/dataset/golden-value-mapper-empty-string.json
new file mode 100644
index 0000000000..f3fc65d92c
--- /dev/null
+++
b/integration-tests/transforms/metadata/dataset/golden-value-mapper-empty-string.json
@@ -0,0 +1,40 @@
+{
+ "base_filename": "golden-value-mapper-empty-string.csv",
+ "name": "golden-value-mapper-empty-string",
+ "description": "",
+ "dataset_fields": [
+ {
+ "field_comment": "",
+ "field_length": 3,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "IATA_Code",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": 50,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "Airport_Name",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": 2,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "Country_Code",
+ "field_format": ""
+ },
+ {
+ "field_comment": "",
+ "field_length": 16,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_name": "Country_Name",
+ "field_format": ""
+ }
+ ],
+ "folder_name": ""
+}
\ No newline at end of file
diff --git
a/integration-tests/transforms/metadata/unit-test/0045-value-mapper-default-value
UNIT.json
b/integration-tests/transforms/metadata/unit-test/0045-value-mapper-default-value
UNIT.json
new file mode 100644
index 0000000000..bad180479d
--- /dev/null
+++
b/integration-tests/transforms/metadata/unit-test/0045-value-mapper-default-value
UNIT.json
@@ -0,0 +1,14 @@
+{
+ "database_replacements": [],
+ "autoOpening": true,
+ "description": "",
+ "persist_filename": "",
+ "test_type": "UNIT_TEST",
+ "variableValues": [],
+ "basePath": "${HOP_UNIT_TESTS_FOLDER}",
+ "golden_data_sets": [],
+ "input_data_sets": [],
+ "name": "0045-value-mapper-default-value UNIT",
+ "trans_test_tweaks": [],
+ "pipeline_filename": "./0045-value-mapper-default-value.hpl"
+}
\ No newline at end of file
diff --git
a/integration-tests/transforms/metadata/unit-test/0045-value-mapper-empty-string
UNIT.json
b/integration-tests/transforms/metadata/unit-test/0045-value-mapper-empty-string
UNIT.json
new file mode 100644
index 0000000000..19f226aa6a
--- /dev/null
+++
b/integration-tests/transforms/metadata/unit-test/0045-value-mapper-empty-string
UNIT.json
@@ -0,0 +1,43 @@
+{
+ "database_replacements": [],
+ "autoOpening": true,
+ "description": "",
+ "persist_filename": "",
+ "test_type": "UNIT_TEST",
+ "variableValues": [],
+ "basePath": "${HOP_UNIT_TESTS_FOLDER}",
+ "golden_data_sets": [
+ {
+ "field_mappings": [
+ {
+ "transform_field": "IATA_Code",
+ "data_set_field": "IATA_Code"
+ },
+ {
+ "transform_field": "Airport_Name",
+ "data_set_field": "Airport_Name"
+ },
+ {
+ "transform_field": "Country_Code",
+ "data_set_field": "Country_Code"
+ },
+ {
+ "transform_field": "Country_Name",
+ "data_set_field": "Country_Name"
+ }
+ ],
+ "field_order": [
+ "IATA_Code",
+ "Airport_Name",
+ "Country_Code",
+ "Country_Name"
+ ],
+ "data_set_name": "golden-value-mapper-empty-string",
+ "transform_name": "Dummy"
+ }
+ ],
+ "input_data_sets": [],
+ "name": "0045-value-mapper-empty-string UNIT",
+ "trans_test_tweaks": [],
+ "pipeline_filename": "./0045-value-mapper-empty-string.hpl"
+}
\ No newline at end of file
diff --git
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapper.java
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapper.java
index 3472fdb1d6..fc63af53e4 100644
---
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapper.java
+++
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapper.java
@@ -35,6 +35,7 @@ import org.apache.hop.pipeline.transform.TransformMeta;
/** Convert Values in a certain fields to other values */
public class ValueMapper extends BaseTransform<ValueMapperMeta,
ValueMapperData> {
private static final Class<?> PKG = ValueMapperMeta.class;
+ public static final String CONST_STRING = "String";
private boolean nonMatchActivated = false;
@@ -99,19 +100,18 @@ public class ValueMapper extends
BaseTransform<ValueMapperMeta, ValueMapperData>
// Use only normal storage type in the HashMap
Object sourceData =
data.sourceValueMeta.convertToNormalStorageType(sourceValue);
Object target = null;
-
- // Null/Empty mapping to value...
- //
- if (data.emptyFieldValue != null && (r[data.keynr] == null || sourceData
== null)) {
- target = data.emptyFieldValue; // that's all there is to it.
- } else {
- if (sourceData != null) {
- if (data.mapValues.containsKey(sourceData)) {
- target = data.mapValues.get(sourceData);
- } else if (nonMatchActivated) {
- // If we have a nonMatchDefault and don't have a match
- target = data.nonMatchDefault;
- }
+ boolean mapped = false;
+
+ if (data.emptySourceMappingDefined && (r[data.keynr] == null || sourceData
== null)) {
+ target = data.emptyFieldValue;
+ mapped = true;
+ } else if (sourceData != null) {
+ if (data.mapValues.containsKey(sourceData)) {
+ target = data.mapValues.get(sourceData);
+ mapped = true;
+ } else if (nonMatchActivated) {
+ target = data.nonMatchDefault;
+ mapped = true;
}
}
@@ -121,8 +121,7 @@ public class ValueMapper extends
BaseTransform<ValueMapperMeta, ValueMapperData>
// Did we find anything to map to?
r[data.outputMeta.size() - 1] = target;
} else {
- // Don't set the original value to null if we don't have a target.
- if (target != null) {
+ if (mapped) {
r[data.keynr] = target;
} else {
// Convert to normal storage type.
@@ -156,7 +155,8 @@ public class ValueMapper extends
BaseTransform<ValueMapperMeta, ValueMapperData>
/** Build the value map, default-on-nonmatch, and empty/null mapping. */
private void builMapValues() throws HopException {
- IValueMeta stringValueMeta = new ValueMetaString("String");
+ data.emptySourceMappingDefined = false;
+ IValueMeta stringValueMeta = new ValueMetaString(CONST_STRING);
// --- Default for non-match --------------
//
try {
@@ -190,14 +190,18 @@ public class ValueMapper extends
BaseTransform<ValueMapperMeta, ValueMapperData>
throw new HopValueException(msg, ce);
}
Object tgtValue;
- try {
- tgtValue = keyFromString(data.targetValueMeta, stringValueMeta, tgt);
- } catch (HopValueException ce) {
- String msg =
- String.format(
- "Mapping entries: cannot convert target [%s] to target type
[%s].",
- src, typeName(data.sourceValueMeta));
- throw new HopValueException(msg, ce);
+ if (v.isEmptyStringEqualsNull() && Utils.isEmpty(tgt)) {
+ tgtValue = null;
+ } else {
+ try {
+ tgtValue = keyFromString(data.targetValueMeta, stringValueMeta, tgt);
+ } catch (HopValueException ce) {
+ String msg =
+ String.format(
+ "Mapping entries: cannot convert target [%s] to target type
[%s].",
+ src, typeName(data.sourceValueMeta));
+ throw new HopValueException(msg, ce);
+ }
}
if (srcValue != null) {
@@ -210,10 +214,15 @@ public class ValueMapper extends
BaseTransform<ValueMapperMeta, ValueMapperData>
//
for (Values v : meta.getValues()) {
if (Utils.isEmpty(v.getSource())) {
- if (data.emptyFieldValue == null) {
+ if (!data.emptySourceMappingDefined) {
String emptyFieldString = resolve(v.getTarget());
- data.emptyFieldValue =
- keyFromString(data.targetValueMeta, stringValueMeta,
emptyFieldString);
+ if (v.isEmptyStringEqualsNull() && Utils.isEmpty(emptyFieldString)) {
+ data.emptyFieldValue = null;
+ } else {
+ data.emptyFieldValue =
+ keyFromString(data.targetValueMeta, stringValueMeta,
emptyFieldString);
+ }
+ data.emptySourceMappingDefined = true;
} else {
throw new HopException(
BaseMessages.getString(
@@ -224,7 +233,8 @@ public class ValueMapper extends
BaseTransform<ValueMapperMeta, ValueMapperData>
}
/**
- * Resolve and set the target output meta type (user-selected, or
backward-compatible default).
+ * Resolve and set the target output meta type (user-selected, or String
when unspecified to match
+ * {@link ValueMapperMeta#getFields}).
*/
private void setTargetMetaType() {
try {
@@ -234,24 +244,19 @@ public class ValueMapper extends
BaseTransform<ValueMapperMeta, ValueMapperData>
if (!Utils.isEmpty(meta.getTargetType())) {
targetValueMetaName = meta.getTargetType();
targetValueMetaId =
ValueMetaFactory.getIdForValueMeta(targetValueMetaName);
+ if (targetValueMetaId == IValueMeta.TYPE_NONE) {
+ targetValueMetaId = IValueMeta.TYPE_STRING;
+ targetValueMetaName = CONST_STRING;
+ }
} else {
- // if inputMeta's size == outputMeta's size, the user has not
specified a new field for the
- // mapped values
- //
- boolean noNewField = data.previousMeta.size() ==
data.outputMeta.size();
-
- // for backward compatibility:
- // if the user hasn't specified a new field, the output type is the
- // same as the source field type, else, is String
- //
- targetValueMetaId = noNewField ? data.sourceValueMeta.getType() :
IValueMeta.TYPE_STRING;
- targetValueMetaName = data.sourceValueMeta.getName();
+ targetValueMetaId = IValueMeta.TYPE_STRING;
+ targetValueMetaName = CONST_STRING;
}
data.targetValueMeta =
ValueMetaFactory.createValueMeta(targetValueMetaName,
targetValueMetaId);
} catch (HopException e) {
- data.targetValueMeta = new ValueMetaString("String");
+ data.targetValueMeta = new ValueMetaString(CONST_STRING);
}
}
diff --git
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperData.java
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperData.java
index c518c9c0f9..8a01440ec3 100644
---
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperData.java
+++
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperData.java
@@ -34,6 +34,9 @@ public class ValueMapperData extends BaseTransformData
implements ITransformData
public Object nonMatchDefault;
public Object emptyFieldValue;
+ /** True when the meta defines the single empty-source mapping row (value
may still be null). */
+ public boolean emptySourceMappingDefined;
+
public IValueMeta targetValueMeta;
public IValueMeta outputValueMeta;
public IValueMeta sourceValueMeta;
diff --git
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperDialog.java
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperDialog.java
index 90371f785a..e6462c834d 100644
---
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperDialog.java
+++
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperDialog.java
@@ -17,6 +17,7 @@
package org.apache.hop.pipeline.transforms.valuemapper;
+import org.apache.hop.core.Const;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.row.IRowMeta;
import org.apache.hop.core.row.value.ValueMetaFactory;
@@ -174,7 +175,7 @@ public class ValueMapperDialog extends BaseTransformDialog {
fdlFields.top = new FormAttachment(wlValueType, margin);
wlFields.setLayoutData(fdlFields);
- final int FieldsCols = 2;
+ final int FieldsCols = 3;
final int FieldsRows = input.getValues().size();
ColumnInfo[] colinf = new ColumnInfo[FieldsCols];
@@ -189,6 +190,12 @@ public class ValueMapperDialog extends BaseTransformDialog
{
ColumnInfo.COLUMN_TYPE_TEXT,
false);
colinf[1].setUsingVariables(true);
+ colinf[2] =
+ new ColumnInfo(
+ BaseMessages.getString(PKG,
"ValueMapperDialog.Fields.Column.EmptyStringEqualsNull"),
+ ColumnInfo.COLUMN_TYPE_CCOMBO,
+ "Y",
+ "N");
wFields =
new TableView(
@@ -266,6 +273,7 @@ public class ValueMapperDialog extends BaseTransformDialog {
if (tgt != null) {
item.setText(2, tgt);
}
+ item.setText(3, v.isEmptyStringEqualsNull() ? "Y" : "N");
}
wFields.setRowNums();
@@ -295,6 +303,8 @@ public class ValueMapperDialog extends BaseTransformDialog {
Values v = new Values();
v.setSource(Utils.isEmpty(item.getText(1)) ? null : item.getText(1));
v.setTarget(item.getText(2));
+ String esn = item.getText(3);
+ v.setEmptyStringEqualsNull(Utils.isEmpty(esn) || Const.toBoolean(esn));
input.getValues().add(v);
}
diff --git
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMeta.java
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMeta.java
index ec20591c1f..9294d03699 100644
---
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMeta.java
+++
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMeta.java
@@ -19,6 +19,8 @@ package org.apache.hop.pipeline.transforms.valuemapper;
import java.util.ArrayList;
import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
import org.apache.hop.core.CheckResult;
import org.apache.hop.core.ICheckResult;
import org.apache.hop.core.annotations.Transform;
@@ -45,6 +47,8 @@ import org.apache.hop.pipeline.transform.TransformMeta;
categoryDescription =
"i18n:org.apache.hop.pipeline.transform:BaseTransform.Category.Transform",
keywords = "i18n::ValueMapperMeta.keyword",
documentationUrl = "/pipeline/transforms/valuemapper.html")
+@Getter
+@Setter
public class ValueMapperMeta extends BaseTransformMeta<ValueMapper,
ValueMapperData> {
private static final Class<?> PKG = ValueMapperMeta.class;
@@ -100,25 +104,25 @@ public class ValueMapperMeta extends
BaseTransformMeta<ValueMapper, ValueMapperD
}
}
- /**
- * @return Returns the fieldValue.
- */
- public List<Values> getValues() {
- return values;
- }
-
- /**
- * @param values The fieldValue to set.
- */
- public void setValues(List<Values> values) {
- this.values = values;
- }
-
@Override
public Object clone() {
return new ValueMapperMeta(this);
}
+ /** Longest mapped target literal including non-match default (for string
field sizing). */
+ private int maxLengthOfMappedStringValues() {
+ int maxlen = -1;
+ for (Values v : this.values) {
+ if (v.getTarget() != null && v.getTarget().length() > maxlen) {
+ maxlen = v.getTarget().length();
+ }
+ }
+ if (nonMatchDefault != null && nonMatchDefault.length() > maxlen) {
+ maxlen = nonMatchDefault.length();
+ }
+ return maxlen;
+ }
+
@Override
public void getFields(
IRowMeta r,
@@ -130,7 +134,7 @@ public class ValueMapperMeta extends
BaseTransformMeta<ValueMapper, ValueMapperD
IValueMeta extra = null;
- // Determine target value meta type (default to String for backward
compatibility)
+ // Determine target value meta type (default to String when unspecified)
String targetTypeName = Utils.isEmpty(getTargetType()) ? "String" :
getTargetType();
int targetTypeId = ValueMetaFactory.getIdForValueMeta(targetTypeName);
// fallback
@@ -149,18 +153,7 @@ public class ValueMapperMeta extends
BaseTransformMeta<ValueMapper, ValueMapperD
// Lengths etc?
// Take the max length of all the strings...
//
- int maxlen = -1;
- for (Values v : this.values) {
- if (v.getTarget() != null && v.getTarget().length() > maxlen) {
- maxlen = v.getTarget().length();
- }
- }
-
- // include default value in max length calculation
- //
- if (nonMatchDefault != null && nonMatchDefault.length() > maxlen) {
- maxlen = nonMatchDefault.length();
- }
+ int maxlen = maxLengthOfMappedStringValues();
extra.setLength(maxlen);
extra.setOrigin(name);
}
@@ -175,6 +168,25 @@ public class ValueMapperMeta extends
BaseTransformMeta<ValueMapper, ValueMapperD
// The output of a changed field or new field is always a normal storage
type...
//
extra.setStorageType(IValueMeta.STORAGE_TYPE_NORMAL);
+
+ // In-place mapping with no explicit target type: treat output as String
(same as new field)
+ if (Utils.isEmpty(getTargetField())
+ && Utils.isEmpty(getTargetType())
+ && extra.getType() != IValueMeta.TYPE_STRING) {
+ int idx = r.indexOfValue(extra.getName());
+ r.removeValueMeta(idx);
+ IValueMeta stringMeta;
+ try {
+ stringMeta = ValueMetaFactory.createValueMeta(extra.getName(),
IValueMeta.TYPE_STRING);
+ } catch (HopPluginException e) {
+ stringMeta = new ValueMetaString(extra.getName());
+ }
+ int maxlen = maxLengthOfMappedStringValues();
+ stringMeta.setLength(maxlen);
+ stringMeta.setOrigin(name);
+ stringMeta.setStorageType(IValueMeta.STORAGE_TYPE_NORMAL);
+ r.addValueMeta(idx, stringMeta);
+ }
}
}
@@ -229,62 +241,4 @@ public class ValueMapperMeta extends
BaseTransformMeta<ValueMapper, ValueMapperD
remarks.add(cr);
}
}
-
- /**
- * @return Returns the fieldToUse.
- */
- public String getFieldToUse() {
- return fieldToUse;
- }
-
- /**
- * @param fieldToUse The fieldToUse to set.
- */
- public void setFieldToUse(String fieldToUse) {
- this.fieldToUse = fieldToUse;
- }
-
- /**
- * @return Returns the targetField.
- */
- public String getTargetField() {
- return targetField;
- }
-
- /**
- * @param targetField The targetField to set.
- */
- public void setTargetField(String targetField) {
- this.targetField = targetField;
- }
-
- /**
- * @return the non match default. This is the string that will be used to
fill in the data when no
- * match is found.
- */
- public String getNonMatchDefault() {
- return nonMatchDefault;
- }
-
- /**
- * @param nonMatchDefault the non match default. This is the string that
will be used to fill in
- * the data when no match is found.
- */
- public void setNonMatchDefault(String nonMatchDefault) {
- this.nonMatchDefault = nonMatchDefault;
- }
-
- /**
- * @return Returns the targetType.
- */
- public String getTargetType() {
- return targetType;
- }
-
- /**
- * @param targetType The targetType to set.
- */
- public void setTargetType(String targetType) {
- this.targetType = targetType;
- }
}
diff --git
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/Values.java
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/Values.java
index 52863c4791..cf14ba54d2 100644
---
a/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/Values.java
+++
b/plugins/transforms/valuemapper/src/main/java/org/apache/hop/pipeline/transforms/valuemapper/Values.java
@@ -18,9 +18,13 @@
package org.apache.hop.pipeline.transforms.valuemapper;
+import lombok.Getter;
+import lombok.Setter;
import org.apache.hop.metadata.api.HopMetadataProperty;
/** The values for mapping */
+@Getter
+@Setter
public class Values {
@HopMetadataProperty(
@@ -35,51 +39,27 @@ public class Values {
injectionKeyDescription = "ValueMapper.Injection.TARGET")
private String target;
+ /**
+ * When true (default), a resolved empty target value is emitted as null
instead of converting an
+ * empty string to the target type.
+ */
+ @HopMetadataProperty(
+ key = "empty_string_equals_null",
+ injectionKey = "EMPTY_STRING_EQUALS_NULL",
+ injectionKeyDescription =
"ValueMapper.Injection.EMPTY_STRING_EQUALS_NULL",
+ defaultBoolean = true)
+ private boolean emptyStringEqualsNull = true;
+
public Values() {}
public Values(Values other) {
this.source = other.source;
this.target = other.target;
+ this.emptyStringEqualsNull = other.emptyStringEqualsNull;
}
public Values(String source, String target) {
this.source = source;
this.target = target;
}
-
- /**
- * Gets source value
- *
- * @return value of source
- */
- public String getSource() {
- return source;
- }
-
- /**
- * Sets source value
- *
- * @param value value of source
- */
- public void setSource(String value) {
- this.source = value;
- }
-
- /**
- * Gets target value
- *
- * @return value of source
- */
- public String getTarget() {
- return target;
- }
-
- /**
- * Sets target value
- *
- * @param value value of target
- */
- public void setTarget(String value) {
- this.target = value;
- }
}
diff --git
a/plugins/transforms/valuemapper/src/main/resources/org/apache/hop/pipeline/transforms/valuemapper/messages/messages_en_US.properties
b/plugins/transforms/valuemapper/src/main/resources/org/apache/hop/pipeline/transforms/valuemapper/messages/messages_en_US.properties
index a3d467f1b7..ff486b5882 100644
---
a/plugins/transforms/valuemapper/src/main/resources/org/apache/hop/pipeline/transforms/valuemapper/messages/messages_en_US.properties
+++
b/plugins/transforms/valuemapper/src/main/resources/org/apache/hop/pipeline/transforms/valuemapper/messages/messages_en_US.properties
@@ -20,6 +20,7 @@ ValueMapper.Injection.FIELDNAME=The source field to use for
mapping
ValueMapper.Injection.NON_MATCH_DEFAULT=The default value if there is not a
match during lookup
ValueMapper.Injection.SOURCE=The source values for mapping
ValueMapper.Injection.TARGET=The target values for mapping
+ValueMapper.Injection.EMPTY_STRING_EQUALS_NULL=When Y, an empty target value
is emitted as null
ValueMapper.Injection.TARGET_FIELDNAME=The target field to use for mapping
ValueMapper.Injection.VALUES=The values for mapping
ValueMapper.Name=Value mapper
@@ -31,6 +32,7 @@ ValueMapperDialog.FailedToGetFields.DialogTitle=Error
ValueMapperDialog.FieldnameToUser.Label=Fieldname to use
ValueMapperDialog.Fields.Column.SourceValue=Source value
ValueMapperDialog.Fields.Column.TargetValue=Target value
+ValueMapperDialog.Fields.Column.EmptyStringEqualsNull=Empty string equals null
ValueMapperDialog.Fields.Label=Field values\:
ValueMapperDialog.NonMatchDefault.Label=Default upon non-matching
ValueMapperDialog.TargetFieldname.Label=Target field name (empty\=overwrite)
diff --git
a/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaInjectionTest.java
b/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaInjectionTest.java
index 44b167358d..429aef1dc6 100644
---
a/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaInjectionTest.java
+++
b/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaInjectionTest.java
@@ -34,11 +34,14 @@ class ValueMapperMetaInjectionTest extends
BaseMetadataInjectionTestJunit5<Value
@Test
void test() throws Exception {
+ meta.getValues().clear();
+ meta.getValues().add(new Values());
check("FIELDNAME", () -> meta.getFieldToUse());
check("TARGET_FIELDNAME", () -> meta.getTargetField());
check("NON_MATCH_DEFAULT", () -> meta.getNonMatchDefault());
check("SOURCE", () -> meta.getValues().get(0).getSource());
check("TARGET", () -> meta.getValues().get(0).getTarget());
+ check("EMPTY_STRING_EQUALS_NULL", () ->
meta.getValues().get(0).isEmptyStringEqualsNull());
check("TARGET_TYPE", () -> meta.getTargetType());
}
}
diff --git
a/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaTest.java
b/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaTest.java
index 97c2aaccfd..f91e1d6f73 100644
---
a/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaTest.java
+++
b/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperMetaTest.java
@@ -17,15 +17,22 @@
package org.apache.hop.pipeline.transforms.valuemapper;
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.assertNull;
+import static org.mockito.Mockito.mock;
+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.IRowMeta;
import org.apache.hop.core.row.IValueMeta;
import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaInteger;
import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.pipeline.PipelineMeta;
import org.apache.hop.pipeline.transform.TransformMeta;
import org.apache.hop.pipeline.transform.TransformSerializationTestUtil;
import org.junit.jupiter.api.BeforeEach;
@@ -40,43 +47,52 @@ class ValueMapperMetaTest {
}
@Test
- void testBackwardNoNewFieldNoType() throws Exception {
+ void testInPlaceNoTargetTypeDefaultsToString() throws Exception {
ValueMapperMeta meta =
TransformSerializationTestUtil.testSerialization(
"/value-mapper-transform.xml", ValueMapperMeta.class);
- // backward-compat scenario: in-place mapping, no explicit type
+ // In-place mapping, no explicit target type: output column type is String
meta.setFieldToUse("Country_Code");
meta.setTargetField("");
meta.setTargetType(null);
- // Input schema: only the source field, String
+ // Input schema: numeric codes mapped to country names → output must be
string
IRowMeta input = new RowMeta();
- ValueMetaString src = new ValueMetaString("Country_Code");
- src.setLength(50);
+ ValueMetaInteger src = new ValueMetaInteger("Country_Code");
input.addValueMeta(src);
- // target field must not exist in input
assertNull(input.searchValueMeta("Country_Name"));
IRowMeta out = input.clone();
meta.getFields(out, "ValueMapper", null, new TransformMeta(), null, null);
- // No new field added
assertEquals(
input.size(), out.size(), "Output meta should have same number of
fields as input");
assertNull(out.searchValueMeta("Country_Name"), "No target field should be
added");
- // Same field present with same name and type (String), unchanged length
- IValueMeta inVm = input.searchValueMeta("Country_Code");
IValueMeta outVm = out.searchValueMeta("Country_Code");
assertNotNull(outVm, "Source field must remain present");
- assertEquals(inVm.getType(), outVm.getType(), "Type should remain
unchanged in-place");
- assertEquals(inVm.getName(), outVm.getName(), "Name should remain
unchanged");
- assertEquals(inVm.getLength(), outVm.getLength(), "Length should remain
unchanged");
+ assertEquals(IValueMeta.TYPE_STRING, outVm.getType(), "Unspecified target
type must be String");
+ assertEquals("Country_Code", outVm.getName(), "Name should remain
unchanged");
+ assertEquals(16, outVm.getLength(), "Length from longest mapping / default
literals");
assertEquals(IValueMeta.STORAGE_TYPE_NORMAL, outVm.getStorageType(),
"Storage must be NORMAL");
}
+ @Test
+ void legacyXmlMissingEmptyStringEqualsNullDefaultsToTrue() throws Exception {
+ ValueMapperMeta meta =
+ TransformSerializationTestUtil.testSerialization(
+ "/value-mapper-transform.xml", ValueMapperMeta.class);
+
+ for (Values v : meta.getValues()) {
+ assertEquals(
+ true,
+ v.isEmptyStringEqualsNull(),
+ "Older pipelines without empty_string_equals_null must default to Y
(true)");
+ }
+ }
+
@Test
void testSerialization() throws Exception {
ValueMapperMeta meta =
@@ -112,4 +128,99 @@ class ValueMapperMetaTest {
out.searchValueMeta("Country_Name").getType(),
"New target field should default to String type when no explicit
target type is set");
}
+
+ @Test
+ void cloneCopiesValuesAndEmptyStringEqualsNull() {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("src");
+ Values v = new Values();
+ v.setSource("x");
+ v.setTarget("y");
+ v.setEmptyStringEqualsNull(false);
+ meta.getValues().add(v);
+
+ ValueMapperMeta clone = (ValueMapperMeta) meta.clone();
+ assertEquals(1, clone.getValues().size());
+ assertFalse(clone.getValues().get(0).isEmptyStringEqualsNull());
+ assertEquals("x", clone.getValues().get(0).getSource());
+ assertEquals("y", clone.getValues().get(0).getTarget());
+ }
+
+ @Test
+ void checkAddsWarningWhenNoPreviousFields() {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ List<ICheckResult> remarks = new ArrayList<>();
+ PipelineMeta pipelineMeta = mock(PipelineMeta.class);
+ TransformMeta transformMeta = mock(TransformMeta.class);
+ meta.check(
+ remarks,
+ pipelineMeta,
+ transformMeta,
+ null,
+ new String[] {"in"},
+ new String[0],
+ null,
+ null,
+ null);
+ assertEquals(2, remarks.size());
+ assertEquals(ICheckResult.TYPE_RESULT_WARNING, remarks.get(0).getType());
+ assertEquals(ICheckResult.TYPE_RESULT_OK, remarks.get(1).getType());
+ }
+
+ @Test
+ void checkAddsErrorWhenNoInputTransforms() {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ List<ICheckResult> remarks = new ArrayList<>();
+ PipelineMeta pipelineMeta = mock(PipelineMeta.class);
+ TransformMeta transformMeta = mock(TransformMeta.class);
+ IRowMeta prev = new RowMeta();
+ prev.addValueMeta(new ValueMetaString("c"));
+ meta.check(
+ remarks, pipelineMeta, transformMeta, prev, new String[0], new
String[0], null, null, null);
+ assertEquals(2, remarks.size());
+ assertEquals(ICheckResult.TYPE_RESULT_OK, remarks.get(0).getType());
+ assertEquals(ICheckResult.TYPE_RESULT_ERROR, remarks.get(1).getType());
+ }
+
+ @Test
+ void getFieldsAddsIntegerTypedTargetColumnWhenSpecified() throws Exception {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("id");
+ meta.setTargetField("amt");
+ meta.setTargetType("Integer");
+ meta.getValues().add(new Values("1", "42"));
+ IRowMeta in = new RowMeta();
+ in.addValueMeta(new ValueMetaString("id"));
+ IRowMeta out = in.clone();
+ meta.getFields(out, "vm", null, new TransformMeta(), null, null);
+ assertEquals(IValueMeta.TYPE_INTEGER,
out.searchValueMeta("amt").getType());
+ }
+
+ @Test
+ void getFieldsUnknownTargetTypeFallsBackToStringForNewColumn() throws
Exception {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("id");
+ meta.setTargetField("out");
+ meta.setTargetType("bogus_type_xyz");
+ meta.getValues().add(new Values("a", "b"));
+ IRowMeta in = new RowMeta();
+ in.addValueMeta(new ValueMetaString("id"));
+ IRowMeta out = in.clone();
+ meta.getFields(out, "vm", null, new TransformMeta(), null, null);
+ assertEquals(IValueMeta.TYPE_STRING, out.searchValueMeta("out").getType());
+ }
+
+ @Test
+ void inPlaceWithExplicitTargetTypeDoesNotCoerceColumnInGetFields() throws
Exception {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("code");
+ meta.setTargetField("");
+ meta.setTargetType("String");
+ meta.getValues().add(new Values("1", "one"));
+ IRowMeta in = new RowMeta();
+ in.addValueMeta(new ValueMetaInteger("code"));
+ IRowMeta out = in.clone();
+ meta.getFields(out, "vm", null, new TransformMeta(), null, null);
+ assertEquals(IValueMeta.TYPE_INTEGER,
out.searchValueMeta("code").getType());
+ }
}
diff --git
a/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperTest.java
b/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperTest.java
new file mode 100644
index 0000000000..44e6cc6592
--- /dev/null
+++
b/plugins/transforms/valuemapper/src/test/java/org/apache/hop/pipeline/transforms/valuemapper/ValueMapperTest.java
@@ -0,0 +1,464 @@
+/*
+ * 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.valuemapper;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopValueException;
+import org.apache.hop.core.logging.ILoggingObject;
+import org.apache.hop.core.plugins.PluginRegistry;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaInteger;
+import org.apache.hop.core.row.value.ValueMetaPluginType;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.pipeline.transforms.mock.TransformMockHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+
+@MockitoSettings(strictness = Strictness.LENIENT)
+class ValueMapperTest {
+
+ @RegisterExtension
+ static RestoreHopEngineEnvironmentExtension env = new
RestoreHopEngineEnvironmentExtension();
+
+ private TransformMockHelper<ValueMapperMeta, ValueMapperData> helper;
+
+ @BeforeAll
+ static void initHop() throws Exception {
+ HopEnvironment.init();
+ PluginRegistry.init();
+ ValueMetaPluginType.getInstance().searchPlugins();
+ }
+
+ @BeforeEach
+ void setUp() {
+ helper =
+ new TransformMockHelper<>("ValueMapperTest", ValueMapperMeta.class,
ValueMapperData.class);
+ when(helper.logChannelFactory.create(any(), any(ILoggingObject.class)))
+ .thenReturn(helper.iLogChannel);
+ when(helper.pipeline.isRunning()).thenReturn(true);
+ when(helper.transformMeta.isPartitioned()).thenReturn(false);
+ helper.pipeline.setVariables(new Variables());
+ }
+
+ @AfterEach
+ void tearDown() {
+ helper.cleanUp();
+ }
+
+ private ValueMapper newSpyTransform(ValueMapperMeta meta, ValueMapperData
data) {
+ ValueMapper transform =
+ spy(
+ new ValueMapper(
+ helper.transformMeta, meta, data, 0, helper.pipelineMeta,
helper.pipeline));
+ transform.setMetadataProvider(new MemoryMetadataProvider());
+ data.mapValues = new HashMap<>();
+ return transform;
+ }
+
+ @Test
+ void inPlaceEmptyTargetWithDefaultFlagEmitsNull() throws Exception {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("code");
+ meta.setTargetField("");
+ meta.setTargetType(null);
+ Values mapRow = new Values();
+ mapRow.setSource("X");
+ mapRow.setTarget("");
+ meta.getValues().add(mapRow);
+
+ ValueMapperData data = new ValueMapperData();
+ ValueMapper vm = newSpyTransform(meta, data);
+ RowMeta inputMeta = new RowMeta();
+ inputMeta.addValueMeta(new ValueMetaString("code"));
+ doReturn(new Object[] {"X"}).doReturn(null).when(vm).getRow();
+ doReturn(inputMeta).when(vm).getInputRowMeta();
+
+ List<Object[]> outputs = new ArrayList<>();
+ doAnswer(
+ inv -> {
+ outputs.add(((Object[]) inv.getArgument(1)).clone());
+ return null;
+ })
+ .when(vm)
+ .putRow(any(IRowMeta.class), any(Object[].class));
+
+ assertTrue(vm.processRow());
+ assertEquals(1, outputs.size());
+ assertNull(outputs.get(0)[0]);
+ assertFalse(vm.processRow());
+ }
+
+ @Test
+ void inPlaceEmptyTargetWithFlagNoEmitsEmptyString() throws Exception {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("code");
+ meta.setTargetField("");
+ meta.setTargetType(null);
+ Values mapRow = new Values();
+ mapRow.setSource("X");
+ mapRow.setTarget("");
+ mapRow.setEmptyStringEqualsNull(false);
+ meta.getValues().add(mapRow);
+
+ ValueMapperData data = new ValueMapperData();
+ ValueMapper vm = newSpyTransform(meta, data);
+ RowMeta inputMeta = new RowMeta();
+ inputMeta.addValueMeta(new ValueMetaString("code"));
+ doReturn(new Object[] {"X"}).doReturn(null).when(vm).getRow();
+ doReturn(inputMeta).when(vm).getInputRowMeta();
+
+ List<Object[]> outputs = new ArrayList<>();
+ doAnswer(
+ inv -> {
+ outputs.add(((Object[]) inv.getArgument(1)).clone());
+ return null;
+ })
+ .when(vm)
+ .putRow(any(IRowMeta.class), any(Object[].class));
+
+ assertTrue(vm.processRow());
+ assertEquals("", outputs.get(0)[0]);
+ }
+
+ @Test
+ void emptySourceWithEmptyTargetAndFlagNEmitsEmptyString() throws Exception {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("code");
+ meta.setTargetField("");
+ Values emptyKey = new Values();
+ emptyKey.setSource(null);
+ emptyKey.setTarget("");
+ emptyKey.setEmptyStringEqualsNull(false);
+ meta.getValues().add(emptyKey);
+
+ ValueMapperData data = new ValueMapperData();
+ ValueMapper vm = newSpyTransform(meta, data);
+ RowMeta inputMeta = new RowMeta();
+ inputMeta.addValueMeta(new ValueMetaString("code"));
+ doReturn(new Object[] {null}).doReturn(null).when(vm).getRow();
+ doReturn(inputMeta).when(vm).getInputRowMeta();
+
+ List<Object[]> outputs = new ArrayList<>();
+ doAnswer(
+ inv -> {
+ outputs.add(((Object[]) inv.getArgument(1)).clone());
+ return null;
+ })
+ .when(vm)
+ .putRow(any(IRowMeta.class), any(Object[].class));
+
+ assertTrue(vm.processRow());
+ assertEquals("", outputs.get(0)[0]);
+ }
+
+ @Test
+ void emptySourceMappingWithEmptyTargetAndFlagYEmitsNull() throws Exception {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("code");
+ meta.setTargetField("");
+ Values emptyKey = new Values();
+ emptyKey.setSource(null);
+ emptyKey.setTarget("");
+ meta.getValues().add(emptyKey);
+
+ ValueMapperData data = new ValueMapperData();
+ ValueMapper vm = newSpyTransform(meta, data);
+ RowMeta inputMeta = new RowMeta();
+ inputMeta.addValueMeta(new ValueMetaString("code"));
+ doReturn(new Object[] {null}).doReturn(null).when(vm).getRow();
+ doReturn(inputMeta).when(vm).getInputRowMeta();
+
+ List<Object[]> outputs = new ArrayList<>();
+ doAnswer(
+ inv -> {
+ outputs.add(((Object[]) inv.getArgument(1)).clone());
+ return null;
+ })
+ .when(vm)
+ .putRow(any(IRowMeta.class), any(Object[].class));
+
+ assertTrue(vm.processRow());
+ assertNull(outputs.get(0)[0]);
+ }
+
+ @Test
+ void nonMatchDefaultUsedWhenNoMapping() throws Exception {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("code");
+ meta.setTargetField("");
+ meta.setNonMatchDefault("DEFAULT");
+ Values mapRow = new Values();
+ mapRow.setSource("A");
+ mapRow.setTarget("B");
+ meta.getValues().add(mapRow);
+
+ ValueMapperData data = new ValueMapperData();
+ ValueMapper vm = newSpyTransform(meta, data);
+ RowMeta inputMeta = new RowMeta();
+ inputMeta.addValueMeta(new ValueMetaString("code"));
+ doReturn(new Object[] {"Z"}).doReturn(null).when(vm).getRow();
+ doReturn(inputMeta).when(vm).getInputRowMeta();
+
+ List<Object[]> outputs = new ArrayList<>();
+ doAnswer(
+ inv -> {
+ outputs.add(((Object[]) inv.getArgument(1)).clone());
+ return null;
+ })
+ .when(vm)
+ .putRow(any(IRowMeta.class), any(Object[].class));
+
+ assertTrue(vm.processRow());
+ assertEquals("DEFAULT", outputs.get(0)[0]);
+ }
+
+ @Test
+ void newTargetFieldAppendsMappedValue() throws Exception {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("code");
+ meta.setTargetField("mapped");
+ meta.setTargetType(null);
+ Values mapRow = new Values();
+ mapRow.setSource("A");
+ mapRow.setTarget("B");
+ meta.getValues().add(mapRow);
+
+ ValueMapperData data = new ValueMapperData();
+ ValueMapper vm = newSpyTransform(meta, data);
+ RowMeta inputMeta = new RowMeta();
+ inputMeta.addValueMeta(new ValueMetaString("code"));
+ doReturn(new Object[] {"A"}).doReturn(null).when(vm).getRow();
+ doReturn(inputMeta).when(vm).getInputRowMeta();
+
+ List<Object[]> outputs = new ArrayList<>();
+ doAnswer(
+ inv -> {
+ outputs.add(((Object[]) inv.getArgument(1)).clone());
+ return null;
+ })
+ .when(vm)
+ .putRow(any(IRowMeta.class), any(Object[].class));
+
+ assertTrue(vm.processRow());
+ Object[] out = outputs.get(0);
+ assertEquals("A", out[0]);
+ assertEquals("B", out[1]);
+ }
+
+ @Test
+ void fieldToUseMissingReturnsFalseAndStops() throws Exception {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("missing");
+ meta.setTargetField("");
+ meta.getValues().add(mapping("A", "B"));
+
+ ValueMapperData data = new ValueMapperData();
+ ValueMapper vm = newSpyTransform(meta, data);
+ RowMeta inputMeta = new RowMeta();
+ inputMeta.addValueMeta(new ValueMetaString("code"));
+ doReturn(new Object[] {"A"}).when(vm).getRow();
+ doReturn(inputMeta).when(vm).getInputRowMeta();
+
+ assertFalse(vm.processRow());
+ verify(helper.pipeline).stopAll();
+ }
+
+ @Test
+ void duplicateEmptySourceRowsThrowHopException() throws Exception {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("code");
+ meta.setTargetField("");
+ Values v1 = new Values();
+ v1.setSource(null);
+ v1.setTarget("a");
+ Values v2 = new Values();
+ v2.setSource(null);
+ v2.setTarget("b");
+ meta.getValues().add(v1);
+ meta.getValues().add(v2);
+
+ ValueMapperData data = new ValueMapperData();
+ ValueMapper vm = newSpyTransform(meta, data);
+ RowMeta inputMeta = new RowMeta();
+ inputMeta.addValueMeta(new ValueMetaString("code"));
+ doReturn(new Object[] {null}).when(vm).getRow();
+ doReturn(inputMeta).when(vm).getInputRowMeta();
+
+ assertThrows(HopException.class, vm::processRow);
+ }
+
+ @Test
+ void disposeDoesNotThrow() {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ ValueMapperData data = new ValueMapperData();
+ ValueMapper vm = newSpyTransform(meta, data);
+ vm.dispose();
+ }
+
+ @Test
+ void initPreparesMap() {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ ValueMapperData data = new ValueMapperData();
+ ValueMapper vm =
+ new ValueMapper(helper.transformMeta, meta, data, 0,
helper.pipelineMeta, helper.pipeline);
+ assertTrue(vm.init());
+ assertTrue(data.mapValues.isEmpty());
+ }
+
+ @Test
+ void inPlaceIntegerSourceMappedToStringValue() throws Exception {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("code");
+ meta.setTargetField("");
+ meta.setTargetType(null);
+ meta.getValues().add(mapping("1", "one"));
+
+ ValueMapperData data = new ValueMapperData();
+ ValueMapper vm = newSpyTransform(meta, data);
+ RowMeta inputMeta = new RowMeta();
+ inputMeta.addValueMeta(new ValueMetaInteger("code"));
+ doReturn(new Object[] {1L}).doReturn(null).when(vm).getRow();
+ doReturn(inputMeta).when(vm).getInputRowMeta();
+
+ List<Object[]> outputs = new ArrayList<>();
+ doAnswer(
+ inv -> {
+ outputs.add(((Object[]) inv.getArgument(1)).clone());
+ return null;
+ })
+ .when(vm)
+ .putRow(any(IRowMeta.class), any(Object[].class));
+
+ assertTrue(vm.processRow());
+ assertEquals("one", outputs.get(0)[0]);
+ }
+
+ @Test
+ void invalidNonMatchDefaultForIntegerTargetThrowsHopValueException() throws
Exception {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("code");
+ meta.setTargetField("");
+ meta.setTargetType("Integer");
+ meta.setNonMatchDefault("not_an_int");
+ meta.getValues().add(mapping("1", "1"));
+
+ ValueMapperData data = new ValueMapperData();
+ ValueMapper vm = newSpyTransform(meta, data);
+ RowMeta inputMeta = new RowMeta();
+ inputMeta.addValueMeta(new ValueMetaString("code"));
+ doReturn(new Object[] {"1"}).when(vm).getRow();
+ doReturn(inputMeta).when(vm).getInputRowMeta();
+
+ assertThrows(HopValueException.class, vm::processRow);
+ }
+
+ @Test
+ void invalidMappedSourceForIntegerFieldThrowsHopValueException() throws
Exception {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("code");
+ meta.setTargetField("");
+ meta.setTargetType("Integer");
+ meta.getValues().add(mapping("not_an_int", "1"));
+
+ ValueMapperData data = new ValueMapperData();
+ ValueMapper vm = newSpyTransform(meta, data);
+ RowMeta inputMeta = new RowMeta();
+ inputMeta.addValueMeta(new ValueMetaInteger("code"));
+ doReturn(new Object[] {1L}).when(vm).getRow();
+ doReturn(inputMeta).when(vm).getInputRowMeta();
+
+ assertThrows(HopValueException.class, vm::processRow);
+ }
+
+ @Test
+ void invalidMappedTargetForIntegerTypeThrowsHopValueException() throws
Exception {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("code");
+ meta.setTargetField("");
+ meta.setTargetType("Integer");
+ meta.getValues().add(mapping("A", "not_an_int"));
+
+ ValueMapperData data = new ValueMapperData();
+ ValueMapper vm = newSpyTransform(meta, data);
+ RowMeta inputMeta = new RowMeta();
+ inputMeta.addValueMeta(new ValueMetaString("code"));
+ doReturn(new Object[] {"A"}).when(vm).getRow();
+ doReturn(inputMeta).when(vm).getInputRowMeta();
+
+ assertThrows(HopValueException.class, vm::processRow);
+ }
+
+ @Test
+ void unknownTargetTypeFallsBackToStringMeta() throws Exception {
+ ValueMapperMeta meta = new ValueMapperMeta();
+ meta.setFieldToUse("code");
+ meta.setTargetField("");
+ meta.setTargetType("NotARealHopTypeName");
+ meta.getValues().add(mapping("A", "B"));
+
+ ValueMapperData data = new ValueMapperData();
+ ValueMapper vm = newSpyTransform(meta, data);
+ RowMeta inputMeta = new RowMeta();
+ inputMeta.addValueMeta(new ValueMetaString("code"));
+ doReturn(new Object[] {"A"}).doReturn(null).when(vm).getRow();
+ doReturn(inputMeta).when(vm).getInputRowMeta();
+
+ List<Object[]> outputs = new ArrayList<>();
+ doAnswer(
+ inv -> {
+ outputs.add(((Object[]) inv.getArgument(1)).clone());
+ return null;
+ })
+ .when(vm)
+ .putRow(any(IRowMeta.class), any(Object[].class));
+
+ assertTrue(vm.processRow());
+ assertEquals("B", outputs.get(0)[0]);
+ }
+
+ private static Values mapping(String source, String target) {
+ Values v = new Values();
+ v.setSource(source);
+ v.setTarget(target);
+ return v;
+ }
+}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
index 295e10b086..a7e5b165ee 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
@@ -953,9 +953,12 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
}
// If we mousedown on action icon but never committed to drag, treat as
click (promote for click
- // handling below)
+ // handling below). Do not promote when this mouseUp is already a
background click: otherwise
+ // stale currentAction makes an empty-canvas release look like an action
click and selection is
+ // never cleared.
if (selectedAction == null
&& currentAction != null
+ && singleClickType != SingleClickType.Workflow
&& startHopAction == null
&& selectionRegion == null
&& selectedActions == null