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 99e6196b0c Fixes #9084 : Imported PDI step types get the Hop transform 
id case, … (#9086)
99e6196b0c is described below

commit 99e6196b0cf6c2d839370d957c2f931fa2ae337d
Author: Bart Maertens <[email protected]>
AuthorDate: Sat Oct 24 09:40:05 2026 +0200

    Fixes #9084 : Imported PDI step types get the Hop transform id case, … 
(#9086)
    
    * Fixes #9084 : Imported PDI step types get the Hop transform id case, Hop 
GUI finds icons regardless of case
    
    * Issue #9084 : Use the main transform plugin id when loading a pipeline 
and when importing from Kettle
---
 .../java/org/apache/hop/pipeline/PipelineMeta.java |  14 ++
 .../org/apache/hop/pipeline/PipelineMetaTest.java  |  30 ++++
 .../apache/hop/imports/kettle/KettleImport.java    |  20 +++
 .../kettle/KettleImportTransformTypeCaseTest.java  | 154 +++++++++++++++++++++
 4 files changed, 218 insertions(+)

diff --git a/engine/src/main/java/org/apache/hop/pipeline/PipelineMeta.java 
b/engine/src/main/java/org/apache/hop/pipeline/PipelineMeta.java
index 647312f6f2..06954b6b4b 100644
--- a/engine/src/main/java/org/apache/hop/pipeline/PipelineMeta.java
+++ b/engine/src/main/java/org/apache/hop/pipeline/PipelineMeta.java
@@ -62,6 +62,9 @@ import org.apache.hop.core.gui.Point;
 import org.apache.hop.core.logging.LogChannel;
 import org.apache.hop.core.parameters.INamedParameters;
 import org.apache.hop.core.parameters.NamedParameters;
+import org.apache.hop.core.plugins.IPlugin;
+import org.apache.hop.core.plugins.PluginRegistry;
+import org.apache.hop.core.plugins.TransformPluginType;
 import org.apache.hop.core.reflection.StringSearchResult;
 import org.apache.hop.core.reflection.StringSearcher;
 import org.apache.hop.core.row.IRowMeta;
@@ -1930,6 +1933,17 @@ public class PipelineMeta extends AbstractMeta
       //
       if (iTransform instanceof Missing missing) {
         addMissingPipeline(missing);
+      } else {
+        // The plugin registry also accepts another case or another id of the 
plugin, for example
+        // CsvInput for CSVInput in a pipeline imported from Kettle. Icons and 
checks on the plugin
+        // id use the main id.
+        //
+        IPlugin plugin =
+            PluginRegistry.getInstance()
+                .getPlugin(TransformPluginType.class, 
transformMeta.getTransformPluginId());
+        if (plugin != null) {
+          transformMeta.setTransformPluginId(plugin.getIds()[0]);
+        }
       }
     }
     dropReferencesToTransformsNotInTheFile();
diff --git a/engine/src/test/java/org/apache/hop/pipeline/PipelineMetaTest.java 
b/engine/src/test/java/org/apache/hop/pipeline/PipelineMetaTest.java
index 3b27d2304f..625c0d0c99 100644
--- a/engine/src/test/java/org/apache/hop/pipeline/PipelineMetaTest.java
+++ b/engine/src/test/java/org/apache/hop/pipeline/PipelineMetaTest.java
@@ -584,6 +584,20 @@ class PipelineMetaTest {
     assertEquals(xml, copy.getXml(variables));
   }
 
+  /** A type written with another case loads as the main plugin id, and is 
saved that way. */
+  @Test
+  void testTypeWithAnotherCaseLoadsAsTheMainPluginId() throws Exception {
+    pipelineMeta.addTransform(new TransformMeta("one", new DummyMeta()));
+    String xml = pipelineMeta.getXml(variables).replace("<type>Dummy</type>", 
"<type>DUMMY</type>");
+    assertTrue(xml.contains("<type>DUMMY</type>"), xml);
+
+    PipelineMeta loaded =
+        new PipelineMeta(XmlHandler.loadXmlString(xml, PipelineMeta.XML_TAG), 
metadataProvider);
+
+    assertEquals("Dummy", loaded.findTransform("one").getTransformPluginId());
+    assertTrue(loaded.getXml(variables).contains("<type>Dummy</type>"));
+  }
+
   @Test
   void testAttributesMapSerialization() throws Exception {
     Map<String, Map<String, String>> map = pipelineMeta.getAttributesMap();
@@ -777,6 +791,22 @@ class PipelineMetaTest {
     assertFalse(source.isDoingErrorHandling());
   }
 
+  /** A file can name the plugin with another case, for example a pipeline 
imported from Kettle. */
+  @Test
+  void lookupReferencesAfterLoadingUsesTheMainPluginId() {
+    TransformMeta dummy = new TransformMeta("Dummy", new DummyMeta());
+    dummy.setTransformPluginId("DUMMY");
+    TransformMeta unknown = new TransformMeta("Unknown", new FakeMeta());
+    unknown.setTransformPluginId("NotARegisteredPlugin");
+    pipelineMeta.addTransform(dummy);
+    pipelineMeta.addTransform(unknown);
+
+    pipelineMeta.lookupReferencesAfterLoading();
+
+    assertEquals("Dummy", dummy.getTransformPluginId());
+    assertEquals("NotARegisteredPlugin", unknown.getTransformPluginId());
+  }
+
   @Test
   void lookupReferencesAfterLoadingKeepsErrorHandlingActiveWhenHopEnabled() {
     TransformMeta source = new TransformMeta("REST client", new FakeMeta());
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 97a16bc182..5333d0b7ca 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
@@ -55,6 +55,7 @@ import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.extension.ExtensionPointHandler;
 import org.apache.hop.core.plugins.IPlugin;
 import org.apache.hop.core.plugins.PluginRegistry;
+import org.apache.hop.core.plugins.TransformPluginType;
 import org.apache.hop.core.row.value.ValueMetaFactory;
 import org.apache.hop.core.util.StringUtil;
 import org.apache.hop.core.util.Utils;
@@ -848,6 +849,22 @@ public class KettleImport extends HopImportBase implements 
IHopImport {
     return StringUtils.isNotEmpty(runConfiguration) ? runConfiguration : 
DEFAULT_RUN_CONFIGURATION;
   }
 
+  /**
+   * Kettle spells some transform ids with a different case than Hop, for 
example {@code CsvInput}
+   * and {@code CSVInput}, or uses another id of the plugin. Hop finds the 
plugin either way, but
+   * icons and checks on the plugin id use the main id, so the imported 
transform gets that one.
+   */
+  private static void useMainTransformId(Node typeNode) {
+    String type = typeNode.getTextContent();
+    if (StringUtils.isEmpty(type)) {
+      return;
+    }
+    IPlugin plugin = 
PluginRegistry.getInstance().getPlugin(TransformPluginType.class, type);
+    if (plugin != null) {
+      typeNode.setTextContent(plugin.getIds()[0]);
+    }
+  }
+
   private void processNode(Document doc, Node node, EntryType entryType, int 
depth) {
     Node nodeToProcess = node;
     NodeList nodeList = nodeToProcess.getChildNodes();
@@ -955,6 +972,9 @@ public class KettleImport extends HopImportBase implements 
IHopImport {
                   && 
childNode.getChildNodes().item(0).getNodeValue().equals("MetaInject")) {
                 entryType = EntryType.METAINJECT;
               }
+              if (childNode.getNodeName().equals("type")) {
+                useMainTransformId(childNode);
+              }
             }
           }
           if (sftpPutStep) {
diff --git 
a/plugins/misc/import/src/test/java/org/apache/hop/imports/kettle/KettleImportTransformTypeCaseTest.java
 
b/plugins/misc/import/src/test/java/org/apache/hop/imports/kettle/KettleImportTransformTypeCaseTest.java
new file mode 100644
index 0000000000..3ed2d68d62
--- /dev/null
+++ 
b/plugins/misc/import/src/test/java/org/apache/hop/imports/kettle/KettleImportTransformTypeCaseTest.java
@@ -0,0 +1,154 @@
+/*
+ * 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.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.hop.core.HopClientEnvironment;
+import org.apache.hop.core.plugins.IPlugin;
+import org.apache.hop.core.plugins.PluginRegistry;
+import org.apache.hop.core.plugins.TransformPluginType;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.core.xml.XmlParserFactoryProducer;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+/**
+ * Kettle spells some step ids with a different case than Hop, for example 
{@code CsvInput} for
+ * Hop's {@code CSVInput}, or uses another id of the plugin. Hop runs the 
transform either way, but
+ * Hop GUI draws it with the missing-plugin icon unless the imported type is 
the main id (issue
+ * #9084).
+ */
+class KettleImportTransformTypeCaseTest {
+
+  private static final String ENTRY_TYPE = 
"org.apache.hop.imports.kettle.KettleImport$EntryType";
+
+  private static final String[][] HOP_IDS = {
+    {"CSVInput"},
+    {"DynamicSqlRow"},
+    {"ExecSql"},
+    {"ExecSqlRow"},
+    {"HttpPost"},
+    {"Flattener", "Flatterner"}
+  };
+
+  private static final List<IPlugin> plugins = new ArrayList<>();
+
+  @BeforeAll
+  static void setUpBeforeClass() throws Exception {
+    HopClientEnvironment.init();
+    for (String[] ids : HOP_IDS) {
+      IPlugin plugin = mock(IPlugin.class);
+      when(plugin.getIds()).thenReturn(ids);
+      when(plugin.getName()).thenReturn(ids[0]);
+      when(plugin.matches(anyString()))
+          .thenAnswer(
+              invocation ->
+                  Arrays.stream(ids)
+                      .anyMatch(id -> 
id.equalsIgnoreCase(invocation.getArgument(0))));
+      PluginRegistry.getInstance().registerPlugin(TransformPluginType.class, 
plugin);
+      plugins.add(plugin);
+    }
+  }
+
+  @AfterAll
+  static void tearDownAfterClass() {
+    for (IPlugin plugin : plugins) {
+      PluginRegistry.getInstance().removePlugin(TransformPluginType.class, 
plugin);
+    }
+  }
+
+  @ParameterizedTest
+  @CsvSource({
+    "CsvInput,CSVInput",
+    "DynamicSQLRow,DynamicSqlRow",
+    "ExecSQL,ExecSql",
+    "ExecSQLRow,ExecSqlRow",
+    "HTTPPOST,HttpPost",
+    "Flatterner,Flattener"
+  })
+  void testKettleCaseBecomesRegisteredId(String kettleType, String hopType) 
throws Exception {
+    assertEquals(hopType, importedType(kettleType));
+  }
+
+  @Test
+  void testRegisteredIdIsKept() throws Exception {
+    assertEquals("CSVInput", importedType("CSVInput"));
+  }
+
+  @Test
+  void testUnknownTypeIsLeftAlone() throws Exception {
+    assertEquals("SomeUnknownStep", importedType("SomeUnknownStep"));
+  }
+
+  /** Only the step type is aligned, not other elements that happen to hold 
the same text. */
+  @Test
+  void testTransformNameIsLeftAlone() throws Exception {
+    Node transform = importStep("CsvInput", "CsvInput");
+    assertEquals("CsvInput", XmlHandler.getTagValue(transform, "name"));
+    assertEquals("CSVInput", XmlHandler.getTagValue(transform, "type"));
+  }
+
+  private static String importedType(String kettleType) throws Exception {
+    return XmlHandler.getTagValue(importStep("read", kettleType), "type");
+  }
+
+  private static Node importStep(String name, String type) throws Exception {
+    Document doc =
+        XmlParserFactoryProducer.createSecureDocBuilderFactory()
+            .newDocumentBuilder()
+            .parse(
+                new ByteArrayInputStream(
+                    ("<transformation><step><name>"
+                            + name
+                            + "</name><type>"
+                            + type
+                            + "</type></step></transformation>")
+                        .getBytes(StandardCharsets.UTF_8)));
+    processNode(doc);
+    return XmlHandler.getSubNode(XmlHandler.getSubNode(doc, "pipeline"), 
"transform");
+  }
+
+  private static 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(new KettleImport(), doc, doc, other, 0);
+  }
+}

Reply via email to