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 9ba6a788ad Add support for specifying target field type in Value 
Mapper #5465 (#5930)
9ba6a788ad is described below

commit 9ba6a788adfc381d9de150f7ed6a86f8df84819c
Author: Matteo <[email protected]>
AuthorDate: Fri Nov 14 15:13:00 2025 +0100

    Add support for specifying target field type in Value Mapper #5465 (#5930)
    
    * Add support for specifying target field type in Value Mapper #5465
    
    * # Fix xml and beam tests to allow compilation
    
    # Conflicts:
    #       
plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/xsdvalidator/XsdValidatorIntTest.java
---
 .../pages/pipeline/transforms/valuemapper.adoc     |   8 +
 .../transforms/0045-value-mapper-output-type.hpl   | 205 +++++++++++++++++++++
 .../transforms/main-0045-value-mapper.hwf          |  71 ++++++-
 .../direct/BeamDirectPipelineEngineTest.java       |   7 +
 .../transforms/valuemapper/ValueMapper.java        | 194 +++++++++++++------
 .../transforms/valuemapper/ValueMapperData.java    |  11 +-
 .../transforms/valuemapper/ValueMapperDialog.java  |  29 ++-
 .../transforms/valuemapper/ValueMapperMeta.java    |  73 ++++++--
 .../valuemapper/messages/messages_en_US.properties |   1 +
 .../valuemapper/messages/messages_it_IT.properties |   1 +
 .../valuemapper/ValueMapperMetaInjectionTest.java  |   1 +
 .../valuemapper/ValueMapperMetaTest.java           |  61 ++++++
 .../hop/pipeline/transforms/xml/xslt/XsltTest.java |   3 +
 13 files changed, 574 insertions(+), 91 deletions(-)

diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/valuemapper.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/valuemapper.adoc
index a2fae779f2..02d1ddd284 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/valuemapper.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/valuemapper.adoc
@@ -63,6 +63,14 @@ The following properties are used to define the mappings:
 |Target field name|Field to use as the mapping Target
 |Default upon non-matching|Defines a default value for situations where the 
source value is not empty, but there is no match
 |Field values table|Contains the mapping of source value to converted target 
value.
+|Target field type |Specifies the data type of the mapped (target) values.
+If not set, the transform falls back to:
+
+- String when writing to a new target field.
+
+- The same type as the source field when overwriting the source field.
+It fails if the values are incompatible.
+
 |===
 
 === Mapping NULL values
diff --git a/integration-tests/transforms/0045-value-mapper-output-type.hpl 
b/integration-tests/transforms/0045-value-mapper-output-type.hpl
new file mode 100644
index 0000000000..f118baec9e
--- /dev/null
+++ b/integration-tests/transforms/0045-value-mapper-output-type.hpl
@@ -0,0 +1,205 @@
+<?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>0045-value-mapper-output-type</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description/>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2025/10/27 12:25:52.296</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2025/10/27 12:25:52.296</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>Data grid</from>
+      <to>Value mapper</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Value mapper</from>
+      <to>Data validator</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>Data grid</name>
+    <type>DataGrid</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <data>
+      <line>
+        <item>a</item>
+        <item>1</item>
+      </line>
+      <line>
+        <item>b</item>
+        <item/>
+      </line>
+      <line>
+        <item>c</item>
+        <item>1</item>
+      </line>
+      <line>
+        <item>d</item>
+        <item>2</item>
+      </line>
+      <line>
+        <item>e</item>
+        <item>2</item>
+      </line>
+      <line>
+        <item>f</item>
+        <item>2</item>
+      </line>
+      <line>
+        <item>g</item>
+        <item>77</item>
+      </line>
+      <line>
+        <item>h</item>
+        <item>77</item>
+      </line>
+    </data>
+    <fields>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>letter</name>
+        <type>String</type>
+      </field>
+      <field>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <name>type</name>
+        <type>Integer</type>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>128</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Data validator</name>
+    <type>Validator</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <concat_errors>N</concat_errors>
+    <concat_separator/>
+    <validate_all>N</validate_all>
+    <validator_field>
+      <allowed_value>
+</allowed_value>
+      <conversion_mask/>
+      <data_type>UUID</data_type>
+      <data_type_verified>N</data_type_verified>
+      <decimal_symbol/>
+      <end_string/>
+      <end_string_not_allowed/>
+      <error_code/>
+      <error_description/>
+      <grouping_symbol/>
+      <is_sourcing_values>N</is_sourcing_values>
+      <max_length/>
+      <max_value/>
+      <min_length/>
+      <min_value/>
+      <name>uuid_type</name>
+      <null_allowed>N</null_allowed>
+      <only_null_allowed>N</only_null_allowed>
+      <only_numeric_allowed>N</only_numeric_allowed>
+      <regular_expression/>
+      <regular_expression_not_allowed/>
+      <sourcing_field/>
+      <sourcing_transform/>
+      <start_string/>
+      <start_string_not_allowed/>
+      <validation_name>uuid_value_type</validation_name>
+    </validator_field>
+    <attributes/>
+    <GUI>
+      <xloc>400</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Value mapper</name>
+    <type>ValueMapper</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <field_to_use>type</field_to_use>
+    <fields>
+      <field>
+        <source_value>1</source_value>
+        <target_value>00000000-0000-0000-0000-000000000001</target_value>
+      </field>
+      <field>
+        <source_value>2</source_value>
+        <target_value>00000000-0000-0000-0000-000000000002</target_value>
+      </field>
+      <field>
+        <target_value>00000000-0000-0000-0000-000000000099</target_value>
+      </field>
+    </fields>
+    <non_match_default>00000000-0000-0000-0000-000000000000</non_match_default>
+    <target_field>uuid_type</target_field>
+    <target_type>UUID</target_type>
+    <attributes/>
+    <GUI>
+      <xloc>256</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/transforms/main-0045-value-mapper.hwf 
b/integration-tests/transforms/main-0045-value-mapper.hwf
index e5995e08ea..73b34d7a01 100644
--- a/integration-tests/transforms/main-0045-value-mapper.hwf
+++ b/integration-tests/transforms/main-0045-value-mapper.hwf
@@ -18,7 +18,7 @@ limitations under the License.
 
 -->
 <workflow>
-  <name>0045-value-mapper</name>
+  <name>main-0045-value-mapper</name>
   <name_sync_with_filename>Y</name_sync_with_filename>
   <description/>
   <extended_description/>
@@ -35,14 +35,15 @@ limitations under the License.
       <description/>
       <type>SPECIAL</type>
       <attributes/>
-      <repeat>N</repeat>
-      <schedulerType>0</schedulerType>
-      <intervalSeconds>0</intervalSeconds>
-      <intervalMinutes>60</intervalMinutes>
+      <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>
-      <DayOfMonth>1</DayOfMonth>
       <parallel>N</parallel>
       <xloc>50</xloc>
       <yloc>50</yloc>
@@ -59,19 +60,73 @@ limitations under the License.
         </test_name>
       </test_names>
       <parallel>N</parallel>
-      <xloc>192</xloc>
+      <xloc>448</xloc>
+      <yloc>48</yloc>
+      <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>
+      <clear_rows>N</clear_rows>
+      <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/>
+      <loglevel>Basic</loglevel>
+      <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>
+      <xloc>224</xloc>
       <yloc>48</yloc>
       <attributes_hac/>
     </action>
+    <action>
+      <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>
+      <attributes_hac/>
+    </action>
   </actions>
   <hops>
     <hop>
       <from>Start</from>
-      <to>Run ValueMapper Unit Test</to>
+      <to>0045-value-mapper-output-type.hpl</to>
       <enabled>Y</enabled>
       <evaluation>Y</evaluation>
       <unconditional>Y</unconditional>
     </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>
+    </hop>
+    <hop>
+      <from>0045-value-mapper-output-type.hpl</from>
+      <to>Error</to>
+      <enabled>Y</enabled>
+      <evaluation>N</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
   </hops>
   <notepads>
   </notepads>
diff --git 
a/plugins/engines/beam/src/test/java/org/apache/hop/beam/engines/direct/BeamDirectPipelineEngineTest.java
 
b/plugins/engines/beam/src/test/java/org/apache/hop/beam/engines/direct/BeamDirectPipelineEngineTest.java
index 23d85762b9..dd71fcde60 100644
--- 
a/plugins/engines/beam/src/test/java/org/apache/hop/beam/engines/direct/BeamDirectPipelineEngineTest.java
+++ 
b/plugins/engines/beam/src/test/java/org/apache/hop/beam/engines/direct/BeamDirectPipelineEngineTest.java
@@ -22,15 +22,22 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import java.util.Arrays;
 import org.apache.hop.beam.engines.BeamBasePipelineEngineTest;
 import org.apache.hop.beam.util.BeamPipelineMetaUtil;
+import org.apache.hop.core.HopEnvironment;
 import org.apache.hop.core.variables.DescribedVariable;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.pipeline.config.IPipelineEngineRunConfiguration;
 import org.apache.hop.pipeline.config.PipelineRunConfiguration;
 import org.apache.hop.pipeline.engine.IPipelineEngine;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 class BeamDirectPipelineEngineTest extends BeamBasePipelineEngineTest {
 
+  @BeforeEach
+  final void beforeCommon() throws Exception {
+    HopEnvironment.init();
+  }
+
   @Test
   void testDirectPipelineEngine() throws Exception {
 
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 33359b39aa..3472fdb1d6 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
@@ -20,7 +20,11 @@ package org.apache.hop.pipeline.transforms.valuemapper;
 import java.util.HashMap;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopValueException;
+import org.apache.hop.core.row.IValueMeta;
 import org.apache.hop.core.row.RowDataUtil;
+import org.apache.hop.core.row.value.ValueMetaFactory;
+import org.apache.hop.core.row.value.ValueMetaString;
 import org.apache.hop.core.util.Utils;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.pipeline.Pipeline;
@@ -78,22 +82,9 @@ public class ValueMapper extends 
BaseTransform<ValueMapperMeta, ValueMapperData>
         return false;
       }
 
-      // If there is an empty entry: we map null or "" to the target at the 
index
-      // 0 or 1 empty mapping is allowed, not 2 or more.
-      //
-      for (Values v : meta.getValues()) {
-        if (Utils.isEmpty(v.getSource())) {
-          if (data.emptyFieldValue == null) {
-            data.emptyFieldValue = resolve(v.getTarget());
-          } else {
-            throw new HopException(
-                BaseMessages.getString(
-                    PKG, 
"ValueMapper.RuntimeError.OnlyOneEmptyMappingAllowed.VALUEMAPPER0004"));
-          }
-        }
-      }
-
       data.sourceValueMeta = getInputRowMeta().getValueMeta(data.keynr);
+      setTargetMetaType();
+      builMapValues();
 
       if (Utils.isEmpty(meta.getTargetField())) {
         data.outputValueMeta = data.outputMeta.getValueMeta(data.keynr); // 
Same field
@@ -103,19 +94,22 @@ public class ValueMapper extends 
BaseTransform<ValueMapperMeta, ValueMapperData>
       }
     }
 
-    Object sourceData = r[data.keynr];
-    String source = data.sourceValueMeta.getCompatibleString(sourceData);
-    String target = null;
+    Object sourceValue = r[data.keynr]; // could be any storage type
+
+    // 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 || 
Utils.isEmpty(source))) {
+    if (data.emptyFieldValue != null && (r[data.keynr] == null || sourceData 
== null)) {
       target = data.emptyFieldValue; // that's all there is to it.
     } else {
-      if (!Utils.isEmpty(source)) {
-        target = data.mapValues.get(source);
-        if (nonMatchActivated && target == null) {
-          // If we do non matching and we don't have a match
+      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;
         }
       }
@@ -125,28 +119,11 @@ public class ValueMapper extends 
BaseTransform<ValueMapperMeta, ValueMapperData>
       // room for the target
       r = RowDataUtil.resizeArray(r, data.outputMeta.size());
       // Did we find anything to map to?
-      if (!Utils.isEmpty(target)) {
-        r[data.outputMeta.size() - 1] = target;
-      } else {
-        r[data.outputMeta.size() - 1] = null;
-      }
+      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 (!target.isEmpty()) {
-          // See if the expected type is a String...
-          //
-          if (data.sourceValueMeta.isString()) {
-            r[data.keynr] = target;
-          } else {
-            // Do implicit conversion of the String to the target type...
-            //
-            r[data.keynr] = data.outputValueMeta.convertData(data.stringMeta, 
target);
-          }
-        } else {
-          // allow target to be set to null since 3.0
-          r[data.keynr] = null;
-        }
+        r[data.keynr] = target;
       } else {
         // Convert to normal storage type.
         // Otherwise we're going to be mixing storage types.
@@ -162,36 +139,133 @@ public class ValueMapper extends 
BaseTransform<ValueMapperMeta, ValueMapperData>
     return true;
   }
 
-  @Override
-  public void dispose() {
-    super.dispose();
+  /**
+   * Convert a String key to the target meta's NORMAL storage object using 
srcMeta as the source
+   * type.
+   */
+  private Object keyFromString(IValueMeta tgtMeta, IValueMeta srcMeta, String 
key)
+      throws HopValueException {
+    Object v = tgtMeta.convertData(srcMeta, key);
+    return tgtMeta.convertToNormalStorageType(v);
   }
 
-  @Override
-  public boolean init() {
+  private String typeName(IValueMeta vm) {
+    return vm == null ? "<unknown>" : vm.toStringMeta();
+  }
 
-    if (super.init()) {
-      data.mapValues = new HashMap<>();
+  /** Build the value map, default-on-nonmatch, and empty/null mapping. */
+  private void builMapValues() throws HopException {
 
+    IValueMeta stringValueMeta = new ValueMetaString("String");
+    // --- Default for non-match --------------
+    //
+    try {
       if (!Utils.isEmpty(meta.getNonMatchDefault())) {
         nonMatchActivated = true;
-        data.nonMatchDefault = resolve(meta.getNonMatchDefault());
+        String nonMatchStr = resolve(meta.getNonMatchDefault());
+        data.nonMatchDefault = keyFromString(data.targetValueMeta, 
stringValueMeta, nonMatchStr);
+      }
+    } catch (HopValueException e) {
+      String msg =
+          String.format(
+              "Cannot convert the \"Default upon non-matching\" value [%s] to 
target type [%s].",
+              resolve(meta.getNonMatchDefault()), 
typeName(data.targetValueMeta));
+      throw new HopValueException(msg, e);
+    }
+
+    // --- HashMap --------------
+    // Add all source to target mappings in here...
+    for (Values v : meta.getValues()) {
+      String src = v.getSource();
+      String tgt = this.resolve(v.getTarget());
+
+      Object srcValue;
+      try {
+        srcValue = keyFromString(data.sourceValueMeta, stringValueMeta, src);
+      } catch (HopValueException ce) {
+        String msg =
+            String.format(
+                "Mapping entries: cannot convert source [%s] to source type 
[%s].",
+                src, typeName(data.sourceValueMeta));
+        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);
       }
 
-      // Add all source to target mappings in here...
-      for (Values v : meta.getValues()) {
-        String src = v.getSource();
-        String tgt = this.resolve(v.getTarget());
+      if (srcValue != null) {
+        data.mapValues.put(srcValue, tgtValue);
+      }
+    }
 
-        if (!Utils.isEmpty(src) && !Utils.isEmpty(tgt)) {
-          data.mapValues.put(src, tgt);
+    // Null handling:
+    // 0 or 1 empty mapping is allowed, not 2 or more.
+    //
+    for (Values v : meta.getValues()) {
+      if (Utils.isEmpty(v.getSource())) {
+        if (data.emptyFieldValue == null) {
+          String emptyFieldString = resolve(v.getTarget());
+          data.emptyFieldValue =
+              keyFromString(data.targetValueMeta, stringValueMeta, 
emptyFieldString);
         } else {
-          if (Utils.isEmpty(tgt)) {
-            // allow target to be set to null since 3.0
-            data.mapValues.put(src, "");
-          }
+          throw new HopException(
+              BaseMessages.getString(
+                  PKG, 
"ValueMapper.RuntimeError.OnlyOneEmptyMappingAllowed.VALUEMAPPER0004"));
         }
       }
+    }
+  }
+
+  /**
+   * Resolve and set the target output meta type (user-selected, or 
backward-compatible default).
+   */
+  private void setTargetMetaType() {
+    try {
+      int targetValueMetaId;
+      String targetValueMetaName;
+
+      if (!Utils.isEmpty(meta.getTargetType())) {
+        targetValueMetaName = meta.getTargetType();
+        targetValueMetaId = 
ValueMetaFactory.getIdForValueMeta(targetValueMetaName);
+      } 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();
+      }
+
+      data.targetValueMeta =
+          ValueMetaFactory.createValueMeta(targetValueMetaName, 
targetValueMetaId);
+    } catch (HopException e) {
+      data.targetValueMeta = new ValueMetaString("String");
+    }
+  }
+
+  @Override
+  public void dispose() {
+    super.dispose();
+  }
+
+  @Override
+  public boolean init() {
+
+    if (super.init()) {
+      data.mapValues = new HashMap<>();
+
       return true;
     }
     return false;
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 8a13a2616f..c518c9c0f9 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
@@ -20,7 +20,6 @@ package org.apache.hop.pipeline.transforms.valuemapper;
 import java.util.HashMap;
 import org.apache.hop.core.row.IRowMeta;
 import org.apache.hop.core.row.IValueMeta;
-import org.apache.hop.core.row.value.ValueMetaString;
 import org.apache.hop.pipeline.transform.BaseTransformData;
 import org.apache.hop.pipeline.transform.ITransformData;
 
@@ -31,11 +30,11 @@ public class ValueMapperData extends BaseTransformData 
implements ITransformData
 
   public int keynr;
 
-  public HashMap<String, String> mapValues;
-  public String nonMatchDefault;
-  public String emptyFieldValue;
+  public HashMap<Object, Object> mapValues;
+  public Object nonMatchDefault;
+  public Object emptyFieldValue;
 
-  public IValueMeta stringMeta;
+  public IValueMeta targetValueMeta;
   public IValueMeta outputValueMeta;
   public IValueMeta sourceValueMeta;
 
@@ -43,7 +42,5 @@ public class ValueMapperData extends BaseTransformData 
implements ITransformData
     super();
 
     mapValues = null;
-
-    stringMeta = new ValueMetaString("string");
   }
 }
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 6d43b0d050..8b5429e85b 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
@@ -19,6 +19,7 @@ package org.apache.hop.pipeline.transforms.valuemapper;
 
 import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.value.ValueMetaFactory;
 import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.i18n.BaseMessages;
@@ -62,6 +63,8 @@ public class ValueMapperDialog extends BaseTransformDialog {
 
   private boolean gotPreviousFields = false;
 
+  private CCombo wTargetType;
+
   public ValueMapperDialog(
       Shell parent,
       IVariables variables,
@@ -191,12 +194,32 @@ public class ValueMapperDialog extends 
BaseTransformDialog {
     fdNonMatchDefault.right = new FormAttachment(100, 0);
     wNonMatchDefault.setLayoutData(fdNonMatchDefault);
 
+    // Type of value
+    /*
+     * Type of Value: String, Number, Date, Boolean, Integer
+     */
+    Label wlValueType = new Label(shell, SWT.RIGHT);
+    wlValueType.setText(BaseMessages.getString(PKG, 
"ValueMapperDialog.TargetType.Label"));
+    FormData fdlValueType = new FormData();
+    fdlValueType.left = new FormAttachment(0, 0);
+    fdlValueType.right = new FormAttachment(middle, -margin);
+    fdlValueType.top = new FormAttachment(wNonMatchDefault, margin);
+    wlValueType.setLayoutData(fdlValueType);
+    wTargetType = new CCombo(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER | 
SWT.READ_ONLY);
+    wTargetType.setItems(ValueMetaFactory.getValueMetaNames());
+    PropsUi.setLook(wTargetType);
+    FormData fdValueType = new FormData();
+    fdValueType.left = new FormAttachment(middle, 0);
+    fdValueType.top = new FormAttachment(wNonMatchDefault, margin);
+    fdValueType.right = new FormAttachment(100, 0);
+    wTargetType.setLayoutData(fdValueType);
+
     Label wlFields = new Label(shell, SWT.NONE);
     wlFields.setText(BaseMessages.getString(PKG, 
"ValueMapperDialog.Fields.Label"));
     PropsUi.setLook(wlFields);
     FormData fdlFields = new FormData();
     fdlFields.left = new FormAttachment(0, 0);
-    fdlFields.top = new FormAttachment(wNonMatchDefault, margin);
+    fdlFields.top = new FormAttachment(wlValueType, margin);
     wlFields.setLayoutData(fdlFields);
 
     final int FieldsCols = 2;
@@ -277,6 +300,9 @@ public class ValueMapperDialog extends BaseTransformDialog {
     if (input.getNonMatchDefault() != null) {
       wNonMatchDefault.setText(input.getNonMatchDefault());
     }
+    if (input.getTargetType() != null) {
+      wTargetType.setText(input.getTargetType());
+    }
 
     int i = 0;
     for (Values v : input.getValues()) {
@@ -315,6 +341,7 @@ public class ValueMapperDialog extends BaseTransformDialog {
     input.setFieldToUse(wFieldName.getText());
     input.setTargetField(wTargetFieldName.getText());
     input.setNonMatchDefault(wNonMatchDefault.getText());
+    input.setTargetType(wTargetType.getText());
 
     input.getValues().clear();
     for (TableItem item : wFields.getNonEmptyItems()) {
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 78ee17dfee..ec20591c1f 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
@@ -22,8 +22,10 @@ import java.util.List;
 import org.apache.hop.core.CheckResult;
 import org.apache.hop.core.ICheckResult;
 import org.apache.hop.core.annotations.Transform;
+import org.apache.hop.core.exception.HopPluginException;
 import org.apache.hop.core.row.IRowMeta;
 import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.value.ValueMetaFactory;
 import org.apache.hop.core.row.value.ValueMetaString;
 import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.variables.IVariables;
@@ -64,6 +66,12 @@ public class ValueMapperMeta extends 
BaseTransformMeta<ValueMapper, ValueMapperD
       injectionKeyDescription = "ValueMapper.Injection.NON_MATCH_DEFAULT")
   private String nonMatchDefault;
 
+  @HopMetadataProperty(
+      key = "target_type",
+      injectionKey = "TARGET_TYPE",
+      injectionKeyDescription = "ValueMapper.Injection.TARGET_TYPE")
+  private String targetType;
+
   @HopMetadataProperty(
       groupKey = "fields",
       key = "field",
@@ -85,6 +93,11 @@ public class ValueMapperMeta extends 
BaseTransformMeta<ValueMapper, ValueMapperD
     this.fieldToUse = meta.fieldToUse;
     this.targetField = meta.targetField;
     this.nonMatchDefault = meta.nonMatchDefault;
+    if (meta.targetType != null && meta.targetType.isEmpty()) {
+      this.targetType = meta.targetType;
+    } else {
+      this.targetType = "String";
+    }
   }
 
   /**
@@ -114,27 +127,43 @@ public class ValueMapperMeta extends 
BaseTransformMeta<ValueMapper, ValueMapperD
       TransformMeta nextTransform,
       IVariables variables,
       IHopMetadataProvider metadataProvider) {
+
     IValueMeta extra = null;
+
+    // Determine target value meta type (default to String for backward 
compatibility)
+    String targetTypeName = Utils.isEmpty(getTargetType()) ? "String" : 
getTargetType();
+    int targetTypeId = ValueMetaFactory.getIdForValueMeta(targetTypeName);
+    // fallback
+    targetTypeId = targetTypeId == IValueMeta.TYPE_NONE ? 
IValueMeta.TYPE_STRING : targetTypeId;
+
     if (!Utils.isEmpty(getTargetField())) {
-      extra = new ValueMetaString(getTargetField());
+      // ADD a new field with the chosen type
+      try {
+        extra = ValueMetaFactory.createValueMeta(getTargetField(), 
targetTypeId);
+      } catch (HopPluginException e) {
+        // fallback if factory fails for some reason
+        extra = new ValueMetaString(getTargetField());
+      }
 
-      // 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();
+      if (extra.getType() == IValueMeta.TYPE_STRING) {
+        // 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();
+        // include default value in max length calculation
+        //
+        if (nonMatchDefault != null && nonMatchDefault.length() > maxlen) {
+          maxlen = nonMatchDefault.length();
+        }
+        extra.setLength(maxlen);
+        extra.setOrigin(name);
       }
-      extra.setLength(maxlen);
-      extra.setOrigin(name);
       r.addValueMeta(extra);
     } else {
       if (!Utils.isEmpty(getFieldToUse())) {
@@ -244,4 +273,18 @@ public class ValueMapperMeta extends 
BaseTransformMeta<ValueMapper, ValueMapperD
   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/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 310c699eeb..a3d467f1b7 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
@@ -41,3 +41,4 @@ 
ValueMapperMeta.CheckResult.ReceivingFieldsFromPreviousTransforms=Transform is c
 ValueMapperMeta.CheckResult.ReceivingInfoFromOtherTransforms=Transform is 
receiving info from other transforms.
 ValueMapperMeta.keyword=value,mapper
 ValueMapperMeta.RuntimeError.UnableToReadXML.VALUEMAPPER0004=Unable to read 
transform information from XML
+ValueMapperDialog.TargetType.Label=Target field type
diff --git 
a/plugins/transforms/valuemapper/src/main/resources/org/apache/hop/pipeline/transforms/valuemapper/messages/messages_it_IT.properties
 
b/plugins/transforms/valuemapper/src/main/resources/org/apache/hop/pipeline/transforms/valuemapper/messages/messages_it_IT.properties
index e1377b7b48..56e23bc603 100644
--- 
a/plugins/transforms/valuemapper/src/main/resources/org/apache/hop/pipeline/transforms/valuemapper/messages/messages_it_IT.properties
+++ 
b/plugins/transforms/valuemapper/src/main/resources/org/apache/hop/pipeline/transforms/valuemapper/messages/messages_it_IT.properties
@@ -33,3 +33,4 @@ 
ValueMapperMeta.CheckResult.NotReceivingFieldsFromPreviousTransforms=Nessuna ric
 ValueMapperMeta.CheckResult.NotReceivingInfoFromOtherTransforms=Nessun input 
ricevuto dagli altri transforms\!
 ValueMapperMeta.CheckResult.ReceivingFieldsFromPreviousTransforms=Il transform 
\u00E8 connesso al precedente, ricezione di {0} campi.
 ValueMapperMeta.CheckResult.ReceivingInfoFromOtherTransforms=Il transform sta 
ricevendo informazioni dagli altri transforms.
+ValueMapperDialog.TargetType.Label=Tipo campo di destinazione
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 d0cd7d86d5..44b167358d 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
@@ -39,5 +39,6 @@ class ValueMapperMetaInjectionTest extends 
BaseMetadataInjectionTestJunit5<Value
     check("NON_MATCH_DEFAULT", () -> meta.getNonMatchDefault());
     check("SOURCE", () -> meta.getValues().get(0).getSource());
     check("TARGET", () -> meta.getValues().get(0).getTarget());
+    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 d6e576d9f4..97c2aaccfd 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,10 +17,16 @@
 package org.apache.hop.pipeline.transforms.valuemapper;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 
 import org.apache.hop.core.HopEnvironment;
 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.ValueMetaString;
+import org.apache.hop.pipeline.transform.TransformMeta;
 import org.apache.hop.pipeline.transform.TransformSerializationTestUtil;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -33,6 +39,44 @@ class ValueMapperMetaTest {
     PluginRegistry.init();
   }
 
+  @Test
+  void testBackwardNoNewFieldNoType() throws Exception {
+    ValueMapperMeta meta =
+        TransformSerializationTestUtil.testSerialization(
+            "/value-mapper-transform.xml", ValueMapperMeta.class);
+
+    // backward-compat scenario: in-place mapping, no explicit type
+    meta.setFieldToUse("Country_Code");
+    meta.setTargetField("");
+    meta.setTargetType(null);
+
+    // Input schema: only the source field, String
+    IRowMeta input = new RowMeta();
+    ValueMetaString src = new ValueMetaString("Country_Code");
+    src.setLength(50);
+    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.STORAGE_TYPE_NORMAL, outVm.getStorageType(), 
"Storage must be NORMAL");
+  }
+
   @Test
   void testSerialization() throws Exception {
     ValueMapperMeta meta =
@@ -50,5 +94,22 @@ class ValueMapperMetaTest {
     assertEquals("Country_Code", meta.getFieldToUse());
     assertEquals("Country_Name", meta.getTargetField());
     assertEquals("[${NOT_FOUND}]", meta.getNonMatchDefault());
+
+    assertNull(meta.getTargetType(), "Expected null target type right after 
deserialization");
+    // Before getFields: input has only the source field
+    IRowMeta input = new RowMeta();
+    input.addValueMeta(new ValueMetaString("Country_Code"));
+    assertNull(input.searchValueMeta("Country_Name"));
+
+    // After getFields: a NEW field "Country_Name" is added and defaults to 
String type
+    IRowMeta out = input.clone();
+    meta.getFields(out, "ValueMapper", null, new TransformMeta(), null, null);
+
+    assertNotNull(
+        out.searchValueMeta("Country_Name"), "Target field should be added by 
getFields()");
+    assertEquals(
+        IValueMeta.TYPE_STRING,
+        out.searchValueMeta("Country_Name").getType(),
+        "New target field should default to String type when no explicit 
target type is set");
   }
 }
diff --git 
a/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/xslt/XsltTest.java
 
b/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/xslt/XsltTest.java
index cc24ff2888..1529bf1a8a 100644
--- 
a/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/xslt/XsltTest.java
+++ 
b/plugins/transforms/xml/src/test/java/org/apache/hop/pipeline/transforms/xml/xslt/XsltTest.java
@@ -36,6 +36,7 @@ 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.ValueMetaString;
+import org.apache.hop.junit.rules.RestoreHopEnvironmentExtension;
 import org.apache.hop.pipeline.Pipeline;
 import org.apache.hop.pipeline.PipelineHopMeta;
 import org.apache.hop.pipeline.PipelineMeta;
@@ -48,7 +49,9 @@ import 
org.apache.hop.pipeline.transforms.injector.InjectorMeta;
 import org.apache.hop.pipeline.transforms.xml.RowTransformCollector;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
 
+@ExtendWith(RestoreHopEnvironmentExtension.class)
 class XsltTest {
 
   private static final String TEST1_XML =


Reply via email to