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 7e6e998a55 merge hop engine types for supported transforms, fixes
#7643 (#7648)
7e6e998a55 is described below
commit 7e6e998a55ef30325086d8212eca242e7070cc71
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Mon Jul 27 18:39:04 2026 +0200
merge hop engine types for supported transforms, fixes #7643 (#7648)
---
.../modules/ROOT/pages/engine-compatibility.adoc | 2 +-
.../apache/hop/ui/hopgui/PaletteEngineFilter.java | 111 ++++++++++++---------
.../hop/ui/hopgui/PaletteEngineFilterTest.java | 103 +++++++++++++++++++
3 files changed, 166 insertions(+), 50 deletions(-)
diff --git a/docs/hop-dev-manual/modules/ROOT/pages/engine-compatibility.adoc
b/docs/hop-dev-manual/modules/ROOT/pages/engine-compatibility.adoc
index 97837548da..afc1648797 100644
--- a/docs/hop-dev-manual/modules/ROOT/pages/engine-compatibility.adoc
+++ b/docs/hop-dev-manual/modules/ROOT/pages/engine-compatibility.adoc
@@ -122,7 +122,7 @@ The deep gate in `Pipeline.prepareExecution()` reads this
variable on each invoc
The pipeline and workflow canvas toolbars carry a **Design for:** combo (next
to the zoom controls). The selection is persisted in `hop-config.json`
(`HopGuiPaletteDesignPipelineEngine`, `HopGuiPaletteDesignWorkflowEngine`) and
filters the right-click palette to only show transforms / actions that resolve
to `SUPPORTED` or `UNKNOWN` on the selected engine. `UNSUPPORTED` entries are
hidden so they cannot be accidentally added; `UNKNOWN` stays visible because it
is the safe default for plu [...]
-The combo offers, in order: **All engines** (disables the filter), then
**Hop** — a synthetic group that unifies the local and remote engines (they
share the same plugin compatibility surface from a designer's perspective, so
the combo doesn't split them) — then every other registered engine,
alphabetically. The Beam engines therefore appear together after Hop, and
third-party engines slot in wherever their display name puts them. Internally
the Hop group expands to `{Local, Remote}` and [...]
+The combo offers, in order: **All engines** (disables the filter), then
**Hop** — a synthetic group that unifies the local, remote and local
single-threaded engines (they share the same plugin compatibility surface from
a designer's perspective, so the combo doesn't split them) — then every other
registered engine, alphabetically. The Beam engines therefore appear together
after Hop, and third-party engines slot in wherever their display name puts
them. Internally the Hop group expands t [...]
== Reference
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/PaletteEngineFilter.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/PaletteEngineFilter.java
index 8dfb36948a..6362e78f6f 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/PaletteEngineFilter.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/PaletteEngineFilter.java
@@ -18,9 +18,8 @@
package org.apache.hop.ui.hopgui;
import java.util.ArrayList;
-import java.util.LinkedHashSet;
+import java.util.Collection;
import java.util.List;
-import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.hop.core.config.HopConfig;
import org.apache.hop.core.logging.LogChannel;
@@ -47,11 +46,11 @@ import
org.apache.hop.workflow.engine.WorkflowEnginePluginType;
* <p>An empty stored value means "no filter" — the palette behaves exactly as
it did before this
* feature was introduced, preserving the new-user experience.
*
- * <p>The local "Local" and remote "Remote" engines are grouped under a single
{@link
- * #HOP_ENGINE_GROUP_ID Hop} entry: from a user's perspective they are the
same engine surface (same
- * plugin compatibility, same transform palette) and splitting them in the
combo adds noise. A
- * plugin is shown when <em>any</em> engine in the group accepts it, hidden
only when <em>every</em>
- * member of the group says UNSUPPORTED.
+ * <p>The Hop engines — local "Local", remote "Remote" and the local
single-threaded "LocalSingle" —
+ * are grouped under a single {@link #HOP_ENGINE_GROUP_ID Hop} entry: from a
user's perspective they
+ * are the same engine surface (same plugin compatibility, same transform
palette) and splitting
+ * them in the combo adds noise. A plugin is shown when <em>any</em> engine in
the group accepts it,
+ * hidden only when <em>every</em> member of the group says UNSUPPORTED.
*/
public final class PaletteEngineFilter {
@@ -67,35 +66,61 @@ public final class PaletteEngineFilter {
public static final String NO_FILTER_LABEL = "All engines";
/**
- * Synthetic engine id used by the combo to represent the unified Hop-engine
group (Local +
- * Remote, both pipeline and workflow). Stored verbatim in {@code
hop-config.json}; when the
+ * Synthetic engine id used by the combo to represent the unified Hop-engine
group (Local, Remote
+ * and — on the pipeline side — LocalSingle). Stored verbatim in {@code
hop-config.json}; when the
* filter loads this value it expands into probes for every member of the
group.
*/
public static final String HOP_ENGINE_GROUP_ID = "Hop";
- /** Engine plugin ids collapsed into the {@link #HOP_ENGINE_GROUP_ID Hop}
group. */
- private static final Set<String> HOP_PIPELINE_ENGINE_IDS = Set.of("Local",
"Remote");
+ /**
+ * Pipeline engine plugin ids collapsed into the {@link #HOP_ENGINE_GROUP_ID
Hop} group. The local
+ * single-threaded engine ("LocalSingle") supports the same transforms as
the local and remote
+ * engines, so it shares their combo entry instead of getting one of its own.
+ */
+ private static final List<String> HOP_PIPELINE_ENGINE_IDS =
+ List.of("Local", "Remote", "LocalSingle");
- private static final Set<String> HOP_WORKFLOW_ENGINE_IDS = Set.of("Local",
"Remote");
+ /** Workflow counterpart — there is no single-threaded workflow engine. */
+ private static final List<String> HOP_WORKFLOW_ENGINE_IDS = List.of("Local",
"Remote");
private final String engineId;
+
+ /**
+ * The concrete engine ids the filter consults, parallel to {@link #probes}
— a single id, or
+ * every member of the Hop group when {@link #HOP_ENGINE_GROUP_ID} is
selected.
+ */
+ private final List<String> engineIds;
+
private final List<EngineProbe> probes;
- private PaletteEngineFilter(String engineId, List<EngineProbe> probes) {
+ private PaletteEngineFilter(String engineId, List<String> engineIds,
List<EngineProbe> probes) {
this.engineId = engineId;
+ this.engineIds = engineIds;
this.probes = probes;
}
/** Reads the saved pipeline-design engine id and instantiates a probe for
it. */
public static PaletteEngineFilter forPipelineDesign() {
- String engineId = StringUtils.trimToEmpty(getPipelineDesignEngineId());
- return new PaletteEngineFilter(engineId, pipelineProbesFor(engineId));
+ return forPipelineEngineId(getPipelineDesignEngineId());
}
/** Reads the saved workflow-design engine id and instantiates a probe for
it. */
public static PaletteEngineFilter forWorkflowDesign() {
- String engineId = StringUtils.trimToEmpty(getWorkflowDesignEngineId());
- return new PaletteEngineFilter(engineId, workflowProbesFor(engineId));
+ return forWorkflowEngineId(getWorkflowDesignEngineId());
+ }
+
+ /** Filter for an explicit pipeline engine id, bypassing the persisted
selection. */
+ static PaletteEngineFilter forPipelineEngineId(String engineId) {
+ String id = StringUtils.trimToEmpty(engineId);
+ List<String> ids = expandGroup(id, HOP_PIPELINE_ENGINE_IDS);
+ return new PaletteEngineFilter(id, ids, probesFor(ids, true));
+ }
+
+ /** Filter for an explicit workflow engine id, bypassing the persisted
selection. */
+ static PaletteEngineFilter forWorkflowEngineId(String engineId) {
+ String id = StringUtils.trimToEmpty(engineId);
+ List<String> ids = expandGroup(id, HOP_WORKFLOW_ENGINE_IDS);
+ return new PaletteEngineFilter(id, ids, probesFor(ids, false));
}
/** True iff a non-empty engine id is stored — i.e. the palette should
actually filter. */
@@ -117,12 +142,11 @@ public final class PaletteEngineFilter {
if (!isActive() || plugin == null) {
return true;
}
- List<String> ids = expandGroup(engineId, HOP_PIPELINE_ENGINE_IDS,
HOP_WORKFLOW_ENGINE_IDS);
- for (int i = 0; i < ids.size(); i++) {
+ for (int i = 0; i < engineIds.size(); i++) {
EngineProbe probe = i < probes.size() ? probes.get(i) : null;
EngineCompatibility verdict =
EngineCompatibilityResolver.resolve(
- plugin, ids.get(i), probe == null ? null : probe::supports);
+ plugin, engineIds.get(i), probe == null ? null :
probe::supports);
if (!verdict.isUnsupported()) {
return true;
}
@@ -209,7 +233,7 @@ public final class PaletteEngineFilter {
}
private static List<String> labelsFor(
- Class<? extends IPluginType> pluginType, Set<String> hopGroupIds) {
+ Class<? extends IPluginType> pluginType, Collection<String> hopGroupIds)
{
List<String> labels = new ArrayList<>();
labels.add(NO_FILTER_LABEL);
try {
@@ -237,7 +261,7 @@ public final class PaletteEngineFilter {
}
private static String labelForId(
- Class<? extends IPluginType> pluginType, String engineId, Set<String>
hopGroupIds) {
+ Class<? extends IPluginType> pluginType, String engineId,
Collection<String> hopGroupIds) {
if (StringUtils.isEmpty(engineId)) {
return NO_FILTER_LABEL;
}
@@ -275,7 +299,7 @@ public final class PaletteEngineFilter {
return "";
}
- private static boolean matchesAnyId(IPlugin plugin, Set<String> ids) {
+ private static boolean matchesAnyId(IPlugin plugin, Collection<String> ids) {
if (plugin == null || plugin.getIds() == null) {
return false;
}
@@ -290,37 +314,26 @@ public final class PaletteEngineFilter {
/**
* Expands a (possibly group) engine id into the concrete plugin ids the
filter must consult.
* Single-engine values pass through unchanged; the synthetic {@link
#HOP_ENGINE_GROUP_ID} fans
- * out to the union of pipeline + workflow Hop ids so that one logical "Hop"
filter covers both
- * canvases that share this class.
+ * out to the Hop engines of the canvas being filtered. The pipeline and
workflow sides are kept
+ * separate on purpose: probing an id that has no engine on this side (e.g.
"LocalSingle" as a
+ * workflow engine) always yields UNKNOWN, and since the group is permissive
that one member would
+ * make every plugin visible and silently disable the filter.
+ *
+ * <p>A bare group member stored in {@code hop-config.json} (from an older
release where "Local",
+ * "Remote" or "LocalSingle" were separate combo entries) expands to the
whole group too, so the
+ * filter matches the "Hop" label the combo shows for it.
*/
- private static List<String> expandGroup(
- String engineId, Set<String> hopPipelineIds, Set<String> hopWorkflowIds)
{
- if (HOP_ENGINE_GROUP_ID.equals(engineId)) {
- Set<String> all = new LinkedHashSet<>();
- all.addAll(hopPipelineIds);
- all.addAll(hopWorkflowIds);
- return new ArrayList<>(all);
+ private static List<String> expandGroup(String engineId, List<String>
hopGroupIds) {
+ if (StringUtils.isEmpty(engineId)) {
+ return List.of();
+ }
+ if (HOP_ENGINE_GROUP_ID.equals(engineId) ||
hopGroupIds.contains(engineId)) {
+ return hopGroupIds;
}
return List.of(engineId);
}
- private static List<EngineProbe> pipelineProbesFor(String engineId) {
- return probesFor(engineId, HOP_PIPELINE_ENGINE_IDS,
HOP_WORKFLOW_ENGINE_IDS, true);
- }
-
- private static List<EngineProbe> workflowProbesFor(String engineId) {
- return probesFor(engineId, HOP_PIPELINE_ENGINE_IDS,
HOP_WORKFLOW_ENGINE_IDS, false);
- }
-
- private static List<EngineProbe> probesFor(
- String engineId,
- Set<String> hopPipelineIds,
- Set<String> hopWorkflowIds,
- boolean forPipeline) {
- if (StringUtils.isEmpty(engineId)) {
- return List.of();
- }
- List<String> ids = expandGroup(engineId, hopPipelineIds, hopWorkflowIds);
+ private static List<EngineProbe> probesFor(List<String> ids, boolean
forPipeline) {
List<EngineProbe> probes = new ArrayList<>(ids.size());
for (String id : ids) {
EngineProbe probe =
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/PaletteEngineFilterTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/PaletteEngineFilterTest.java
new file mode 100644
index 0000000000..7d0752f888
--- /dev/null
+++ b/ui/src/test/java/org/apache/hop/ui/hopgui/PaletteEngineFilterTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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.ui.hopgui;
+
+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.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.apache.hop.core.plugins.IPlugin;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Covers the unified "Hop" engine group behind the canvas "Design for:" combo
— in particular that
+ * the local single-threaded pipeline engine ("LocalSingle") is part of it,
and that it does not
+ * leak into the workflow side where no such engine exists.
+ */
+class PaletteEngineFilterTest {
+
+ private static IPlugin plugin(String[] supported, String[] excluded) {
+ IPlugin plugin = mock(IPlugin.class);
+ when(plugin.getSupportedEngines()).thenReturn(supported);
+ when(plugin.getExcludedEngines()).thenReturn(excluded);
+ return plugin;
+ }
+
+ @Test
+ void singleThreadedEngineIsLabelledAsTheHopGroup() {
+ assertEquals(
+ PaletteEngineFilter.HOP_ENGINE_GROUP_ID,
+ PaletteEngineFilter.getPipelineEngineLabelForId("LocalSingle"));
+ assertEquals(
+ PaletteEngineFilter.HOP_ENGINE_GROUP_ID,
+ PaletteEngineFilter.getPipelineEngineLabelForId("Local"));
+ assertEquals(
+ PaletteEngineFilter.HOP_ENGINE_GROUP_ID,
+ PaletteEngineFilter.getPipelineEngineLabelForId("Remote"));
+ assertEquals(
+ PaletteEngineFilter.NO_FILTER_LABEL,
PaletteEngineFilter.getPipelineEngineLabelForId(""));
+ }
+
+ @Test
+ void hopGroupAllowsTransformsDeclaredForAnyOfItsEngines() {
+ PaletteEngineFilter filter =
+
PaletteEngineFilter.forPipelineEngineId(PaletteEngineFilter.HOP_ENGINE_GROUP_ID);
+
+ assertTrue(filter.isActive());
+ assertTrue(filter.isPluginAllowed(plugin(new String[] {"Local"}, new
String[0])));
+ assertTrue(filter.isPluginAllowed(plugin(new String[] {"LocalSingle"}, new
String[0])));
+ assertTrue(filter.isPluginAllowed(plugin(new String[0], new String[0])));
+ }
+
+ @Test
+ void hopGroupHidesTransformsRefusedByEveryMemberEngine() {
+ PaletteEngineFilter filter =
+
PaletteEngineFilter.forPipelineEngineId(PaletteEngineFilter.HOP_ENGINE_GROUP_ID);
+
+ assertFalse(filter.isPluginAllowed(plugin(new String[] {"Beam*"}, new
String[0])));
+ assertFalse(
+ filter.isPluginAllowed(
+ plugin(new String[0], new String[] {"Local", "Remote",
"LocalSingle"})));
+ }
+
+ /** A stored single-member id from an older release still filters as the
whole group. */
+ @Test
+ void storedGroupMemberIdExpandsToTheGroup() {
+ PaletteEngineFilter filter =
PaletteEngineFilter.forPipelineEngineId("LocalSingle");
+
+ assertTrue(filter.isPluginAllowed(plugin(new String[] {"Local"}, new
String[0])));
+ assertFalse(filter.isPluginAllowed(plugin(new String[] {"Beam*"}, new
String[0])));
+ }
+
+ /**
+ * There is no single-threaded workflow engine, so "LocalSingle" must not be
probed on the
+ * workflow side — an id without an engine always resolves UNKNOWN, and
since the group is
+ * permissive that would make every action visible and silently disable the
filter.
+ */
+ @Test
+ void workflowGroupDoesNotIncludeTheSingleThreadedEngine() {
+ PaletteEngineFilter filter =
+
PaletteEngineFilter.forWorkflowEngineId(PaletteEngineFilter.HOP_ENGINE_GROUP_ID);
+
+ assertFalse(
+ filter.isPluginAllowed(plugin(new String[0], new String[] {"Local",
"Remote"})),
+ "action excluded on both Hop workflow engines must stay hidden");
+ }
+}