This is an automated email from the ASF dual-hosted git repository.

bamaer 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 7434abb818 fix simple mapping import, fixes #4119 (#8582)
7434abb818 is described below

commit 7434abb8181b91033158c72f99721a7b7a9e0adc
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Fri Sep 25 11:24:39 2026 +0200

    fix simple mapping import, fixes #4119 (#8582)
    
    * fix simple mapping import, fixes #4119
    
    * fix simple mapping parameters import from kettle, fixes #4501
---
 .../org/apache/hop/imports/kettle/KettleConst.java |   4 +-
 .../apache/hop/imports/kettle/KettleImport.java    | 104 ++++++---
 .../imports/kettle/KettleImportMappingTest.java    | 255 +++++++++++++++++++++
 3 files changed, 333 insertions(+), 30 deletions(-)

diff --git 
a/plugins/misc/import/src/main/java/org/apache/hop/imports/kettle/KettleConst.java
 
b/plugins/misc/import/src/main/java/org/apache/hop/imports/kettle/KettleConst.java
index 03f5a93d16..234ca9357e 100644
--- 
a/plugins/misc/import/src/main/java/org/apache/hop/imports/kettle/KettleConst.java
+++ 
b/plugins/misc/import/src/main/java/org/apache/hop/imports/kettle/KettleConst.java
@@ -45,7 +45,6 @@ public class KettleConst {
                 {"step_performance_capturing_delay", 
"transform_performance_capturing_delay"},
                 {"transformationPath", "pipelinePath"},
                 {"SUB_STEP", "subTransform"},
-                {"variablemapping", "variable_mapping"},
                 // jobs
                 {"job", "workflow"},
                 {"job_version", "workflow_version"},
@@ -249,6 +248,9 @@ public class KettleConst {
 
   public static final List<String> transTypes = Arrays.asList(CONST_TRANS);
 
+  /** Kettle's "Mapping" and "Simple Mapping" steps both become a Hop Simple 
Mapping transform. */
+  public static final List<String> mappingTypes = Arrays.asList("Mapping", 
"SimpleMapping");
+
   public KettleConst() {
     // Do nothing
   }
diff --git 
a/plugins/misc/import/src/main/java/org/apache/hop/imports/kettle/KettleImport.java
 
b/plugins/misc/import/src/main/java/org/apache/hop/imports/kettle/KettleImport.java
index 142848bc91..09e71a80dc 100644
--- 
a/plugins/misc/import/src/main/java/org/apache/hop/imports/kettle/KettleImport.java
+++ 
b/plugins/misc/import/src/main/java/org/apache/hop/imports/kettle/KettleImport.java
@@ -84,6 +84,7 @@ public class KettleImport extends HopImportBase implements 
IHopImport {
   public static final String CONST_SERVERNAME = "servername";
   public static final String CONST_PASSWORD = "password";
   private static final String SFTP_PUT_TYPE = "SFTPPut";
+  private static final String TRANS_EXECUTOR_TYPE = "TransExecutor";
   private static final String SFTP_CONNECTION_METADATA_KEY = "sftp-connection";
 
   /** The elements of a Kettle SFTPPut step which describe the server, not the 
upload itself. */
@@ -628,6 +629,22 @@ public class KettleImport extends HopImportBase implements 
IHopImport {
     setChildElement(doc, stepNode, "connection", connectionName);
   }
 
+  /**
+   * Kettle writes the parameters of a mapping, a job executor and a 
transformation executor step as
+   * {@code <parameters><variablemapping>}. Hop reads them the same way, 
except for the pipeline
+   * executor, which reads {@code <variable_mapping>}.
+   */
+  private void migrateTransExecutorParameters(Document doc, Node stepNode) {
+    Element parametersElement = getChildElement(stepNode, "parameters");
+    if (parametersElement == null) {
+      return;
+    }
+    Element variableMapping;
+    while ((variableMapping = getChildElement(parametersElement, 
"variablemapping")) != null) {
+      renameNode(doc, variableMapping, "variable_mapping");
+    }
+  }
+
   /**
    * Create an SFTP connection in the metadata for the given step settings, or 
return the name of
    * the one created earlier for the very same settings: a transformation with 
five steps talking to
@@ -838,6 +855,7 @@ public class KettleImport extends HopImportBase implements 
IHopImport {
         if (currentNode.getNodeName().equals("step")) {
           entryType = EntryType.OTHER;
           boolean sftpPutStep = false;
+          boolean transExecutorStep = false;
           NodeList currentNodeChildNodes = currentNode.getChildNodes();
           for (int i1 = 0; i1 < currentNodeChildNodes.getLength(); i1++) {
             Node childNode = currentNodeChildNodes.item(i1);
@@ -846,6 +864,10 @@ public class KettleImport extends HopImportBase implements 
IHopImport {
                   && 
childNode.getChildNodes().item(0).getNodeValue().equals(SFTP_PUT_TYPE)) {
                 sftpPutStep = true;
               }
+              if (childNode.getNodeName().equals("type")
+                  && 
childNode.getChildNodes().item(0).getNodeValue().equals(TRANS_EXECUTOR_TYPE)) {
+                transExecutorStep = true;
+              }
               if (childNode.getNodeName().equals("type")
                   && 
childNode.getChildNodes().item(0).getNodeValue().equals("Formula")) {
                 entryType = EntryType.FORMULA;
@@ -859,7 +881,8 @@ public class KettleImport extends HopImportBase implements 
IHopImport {
                 entryType = EntryType.GOOGLE_SHEETS_INPUT;
               }
               if (childNode.getNodeName().equals("type")
-                  && 
childNode.getChildNodes().item(0).getNodeValue().equals("Mapping")) {
+                  && KettleConst.mappingTypes.contains(
+                      childNode.getChildNodes().item(0).getNodeValue())) {
                 entryType = EntryType.SIMPLE_MAPPING;
               }
               if (childNode.getNodeName().equals("type")
@@ -871,6 +894,9 @@ public class KettleImport extends HopImportBase implements 
IHopImport {
           if (sftpPutStep) {
             migrateSftpPutStep(doc, currentNode);
           }
+          if (transExecutorStep) {
+            migrateTransExecutorParameters(doc, currentNode);
+          }
         }
 
         // remove superfluous elements
@@ -981,36 +1007,16 @@ public class KettleImport extends HopImportBase 
implements IHopImport {
       if ((entryType == EntryType.SIMPLE_MAPPING || entryType == 
EntryType.METAINJECT)
           && currentNode.getNodeName().equals("transform")) {
 
-        Node filenameNode = null;
-        String transName = "";
-        String directoryPath = "";
-        // get trans name, file name, path, set correct filename when needed.
-        for (int j = 0; j < currentNode.getChildNodes().getLength(); j++) {
-          if 
(currentNode.getChildNodes().item(j).getNodeName().equals("directory_path")) {
-            directoryPath = 
currentNode.getChildNodes().item(j).getTextContent();
-            currentNode.removeChild(currentNode.getChildNodes().item(j));
-          }
-          if 
(currentNode.getChildNodes().item(j).getNodeName().equals("trans_name")) {
-            transName = currentNode.getChildNodes().item(j).getTextContent();
-            currentNode.removeChild(currentNode.getChildNodes().item(j));
-          }
-          if 
(currentNode.getChildNodes().item(j).getNodeName().equals("filename")) {
-            filenameNode = currentNode.getChildNodes().item(j);
-          }
-        }
-
-        // if we have a trans name and directory path, use it to update the 
mapping or injectable
-        // pipeline
-        // filename.
-        if (!StringUtils.isEmpty(transName) && 
!StringUtils.isEmpty(directoryPath)) {
-          filenameNode.setTextContent(
-              Const.VAR_PROJECT_HOME + directoryPath + '/' + transName + 
".hpl");
-        }
+        migrateTransformationReference(doc, currentNode);
 
         // add the default pipeline run configuration.
-        Element runConfigElement = doc.createElement("runConfiguration");
-        
runConfigElement.appendChild(doc.createTextNode(defaultPipelineRunConfiguration));
-        currentNode.appendChild(runConfigElement);
+        String runConfigElementName =
+            entryType == EntryType.METAINJECT ? "run_configuration" : 
"runConfiguration";
+        if (getChildElement(currentNode, runConfigElementName) == null) {
+          Element runConfigElement = doc.createElement(runConfigElementName);
+          
runConfigElement.appendChild(doc.createTextNode(defaultPipelineRunConfiguration));
+          currentNode.appendChild(runConfigElement);
+        }
       }
 
       if (entryType == EntryType.GOOGLE_SHEETS_INPUT
@@ -1091,6 +1097,46 @@ public class KettleImport extends HopImportBase 
implements IHopImport {
     }
   }
 
+  /**
+   * A Kettle mapping or metadata injection step refers to its 
sub-transformation either by filename
+   * or, when it was saved in a repository, by name and repository folder. Hop 
only knows filenames,
+   * so a repository reference becomes a path below the project home. The 
repository elements are
+   * removed either way.
+   */
+  private void migrateTransformationReference(Document doc, Node 
transformNode) {
+    String specificationMethod = getChildText(transformNode, 
"specification_method");
+    String transName = getChildText(transformNode, "trans_name");
+    String directoryPath = getChildText(transformNode, "directory_path");
+    for (String name :
+        new String[] {"specification_method", "trans_object_id", "trans_name", 
"directory_path"}) {
+      removeChildElement(transformNode, name);
+    }
+
+    Element filenameElement = getChildElement(transformNode, "filename");
+    String filename = filenameElement == null ? "" : 
filenameElement.getTextContent();
+
+    // Kettle keeps the repository name when you switch a step to a filename, 
so a filename wins
+    // unless the step explicitly refers to the repository.
+    if (StringUtils.isEmpty(transName)
+        || (StringUtils.isNotEmpty(filename)
+            && !StringUtils.startsWith(specificationMethod, "REPOSITORY"))) {
+      return;
+    }
+
+    String folder = directoryPath == null ? "" : 
StringUtils.strip(directoryPath, "/");
+    if (filenameElement == null) {
+      filenameElement = doc.createElement("filename");
+      transformNode.appendChild(filenameElement);
+    }
+    filenameElement.setTextContent(
+        Const.VAR_PROJECT_HOME + (folder.isEmpty() ? "" : "/" + folder) + "/" 
+ transName + ".hpl");
+  }
+
+  private String getChildText(Node parent, String name) {
+    Element child = getChildElement(parent, name);
+    return child == null ? null : child.getTextContent();
+  }
+
   private Node processRepositoryNode(Node repositoryNode) {
 
     String filename = "";
diff --git 
a/plugins/misc/import/src/test/java/org/apache/hop/imports/kettle/KettleImportMappingTest.java
 
b/plugins/misc/import/src/test/java/org/apache/hop/imports/kettle/KettleImportMappingTest.java
new file mode 100644
index 0000000000..dc5e6b8b90
--- /dev/null
+++ 
b/plugins/misc/import/src/test/java/org/apache/hop/imports/kettle/KettleImportMappingTest.java
@@ -0,0 +1,255 @@
+/*
+ * 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.imports.kettle;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.io.ByteArrayInputStream;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import org.apache.hop.core.HopClientEnvironment;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.core.xml.XmlParserFactoryProducer;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+/**
+ * Kettle mapping and metadata injection steps saved in a repository refer to 
their
+ * sub-transformation by name and folder instead of by filename (issue #4119). 
Their parameters must
+ * survive the import (issue #4501).
+ */
+class KettleImportMappingTest {
+
+  private static final String ENTRY_TYPE = 
"org.apache.hop.imports.kettle.KettleImport$EntryType";
+
+  private KettleImport kettleImport;
+
+  @BeforeAll
+  static void setUpBeforeClass() throws Exception {
+    HopClientEnvironment.init();
+  }
+
+  @BeforeEach
+  void setUp() {
+    kettleImport = new KettleImport();
+    kettleImport.setDefaultPipelineRunConfiguration("local");
+  }
+
+  @ParameterizedTest
+  @ValueSource(strings = {"Mapping", "SimpleMapping"})
+  void testRepositoryReferenceBecomesFilename(String type) throws Exception {
+    Node transform =
+        importStep(type, "REPOSITORY_BY_NAME", "child", "", "/public/sub", 
"runConfiguration");
+
+    assertEquals("SimpleMapping", XmlHandler.getTagValue(transform, "type"));
+    assertEquals(
+        "${PROJECT_HOME}/public/sub/child.hpl", 
XmlHandler.getTagValue(transform, "filename"));
+    assertEquals("local", XmlHandler.getTagValue(transform, 
"runConfiguration"));
+    assertRepositoryElementsRemoved(transform);
+  }
+
+  @Test
+  void testMetaInjectGetsItsOwnRunConfigurationElement() throws Exception {
+    Node transform =
+        importStep("MetaInject", "REPOSITORY_BY_NAME", "child", "", 
"/public/sub", null);
+
+    assertEquals(
+        "${PROJECT_HOME}/public/sub/child.hpl", 
XmlHandler.getTagValue(transform, "filename"));
+    assertEquals("local", XmlHandler.getTagValue(transform, 
"run_configuration"));
+    assertNull(XmlHandler.getSubNode(transform, "runConfiguration"));
+    assertRepositoryElementsRemoved(transform);
+  }
+
+  @Test
+  void testRootFolderHasNoDoubleSlash() throws Exception {
+    Node transform = importStep("SimpleMapping", "REPOSITORY_BY_NAME", 
"child", "", "/", null);
+
+    assertEquals("${PROJECT_HOME}/child.hpl", 
XmlHandler.getTagValue(transform, "filename"));
+  }
+
+  /** Kettle keeps the old repository name after the step is switched to a 
filename. */
+  @Test
+  void testFilenameWinsOverStaleRepositoryName() throws Exception {
+    Node transform =
+        importStep(
+            "SimpleMapping",
+            "FILENAME",
+            "old-child",
+            "${Internal.Entry.Current.Directory}/child.ktr",
+            "/public/old",
+            null);
+
+    assertEquals(
+        "${Internal.Entry.Current.Folder}/child.hpl",
+        XmlHandler.getTagValue(transform, "filename"));
+    assertRepositoryElementsRemoved(transform);
+  }
+
+  @Test
+  void testMissingFilenameElementIsAdded() throws Exception {
+    Document doc =
+        parse(
+            "<transformation><step><name>map</name><type>SimpleMapping</type>"
+                + 
"<trans_name>child</trans_name><directory_path>/sub</directory_path>"
+                + "</step></transformation>");
+    processNode(doc);
+
+    Node transform = XmlHandler.getSubNode(XmlHandler.getSubNode(doc, 
"pipeline"), "transform");
+    assertEquals("${PROJECT_HOME}/sub/child.hpl", 
XmlHandler.getTagValue(transform, "filename"));
+  }
+
+  /** The parameters of a mapping step keep their Kettle layout in Hop (issue 
#4501). */
+  @ParameterizedTest
+  @ValueSource(strings = {"Mapping", "SimpleMapping"})
+  void testMappingParametersAreKept(String type) throws Exception {
+    Document doc =
+        parse(
+            "<transformation><step><name>map</name><type>"
+                + type
+                + "</type><filename>child.ktr</filename><mappings>"
+                + PARAMETERS
+                + "</mappings></step></transformation>");
+    processNode(doc);
+
+    Node transform = XmlHandler.getSubNode(XmlHandler.getSubNode(doc, 
"pipeline"), "transform");
+    assertParameters(XmlHandler.getSubNode(transform, "mappings", 
"parameters"), "variablemapping");
+  }
+
+  /** Only the pipeline executor reads its parameters from a differently named 
element. */
+  @ParameterizedTest
+  @CsvSource({
+    "TransExecutor, PipelineExecutor, variable_mapping",
+    "JobExecutor, WorkflowExecutor, variablemapping"
+  })
+  void testExecutorParameters(String kettleType, String hopType, String 
variableTag)
+      throws Exception {
+    Document doc =
+        parse(
+            "<transformation><step><name>exec</name><type>"
+                + kettleType
+                + "</type>"
+                + PARAMETERS
+                + "</step></transformation>");
+    processNode(doc);
+
+    Node transform = XmlHandler.getSubNode(XmlHandler.getSubNode(doc, 
"pipeline"), "transform");
+    assertEquals(hopType, XmlHandler.getTagValue(transform, "type"));
+    assertParameters(XmlHandler.getSubNode(transform, "parameters"), 
variableTag);
+  }
+
+  private static final String PARAMETERS =
+      "<parameters>"
+          + 
"<variablemapping><variable>A</variable><input>${PARENT_A}</input></variablemapping>"
+          + 
"<variablemapping><variable>B</variable><input>${PARENT_B}</input></variablemapping>"
+          + "<inherit_all_vars>N</inherit_all_vars>"
+          + "</parameters>";
+
+  private static void assertParameters(Node parameters, String variableTag) {
+    assertEquals(2, XmlHandler.countNodes(parameters, variableTag));
+    assertEquals(
+        0,
+        XmlHandler.countNodes(
+            parameters,
+            "variable_mapping".equals(variableTag) ? "variablemapping" : 
"variable_mapping"));
+    assertEquals(
+        "A",
+        XmlHandler.getTagValue(XmlHandler.getSubNodeByNr(parameters, 
variableTag, 0), "variable"));
+    assertEquals(
+        "${PARENT_B}",
+        XmlHandler.getTagValue(XmlHandler.getSubNodeByNr(parameters, 
variableTag, 1), "input"));
+    assertEquals("N", XmlHandler.getTagValue(parameters, "inherit_all_vars"));
+  }
+
+  private static void assertRepositoryElementsRemoved(Node transform) {
+    for (String tag :
+        new String[] {"specification_method", "trans_object_id", "trans_name", 
"directory_path"}) {
+      assertNull(XmlHandler.getSubNode(transform, tag), tag + " should have 
been removed");
+    }
+  }
+
+  /** A step laid out the way Kettle writes it, whitespace included. */
+  private Node importStep(
+      String type,
+      String specificationMethod,
+      String transName,
+      String filename,
+      String directoryPath,
+      String existingRunConfigurationTag)
+      throws Exception {
+    Document doc =
+        parse(
+            "<transformation>\n"
+                + "  <step>\n"
+                + "    <name>map</name>\n"
+                + "    <type>"
+                + type
+                + "</type>\n"
+                + "    <specification_method>"
+                + specificationMethod
+                + "</specification_method>\n"
+                + "    <trans_object_id/>\n"
+                + "    <trans_name>"
+                + transName
+                + "</trans_name>\n"
+                + "    <filename>"
+                + filename
+                + "</filename>\n"
+                + "    <directory_path>"
+                + directoryPath
+                + "</directory_path>\n"
+                + "    <mappings>\n"
+                + "      <input><mapping><input_step/></mapping></input>\n"
+                + "    </mappings>\n"
+                + "  </step>\n"
+                + "</transformation>");
+    processNode(doc);
+    Node transform = XmlHandler.getSubNode(XmlHandler.getSubNode(doc, 
"pipeline"), "transform");
+    if (existingRunConfigurationTag != null) {
+      // The run configuration is only added once.
+      assertEquals(1, XmlHandler.countNodes(transform, 
existingRunConfigurationTag));
+    }
+    return transform;
+  }
+
+  private void processNode(Document doc) throws Exception {
+    Class<?> entryTypeClass = Class.forName(ENTRY_TYPE);
+    Object other = null;
+    for (Object constant : entryTypeClass.getEnumConstants()) {
+      if ("OTHER".equals(constant.toString())) {
+        other = constant;
+      }
+    }
+    Method method =
+        KettleImport.class.getDeclaredMethod(
+            "processNode", Document.class, Node.class, entryTypeClass, 
int.class);
+    method.setAccessible(true);
+    method.invoke(kettleImport, doc, doc, other, 0);
+  }
+
+  private static Document parse(String xml) throws Exception {
+    return XmlParserFactoryProducer.createSecureDocBuilderFactory()
+        .newDocumentBuilder()
+        .parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
+  }
+}

Reply via email to