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 =