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);
+ }
+}