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 904fcb54b7 Issue #4248 : Ask where to save an inherited metadata
duplicate (#8688)
904fcb54b7 is described below
commit 904fcb54b753ce7e9d3e4461c522b61dde3f7f25
Author: Matt Casters <[email protected]>
AuthorDate: Thu Oct 1 09:13:01 2026 +0200
Issue #4248 : Ask where to save an inherited metadata duplicate (#8688)
---
.../apache/hop/metadata/util/HopMetadataUtil.java | 27 ++++++++
.../hop/metadata/serializer/MetadataGuiFlows.java | 20 +++++-
.../multi/MultiMetadataLegacyKeysTest.java | 74 +++++++++++++++++++++-
.../ROOT/pages/hop-gui/perspective-metadata.adoc | 1 +
.../perspective/metadata/MetadataPerspective.java | 24 +++++++
.../metadata/messages/messages_en_US.properties | 2 +
6 files changed, 144 insertions(+), 4 deletions(-)
diff --git
a/core/src/main/java/org/apache/hop/metadata/util/HopMetadataUtil.java
b/core/src/main/java/org/apache/hop/metadata/util/HopMetadataUtil.java
index 92bfc3ad86..fc29287bf9 100644
--- a/core/src/main/java/org/apache/hop/metadata/util/HopMetadataUtil.java
+++ b/core/src/main/java/org/apache/hop/metadata/util/HopMetadataUtil.java
@@ -112,6 +112,33 @@ public class HopMetadataUtil {
return providers;
}
+ /**
+ * Metadata locations a duplicate can be written to when the original is not
stored in the active
+ * project. The active project (the last provider) is first.
+ *
+ * @param provider the metadata provider of the project, possibly a {@link
MultiMetadataProvider}
+ * @param sourceProviderName {@link IHopMetadata#getMetadataProviderName()}
of the original, or
+ * null
+ * @return provider descriptions, active project first, or an empty list
when there is nothing to
+ * choose (a single location, or the original already belongs to the
active project)
+ */
+ public static List<String> duplicateProviderChoices(
+ IHopMetadataProvider provider, String sourceProviderName) {
+ List<IHopMetadataProvider> providers = getProviders(provider);
+ if (providers.size() < 2) {
+ return Collections.emptyList();
+ }
+ String activeProject = providers.get(providers.size() -
1).getDescription();
+ if (StringUtils.isEmpty(sourceProviderName) ||
sourceProviderName.equals(activeProject)) {
+ return Collections.emptyList();
+ }
+ List<String> choices = new ArrayList<>();
+ for (int i = providers.size() - 1; i >= 0; i--) {
+ choices.add(providers.get(i).getDescription());
+ }
+ return choices;
+ }
+
public static <T extends IHopMetadata> HopMetadata getHopMetadataAnnotation(
Class<T> managedClass) {
return managedClass.getAnnotation(HopMetadata.class);
diff --git
a/core/src/test/java/org/apache/hop/metadata/serializer/MetadataGuiFlows.java
b/core/src/test/java/org/apache/hop/metadata/serializer/MetadataGuiFlows.java
index ebe12ee59a..77f0ffaa4c 100644
---
a/core/src/test/java/org/apache/hop/metadata/serializer/MetadataGuiFlows.java
+++
b/core/src/test/java/org/apache/hop/metadata/serializer/MetadataGuiFlows.java
@@ -17,6 +17,7 @@
package org.apache.hop.metadata.serializer;
+import org.apache.commons.lang3.StringUtils;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.metadata.api.IHopMetadata;
import org.apache.hop.metadata.api.IHopMetadataSerializer;
@@ -72,20 +73,37 @@ public final class MetadataGuiFlows {
* Duplicating an element in the metadata perspective: {@code
* MetadataPerspective.duplicateMetadata()}.
*
+ * <p>The copy is a new object of the active project (the last metadata
provider). That is the
+ * place the duplicate dialog selects by default when the original belongs
to a parent project.
+ * Pass a provider description to save the copy there instead.
+ *
+ * @param targetProviderName provider to save the copy in, or null for the
active project
* @return the name of the copy
*/
public static <T extends IHopMetadata> String duplicate(
- IHopMetadataSerializer<T> serializer, String name) throws HopException {
+ IHopMetadataSerializer<T> serializer, String name, String
targetProviderName)
+ throws HopException {
T metadata = serializer.load(name);
int copyNr = 2;
while (true) {
String newName = name + " " + copyNr;
if (!serializer.exists(newName)) {
metadata.setName(newName);
+ metadata.setMetadataProviderName(
+ StringUtils.isEmpty(targetProviderName) ? null :
targetProviderName);
serializer.save(metadata);
return newName;
}
copyNr++;
}
}
+
+ /**
+ * @return the name of the copy, saved in the active project
+ * @see #duplicate(IHopMetadataSerializer, String, String)
+ */
+ public static <T extends IHopMetadata> String duplicate(
+ IHopMetadataSerializer<T> serializer, String name) throws HopException {
+ return duplicate(serializer, name, null);
+ }
}
diff --git
a/core/src/test/java/org/apache/hop/metadata/serializer/multi/MultiMetadataLegacyKeysTest.java
b/core/src/test/java/org/apache/hop/metadata/serializer/multi/MultiMetadataLegacyKeysTest.java
index 2007b0f8d5..a9c4ae71a1 100644
---
a/core/src/test/java/org/apache/hop/metadata/serializer/multi/MultiMetadataLegacyKeysTest.java
+++
b/core/src/test/java/org/apache/hop/metadata/serializer/multi/MultiMetadataLegacyKeysTest.java
@@ -227,21 +227,89 @@ class MultiMetadataLegacyKeysTest {
assertEquals("child", serializer.load("mine").getDescription());
}
- /** The copy goes where the original lives, in the current folder. */
+ /**
+ * The copy is a new object of the active (child) project, in the current
folder. It is not
+ * written next to the original in the parent project, which would share it
with every project
+ * that inherits that parent.
+ */
@Test
- void testDuplicateInParentProject() throws Exception {
+ void testDuplicateInParentProjectGoesToTheChild() throws Exception {
write(parentFolder, LEGACY, "conn", "parent");
write(childFolder, LEGACY, "conn 2", "child");
String copy = MetadataGuiFlows.duplicate(serializer, "conn");
assertEquals("conn 3", copy);
- assertTrue(has(parentFolder, CURRENT, "conn 3"));
+ assertTrue(has(childFolder, CURRENT, "conn 3"));
+ assertFalse(has(parentFolder, CURRENT, "conn 3"));
+ assertFalse(has(parentFolder, LEGACY, "conn 3"));
assertTrue(has(parentFolder, LEGACY, "conn"), "the original isn't moved");
assertTrue(has(childFolder, LEGACY, "conn 2"));
+ assertEquals(
+ multi.getProviders().get(1).getDescription(),
+ serializer.load(copy).getMetadataProviderName());
assertEquals(List.of("conn", "conn 2", "conn 3"), sortedNames());
}
+ /** Accepting the dialog default passes the active project's provider
description. */
+ @Test
+ void testDuplicateInParentProjectTargetsTheChildExplicitly() throws
Exception {
+ write(parentFolder, LEGACY, "conn", "parent");
+
+ String childProvider = multi.getProviders().get(1).getDescription();
+ String copy = MetadataGuiFlows.duplicate(serializer, "conn",
childProvider);
+
+ assertEquals("conn 2", copy);
+ assertTrue(has(childFolder, CURRENT, "conn 2"));
+ assertFalse(has(parentFolder, CURRENT, "conn 2"));
+ assertTrue(has(parentFolder, LEGACY, "conn"), "the original isn't moved");
+ assertEquals(childProvider,
serializer.load(copy).getMetadataProviderName());
+ }
+
+ /** The duplicate dialog can still save the copy in the project that owns
the original. */
+ @Test
+ void testDuplicateCanStayWithTheOriginal() throws Exception {
+ write(parentFolder, LEGACY, "conn", "parent");
+ write(childFolder, CURRENT, "other", "child");
+
+ String parentProvider = multi.getProviders().get(0).getDescription();
+ String copy = MetadataGuiFlows.duplicate(serializer, "conn",
parentProvider);
+
+ assertEquals("conn 2", copy);
+ assertTrue(has(parentFolder, CURRENT, "conn 2"));
+ assertFalse(has(childFolder, CURRENT, "conn 2"));
+ assertTrue(has(parentFolder, LEGACY, "conn"), "the original isn't moved");
+ assertEquals(parentProvider,
serializer.load(copy).getMetadataProviderName());
+ }
+
+ @Test
+ void testDuplicateOfLocalObjectStaysInTheChild() throws Exception {
+ write(childFolder, CURRENT, "local", "child");
+
+ String copy = MetadataGuiFlows.duplicate(serializer, "local");
+
+ assertEquals("local 2", copy);
+ assertTrue(has(childFolder, CURRENT, "local 2"));
+ assertFalse(has(parentFolder, CURRENT, "local 2"));
+ assertFalse(Files.exists(parentFolder.resolve(CURRENT)));
+ }
+
+ @Test
+ void testDuplicateProviderChoices() {
+ List<IHopMetadataProvider> providers = multi.getProviders();
+ String parent = providers.get(0).getDescription();
+ String child = providers.get(1).getDescription();
+
+ assertEquals(List.of(), HopMetadataUtil.duplicateProviderChoices(multi,
null));
+ assertEquals(List.of(), HopMetadataUtil.duplicateProviderChoices(multi,
""));
+ assertEquals(List.of(), HopMetadataUtil.duplicateProviderChoices(multi,
child));
+ assertEquals(List.of(),
HopMetadataUtil.duplicateProviderChoices(providers.get(1), parent));
+ assertEquals(List.of(child, parent),
HopMetadataUtil.duplicateProviderChoices(multi, parent));
+ assertEquals(
+ List.of(child, parent),
+ HopMetadataUtil.duplicateProviderChoices(multi, "JSON metadata in
folder /missing"));
+ }
+
/**
* Deleting an object the child overrides removes the child's copies only,
in all its folders. The
* parent's object then shows through again, as it did before legacy keys
existed.
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-metadata.adoc
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-metadata.adoc
index 84a11eafd7..516803a67c 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-metadata.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-metadata.adoc
@@ -37,6 +37,7 @@
image::hop-gui/perspective-data-orchestration-left-hand-tree.png[The left-hand t
=== Managing elements
You can right-click on any element to edit, rename, duplicate or delete an
element. You can also use this menu to create new elements.
+Duplicating an element that is stored in a parent project asks which metadata
folder should receive the copy. The active project is selected by default, so
the copy is not written next to the original and shared with every project that
inherits it.
To create a new element you can also double-click on the element type itself.
You can also use the toolbar icons in the toolbar located right above the
metadata tree.
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/metadata/MetadataPerspective.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/metadata/MetadataPerspective.java
index 4fb6192724..5399853e5d 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/metadata/MetadataPerspective.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/metadata/MetadataPerspective.java
@@ -70,6 +70,7 @@ import org.apache.hop.ui.core.PropsUi;
import org.apache.hop.ui.core.bus.HopGuiEvents;
import org.apache.hop.ui.core.dialog.BaseDialog;
import org.apache.hop.ui.core.dialog.DetailsDialog;
+import org.apache.hop.ui.core.dialog.EnterSelectionDialog;
import org.apache.hop.ui.core.dialog.EnterStringDialog;
import org.apache.hop.ui.core.dialog.ErrorDialog;
import org.apache.hop.ui.core.dialog.MessageBox;
@@ -2291,11 +2292,34 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
MetadataManager<IHopMetadata> manager = getMetadataManager(objectKey);
IHopMetadata metadata = manager.loadElement(objectName);
+ String targetProviderName = metadata.getMetadataProviderName();
+ List<String> providerChoices =
+ HopMetadataUtil.duplicateProviderChoices(
+ hopGui.getMetadataProvider(), targetProviderName);
+ if (!providerChoices.isEmpty()) {
+ EnterSelectionDialog dialog =
+ new EnterSelectionDialog(
+ getShell(),
+ providerChoices.toArray(new String[0]),
+ BaseMessages.getString(
+ PKG,
"MetadataPerspective.DuplicateMetadata.SelectProvider.Title"),
+ BaseMessages.getString(
+ PKG,
+
"MetadataPerspective.DuplicateMetadata.SelectProvider.Message",
+ objectName));
+ String chosen = dialog.open(0);
+ if (chosen == null) {
+ return;
+ }
+ targetProviderName = chosen;
+ }
+
int copyNr = 2;
while (true) {
String newName = objectName + " " + copyNr;
if (!manager.getSerializer().exists(newName)) {
metadata.setName(newName);
+ metadata.setMetadataProviderName(targetProviderName);
manager.getSerializer().save(metadata);
break;
} else {
diff --git
a/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/metadata/messages/messages_en_US.properties
b/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/metadata/messages/messages_en_US.properties
index 0832d56102..aeaebe20fd 100644
---
a/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/metadata/messages/messages_en_US.properties
+++
b/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/metadata/messages/messages_en_US.properties
@@ -25,6 +25,8 @@ MetadataPerspective.CreateMetadata.Error.Message=Error
creating metadata
MetadataPerspective.Description=The Hop Metatada Perspective
MetadataPerspective.DuplicateMetadata.Error.Header=Error
MetadataPerspective.DuplicateMetadata.Error.Message=Error duplicating metadata
+MetadataPerspective.DuplicateMetadata.SelectProvider.Title=Duplicate metadata
+MetadataPerspective.DuplicateMetadata.SelectProvider.Message=''{0}'' is not
stored in the active project.\nSelect where to save the copy. The active
project is already selected.
MetadataPerspective.EditMetadata.Error.Header=Error
MetadataPerspective.EditMetadata.Error.Message=Error editing metadata
MetadataPerspective.GuiPlugin.Description=This perspective allows you to see
and edit all available metadata