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 54eb878da8 ability to install multiple marketplace plugins, fixes
#7799 (#7800)
54eb878da8 is described below
commit 54eb878da850fa20b10bac6e6b6d183a7e6cf518
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Thu Aug 6 15:08:26 2026 +0200
ability to install multiple marketplace plugins, fixes #7799 (#7800)
---
assemblies/client/src/assembly/assembly.xml | 2 -
assemblies/plugins/pom.xml | 116 +++----
.../hop/core/extension/ExtensionPointMap.java | 17 +
.../hop/core/extension/HopExtensionPoint.java | 8 +
.../hop/core/extension/ExtensionPointMapTest.java | 13 +
.../ROOT/pages/hop-tools/hop-marketplace.adoc | 9 +-
.../metadata/SerializableMetadataProvider.java | 31 +-
.../metadata/SerializableMetadataProviderTest.java | 106 ++++++
.../marketplace/command/MarketplaceCommand.java | 128 +++++--
.../hop/marketplace/gui/MarketplaceDialog.java | 380 ++++++++++++++++-----
.../xp/SearchMarketplaceExtensionPoint.java | 58 ++++
.../gui/messages/messages_en_US.properties | 9 +
.../command/InstallCommandParsingTest.java | 68 ++++
plugins/tech/azure/pom.xml | 11 +
.../transforms/missing/MissingPipelineDialog.java | 61 ++++
.../actions/missing/MissingActionDialog.java | 61 ++++
.../missing/messages/messages_en_US.properties | 3 +
.../missing/messages/messages_en_US.properties | 3 +
18 files changed, 882 insertions(+), 202 deletions(-)
diff --git a/assemblies/client/src/assembly/assembly.xml
b/assemblies/client/src/assembly/assembly.xml
index c5b6534a64..d25f356db3 100644
--- a/assemblies/client/src/assembly/assembly.xml
+++ b/assemblies/client/src/assembly/assembly.xml
@@ -40,8 +40,6 @@
<include>org.apache.hop:hop-core:zip</include>
<include>org.apache.hop:hop-engine:zip</include>
<include>org.apache.hop:hop-ui:zip</include>
- <!-- Beam is a marketplace plugin; bundled only when the
dependency is
- present (-Pbeam / -Pfull). It brings its own lib/core
libs and lib/spark-client. -->
<include>org.apache.hop:hop-engines-beam:zip</include>
<include>org.apache.hop:hop-ui-rcp:zip</include>
</includes>
diff --git a/assemblies/plugins/pom.xml b/assemblies/plugins/pom.xml
index 87eb032708..97ce2f803d 100644
--- a/assemblies/plugins/pom.xml
+++ b/assemblies/plugins/pom.xml
@@ -618,7 +618,6 @@
<version>${project.version}</version>
<type>zip</type>
</dependency>
- <!-- marketplace optional: hop-engines-spark -->
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-misc-async</artifactId>
@@ -631,7 +630,6 @@
<version>${project.version}</version>
<type>zip</type>
</dependency>
- <!-- marketplace optional: hop-misc-documentation -->
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-misc-execution-database</artifactId>
@@ -644,8 +642,12 @@
<version>${project.version}</version>
<type>zip</type>
</dependency>
- <!-- marketplace optional: hop-misc-import -->
- <!-- marketplace optional: hop-misc-mail -->
+ <dependency>
+ <groupId>org.apache.hop</groupId>
+ <artifactId>hop-misc-mail</artifactId>
+ <version>${project.version}</version>
+ <type>zip</type>
+ </dependency>
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-misc-marketplace</artifactId>
@@ -664,14 +666,12 @@
<version>${project.version}</version>
<type>zip</type>
</dependency>
- <!-- marketplace optional: hop-misc-py4j -->
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-misc-reflection</artifactId>
<version>${project.version}</version>
<type>zip</type>
</dependency>
- <!-- marketplace optional: hop-misc-rest -->
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-misc-rest</artifactId>
@@ -696,23 +696,24 @@
<version>2.19.0-SNAPSHOT</version>
<type>zip</type>
</dependency>
- <!-- marketplace optional: hop-tech-arrow -->
- <!-- marketplace optional: hop-tech-avro -->
- <!-- marketplace optional: hop-tech-aws -->
- <!-- marketplace optional: hop-tech-azure -->
- <!-- marketplace optional: hop-tech-cassandra -->
- <!-- marketplace optional: hop-tech-databricks -->
- <!-- marketplace optional: hop-tech-dropbox -->
- <!-- marketplace optional: hop-tech-elastic -->
- <!-- marketplace optional: hop-tech-minio -->
- <!-- marketplace optional: hop-tech-mongodb -->
- <!-- marketplace optional: hop-tech-neo4j -->
- <!-- marketplace optional: hop-tech-opensearch -->
- <!-- marketplace optional: hop-tech-parquet -->
- <!-- marketplace optional: hop-tech-redis -->
- <!-- marketplace optional: hop-tech-salesforce -->
- <!-- Also listed in the marketplace catalog (optional-plugins.yaml) so
slim
- distributions can install it, but it ships with the client. -->
+ <dependency>
+ <groupId>org.apache.hop</groupId>
+ <artifactId>hop-tech-aws</artifactId>
+ <version>${project.version}</version>
+ <type>zip</type>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.hop</groupId>
+ <artifactId>hop-tech-azure</artifactId>
+ <version>${project.version}</version>
+ <type>zip</type>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.hop</groupId>
+ <artifactId>hop-tech-google</artifactId>
+ <version>${project.version}</version>
+ <type>zip</type>
+ </dependency>
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-tech-sftp</artifactId>
@@ -917,7 +918,6 @@
<version>${project.version}</version>
<type>zip</type>
</dependency>
- <!-- marketplace optional: hop-transform-detectlanguage -->
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-transform-detectlastrow</artifactId>
@@ -930,14 +930,18 @@
<version>${project.version}</version>
<type>zip</type>
</dependency>
- <!-- marketplace optional: hop-transform-drools -->
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-transform-dynamicsqlrow</artifactId>
<version>${project.version}</version>
<type>zip</type>
</dependency>
- <!-- marketplace optional: hop-transform-edi2xml -->
+ <dependency>
+ <groupId>org.apache.hop</groupId>
+ <artifactId>hop-transform-edi2xml</artifactId>
+ <version>${project.version}</version>
+ <type>zip</type>
+ </dependency>
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-transform-excel</artifactId>
@@ -962,7 +966,12 @@
<version>${project.version}</version>
<type>zip</type>
</dependency>
- <!-- marketplace optional: hop-transform-fake -->
+ <dependency>
+ <groupId>org.apache.hop</groupId>
+ <artifactId>hop-transform-fake</artifactId>
+ <version>${project.version}</version>
+ <type>zip</type>
+ </dependency>
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-transform-fieldschangesequence</artifactId>
@@ -1323,7 +1332,12 @@
<version>${project.version}</version>
<type>zip</type>
</dependency>
- <!-- marketplace optional: hop-transform-script -->
+ <dependency>
+ <groupId>org.apache.hop</groupId>
+ <artifactId>hop-transform-script</artifactId>
+ <version>${project.version}</version>
+ <type>zip</type>
+ </dependency>
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-transform-selectvalues</artifactId>
@@ -1408,7 +1422,6 @@
<version>${project.version}</version>
<type>zip</type>
</dependency>
- <!-- marketplace optional: hop-transform-stanfordnlp -->
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-transform-streamlookup</artifactId>
@@ -1487,7 +1500,6 @@
<version>${project.version}</version>
<type>zip</type>
</dependency>
- <!-- marketplace optional: hop-transform-tika -->
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-transform-tokenreplacement</artifactId>
@@ -1524,7 +1536,6 @@
<version>${project.version}</version>
<type>zip</type>
</dependency>
- <!-- marketplace optional: hop-transform-vcard -->
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-transform-webserviceavailable</artifactId>
@@ -1573,7 +1584,6 @@
<version>${project.version}</version>
<type>zip</type>
</dependency>
-
</dependencies>
<!--
@@ -1613,12 +1623,6 @@
<version>${project.version}</version>
<type>zip</type>
</dependency>
- <dependency>
- <groupId>org.apache.hop</groupId>
- <artifactId>hop-misc-mail</artifactId>
- <version>${project.version}</version>
- <type>zip</type>
- </dependency>
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-misc-py4j</artifactId>
@@ -1643,18 +1647,6 @@
<version>${project.version}</version>
<type>zip</type>
</dependency>
- <dependency>
- <groupId>org.apache.hop</groupId>
- <artifactId>hop-tech-aws</artifactId>
- <version>${project.version}</version>
- <type>zip</type>
- </dependency>
- <dependency>
- <groupId>org.apache.hop</groupId>
- <artifactId>hop-tech-azure</artifactId>
- <version>${project.version}</version>
- <type>zip</type>
- </dependency>
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-tech-cassandra</artifactId>
@@ -1679,12 +1671,6 @@
<version>${project.version}</version>
<type>zip</type>
</dependency>
- <dependency>
- <groupId>org.apache.hop</groupId>
- <artifactId>hop-tech-google</artifactId>
- <version>${project.version}</version>
- <type>zip</type>
- </dependency>
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-tech-minio</artifactId>
@@ -1739,24 +1725,6 @@
<version>${project.version}</version>
<type>zip</type>
</dependency>
- <dependency>
- <groupId>org.apache.hop</groupId>
- <artifactId>hop-transform-edi2xml</artifactId>
- <version>${project.version}</version>
- <type>zip</type>
- </dependency>
- <dependency>
- <groupId>org.apache.hop</groupId>
- <artifactId>hop-transform-fake</artifactId>
- <version>${project.version}</version>
- <type>zip</type>
- </dependency>
- <dependency>
- <groupId>org.apache.hop</groupId>
- <artifactId>hop-transform-script</artifactId>
- <version>${project.version}</version>
- <type>zip</type>
- </dependency>
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-transform-stanfordnlp</artifactId>
diff --git
a/core/src/main/java/org/apache/hop/core/extension/ExtensionPointMap.java
b/core/src/main/java/org/apache/hop/core/extension/ExtensionPointMap.java
index 41b0cf9ac7..4d27c1fc31 100644
--- a/core/src/main/java/org/apache/hop/core/extension/ExtensionPointMap.java
+++ b/core/src/main/java/org/apache/hop/core/extension/ExtensionPointMap.java
@@ -158,6 +158,23 @@ public class ExtensionPointMap {
}
}
+ /**
+ * Is anything listening to this extension point id?
+ *
+ * <p>Lets the GUI leave out actions that only an optional plugin can
perform: the marketplace
+ * button on the missing plugins dialog is hidden when the marketplace
plugin isn't installed.
+ *
+ * @param id the id of the extension point interface
+ */
+ public boolean isRegistered(String id) {
+ lock.readLock().lock();
+ try {
+ return extensionPointPluginMap.containsRow(id);
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
/**
* Returns the element in the position (rowId,columnId) of the table
*
diff --git
a/core/src/main/java/org/apache/hop/core/extension/HopExtensionPoint.java
b/core/src/main/java/org/apache/hop/core/extension/HopExtensionPoint.java
index c145a789a8..b4c8d2e621 100644
--- a/core/src/main/java/org/apache/hop/core/extension/HopExtensionPoint.java
+++ b/core/src/main/java/org/apache/hop/core/extension/HopExtensionPoint.java
@@ -165,6 +165,14 @@ public enum HopExtensionPoint {
HopGuiGetControlSpaceSortOrderPrefix(
"Gets a prefix to steer the sort order of variables when using
CTRL-SPACE. Defaults range from 900_ to 400_. Set prefixes in
Map<String,String>"),
+ /**
+ * Open the marketplace so a missing plugin can be looked up and installed.
Payload is the plugin
+ * id to search for (String); blank or null opens the marketplace
unfiltered. Only listened to
+ * when the marketplace plugin is installed, so callers should check {@link
+ * ExtensionPointMap#isRegistered(String)} before offering it.
+ */
+ HopGuiSearchMarketplace("Open the marketplace, searching for a plugin id
(String)"),
+
HopImportStart("Executed at the start of the 'hop-import' command line
tool"),
HopImportEnd("Executed at the end of the 'hop-import' command line tool"),
diff --git
a/core/src/test/java/org/apache/hop/core/extension/ExtensionPointMapTest.java
b/core/src/test/java/org/apache/hop/core/extension/ExtensionPointMapTest.java
index 73fb536dde..a4e4bbb1bc 100644
---
a/core/src/test/java/org/apache/hop/core/extension/ExtensionPointMapTest.java
+++
b/core/src/test/java/org/apache/hop/core/extension/ExtensionPointMapTest.java
@@ -18,6 +18,8 @@
package org.apache.hop.core.extension;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -63,6 +65,17 @@ class ExtensionPointMapTest {
verify(pluginInterface, never()).loadClass(any(Class.class));
}
+ @Test
+ void isRegisteredTest() {
+ assertFalse(ExtensionPointMap.getInstance().isRegistered(TEST_NAME));
+
+ ExtensionPointMap.getInstance().addExtensionPoint(pluginInterface);
+ assertTrue(ExtensionPointMap.getInstance().isRegistered(TEST_NAME));
+
+ ExtensionPointMap.getInstance().removeExtensionPoint(pluginInterface);
+ assertFalse(ExtensionPointMap.getInstance().isRegistered(TEST_NAME));
+ }
+
@Test
void addExtensionPointTest() {
ExtensionPointMap.getInstance().addExtensionPoint(pluginInterface);
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-marketplace.adoc
b/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-marketplace.adoc
index f02b57bf8c..556c1dda09 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-marketplace.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-marketplace.adoc
@@ -76,6 +76,7 @@ From the Hop install directory:
./hop marketplace install hop-tech-parquet
./hop marketplace install hop-engines-spark:2.19.0
./hop marketplace install hop-engines-spark:2.19.0 --repo asf
+./hop marketplace install hop-tech-parquet hop-transform-tika datavault
./hop marketplace list
./hop marketplace query parquet
./hop marketplace query "spark engine"
@@ -99,6 +100,11 @@ After install, apply, or uninstall, *restart Hop* so the
plugin registry reloads
Coordinates for `install`: `artifactId`, `artifactId:version`, or
`groupId:artifactId:version`.
Optional `--repo <id>` forces a single repository (skips the fallback chain).
+`install` accepts several coordinates in one command. All of them are resolved
before the first
+download, so an unknown or ambiguous name fails before anything is fetched.
Downloads then run one
+after another with a `[2/5]` progress line per plugin; a plugin that fails to
install does not stop
+the others, and the command exits non-zero when any of them failed.
+
`query` searches the local optional-plugin catalog (not Maven Central). The
filter is a
case-insensitive substring matched against artifact id, name, category,
description, and install
path. Omit the filter (or pass an empty string) to list every optional plugin.
Matches may show
@@ -141,7 +147,8 @@ Use this tab to work with the local optional-plugin catalog:
** *Installed* — present on disk with a marketplace install receipt (version
shown when known)
** *Present on disk* — plugin folder exists but no marketplace receipt
** *Not installed* — not found under `plugins/`
-* *Install* / *Uninstall* / *Refresh* — act on the selected plugin (uninstall
requires a marketplace receipt)
+* *Install* / *Uninstall* / *Refresh* — act on the selected plugins (uninstall
requires a marketplace receipt)
+* Several plugins can be selected at once with ctrl-click, shift-click, or
ctrl-A, and installed or uninstalled in one go. Installs run one after another
behind a single progress dialog; a plugin that fails does not stop the others
and every failure is reported together at the end.
Install uses the *primary* repository first, then other *enabled* repositories
(see <<marketplace-gui-repositories>>).
diff --git
a/engine/src/main/java/org/apache/hop/core/metadata/SerializableMetadataProvider.java
b/engine/src/main/java/org/apache/hop/core/metadata/SerializableMetadataProvider.java
index bf3fb94a7d..d1c0d5e455 100644
---
a/engine/src/main/java/org/apache/hop/core/metadata/SerializableMetadataProvider.java
+++
b/engine/src/main/java/org/apache/hop/core/metadata/SerializableMetadataProvider.java
@@ -21,7 +21,9 @@ import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonToken;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
+import org.apache.hop.core.Const;
import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.logging.HopLogStore;
import org.apache.hop.core.logging.LogChannel;
import org.apache.hop.metadata.api.HopMetadata;
import org.apache.hop.metadata.api.IHopMetadata;
@@ -46,6 +48,10 @@ public class SerializableMetadataProvider extends
MemoryMetadataProvider
/**
* Create a copy of all elements in an existing metadata.
*
+ * <p>Objects which can't be loaded, typically because the plugin they
reference isn't installed
+ * (a Beam or Spark pipeline run configuration in a plain client, ...), are
logged and skipped.
+ * They shouldn't keep a pipeline or workflow which doesn't use them from
running.
+ *
* @param source the source store to copy over
*/
public SerializableMetadataProvider(IHopMetadataProvider source) throws
HopException {
@@ -60,11 +66,34 @@ public class SerializableMetadataProvider extends
MemoryMetadataProvider
// Loop over the available objects of the class and copy the information
over.
//
for (String name : sourceSerializer.listObjectNames()) {
- targetSerializer.save(sourceSerializer.load(name));
+ try {
+ IHopMetadata object = sourceSerializer.load(name);
+ if (object != null) {
+ targetSerializer.save(object);
+ }
+ } catch (Exception e) {
+ logSkippedObject(name, metadataClass, e);
+ }
}
}
}
+ private static void logSkippedObject(String name, Class<?> metadataClass,
Exception e) {
+ String message =
+ "Error copying metadata object '"
+ + name
+ + "' of type "
+ + metadataClass.getSimpleName()
+ + ". The object is ignored.";
+ if (HopLogStore.isInitialized()) {
+ LogChannel.GENERAL.logError(message, e);
+ } else {
+ // Metadata is also copied before any logging is available.
+ //
+ System.err.println(message + Const.CR + Const.getStackTracker(e));
+ }
+ }
+
public String toJson() throws HopException {
JSONObject jStore = new JSONObject();
diff --git
a/engine/src/test/java/org/apache/hop/core/metadata/SerializableMetadataProviderTest.java
b/engine/src/test/java/org/apache/hop/core/metadata/SerializableMetadataProviderTest.java
index 3f03658013..66b8dc0efe 100644
---
a/engine/src/test/java/org/apache/hop/core/metadata/SerializableMetadataProviderTest.java
+++
b/engine/src/test/java/org/apache/hop/core/metadata/SerializableMetadataProviderTest.java
@@ -19,9 +19,14 @@ package org.apache.hop.core.metadata;
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 java.util.List;
+import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.util.TestUtil;
import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.metadata.api.IHopMetadata;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
import org.apache.hop.metadata.api.IHopMetadataSerializer;
import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
import org.apache.hop.server.HopServerMeta;
@@ -73,4 +78,105 @@ class SerializableMetadataProviderTest {
assertEquals(sourceServer2.getUsername(), targetServer2.getUsername());
assertEquals(sourceServer2.getPassword(), targetServer2.getPassword());
}
+
+ /**
+ * An object which can't be loaded, for example a pipeline run configuration
referencing an engine
+ * plugin which isn't installed, shouldn't keep the other objects from being
copied. Otherwise a
+ * pipeline or workflow which doesn't use that object at all can't even be
prepared for execution.
+ */
+ @Test
+ void testUnloadableObjectIsSkipped() throws Exception {
+ MemoryMetadataProvider source =
+ new MemoryMetadataProvider() {
+ @Override
+ public <T extends IHopMetadata> IHopMetadataSerializer<T>
getSerializer(
+ Class<T> managedClass) throws HopException {
+ IHopMetadataSerializer<T> serializer =
super.getSerializer(managedClass);
+ if (HopServerMeta.class.equals(managedClass)) {
+ return new FailingOnLoadSerializer<>(serializer, "broken");
+ }
+ return serializer;
+ }
+ };
+ IHopMetadataSerializer<HopServerMeta> sourceSerializer =
+ source.getSerializer(HopServerMeta.class);
+ sourceSerializer.save(
+ new HopServerMeta(
+ "server1", "hostname1", "8181", "username1", "password1", null,
null, null, false));
+ sourceSerializer.save(
+ new HopServerMeta(
+ "broken", "hostname2", "8282", "username2", "password2", null,
null, null, false));
+
+ SerializableMetadataProvider target = new
SerializableMetadataProvider(source);
+ IHopMetadataSerializer<HopServerMeta> targetSerializer =
+ target.getSerializer(HopServerMeta.class);
+
+ assertNotNull(targetSerializer.load("server1"));
+ assertNull(targetSerializer.load("broken"));
+
+ // The same needs to hold for the JSON round trip used to ship metadata to
an engine.
+ //
+ SerializableMetadataProvider roundTripped = new
SerializableMetadataProvider(target.toJson());
+
assertNotNull(roundTripped.getSerializer(HopServerMeta.class).load("server1"));
+ }
+
+ /** Delegates everything but throws when one specific object is loaded. */
+ private static class FailingOnLoadSerializer<T extends IHopMetadata>
+ implements IHopMetadataSerializer<T> {
+ private final IHopMetadataSerializer<T> delegate;
+ private final String failingName;
+
+ FailingOnLoadSerializer(IHopMetadataSerializer<T> delegate, String
failingName) {
+ this.delegate = delegate;
+ this.failingName = failingName;
+ }
+
+ @Override
+ public T load(String objectName) throws HopException {
+ if (failingName.equals(objectName)) {
+ throw new HopException("Unable to find the plugin for object '" +
objectName + "'");
+ }
+ return delegate.load(objectName);
+ }
+
+ @Override
+ public String getDescription() {
+ return delegate.getDescription();
+ }
+
+ @Override
+ public void save(T object) throws HopException {
+ delegate.save(object);
+ }
+
+ @Override
+ public T delete(String name) throws HopException {
+ return delegate.delete(name);
+ }
+
+ @Override
+ public List<String> listObjectNames() throws HopException {
+ return delegate.listObjectNames();
+ }
+
+ @Override
+ public List<T> loadAll() throws HopException {
+ return delegate.loadAll();
+ }
+
+ @Override
+ public boolean exists(String name) throws HopException {
+ return delegate.exists(name);
+ }
+
+ @Override
+ public Class<T> getManagedClass() {
+ return delegate.getManagedClass();
+ }
+
+ @Override
+ public IHopMetadataProvider getMetadataProvider() {
+ return delegate.getMetadataProvider();
+ }
+ }
}
diff --git
a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/command/MarketplaceCommand.java
b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/command/MarketplaceCommand.java
index 074d8fc04a..fa5507f51f 100644
---
a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/command/MarketplaceCommand.java
+++
b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/command/MarketplaceCommand.java
@@ -136,16 +136,21 @@ public class MarketplaceCommand implements Runnable,
IHopCommand, IHasHopMetadat
mixinStandardHelpOptions = true,
name = "install",
description =
- "Download and install a plugin zip. Short names resolve via the same
discovery as query"
- + " (e.g. datavault → hop-datavault:0.4.0-SNAPSHOT from a browse
repo).")
+ "Download and install one or more plugin zips. Short names resolve
via the same discovery"
+ + " as query (e.g. datavault → hop-datavault:0.4.0-SNAPSHOT from
a browse repo)."
+ + " Every coordinate is resolved before the first download, so a
typo fails before"
+ + " anything is fetched. A plugin that fails to install does not
stop the others;"
+ + " the command exits non-zero when any of them failed.")
static class InstallCommand extends MarketplaceSubCommand {
@Parameters(
index = "0",
+ arity = "1..*",
paramLabel = "COORDINATE",
description =
- "Short name, artifactId, artifactId:version, or
groupId:artifactId:version"
- + " (e.g. datavault, hop-tech-parquet,
hop-datavault:0.4.0-SNAPSHOT)")
- private String coordinate;
+ "One or more short names, artifactIds, artifactId:version, or"
+ + " groupId:artifactId:version, separated by spaces"
+ + " (e.g. datavault hop-tech-parquet
hop-datavault:0.4.0-SNAPSHOT)")
+ private List<String> coordinates;
@Option(
names = {"--repo"},
@@ -162,44 +167,67 @@ public class MarketplaceCommand implements Runnable,
IHopCommand, IHasHopMetadat
throw new HopException("Marketplace is disabled in hop-config.json");
}
Path hopHome = HopHome.resolve();
+ PluginInstaller installer = new PluginInstaller(log, hopHome, config);
// Activate any previously staged plugins first
- new PluginInstaller(log, hopHome, config).activateAllPending();
-
- PluginDiscovery.InstallTarget target =
- PluginDiscovery.resolveInstall(
- coordinate, config.getGroupId(),
resolveDefaultVersion(config), config, log);
- MavenCoordinates gav = target.coordinates();
- if (StringUtils.isNotBlank(target.preferredRepoId()) &&
StringUtils.isBlank(repoId)) {
- System.out.println(
- "Resolved "
- + coordinate
- + " → "
- + gav.gav()
- + " (prefer repo '"
- + target.preferredRepoId()
- + "')");
- } else if (!coordinate.trim().equals(gav.artifactId()) ||
!coordinate.contains(":")) {
- System.out.println("Resolved " + coordinate + " → " + gav.gav());
+ installer.activateAllPending();
+
+ // Resolve the whole list before fetching anything: an unknown or
ambiguous coordinate at
+ // the end of the line should not surface after three plugins have
already downloaded.
+ List<PluginDiscovery.InstallTarget> targets = new ArrayList<>();
+ for (String coordinate : coordinates) {
+ PluginDiscovery.InstallTarget target =
+ PluginDiscovery.resolveInstall(
+ coordinate, config.getGroupId(),
resolveDefaultVersion(config), config, log);
+ printResolution(coordinate, target);
+ targets.add(target);
}
+
+ List<String> failed = new ArrayList<>();
+ int installed = 0;
ConsoleInstallListener progress = ConsoleInstallListener.forStdOut();
- InstallReceipt receipt;
try {
- receipt =
- new PluginInstaller(log, hopHome, config)
- .install(gav, true, repoId, target.preferredRepoId(),
progress);
+ for (int i = 0; i < targets.size(); i++) {
+ PluginDiscovery.InstallTarget target = targets.get(i);
+ MavenCoordinates gav = target.coordinates();
+ // Prints "[2/5] hop-tech-parquet"; silent for a single install.
+ progress.item(gav.artifactId(), i, targets.size());
+ try {
+ InstallReceipt receipt =
+ installer.install(gav, true, repoId,
target.preferredRepoId(), progress);
+ installed++;
+ System.out.println(installedMessage(gav, receipt, hopHome,
targets.size() == 1));
+ } catch (Exception e) {
+ // One plugin failing shouldn't cost the user the rest of the
batch. Everything that
+ // went wrong is listed at the end, and the exit code still
reports failure.
+ // Close any half-drawn progress bar so the error starts on its
own row.
+ progress.complete();
+ failed.add(gav.artifactId());
+ System.err.println("ERROR: failed to install " + gav.gav() + ":
" + e.getMessage());
+ if (log != null) {
+ log.logError("Marketplace install failed for " + gav.gav(), e);
+ }
+ }
+ }
} finally {
// Close off the in-place bar line so what follows starts on a fresh
row.
progress.complete();
}
- System.out.println(
- "Plugin "
- + gav.gav()
- + " installed under "
- + hopHome
- + (receipt.getRepositoryId() != null
- ? " from repo '" + receipt.getRepositoryId() + "'"
- : "")
- + ". Restart Hop to load it.");
+
+ if (targets.size() > 1) {
+ System.out.println(
+ installed
+ + " of "
+ + targets.size()
+ + " plugins installed under "
+ + hopHome
+ + ". Restart Hop to load them.");
+ }
+ if (!failed.isEmpty()) {
+ throw new CommandLine.ExecutionException(
+ new CommandLine(this), "failed to install: " + String.join(", ",
failed));
+ }
+ } catch (CommandLine.ExecutionException e) {
+ throw e;
} catch (Exception e) {
System.err.println("ERROR: " + e.getMessage());
if (log != null) {
@@ -209,6 +237,36 @@ public class MarketplaceCommand implements Runnable,
IHopCommand, IHasHopMetadat
new CommandLine(this), e.getMessage() == null ? "install failed" :
e.getMessage(), e);
}
}
+
+ /** Report what a short name resolved to, unless the user already typed
the full coordinate. */
+ private void printResolution(String coordinate,
PluginDiscovery.InstallTarget target) {
+ MavenCoordinates gav = target.coordinates();
+ if (StringUtils.isNotBlank(target.preferredRepoId()) &&
StringUtils.isBlank(repoId)) {
+ System.out.println(
+ "Resolved "
+ + coordinate
+ + " → "
+ + gav.gav()
+ + " (prefer repo '"
+ + target.preferredRepoId()
+ + "')");
+ } else if (!coordinate.trim().equals(gav.artifactId()) ||
!coordinate.contains(":")) {
+ System.out.println("Resolved " + coordinate + " → " + gav.gav());
+ }
+ }
+
+ private static String installedMessage(
+ MavenCoordinates gav, InstallReceipt receipt, Path hopHome, boolean
single) {
+ return "Plugin "
+ + gav.gav()
+ + " installed under "
+ + hopHome
+ + (receipt.getRepositoryId() != null
+ ? " from repo '" + receipt.getRepositoryId() + "'"
+ : "")
+ // For a batch the restart hint belongs on the summary line, not on
every plugin.
+ + (single ? ". Restart Hop to load it." : ".");
+ }
}
@Command(
diff --git
a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/gui/MarketplaceDialog.java
b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/gui/MarketplaceDialog.java
index 0decbf76f3..605594b792 100644
---
a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/gui/MarketplaceDialog.java
+++
b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/gui/MarketplaceDialog.java
@@ -21,8 +21,11 @@ import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.hop.core.Const;
import org.apache.hop.core.IRunnableWithProgress;
@@ -103,9 +106,20 @@ public class MarketplaceDialog extends Dialog {
private boolean searchPending;
private long searchRequestedAt;
+ /** Pre-filled search text, e.g. the plugin id from the missing plugins
dialog. */
+ private final String initialSearch;
+
public MarketplaceDialog(Shell parent) {
+ this(parent, null);
+ }
+
+ /**
+ * @param initialSearch search box content on open, or null/blank to list
everything
+ */
+ public MarketplaceDialog(Shell parent, String initialSearch) {
super(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL | SWT.RESIZE |
SWT.MAX);
this.props = PropsUi.getInstance();
+ this.initialSearch = initialSearch;
}
public void open() {
@@ -181,6 +195,11 @@ public class MarketplaceDialog extends Dialog {
BaseTransformDialog.setSize(shell);
shell.open();
+ if (StringUtils.isNotBlank(initialSearch) && !wSearch.isDisposed()) {
+ // Selected, so typing replaces the suggested plugin id when it turns up
nothing.
+ wSearch.setFocus();
+ wSearch.selectAll();
+ }
Display display = parent.getDisplay();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
@@ -242,6 +261,11 @@ public class MarketplaceDialog extends Dialog {
fdSearch.top = new FormAttachment(wlSearch, 0, SWT.CENTER);
fdSearch.right = new FormAttachment(wClearSearch, -PropsUi.getMargin());
wSearch.setLayoutData(fdSearch);
+ // Set before the listener is added: the first refreshTable() below
already picks this up, a
+ // Modify event here would only schedule a second, identical query.
+ if (StringUtils.isNotBlank(initialSearch)) {
+ wSearch.setText(initialSearch.trim());
+ }
wSearch.addListener(SWT.Modify, e -> scheduleSearch());
Label wSearchSep = new Label(comp, SWT.HORIZONTAL | SWT.SEPARATOR);
@@ -292,7 +316,9 @@ public class MarketplaceDialog extends Dialog {
new TableView(
Variables.getADefaultVariableSpace(),
comp,
- SWT.BORDER | SWT.FULL_SELECTION | SWT.SINGLE | SWT.V_SCROLL,
+ // No SWT.SINGLE: TableView adds SWT.MULTI, so several plugins can
be installed or
+ // removed in one go.
+ SWT.BORDER | SWT.FULL_SELECTION | SWT.V_SCROLL,
columns,
1,
true,
@@ -482,8 +508,7 @@ public class MarketplaceDialog extends Dialog {
for (OptionalPluginInfo info : plugins) {
// Column 0 is the (hidden) index column; data columns are 1-based.
TableItem item = new TableItem(wTable.table, SWT.NONE);
- String name = Const.NVL(info.getName(), info.getArtifactId());
- item.setText(1, Const.NVL(name, ""));
+ item.setText(1, Const.NVL(displayName(info), ""));
item.setText(2, Const.NVL(info.getVersion(), ""));
item.setText(3, Const.NVL(info.getArtifactId(), ""));
item.setText(4, Const.NVL(info.getCategory(), ""));
@@ -604,12 +629,28 @@ public class MarketplaceDialog extends Dialog {
return true;
}
- private OptionalPluginInfo selected() {
+ /** The plugins highlighted in the table, in table order. Empty when nothing
is selected. */
+ private List<OptionalPluginInfo> selected() {
TableItem[] items = wTable.table.getSelection();
- if (items == null || items.length == 0) {
- return null;
+ if (items == null) {
+ return List.of();
}
- return (OptionalPluginInfo) items[0].getData();
+ List<OptionalPluginInfo> selection = new ArrayList<>();
+ for (TableItem item : items) {
+ if (item.getData() instanceof OptionalPluginInfo info) {
+ selection.add(info);
+ }
+ }
+ return selection;
+ }
+
+ /** Plugin names as the table shows them, one per line, for confirmations
and summaries. */
+ private static String nameList(List<String> names) {
+ return names.stream().map(name -> "- " +
name).collect(Collectors.joining("\n"));
+ }
+
+ private static String displayName(OptionalPluginInfo info) {
+ return Const.NVL(info.getName(), info.getArtifactId());
}
/**
@@ -632,77 +673,167 @@ public class MarketplaceDialog extends Dialog {
return source;
}
+ /** One plugin ready to install: where to get it, and what to call it while
it downloads. */
+ private record InstallRequest(
+ MavenCoordinates coordinates, String preferredRepo, String displayName)
{}
+
+ /** What a batch install did: what went in, what didn't, and whether the
user stopped it. */
+ private record InstallOutcome(
+ List<String> installed, Map<String, Exception> failures, boolean
cancelled) {}
+
+ /** Coordinates, repository and display name for one selected row. */
+ private InstallRequest toInstallRequest(OptionalPluginInfo info) {
+ String groupId =
+ StringUtils.isNotBlank(info.getGroupId()) ? info.getGroupId() :
config.getGroupId();
+ String version =
+ StringUtils.isNotBlank(info.getVersion())
+ ? info.getVersion()
+ : MarketplaceCommand.resolveDefaultVersion(config);
+ String preferredRepo =
+ StringUtils.isNotBlank(info.getSource()) &&
!"apache".equalsIgnoreCase(info.getSource())
+ ? info.getSource()
+ : null;
+ return new InstallRequest(
+ new MavenCoordinates(groupId, info.getArtifactId(), version),
+ preferredRepo,
+ // Same name the table shows, not the Maven coordinate.
+ displayName(info));
+ }
+
private void installSelected() {
- OptionalPluginInfo info = selected();
- if (info == null) {
+ List<OptionalPluginInfo> selection = selected();
+ if (selection.isEmpty()) {
return;
}
+ if (repositoriesPanel != null) {
+ config = repositoriesPanel.getConfig();
+ }
+ List<InstallRequest> requests =
selection.stream().map(this::toInstallRequest).toList();
+ InstallOutcome outcome;
try {
- if (repositoriesPanel != null) {
- config = repositoriesPanel.getConfig();
- }
- String groupId =
- StringUtils.isNotBlank(info.getGroupId()) ? info.getGroupId() :
config.getGroupId();
- String version =
- StringUtils.isNotBlank(info.getVersion())
- ? info.getVersion()
- : MarketplaceCommand.resolveDefaultVersion(config);
- MavenCoordinates coords = new MavenCoordinates(groupId,
info.getArtifactId(), version);
- String preferredRepo =
- StringUtils.isNotBlank(info.getSource()) &&
!"apache".equalsIgnoreCase(info.getSource())
- ? info.getSource()
- : null;
- // Same name the table shows, not the Maven coordinate.
- String displayName = Const.NVL(info.getName(), info.getArtifactId());
- wStatus.setText(
- BaseMessages.getString(PKG, "MarketplaceDialog.Status.Installing",
coords.gav()));
+ wStatus.setText(installingStatus(requests));
shell.update();
- if (!runInstall(coords, preferredRepo, displayName)) {
- // Cancelled by the user — no success popup, and the table still
reflects reality.
- refreshTable(RefreshReason.AFTER_CHANGE);
- wStatus.setText(BaseMessages.getString(PKG,
"MarketplaceDialog.Status.Cancelled"));
- return;
- }
- MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_INFORMATION);
- box.setText(BaseMessages.getString(PKG,
"MarketplaceDialog.Install.Done.Header"));
- box.setMessage(
- BaseMessages.getString(PKG,
"MarketplaceDialog.Install.Done.Message", coords.gav()));
- box.open();
- refreshTable(RefreshReason.AFTER_CHANGE);
- wStatus.setText(BaseMessages.getString(PKG,
"MarketplaceDialog.Status.RestartHint"));
+ outcome = runInstall(requests);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
- showInstallError(info, e);
+ showInstallError(requests, e);
+ return;
} catch (Exception e) {
- showInstallError(info, e);
+ showInstallError(requests, e);
+ return;
+ }
+
+ // The table reflects what is on disk either way: a batch can be part
installed.
+ refreshTable(RefreshReason.AFTER_CHANGE);
+ if (!outcome.failures().isEmpty()) {
+ showInstallFailures(outcome);
+ wStatus.setText(BaseMessages.getString(PKG,
"MarketplaceDialog.Status.RestartHint"));
+ return;
+ }
+ if (outcome.cancelled()) {
+ // Cancelled by the user — no success popup.
+ wStatus.setText(BaseMessages.getString(PKG,
"MarketplaceDialog.Status.Cancelled"));
+ return;
}
+ boolean single = outcome.installed().size() == 1;
+ MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_INFORMATION);
+ box.setText(
+ BaseMessages.getString(
+ PKG,
+ single
+ ? "MarketplaceDialog.Install.Done.Header"
+ : "MarketplaceDialog.Install.Done.HeaderMultiple"));
+ box.setMessage(
+ single
+ ? BaseMessages.getString(
+ PKG, "MarketplaceDialog.Install.Done.Message",
outcome.installed().get(0))
+ : BaseMessages.getString(
+ PKG,
+ "MarketplaceDialog.Install.Done.MessageMultiple",
+ Integer.toString(outcome.installed().size()),
+ nameList(outcome.installed())));
+ box.open();
+ wStatus.setText(BaseMessages.getString(PKG,
"MarketplaceDialog.Status.RestartHint"));
+ }
+
+ private String installingStatus(List<InstallRequest> requests) {
+ return requests.size() == 1
+ ? BaseMessages.getString(
+ PKG, "MarketplaceDialog.Status.Installing",
requests.get(0).coordinates().gav())
+ : BaseMessages.getString(
+ PKG, "MarketplaceDialog.Status.InstallingCount",
Integer.toString(requests.size()));
}
- private void showInstallError(OptionalPluginInfo info, Exception e) {
+ /** The batch never started, or the dialog itself broke down. */
+ private void showInstallError(List<InstallRequest> requests, Exception e) {
// ProgressMonitorDialog wraps the real failure; unwrap so the dialog
shows the cause.
Throwable cause =
e instanceof InvocationTargetException ite && ite.getCause() != null ?
ite.getCause() : e;
new ErrorDialog(
shell,
BaseMessages.getString(PKG, "MarketplaceDialog.Error.Header"),
- BaseMessages.getString(PKG, "MarketplaceDialog.Install.Error",
info.getArtifactId()),
+ BaseMessages.getString(
+ PKG,
+ "MarketplaceDialog.Install.Error",
+
requests.stream().map(InstallRequest::displayName).collect(Collectors.joining(",
"))),
cause);
}
+ /**
+ * Report the plugins the batch could not install. One dialog for the lot: a
failing repository
+ * usually takes every plugin down with it, and a popup per plugin would
have to be clicked away
+ * as many times.
+ */
+ private void showInstallFailures(InstallOutcome outcome) {
+ StringBuilder detail = new StringBuilder();
+ for (Map.Entry<String, Exception> failure : outcome.failures().entrySet())
{
+ log.logError("Failed to install " + failure.getKey(),
failure.getValue());
+ detail
+ .append("- ")
+ .append(failure.getKey())
+ .append(": ")
+ .append(Const.NVL(failure.getValue().getMessage(),
failure.getValue().toString()))
+ .append(Const.CR);
+ }
+ String header =
+ outcome.failures().size() == 1
+ ? BaseMessages.getString(
+ PKG,
+ "MarketplaceDialog.Install.Error",
+ outcome.failures().keySet().iterator().next())
+ : BaseMessages.getString(
+ PKG,
+ "MarketplaceDialog.Install.Error.Multiple",
+ Integer.toString(outcome.failures().size()),
+ Integer.toString(outcome.failures().size() +
outcome.installed().size()));
+ // The first failure carries the stack trace; the rest are summarised in
the message.
+ Exception first = outcome.failures().values().iterator().next();
+ new ErrorDialog(
+ shell,
+ BaseMessages.getString(PKG, "MarketplaceDialog.Error.Header"),
+ header + Const.CR + Const.CR + detail,
+ first);
+ }
+
/**
* Download and install on a worker thread behind a cancellable progress
dialog, so the UI keeps
- * repainting and the user can see the zip arrive.
+ * repainting and the user can see the zips arrive.
*
* <p>{@link ProgressMonitorDialog#run(boolean, IRunnableWithProgress)}
pumps the event loop until
* the work finishes and only then returns, so everything after the call is
back on the UI thread
* and needs no marshalling.
*
- * @return true when the plugin was installed, false when the user cancelled
- * @throws InvocationTargetException wrapping whatever the install failed
with
+ * <p>A failing plugin does not abort the batch — one unavailable artifact
shouldn't cost the user
+ * the other nine installs — so failures are collected and reported together
at the end. Nothing
+ * is thrown out of the operation, which also sidesteps a trap: {@code
monitor.done()} disposes
+ * the progress shell and ends the pump loop, so anything thrown after it is
never observed and a
+ * failed install would be reported as a success.
*/
- private boolean runInstall(MavenCoordinates coords, String preferredRepo,
String displayName)
+ private InstallOutcome runInstall(List<InstallRequest> requests)
throws InvocationTargetException, InterruptedException {
PluginInstaller installer = new PluginInstaller(log, hopHome, config);
+ List<String> installed = new ArrayList<>();
+ Map<String, Exception> failures = new LinkedHashMap<>();
AtomicBoolean cancelled = new AtomicBoolean(false);
ProgressMonitorDialog dialog = new ProgressMonitorDialog(shell);
@@ -710,63 +841,134 @@ public class MarketplaceDialog extends Dialog {
monitor -> {
ProgressMonitorInstallListener listener = new
ProgressMonitorInstallListener(monitor);
listener.begin(
- BaseMessages.getString(PKG,
"MarketplaceDialog.Progress.Installing", displayName),
- displayName);
- try {
- installer.install(coords, true, null, preferredRepo, listener);
- } catch (Exception e) {
- if (!monitor.isCanceled()) {
- // Throw WITHOUT calling monitor.done(). done() disposes the
progress shell, which
- // ends ProgressMonitorDialog's pump loop before it has a chance
to observe this
- // exception — a failed install would then be reported as a
success. Leaving the shell
- // up lets the pump see the exception and rethrow it. Catching
Exception (not just
- // HopException) is what keeps a RuntimeException from killing
the worker thread
- // silently and hanging the dialog forever.
- throw new InvocationTargetException(e, e.getMessage());
+ installTaskName(requests),
+ requests.size() == 1 ? requests.get(0).displayName() : null);
+ for (int i = 0; i < requests.size(); i++) {
+ if (monitor.isCanceled()) {
+ cancelled.set(true);
+ break;
+ }
+ InstallRequest request = requests.get(i);
+ // Drives the "3 of 12" line and scales the bar across the batch.
+ listener.item(request.displayName(), i, requests.size());
+ try {
+ installer.install(
+ request.coordinates(), true, null, request.preferredRepo(),
listener);
+ installed.add(request.coordinates().gav());
+ } catch (Exception e) {
+ // Catching Exception (not just HopException) is what keeps a
RuntimeException from
+ // killing the worker thread silently and hanging the dialog
forever.
+ if (monitor.isCanceled()) {
+ cancelled.set(true);
+ break;
+ }
+ failures.put(request.displayName(), e);
}
- cancelled.set(true);
}
listener.complete();
monitor.done();
};
dialog.run(true, operation);
- return !cancelled.get();
+ return new InstallOutcome(installed, failures, cancelled.get());
+ }
+
+ private String installTaskName(List<InstallRequest> requests) {
+ return requests.size() == 1
+ ? BaseMessages.getString(
+ PKG, "MarketplaceDialog.Progress.Installing",
requests.get(0).displayName())
+ : BaseMessages.getString(
+ PKG, "MarketplaceDialog.Progress.InstallingCount",
Integer.toString(requests.size()));
}
private void uninstallSelected() {
- OptionalPluginInfo info = selected();
- if (info == null) {
+ List<OptionalPluginInfo> selection = selected();
+ if (selection.isEmpty()) {
return;
}
+ List<OptionalPluginInfo> removable = new ArrayList<>();
+ List<String> withoutReceipt = new ArrayList<>();
try {
- InstallReceipt receipt = PluginInstaller.readReceipt(hopHome,
info.getArtifactId());
- if (receipt == null) {
- MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_WARNING);
- box.setText(BaseMessages.getString(PKG,
"MarketplaceDialog.Uninstall.NoReceipt.Header"));
- box.setMessage(
- BaseMessages.getString(
- PKG, "MarketplaceDialog.Uninstall.NoReceipt.Message",
info.getArtifactId()));
- box.open();
- return;
+ for (OptionalPluginInfo info : selection) {
+ if (PluginInstaller.readReceipt(hopHome, info.getArtifactId()) ==
null) {
+ withoutReceipt.add(info.getArtifactId());
+ } else {
+ removable.add(info);
+ }
}
- MessageBox confirm = new MessageBox(shell, SWT.YES | SWT.NO |
SWT.ICON_QUESTION);
- confirm.setText(BaseMessages.getString(PKG,
"MarketplaceDialog.Uninstall.Confirm.Header"));
- confirm.setMessage(
- BaseMessages.getString(
- PKG, "MarketplaceDialog.Uninstall.Confirm.Message",
info.getArtifactId()));
- if (confirm.open() != SWT.YES) {
+ } catch (Exception e) {
+
showUninstallError(selection.stream().map(OptionalPluginInfo::getArtifactId).toList(),
e);
+ return;
+ }
+
+ // Plugins that ship with the client have no receipt; say so instead of
silently skipping them.
+ if (!withoutReceipt.isEmpty()) {
+ MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_WARNING);
+ box.setText(BaseMessages.getString(PKG,
"MarketplaceDialog.Uninstall.NoReceipt.Header"));
+ box.setMessage(
+ withoutReceipt.size() == 1
+ ? BaseMessages.getString(
+ PKG, "MarketplaceDialog.Uninstall.NoReceipt.Message",
withoutReceipt.get(0))
+ : BaseMessages.getString(
+ PKG,
+ "MarketplaceDialog.Uninstall.NoReceipt.MessageMultiple",
+ nameList(withoutReceipt)));
+ box.open();
+ if (removable.isEmpty()) {
return;
}
- new PluginUninstaller(log, hopHome).uninstall(info.getArtifactId());
- refreshTable(RefreshReason.AFTER_CHANGE);
- wStatus.setText(BaseMessages.getString(PKG,
"MarketplaceDialog.Status.RestartHint"));
- } catch (Exception e) {
- new ErrorDialog(
- shell,
- BaseMessages.getString(PKG, "MarketplaceDialog.Error.Header"),
- BaseMessages.getString(PKG, "MarketplaceDialog.Uninstall.Error",
info.getArtifactId()),
- e);
}
+
+ List<String> artifactIds =
removable.stream().map(OptionalPluginInfo::getArtifactId).toList();
+ MessageBox confirm = new MessageBox(shell, SWT.YES | SWT.NO |
SWT.ICON_QUESTION);
+ confirm.setText(
+ BaseMessages.getString(
+ PKG,
+ artifactIds.size() == 1
+ ? "MarketplaceDialog.Uninstall.Confirm.Header"
+ : "MarketplaceDialog.Uninstall.Confirm.HeaderMultiple"));
+ confirm.setMessage(
+ artifactIds.size() == 1
+ ? BaseMessages.getString(
+ PKG, "MarketplaceDialog.Uninstall.Confirm.Message",
artifactIds.get(0))
+ : BaseMessages.getString(
+ PKG,
+ "MarketplaceDialog.Uninstall.Confirm.MessageMultiple",
+ Integer.toString(artifactIds.size()),
+ nameList(artifactIds)));
+ if (confirm.open() != SWT.YES) {
+ return;
+ }
+
+ PluginUninstaller uninstaller = new PluginUninstaller(log, hopHome);
+ List<String> failed = new ArrayList<>();
+ Exception firstFailure = null;
+ for (String artifactId : artifactIds) {
+ try {
+ uninstaller.uninstall(artifactId);
+ } catch (Exception e) {
+ // Same reasoning as the install batch: report everything that went
wrong, once.
+ log.logError("Failed to uninstall " + artifactId, e);
+ failed.add(artifactId);
+ if (firstFailure == null) {
+ firstFailure = e;
+ }
+ }
+ }
+ refreshTable(RefreshReason.AFTER_CHANGE);
+ if (firstFailure != null) {
+ showUninstallError(failed, firstFailure);
+ return;
+ }
+ wStatus.setText(BaseMessages.getString(PKG,
"MarketplaceDialog.Status.RestartHint"));
+ }
+
+ private void showUninstallError(List<String> artifactIds, Exception e) {
+ new ErrorDialog(
+ shell,
+ BaseMessages.getString(PKG, "MarketplaceDialog.Error.Header"),
+ BaseMessages.getString(
+ PKG, "MarketplaceDialog.Uninstall.Error", String.join(", ",
artifactIds)),
+ e);
}
}
diff --git
a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/xp/SearchMarketplaceExtensionPoint.java
b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/xp/SearchMarketplaceExtensionPoint.java
new file mode 100644
index 0000000000..d1c77e841e
--- /dev/null
+++
b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/xp/SearchMarketplaceExtensionPoint.java
@@ -0,0 +1,58 @@
+/*
+ * 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.marketplace.xp;
+
+import org.apache.hop.core.extension.ExtensionPoint;
+import org.apache.hop.core.extension.IExtensionPoint;
+import org.apache.hop.core.logging.ILogChannel;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.marketplace.gui.MarketplaceDialog;
+import org.apache.hop.marketplace.gui.MarketplaceGuiPlugin;
+import org.apache.hop.ui.core.dialog.ErrorDialog;
+import org.apache.hop.ui.hopgui.HopGui;
+
+/**
+ * Opens the marketplace with the search box pre-filled, so the GUI can offer
a way out of a missing
+ * plugin without depending on the marketplace plugin being installed.
+ *
+ * <p>The payload is the plugin id to search for; blank or null lists
everything.
+ */
+@ExtensionPoint(
+ id = "SearchMarketplaceExtensionPoint",
+ description = "Open the marketplace, searching for a plugin id",
+ extensionPointId = "HopGuiSearchMarketplace")
+public class SearchMarketplaceExtensionPoint implements
IExtensionPoint<Object> {
+
+ private static final Class<?> PKG = MarketplaceGuiPlugin.class;
+
+ @Override
+ public void callExtensionPoint(ILogChannel log, IVariables variables, Object
object) {
+ HopGui hopGui = HopGui.getInstance();
+ String search = object instanceof String s ? s : null;
+ try {
+ new MarketplaceDialog(hopGui.getShell(), search).open();
+ } catch (Exception e) {
+ new ErrorDialog(
+ hopGui.getShell(),
+ BaseMessages.getString(PKG, "MarketplaceDialog.Error.Header"),
+ BaseMessages.getString(PKG, "MarketplaceDialog.Error.Open"),
+ e);
+ }
+ }
+}
diff --git
a/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_en_US.properties
b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_en_US.properties
index 3bd79da568..960f21ea8d 100644
---
a/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_en_US.properties
+++
b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_en_US.properties
@@ -79,11 +79,13 @@ MarketplaceDialog.Status.Present=Present on disk
MarketplaceDialog.Status.NotInstalled=Not installed
MarketplaceDialog.Status.RestartHint=Restart Hop after install, apply, or
uninstall so plugins are reloaded. Install tries the primary repository, then
other enabled repositories.
MarketplaceDialog.Status.Installing=Installing {0}...
+MarketplaceDialog.Status.InstallingCount=Installing {0} plugins...
MarketplaceDialog.Status.Validating=Validating environment file...
MarketplaceDialog.Status.Applying=Applying environment file...
MarketplaceDialog.Status.Drift=Environment drift detected — use Apply to fix
MarketplaceDialog.Status.Cancelled=Cancelled.
MarketplaceDialog.Progress.Installing=Installing {0}
+MarketplaceDialog.Progress.InstallingCount=Installing {0} plugins
MarketplaceDialog.Progress.Applying=Applying environment file
MarketplaceDialog.Progress.Searching=Searching repositories...
MarketplaceDialog.Progress.Item={0} ({1} of {2})
@@ -105,14 +107,21 @@ MarketplaceDialog.Progress.Eta.Hour={0} hr left
MarketplaceDialog.Progress.Eta.Hours={0} hrs left
MarketplaceDialog.Refresh.Error=Failed to list plugins from the configured
repositories
MarketplaceDialog.Error.Header=Marketplace error
+MarketplaceDialog.Error.Open=Unable to open the marketplace dialog
MarketplaceDialog.Error.ResolveHome=Could not determine the Hop installation
directory. Start Hop from the install root using the hop launcher.
MarketplaceDialog.Install.Done.Header=Plugin installed
+MarketplaceDialog.Install.Done.HeaderMultiple=Plugins installed
MarketplaceDialog.Install.Done.Message=Installed {0}.\n\nPlease restart Hop to
load the plugin.
+MarketplaceDialog.Install.Done.MessageMultiple=Installed {0}
plugins:\n\n{1}\n\nPlease restart Hop to load them.
MarketplaceDialog.Install.Error=Failed to install {0}
+MarketplaceDialog.Install.Error.Multiple=Failed to install {0} of {1} plugins
MarketplaceDialog.Uninstall.Confirm.Header=Uninstall plugin
+MarketplaceDialog.Uninstall.Confirm.HeaderMultiple=Uninstall plugins
MarketplaceDialog.Uninstall.Confirm.Message=Remove marketplace-installed
plugin {0}?
+MarketplaceDialog.Uninstall.Confirm.MessageMultiple=Remove {0}
marketplace-installed plugins?\n\n{1}
MarketplaceDialog.Uninstall.NoReceipt.Header=Cannot uninstall
MarketplaceDialog.Uninstall.NoReceipt.Message=No marketplace install receipt
for {0}. Only plugins installed via the marketplace can be uninstalled here.
+MarketplaceDialog.Uninstall.NoReceipt.MessageMultiple=No marketplace install
receipt for:\n\n{0}\n\nOnly plugins installed via the marketplace can be
uninstalled here.
MarketplaceDialog.Uninstall.Error=Failed to uninstall {0}
MarketplaceDialog.Validate.Ok.Header=Environment OK
MarketplaceDialog.Validate.Ok.Message=Local install matches:\n{0}
diff --git
a/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/command/InstallCommandParsingTest.java
b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/command/InstallCommandParsingTest.java
new file mode 100644
index 0000000000..ef106bafd6
--- /dev/null
+++
b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/command/InstallCommandParsingTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.marketplace.command;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import picocli.CommandLine;
+
+/**
+ * {@code hop marketplace install} takes a list of coordinates. Arity is easy
to break by accident
+ * (an {@code index = "0"} positional silently swallows only the first word),
so the contract is
+ * pinned here rather than found out on a batch install.
+ */
+class InstallCommandParsingTest {
+
+ private static List<String> parseCoordinates(String... args) {
+ CommandLine commandLine = new CommandLine(new
MarketplaceCommand.InstallCommand());
+ return commandLine.parseArgs(args).matchedPositional(0).getValue();
+ }
+
+ @Test
+ void severalCoordinatesAreAllCaptured() {
+ assertEquals(
+ List.of("datavault", "hop-tech-parquet",
"hop-datavault:0.4.0-SNAPSHOT"),
+ parseCoordinates("datavault", "hop-tech-parquet",
"hop-datavault:0.4.0-SNAPSHOT"));
+ }
+
+ @Test
+ void aSingleCoordinateStillWorks() {
+ assertEquals(List.of("datavault"), parseCoordinates("datavault"));
+ }
+
+ @Test
+ void optionsDoNotEndUpInTheCoordinateList() {
+ CommandLine commandLine = new CommandLine(new
MarketplaceCommand.InstallCommand());
+ CommandLine.ParseResult parsed =
+ commandLine.parseArgs("--repo", "local-nexus", "datavault",
"hop-tech-parquet");
+ assertEquals(List.of("datavault", "hop-tech-parquet"),
parsed.matchedPositional(0).getValue());
+ assertEquals("local-nexus", parsed.matchedOptionValue("--repo", null));
+ }
+
+ @Test
+ void atLeastOneCoordinateIsRequired() {
+ assertThrows(CommandLine.MissingParameterException.class,
this::parseNothing);
+ }
+
+ private void parseNothing() {
+ new CommandLine(new MarketplaceCommand.InstallCommand()).parseArgs();
+ }
+}
diff --git a/plugins/tech/azure/pom.xml b/plugins/tech/azure/pom.xml
index 15c1e1cd81..9bb2dd2717 100755
--- a/plugins/tech/azure/pom.xml
+++ b/plugins/tech/azure/pom.xml
@@ -323,6 +323,17 @@
<artifactId>netty-codec-http2</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-codec-http3</artifactId>
+ <scope>provided</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-codec-native-quic</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-common</artifactId>
diff --git
a/ui/src/main/java/org/apache/hop/ui/pipeline/transforms/missing/MissingPipelineDialog.java
b/ui/src/main/java/org/apache/hop/ui/pipeline/transforms/missing/MissingPipelineDialog.java
index 86a285619e..d8b54c7ffa 100644
---
a/ui/src/main/java/org/apache/hop/ui/pipeline/transforms/missing/MissingPipelineDialog.java
+++
b/ui/src/main/java/org/apache/hop/ui/pipeline/transforms/missing/MissingPipelineDialog.java
@@ -17,6 +17,10 @@
package org.apache.hop.ui.pipeline.transforms.missing;
import java.util.List;
+import org.apache.hop.core.extension.ExtensionPointHandler;
+import org.apache.hop.core.extension.ExtensionPointMap;
+import org.apache.hop.core.extension.HopExtensionPoint;
+import org.apache.hop.core.logging.LogChannel;
import org.apache.hop.core.variables.IVariables;
import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.pipeline.PipelineMeta;
@@ -25,6 +29,7 @@ import org.apache.hop.pipeline.transform.ITransformMeta;
import org.apache.hop.pipeline.transforms.missing.Missing;
import org.apache.hop.ui.core.PropsUi;
import org.apache.hop.ui.core.dialog.BaseDialog;
+import org.apache.hop.ui.core.dialog.ErrorDialog;
import org.apache.hop.ui.core.gui.GuiResource;
import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
import org.eclipse.swt.SWT;
@@ -92,6 +97,34 @@ public class MissingPipelineDialog extends
BaseTransformDialog {
return entries.toString();
}
+ /**
+ * The plugin id to hand to the marketplace search. A single missing plugin
gives an obvious
+ * search term; a file missing several different plugins gives none, so the
marketplace opens
+ * unfiltered and the user picks from the list in the message.
+ */
+ private String marketplaceSearchFilter() {
+ if (mode != MISSING_PIPELINE_TRANSFORMS) {
+ return ((Missing) baseTransformMeta).getMissingPluginId();
+ }
+ List<String> pluginIds =
+
missingPipeline.stream().map(Missing::getMissingPluginId).distinct().toList();
+ return pluginIds.size() == 1 ? pluginIds.get(0) : null;
+ }
+
+ /** Hand the plugin id to whoever implements the marketplace (the
marketplace plugin). */
+ private void openMarketplace(String filter) {
+ try {
+ ExtensionPointHandler.callExtensionPoint(
+ LogChannel.UI, variables,
HopExtensionPoint.HopGuiSearchMarketplace.id, filter);
+ } catch (Exception e) {
+ new ErrorDialog(
+ shellParent,
+ BaseMessages.getString(PKG, "MissingPipelineDialog.MissingPlugins"),
+ BaseMessages.getString(PKG,
"MissingPipelineDialog.SearchMarketplace.Error"),
+ e);
+ }
+ }
+
private String buildMessage() {
if (mode == MISSING_PIPELINE_TRANSFORMS) {
return BaseMessages.getString(
@@ -177,6 +210,7 @@ public class MissingPipelineDialog extends
BaseTransformDialog {
}
});
+ Button previousButton = closeButton;
if (showOpenFile) {
Button openButton = new Button(dialogShell, SWT.PUSH);
PropsUi.setLook(openButton);
@@ -192,6 +226,33 @@ public class MissingPipelineDialog extends
BaseTransformDialog {
confirm.run();
}
});
+ previousButton = openButton;
+ }
+
+ // Only when the marketplace plugin is installed to answer the call.
+ if (ExtensionPointMap.getInstance()
+ .isRegistered(HopExtensionPoint.HopGuiSearchMarketplace.id)) {
+ Button marketplaceButton = new Button(dialogShell, SWT.PUSH);
+ PropsUi.setLook(marketplaceButton);
+ FormData fdMarketplace = new FormData();
+ fdMarketplace.right = new FormAttachment(previousButton, -5);
+ fdMarketplace.bottom = new FormAttachment(closeButton, 0, SWT.BOTTOM);
+ marketplaceButton.setLayoutData(fdMarketplace);
+ marketplaceButton.setText(
+ BaseMessages.getString(PKG,
"MissingPipelineDialog.SearchMarketplace"));
+ marketplaceButton.setToolTipText(
+ BaseMessages.getString(PKG,
"MissingPipelineDialog.SearchMarketplace.Tooltip"));
+ marketplaceButton.addSelectionListener(
+ new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ String filter = marketplaceSearchFilter();
+ cancel.run();
+ // The marketplace is modal and runs its own event loop, so let
this dialog finish
+ // closing first instead of nesting one modal shell inside
another.
+ display.asyncExec(() -> openMarketplace(filter));
+ }
+ });
}
BaseDialog.defaultShellHandling(dialogShell, v -> confirm.run(), v ->
cancel.run());
diff --git
a/ui/src/main/java/org/apache/hop/ui/workflow/actions/missing/MissingActionDialog.java
b/ui/src/main/java/org/apache/hop/ui/workflow/actions/missing/MissingActionDialog.java
index e43290b271..af8028b5fd 100644
---
a/ui/src/main/java/org/apache/hop/ui/workflow/actions/missing/MissingActionDialog.java
+++
b/ui/src/main/java/org/apache/hop/ui/workflow/actions/missing/MissingActionDialog.java
@@ -18,10 +18,15 @@
package org.apache.hop.ui.workflow.actions.missing;
import java.util.List;
+import org.apache.hop.core.extension.ExtensionPointHandler;
+import org.apache.hop.core.extension.ExtensionPointMap;
+import org.apache.hop.core.extension.HopExtensionPoint;
+import org.apache.hop.core.logging.LogChannel;
import org.apache.hop.core.variables.IVariables;
import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.ui.core.PropsUi;
import org.apache.hop.ui.core.dialog.BaseDialog;
+import org.apache.hop.ui.core.dialog.ErrorDialog;
import org.apache.hop.ui.core.gui.GuiResource;
import org.apache.hop.ui.hopgui.HopGui;
import org.apache.hop.ui.workflow.action.ActionDialog;
@@ -81,6 +86,34 @@ public class MissingActionDialog extends ActionDialog {
return entries.toString();
}
+ /**
+ * The plugin id to hand to the marketplace search. A single missing plugin
gives an obvious
+ * search term; a file missing several different plugins gives none, so the
marketplace opens
+ * unfiltered and the user picks from the list in the message.
+ */
+ private String marketplaceSearchFilter() {
+ if (mode != MISSING_ACTIONS) {
+ return ((MissingAction) action).getMissingPluginId();
+ }
+ List<String> pluginIds =
+
missingActions.stream().map(MissingAction::getMissingPluginId).distinct().toList();
+ return pluginIds.size() == 1 ? pluginIds.get(0) : null;
+ }
+
+ /** Hand the plugin id to whoever implements the marketplace (the
marketplace plugin). */
+ private void openMarketplace(String filter) {
+ try {
+ ExtensionPointHandler.callExtensionPoint(
+ LogChannel.UI, variables,
HopExtensionPoint.HopGuiSearchMarketplace.id, filter);
+ } catch (Exception e) {
+ new ErrorDialog(
+ getParent(),
+ BaseMessages.getString(PKG, "MissingActionDialog.MissingPlugins"),
+ BaseMessages.getString(PKG,
"MissingActionDialog.SearchMarketplace.Error"),
+ e);
+ }
+ }
+
private String buildMessage() {
if (mode == MISSING_ACTIONS) {
return BaseMessages.getString(
@@ -163,6 +196,7 @@ public class MissingActionDialog extends ActionDialog {
}
});
+ Button previousButton = closeButton;
if (showOpenFile) {
Button openButton = new Button(dialogShell, SWT.PUSH);
PropsUi.setLook(openButton);
@@ -178,6 +212,33 @@ public class MissingActionDialog extends ActionDialog {
confirm.run();
}
});
+ previousButton = openButton;
+ }
+
+ // Only when the marketplace plugin is installed to answer the call.
+ if (ExtensionPointMap.getInstance()
+ .isRegistered(HopExtensionPoint.HopGuiSearchMarketplace.id)) {
+ Button marketplaceButton = new Button(dialogShell, SWT.PUSH);
+ PropsUi.setLook(marketplaceButton);
+ FormData fdMarketplace = new FormData();
+ fdMarketplace.right = new FormAttachment(previousButton, -5);
+ fdMarketplace.bottom = new FormAttachment(closeButton, 0, SWT.BOTTOM);
+ marketplaceButton.setLayoutData(fdMarketplace);
+ marketplaceButton.setText(
+ BaseMessages.getString(PKG,
"MissingActionDialog.SearchMarketplace"));
+ marketplaceButton.setToolTipText(
+ BaseMessages.getString(PKG,
"MissingActionDialog.SearchMarketplace.Tooltip"));
+ marketplaceButton.addSelectionListener(
+ new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ String filter = marketplaceSearchFilter();
+ cancel.run();
+ // The marketplace is modal and runs its own event loop, so let
this dialog finish
+ // closing first instead of nesting one modal shell inside
another.
+ display.asyncExec(() -> openMarketplace(filter));
+ }
+ });
}
BaseDialog.defaultShellHandling(dialogShell, v -> confirm.run(), v ->
cancel.run());
diff --git
a/ui/src/main/resources/org/apache/hop/ui/pipeline/transforms/missing/messages/messages_en_US.properties
b/ui/src/main/resources/org/apache/hop/ui/pipeline/transforms/missing/messages/messages_en_US.properties
index 55afd8cfd6..7ea2c73655 100644
---
a/ui/src/main/resources/org/apache/hop/ui/pipeline/transforms/missing/messages/messages_en_US.properties
+++
b/ui/src/main/resources/org/apache/hop/ui/pipeline/transforms/missing/messages/messages_en_US.properties
@@ -21,3 +21,6 @@ MissingPipelineDialog.MissingPipelineTransformId=The {0}
plugin is missing.\nRem
MissingPipelineDialog.MissingPipelineTransforms=This file has missing plugins
and will not succesfully run.\nYou can open the file and replace the missing
transform(s):\n\n{0}Or install the missing plugin(s).
MissingPipelineDialog.MissingPlugins=Missing Plugins
MissingPipelineDialog.OpenFile=Open the File
+MissingPipelineDialog.SearchMarketplace=Search the Marketplace
+MissingPipelineDialog.SearchMarketplace.Error=Unable to open the marketplace
+MissingPipelineDialog.SearchMarketplace.Tooltip=Look for the missing plugin in
the marketplace
diff --git
a/ui/src/main/resources/org/apache/hop/ui/workflow/actions/missing/messages/messages_en_US.properties
b/ui/src/main/resources/org/apache/hop/ui/workflow/actions/missing/messages/messages_en_US.properties
index 2e5a3afe0e..02112c846d 100644
---
a/ui/src/main/resources/org/apache/hop/ui/workflow/actions/missing/messages/messages_en_US.properties
+++
b/ui/src/main/resources/org/apache/hop/ui/workflow/actions/missing/messages/messages_en_US.properties
@@ -21,3 +21,6 @@ MissingActionDialog.MissingActionId=The {0} plugin is
missing.\nRemove this acti
MissingActionDialog.MissingActions=This file has missing plugins and will not
succesfully run.\nYou can open the file and replace the missing
actions:\n\n{0}Or install the missing plugin(s).
MissingActionDialog.MissingPlugins=Missing Plugins
MissingActionDialog.OpenFile=Open the File
+MissingActionDialog.SearchMarketplace=Search the Marketplace
+MissingActionDialog.SearchMarketplace.Error=Unable to open the marketplace
+MissingActionDialog.SearchMarketplace.Tooltip=Look for the missing plugin in
the marketplace