This is an automated email from the ASF dual-hosted git repository.
mattcasters 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 d3eab57849 Issue #8047 : Isolate Hop Web GUI state across RAP sessions
(#8108)
d3eab57849 is described below
commit d3eab57849258f9021b9df390ad0441b23ba8b50
Author: Matt Casters <[email protected]>
AuthorDate: Wed Aug 26 22:28:20 2026 +0200
Issue #8047 : Isolate Hop Web GUI state across RAP sessions (#8108)
* Issue #8047 : Isolate Hop Web GUI state across RAP sessions
Stop sharing SWT widgets, images, and session-owned GUI objects between
Hop Web UISessions so concurrent users no longer hit Invalid thread
access or disposed widget errors.
* Issue #8047 : Fix DrillDownGuiPlugin javadoc after sniffer map rename
* Issue #8047 : Do not construct HopGui from perspective getInstance()
Calling HopGui.getInstance() from Explorer/Metadata/Git helpers started a
full GUI during hop-ui-rcp tests, flooding SWT and the session display.
peekInstance() returns an existing GUI only. Guard MetaSelectionLine
getEditIcon when the managed class is unset (widget-disable harness).
---
.../ROOT/pages/hopweb/hopweb-antipatterns.adoc | 12 ++
.../org/apache/hop/git/GitCommitPerspective.java | 15 +-
.../main/java/org/apache/hop/git/GitGuiPlugin.java | 18 +-
.../java/org/apache/hop/git/GitPerspective.java | 15 +-
.../main/java/org/apache/hop/git/GitResource.java | 19 +-
.../java/org/apache/hop/git/TextDiffDialog.java | 2 +-
.../apache/hop/git/info/DiffStyledTextComp.java | 28 +--
.../hop/git/GitGuiPluginSessionIsolationTest.java | 55 ++++++
.../HopGuiImpl.java => core/PropsUiImpl.java} | 7 +-
.../java/org/apache/hop/ui/hopgui/HopGuiImpl.java | 13 ++
.../{HopGuiImpl.java => HopGuiKeyHandlerImpl.java} | 4 +-
.../org/apache/hop/ui/hopgui/HopWebEntryPoint.java | 2 +
.../HopGuiImpl.java => core/PropsUiImpl.java} | 10 +-
.../java/org/apache/hop/ui/hopgui/HopGuiImpl.java | 5 +
.../{HopGuiImpl.java => HopGuiKeyHandlerImpl.java} | 6 +-
.../hop/ui/hopgui/HopGuiPeekInstanceTest.java | 42 +++++
.../main/java/org/apache/hop/ui/core/PropsUi.java | 49 ++++-
.../org/apache/hop/ui/core/gui/HopNamespace.java | 26 ++-
.../apache/hop/ui/core/vfs/HopVfsFileDialog.java | 15 +-
.../hop/ui/core/widget/MetaSelectionLine.java | 10 +-
.../apache/hop/ui/core/widget/StyledTextVar.java | 3 +-
.../org/apache/hop/ui/core/widget/TreeMemory.java | 18 +-
.../main/java/org/apache/hop/ui/hopgui/HopGui.java | 57 ++++++
.../org/apache/hop/ui/hopgui/HopGuiKeyHandler.java | 64 +++++--
.../apache/hop/ui/hopgui/ISingletonProvider.java | 8 +
.../hopgui/file/pipeline/HopGuiPipelineGraph.java | 6 +-
.../ui/hopgui/file/shared/DrillDownGuiPlugin.java | 210 +++++++++++++++++----
.../hopgui/file/workflow/HopGuiWorkflowGraph.java | 3 +-
.../configuration/ConfigurationPerspective.java | 16 +-
.../tabs/ConfigGeneralOptionsTab.java | 4 +-
.../configuration/tabs/ConfigGuiOptionsTab.java | 24 +--
.../tabs/ConfigKeyboardShortcutsTab.java | 4 +-
.../configuration/tabs/ConfigPluginOptionsTab.java | 29 ++-
.../execution/ExecutionPerspective.java | 14 +-
.../perspective/explorer/ExplorerPerspective.java | 14 +-
.../perspective/metadata/MetadataPerspective.java | 15 +-
.../hop/ui/hopgui/shared/SashFormMemory.java | 43 ++++-
.../org/apache/hop/ui/hopgui/shared/SwtGc.java | 61 +++---
.../apache/hop/ui/hopgui/HopGuiKeyHandlerTest.java | 14 ++
.../ui/hopgui/StaticSwtSessionResourceTest.java | 110 +++++++++++
.../hopgui/file/shared/DrillDownGuiPluginTest.java | 72 +++++++
41 files changed, 977 insertions(+), 165 deletions(-)
diff --git
a/docs/hop-dev-manual/modules/ROOT/pages/hopweb/hopweb-antipatterns.adoc
b/docs/hop-dev-manual/modules/ROOT/pages/hopweb/hopweb-antipatterns.adoc
index c30b5be8be..a6cbf4b418 100644
--- a/docs/hop-dev-manual/modules/ROOT/pages/hopweb/hopweb-antipatterns.adoc
+++ b/docs/hop-dev-manual/modules/ROOT/pages/hopweb/hopweb-antipatterns.adoc
@@ -162,6 +162,18 @@ public class HadoopClusterViewTreeExtension implements
ExtensionPointInterface {
}
----
+== Hop Web session isolation
+
+Hop Web creates one RAP `UISession` (and one SWT `Display`) per browser
session. `HopGui`, `GuiResource` and `GuiContextUtil` are already
session-scoped via `ISingletonProvider` and RAP `SingletonUtil`. Anything else
that holds SWT widgets or session GUI identity must follow one of these
patterns:
+
+* **ui-module classes:** `ISingletonProvider` plus `*Impl` in `rcp` (process
singleton) and `rap` (`SingletonUtil.getSessionInstance`).
+* **plugin classes:**
`HopGui.getInstance().getSessionSingleton(TheClass.class, TheClass::new)`.
Plugin classloaders cannot load RAP `*Impl` classes through
`ImplementationLoader`.
+* **Perspectives:** `getInstance()` must prefer
`HopGui.findSessionPerspective(...)`. A static `instance` field is only a
fallback for tests and disabled perspectives.
+* **Caches of SWT `Image`s:** key by `Device`/`Display` and dispose on *that*
device. Never one process-wide dispose hook.
+* **Engine-thread maps (drill-down):** stamp `hopGui.getId()` on the engine in
the GUI thread; do not call `HopGui.getInstance()` from pipeline worker threads
(no RAP `UISession`).
+
+Do not store `Control`, `Shell`, `SashForm`, `Image`, `Color`, `Font` or `GC`
in `static` fields. When session A ends RAP disposes those objects; session B
then hits `Invalid thread access` or `Widget is disposed`.
+
== Avoid use of unimplemented SWT APIs
=== Example: drawing on an Image
diff --git
a/plugins/misc/git/src/main/java/org/apache/hop/git/GitCommitPerspective.java
b/plugins/misc/git/src/main/java/org/apache/hop/git/GitCommitPerspective.java
index 14d4cdb2b4..a2fbb6ecc3 100644
---
a/plugins/misc/git/src/main/java/org/apache/hop/git/GitCommitPerspective.java
+++
b/plugins/misc/git/src/main/java/org/apache/hop/git/GitCommitPerspective.java
@@ -23,7 +23,6 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import lombok.Getter;
import org.apache.commons.vfs2.FileObject;
import org.apache.hop.core.Props;
import org.apache.hop.core.exception.HopException;
@@ -116,7 +115,7 @@ public class GitCommitPerspective implements
IHopPerspective {
private static final String STAGED_LABEL =
"GitCommitPerspective.Status.Staged.Label";
private static final String UNSTAGED_LABEL =
"GitCommitPerspective.Status.Unstaged.Label";
private static final String UNTRACKED_LABEL =
"GitCommitPerspective.Status.Untracked.Label";
- @Getter private static GitCommitPerspective instance;
+ private static GitCommitPerspective instance;
private HopGui hopGui;
private SashForm wSashForm;
@@ -137,6 +136,18 @@ public class GitCommitPerspective implements
IHopPerspective {
instance = this;
}
+ public static GitCommitPerspective getInstance() {
+ try {
+ GitCommitPerspective fromGui =
HopGui.findSessionPerspective(GitCommitPerspective.class);
+ if (fromGui != null) {
+ return fromGui;
+ }
+ } catch (Throwable e) {
+ // No HopGuiImpl in unit tests
+ }
+ return instance;
+ }
+
@Override
public String getId() {
return "GitCommitPerspective";
diff --git
a/plugins/misc/git/src/main/java/org/apache/hop/git/GitGuiPlugin.java
b/plugins/misc/git/src/main/java/org/apache/hop/git/GitGuiPlugin.java
index ff9626580e..ee5a81f618 100644
--- a/plugins/misc/git/src/main/java/org/apache/hop/git/GitGuiPlugin.java
+++ b/plugins/misc/git/src/main/java/org/apache/hop/git/GitGuiPlugin.java
@@ -123,19 +123,25 @@ public class GitGuiPlugin
public static final String CONST_GIT = "git: ";
public static final String CONST_S_S_S = "%s (%s -> %s)";
- private static GitGuiPlugin instance;
-
- private static UIGit git;
+ private UIGit git;
@Getter private Map<String, UIFile> changedFiles;
@Getter private Map<String, String> ignoredFiles;
+ private static GitGuiPlugin fallback;
+
public static GitGuiPlugin getInstance() {
- if (instance == null) {
- instance = new GitGuiPlugin();
+ HopGui hopGui = HopGui.peekInstance();
+ if (hopGui != null) {
+ return hopGui.getSessionSingleton(GitGuiPlugin.class, GitGuiPlugin::new);
+ }
+ synchronized (GitGuiPlugin.class) {
+ if (fallback == null) {
+ fallback = new GitGuiPlugin();
+ }
+ return fallback;
}
- return instance;
}
public GitGuiPlugin() {
diff --git
a/plugins/misc/git/src/main/java/org/apache/hop/git/GitPerspective.java
b/plugins/misc/git/src/main/java/org/apache/hop/git/GitPerspective.java
index 3ae7005c9d..6a0e58af45 100644
--- a/plugins/misc/git/src/main/java/org/apache/hop/git/GitPerspective.java
+++ b/plugins/misc/git/src/main/java/org/apache/hop/git/GitPerspective.java
@@ -25,7 +25,6 @@ import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
-import lombok.Getter;
import org.apache.hop.core.Const;
import org.apache.hop.core.Props;
import org.apache.hop.core.config.HopConfig;
@@ -195,7 +194,7 @@ public class GitPerspective implements IHopPerspective {
public static final String OPTION_SHOW_ALL_REF = "Git.ShowAllRef";
- @Getter private static GitPerspective instance;
+ private static GitPerspective instance;
private HopGui hopGui;
private SashForm wSashForm;
@@ -229,6 +228,18 @@ public class GitPerspective implements IHopPerspective {
instance = this;
}
+ public static GitPerspective getInstance() {
+ try {
+ GitPerspective fromGui =
HopGui.findSessionPerspective(GitPerspective.class);
+ if (fromGui != null) {
+ return fromGui;
+ }
+ } catch (Throwable e) {
+ // No HopGuiImpl in unit tests
+ }
+ return instance;
+ }
+
@Override
public String getId() {
return "GitPerspective";
diff --git a/plugins/misc/git/src/main/java/org/apache/hop/git/GitResource.java
b/plugins/misc/git/src/main/java/org/apache/hop/git/GitResource.java
index c9befab5c9..d536611040 100644
--- a/plugins/misc/git/src/main/java/org/apache/hop/git/GitResource.java
+++ b/plugins/misc/git/src/main/java/org/apache/hop/git/GitResource.java
@@ -22,6 +22,7 @@ import lombok.Getter;
import org.apache.hop.ui.core.ConstUi;
import org.apache.hop.ui.core.PropsUi;
import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.hopgui.HopGui;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
@@ -64,10 +65,8 @@ public class GitResource {
@Getter private final Image resetImage;
@Getter private final Image tagImage;
- private static GitResource instance;
-
/** Utility class */
- private GitResource() {
+ GitResource() {
GuiResource resource = GuiResource.getInstance();
textInsertForegroundColor = resource.getColor(0, 255, 0);
@@ -118,11 +117,19 @@ public class GitResource {
tagImage = getImage("tag.svg");
}
+ private static GitResource fallback;
+
public static GitResource getInstance() {
- if (instance == null) {
- instance = new GitResource();
+ HopGui hopGui = HopGui.peekInstance();
+ if (hopGui != null) {
+ return hopGui.getSessionSingleton(GitResource.class, GitResource::new);
+ }
+ synchronized (GitResource.class) {
+ if (fallback == null) {
+ fallback = new GitResource();
+ }
+ return fallback;
}
- return instance;
}
public Image getImage(String location) {
diff --git
a/plugins/misc/git/src/main/java/org/apache/hop/git/TextDiffDialog.java
b/plugins/misc/git/src/main/java/org/apache/hop/git/TextDiffDialog.java
index 12c4a23bf8..30d5deef73 100644
--- a/plugins/misc/git/src/main/java/org/apache/hop/git/TextDiffDialog.java
+++ b/plugins/misc/git/src/main/java/org/apache/hop/git/TextDiffDialog.java
@@ -387,7 +387,7 @@ public class TextDiffDialog extends Dialog {
path.dispose();
}
-
gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WIDGET_BORDER));
+
gc.setForeground(Display.getCurrent().getSystemColor(SWT.COLOR_WIDGET_BORDER));
gc.drawLine(0, 0, 0, area.height);
gc.drawLine(area.width - 1, 0, area.width - 1, area.height);
}
diff --git
a/plugins/misc/git/src/main/java/org/apache/hop/git/info/DiffStyledTextComp.java
b/plugins/misc/git/src/main/java/org/apache/hop/git/info/DiffStyledTextComp.java
index 4e28b98134..5d89401c2a 100644
---
a/plugins/misc/git/src/main/java/org/apache/hop/git/info/DiffStyledTextComp.java
+++
b/plugins/misc/git/src/main/java/org/apache/hop/git/info/DiffStyledTextComp.java
@@ -35,17 +35,20 @@ import org.eclipse.swt.widgets.Display;
*/
public class DiffStyledTextComp extends StyledTextVar {
- private static final Color COLOR_ADDITION = new Color(Display.getDefault(),
0, 128, 0); // Green
- private static final Color COLOR_DELETION = new Color(Display.getDefault(),
255, 0, 0); // Red
- private static final Color COLOR_FILE_HEADER =
- new Color(Display.getDefault(), 0, 128, 128); // Cyan
- private static final Color COLOR_HUNK_HEADER =
- new Color(Display.getDefault(), 153, 102, 0); // Brown/Orange
+ private final Color colorAddition;
+ private final Color colorDeletion;
+ private final Color colorFileHeader;
+ private final Color colorHunkHeader;
public DiffStyledTextComp(IVariables variables, Composite parent, int style)
{
super(variables, parent, style, false, false, true, STYLE_TYPE_DIFF); //
No variable support
// Set read-only and disable editing
getTextWidget().setEditable(false);
+ Display display = parent.getDisplay();
+ colorAddition = new Color(display, 0, 128, 0);
+ colorDeletion = new Color(display, 255, 0, 0);
+ colorFileHeader = new Color(display, 0, 128, 128);
+ colorHunkHeader = new Color(display, 153, 102, 0);
}
/**
@@ -80,10 +83,10 @@ public class DiffStyledTextComp extends StyledTextVar {
if (line.startsWith("+")) {
// Addition line (green)
- styleRange = new StyleRange(offset, lineLength, COLOR_ADDITION, null);
+ styleRange = new StyleRange(offset, lineLength, colorAddition, null);
} else if (line.startsWith("-")) {
// Deletion line (red)
- styleRange = new StyleRange(offset, lineLength, COLOR_DELETION, null);
+ styleRange = new StyleRange(offset, lineLength, colorDeletion, null);
} else if (line.startsWith("diff --git")
|| line.startsWith("+++")
|| line.startsWith("---")
@@ -94,13 +97,13 @@ public class DiffStyledTextComp extends StyledTextVar {
|| line.startsWith("rename from")
|| line.startsWith("rename to")) {
// File header (cyan)
- styleRange = new StyleRange(offset, lineLength, COLOR_FILE_HEADER,
null);
+ styleRange = new StyleRange(offset, lineLength, colorFileHeader, null);
if (line.startsWith("+++") || line.startsWith("---")) {
styleRange.fontStyle = SWT.BOLD;
}
} else if (line.startsWith("@@")) {
// Hunk header (brown/orange)
- styleRange = new StyleRange(offset, lineLength, COLOR_HUNK_HEADER,
null);
+ styleRange = new StyleRange(offset, lineLength, colorHunkHeader, null);
styleRange.fontStyle = SWT.BOLD;
}
@@ -115,7 +118,10 @@ public class DiffStyledTextComp extends StyledTextVar {
@Override
public void dispose() {
- // Colors are shared and managed by Display, no need to dispose them
explicitly
+ colorAddition.dispose();
+ colorDeletion.dispose();
+ colorFileHeader.dispose();
+ colorHunkHeader.dispose();
super.dispose();
}
}
diff --git
a/plugins/misc/git/src/test/java/org/apache/hop/git/GitGuiPluginSessionIsolationTest.java
b/plugins/misc/git/src/test/java/org/apache/hop/git/GitGuiPluginSessionIsolationTest.java
new file mode 100644
index 0000000000..a59f66edc3
--- /dev/null
+++
b/plugins/misc/git/src/test/java/org/apache/hop/git/GitGuiPluginSessionIsolationTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.git;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import org.junit.jupiter.api.Test;
+
+class GitGuiPluginSessionIsolationTest {
+
+ @Test
+ void gitHandleIsNotAProcessWideStatic() throws Exception {
+ Field git = GitGuiPlugin.class.getDeclaredField("git");
+ assertFalse(
+ Modifier.isStatic(git.getModifiers()),
+ "UIGit must be per HopGui / RAP UISession, not a JVM static");
+ }
+
+ @Test
+ void gitPluginIsNotAProcessWideStaticSingletonField() {
+ for (Field field : GitGuiPlugin.class.getDeclaredFields()) {
+ if ("instance".equals(field.getName())) {
+ assertFalse(
+ Modifier.isStatic(field.getModifiers()),
+ "GitGuiPlugin must not keep a static instance field");
+ }
+ }
+ }
+
+ @Test
+ void gitResourceIsNotAProcessWideStaticSingletonField() {
+ for (Field field : GitResource.class.getDeclaredFields()) {
+ if ("instance".equals(field.getName())) {
+ assertFalse(Modifier.isStatic(field.getModifiers()));
+ }
+ }
+ }
+}
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java
b/rap/src/main/java/org/apache/hop/ui/core/PropsUiImpl.java
similarity index 81%
copy from rap/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java
copy to rap/src/main/java/org/apache/hop/ui/core/PropsUiImpl.java
index 5a6a6660e3..fb09e7be6a 100644
--- a/rap/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java
+++ b/rap/src/main/java/org/apache/hop/ui/core/PropsUiImpl.java
@@ -15,13 +15,14 @@
* limitations under the License.
*/
-package org.apache.hop.ui.hopgui;
+package org.apache.hop.ui.core;
+import org.apache.hop.ui.hopgui.ISingletonProvider;
import org.eclipse.rap.rwt.SingletonUtil;
-public class HopGuiImpl implements ISingletonProvider {
+public class PropsUiImpl implements ISingletonProvider {
@Override
public Object getInstanceInternal() {
- return SingletonUtil.getSessionInstance(HopGui.class);
+ return SingletonUtil.getSessionInstance(PropsUi.class);
}
}
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java
b/rap/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java
index 5a6a6660e3..277c05fde6 100644
--- a/rap/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java
@@ -17,6 +17,7 @@
package org.apache.hop.ui.hopgui;
+import org.eclipse.rap.rwt.RWT;
import org.eclipse.rap.rwt.SingletonUtil;
public class HopGuiImpl implements ISingletonProvider {
@@ -24,4 +25,16 @@ public class HopGuiImpl implements ISingletonProvider {
public Object getInstanceInternal() {
return SingletonUtil.getSessionInstance(HopGui.class);
}
+
+ @Override
+ public Object peekInstanceInternal() {
+ try {
+ if (RWT.getUISession() == null) {
+ return null;
+ }
+ return SingletonUtil.getSessionInstance(HopGui.class);
+ } catch (Exception e) {
+ return null;
+ }
+ }
}
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java
b/rap/src/main/java/org/apache/hop/ui/hopgui/HopGuiKeyHandlerImpl.java
similarity index 87%
copy from rap/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java
copy to rap/src/main/java/org/apache/hop/ui/hopgui/HopGuiKeyHandlerImpl.java
index 5a6a6660e3..cfed51f0e5 100644
--- a/rap/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/HopGuiKeyHandlerImpl.java
@@ -19,9 +19,9 @@ package org.apache.hop.ui.hopgui;
import org.eclipse.rap.rwt.SingletonUtil;
-public class HopGuiImpl implements ISingletonProvider {
+public class HopGuiKeyHandlerImpl implements ISingletonProvider {
@Override
public Object getInstanceInternal() {
- return SingletonUtil.getSessionInstance(HopGui.class);
+ return SingletonUtil.getSessionInstance(HopGuiKeyHandler.class);
}
}
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebEntryPoint.java
b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebEntryPoint.java
index 59ae0cb0a9..ce87d5e55c 100644
--- a/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebEntryPoint.java
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebEntryPoint.java
@@ -36,6 +36,7 @@ import org.apache.hop.history.AuditManager;
import org.apache.hop.history.AuditState;
import org.apache.hop.ui.core.PropsUi;
import org.apache.hop.ui.hopgui.canvas.CanvasGraphRegistry;
+import org.apache.hop.ui.hopgui.file.shared.DrillDownGuiPlugin;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.rap.rwt.application.AbstractEntryPoint;
import org.eclipse.rap.rwt.client.service.JavaScriptExecutor;
@@ -269,6 +270,7 @@ public class HopWebEntryPoint extends AbstractEntryPoint {
return;
}
hopGui.auditDelegate.writeLastOpenFiles();
+ DrillDownGuiPlugin.cleanupSession(hopGui.getId());
} catch (SWTException e) {
if (e.code != SWT.ERROR_WIDGET_DISPOSED) {
LogChannel.UI.logError("Error persisting open files on
session end", e);
diff --git a/rcp/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java
b/rcp/src/main/java/org/apache/hop/ui/core/PropsUiImpl.java
similarity index 81%
copy from rcp/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java
copy to rcp/src/main/java/org/apache/hop/ui/core/PropsUiImpl.java
index 927d1aca6e..0d40ac2f9e 100644
--- a/rcp/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java
+++ b/rcp/src/main/java/org/apache/hop/ui/core/PropsUiImpl.java
@@ -15,15 +15,17 @@
* limitations under the License.
*/
-package org.apache.hop.ui.hopgui;
+package org.apache.hop.ui.core;
-public class HopGuiImpl implements ISingletonProvider {
- private static HopGui instance;
+import org.apache.hop.ui.hopgui.ISingletonProvider;
+
+public class PropsUiImpl implements ISingletonProvider {
+ private static PropsUi instance;
@Override
public Object getInstanceInternal() {
if (instance == null) {
- instance = new HopGui();
+ instance = new PropsUi();
}
return instance;
}
diff --git a/rcp/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java
b/rcp/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java
index 927d1aca6e..3fb5b5889a 100644
--- a/rcp/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java
+++ b/rcp/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java
@@ -27,4 +27,9 @@ public class HopGuiImpl implements ISingletonProvider {
}
return instance;
}
+
+ @Override
+ public Object peekInstanceInternal() {
+ return instance;
+ }
}
diff --git a/rcp/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java
b/rcp/src/main/java/org/apache/hop/ui/hopgui/HopGuiKeyHandlerImpl.java
similarity index 86%
copy from rcp/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java
copy to rcp/src/main/java/org/apache/hop/ui/hopgui/HopGuiKeyHandlerImpl.java
index 927d1aca6e..7ff4872b26 100644
--- a/rcp/src/main/java/org/apache/hop/ui/hopgui/HopGuiImpl.java
+++ b/rcp/src/main/java/org/apache/hop/ui/hopgui/HopGuiKeyHandlerImpl.java
@@ -17,13 +17,13 @@
package org.apache.hop.ui.hopgui;
-public class HopGuiImpl implements ISingletonProvider {
- private static HopGui instance;
+public class HopGuiKeyHandlerImpl implements ISingletonProvider {
+ private static HopGuiKeyHandler instance;
@Override
public Object getInstanceInternal() {
if (instance == null) {
- instance = new HopGui();
+ instance = new HopGuiKeyHandler();
}
return instance;
}
diff --git
a/rcp/src/test/java/org/apache/hop/ui/hopgui/HopGuiPeekInstanceTest.java
b/rcp/src/test/java/org/apache/hop/ui/hopgui/HopGuiPeekInstanceTest.java
new file mode 100644
index 0000000000..29201f8438
--- /dev/null
+++ b/rcp/src/test/java/org/apache/hop/ui/hopgui/HopGuiPeekInstanceTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.assertSame;
+
+import
org.apache.hop.ui.hopgui.perspective.configuration.ConfigurationPerspective;
+import org.apache.hop.ui.hopgui.perspective.execution.ExecutionPerspective;
+import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
+import org.apache.hop.ui.hopgui.perspective.metadata.MetadataPerspective;
+import org.junit.jupiter.api.Test;
+
+class HopGuiPeekInstanceTest {
+
+ @Test
+ void perspectiveGetInstanceDoesNotConstructHopGui() {
+ HopGui before = HopGui.peekInstance();
+ ExplorerPerspective.getInstance();
+ MetadataPerspective.getInstance();
+ ExecutionPerspective.getInstance();
+ ConfigurationPerspective.getInstance();
+ assertSame(
+ before,
+ HopGui.peekInstance(),
+ "Perspective getInstance() must not start a HopGui (that floods SWT
tests and the display)");
+ }
+}
diff --git a/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
b/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
index 95aa7e6a71..9c86b941e9 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
@@ -35,6 +35,8 @@ import org.apache.hop.ui.core.gui.WindowProperty;
import org.apache.hop.ui.core.widget.OsHelper;
import org.apache.hop.ui.hopgui.HopGui;
import org.apache.hop.ui.hopgui.HopGuiKeyHandler;
+import org.apache.hop.ui.hopgui.ISingletonProvider;
+import org.apache.hop.ui.hopgui.ImplementationLoader;
import org.apache.hop.ui.hopgui.TextSizeUtilFacade;
import org.apache.hop.ui.util.EnvironmentUtils;
import org.eclipse.swt.SWT;
@@ -64,7 +66,7 @@ import org.eclipse.swt.widgets.Widget;
public class PropsUi extends Props {
private static final String OS = System.getProperty("os.name").toLowerCase();
- private static double nativeZoomFactor;
+ private double nativeZoomFactor;
private static final String STRING_SHOW_COPY_OR_DISTRIBUTE_WARNING =
"ShowCopyOrDistributeWarning";
private static final String SHOW_TOOL_TIPS = "ShowToolTips";
@@ -117,7 +119,32 @@ public class PropsUi extends Props {
public static final int DEFAULT_MAX_EXECUTION_LOGGING_TEXT_SIZE = 2000000;
private Map<RGB, RGB> contrastingColors;
- private static PropsUi instance;
+
+ /**
+ * Hop Web session override for dark mode so one user on /ui-dark does not
rewrite hop-config for
+ * every other session.
+ */
+ private Boolean darkModeOverride;
+
+ private static PropsUi fallback;
+
+ private static final ISingletonProvider PROVIDER = loadProvider();
+
+ private static ISingletonProvider loadProvider() {
+ try {
+ return (ISingletonProvider)
ImplementationLoader.newInstance(PropsUi.class);
+ } catch (Throwable e) {
+ // hop-ui unit tests have no rcp/rap *Impl on the classpath.
+ return () -> {
+ synchronized (PropsUi.class) {
+ if (fallback == null) {
+ fallback = new PropsUi();
+ }
+ return fallback;
+ }
+ };
+ }
+ }
/**
* Session-only window position storage for dialogs. This map is kept in
memory only and is
@@ -127,13 +154,10 @@ public class PropsUi extends Props {
private final Map<String, WindowProperty> sessionWindowProperties = new
HashMap<>();
public static PropsUi getInstance() {
- if (instance == null) {
- instance = new PropsUi();
- }
- return instance;
+ return (PropsUi) PROVIDER.getInstanceInternal();
}
- private PropsUi() {
+ public PropsUi() {
super();
// If the zoom factor is set with variable HOP_GUI_ZOOM_FACTOR we set this
first.
@@ -1150,10 +1174,17 @@ public class PropsUi extends Props {
}
public boolean isDarkMode() {
+ if (darkModeOverride != null) {
+ return darkModeOverride;
+ }
return YES.equalsIgnoreCase(getProperty(DARK_MODE, NO));
}
public void setDarkMode(boolean darkMode) {
+ if (EnvironmentUtils.getInstance().isWeb()) {
+ darkModeOverride = darkMode;
+ return;
+ }
setProperty(DARK_MODE, darkMode ? YES : NO);
}
@@ -1366,14 +1397,14 @@ public class PropsUi extends Props {
* @return value of nativeZoomFactor
*/
public static double getNativeZoomFactor() {
- return nativeZoomFactor;
+ return getInstance().nativeZoomFactor;
}
/**
* @param nativeZoomFactor The nativeZoomFactor to set
*/
public static void setNativeZoomFactor(double nativeZoomFactor) {
- PropsUi.nativeZoomFactor = nativeZoomFactor;
+ getInstance().nativeZoomFactor = nativeZoomFactor;
}
private void populateContrastingColors() {
diff --git a/ui/src/main/java/org/apache/hop/ui/core/gui/HopNamespace.java
b/ui/src/main/java/org/apache/hop/ui/core/gui/HopNamespace.java
index 5936eb0cb3..a507eace48 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/gui/HopNamespace.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/gui/HopNamespace.java
@@ -17,8 +17,12 @@
package org.apache.hop.ui.core.gui;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import org.apache.hop.core.exception.HopRuntimeException;
import org.apache.hop.core.util.Utils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Display;
/**
* This keeps track of the currently active namespace for all the current. It
makes it easy to see
@@ -31,6 +35,12 @@ public class HopNamespace {
private String namespace;
+ /**
+ * Hop Web has one Display per UISession. Keying by Display keeps
project/namespace isolation
+ * without calling HopGui.getInstance() from this class (which would
construct a GUI).
+ */
+ private static final Map<Display, String> NAMESPACE_BY_DISPLAY = new
ConcurrentHashMap<>();
+
private HopNamespace() {}
public static final HopNamespace getInstance() {
@@ -46,11 +56,18 @@ public class HopNamespace {
* @return value of namespace
*/
public static final String getNamespace() {
+ Display display = Display.getCurrent();
+ if (display != null && !display.isDisposed()) {
+ String sessionNamespace = NAMESPACE_BY_DISPLAY.get(display);
+ if (!Utils.isEmpty(sessionNamespace)) {
+ return sessionNamespace;
+ }
+ }
String namespace = getInstance().namespace;
if (Utils.isEmpty(namespace)) {
throw new HopRuntimeException("Please set a namespace before using one");
}
- return getInstance().namespace;
+ return namespace;
}
/**
@@ -58,5 +75,12 @@ public class HopNamespace {
*/
public static final void setNamespace(String namespace) {
getInstance().namespace = namespace;
+ Display display = Display.getCurrent();
+ if (display != null && !display.isDisposed()) {
+ if (!NAMESPACE_BY_DISPLAY.containsKey(display)) {
+ display.addListener(SWT.Dispose, e ->
NAMESPACE_BY_DISPLAY.remove(display));
+ }
+ NAMESPACE_BY_DISPLAY.put(display, namespace);
+ }
}
}
diff --git a/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java
b/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java
index 862e896672..7aa2d4bc09 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java
@@ -185,7 +185,7 @@ public class HopVfsFileDialog implements IFileDialog,
IDirectoryDialog {
private Image folderImage;
private Image fileImage;
- @Getter private static HopVfsFileDialog instance;
+ private static HopVfsFileDialog instance;
private java.util.List<String> navigationHistory;
private int navigationIndex;
@@ -303,6 +303,7 @@ public class HopVfsFileDialog implements IFileDialog,
IDirectoryDialog {
}
});
instance = this;
+ HopGui.getInstance().setOpenVfsFileDialog(this);
FormLayout formLayout = new FormLayout();
formLayout.marginWidth = PropsUi.getFormMargin();
@@ -1375,8 +1376,20 @@ public class HopVfsFileDialog implements IFileDialog,
IDirectoryDialog {
}
}
+ public static HopVfsFileDialog getInstance() {
+ HopGui hopGui = HopGui.peekInstance();
+ if (hopGui != null && hopGui.getOpenVfsFileDialog() != null) {
+ return hopGui.getOpenVfsFileDialog();
+ }
+ return instance;
+ }
+
private void dispose() {
instance = null;
+ HopGui hopGui = HopGui.peekInstance();
+ if (hopGui != null && hopGui.getOpenVfsFileDialog() == this) {
+ hopGui.setOpenVfsFileDialog(null);
+ }
try {
// Save the navigation history
//
diff --git
a/ui/src/main/java/org/apache/hop/ui/core/widget/MetaSelectionLine.java
b/ui/src/main/java/org/apache/hop/ui/core/widget/MetaSelectionLine.java
index 5010739331..8521ba7d9c 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/widget/MetaSelectionLine.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/MetaSelectionLine.java
@@ -263,8 +263,14 @@ public class MetaSelectionLine<T extends IHopMetadata>
extends Composite {
}
public static String getEditIcon(Object guiPluginObject) {
- MetaSelectionLine<?> line = (MetaSelectionLine<?>) guiPluginObject;
- return line.getManagedClass().getAnnotation(HopMetadata.class).image();
+ if (!(guiPluginObject instanceof MetaSelectionLine<?> line) ||
line.getManagedClass() == null) {
+ return "ui/images/metadata.svg";
+ }
+ HopMetadata annotation =
line.getManagedClass().getAnnotation(HopMetadata.class);
+ if (annotation == null || Utils.isEmpty(annotation.image())) {
+ return "ui/images/metadata.svg";
+ }
+ return annotation.image();
}
@GuiToolbarElement(
diff --git a/ui/src/main/java/org/apache/hop/ui/core/widget/StyledTextVar.java
b/ui/src/main/java/org/apache/hop/ui/core/widget/StyledTextVar.java
index 95ff36842c..c9a49d70e9 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/widget/StyledTextVar.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/StyledTextVar.java
@@ -331,7 +331,8 @@ public class StyledTextVar extends TextComposite {
getToolbar().setEnabled(enabled);
}
// StyledText component does not get the "disabled" look, so it needs to
be applied explicitly
- if (Display.getDefault() != null) {
+ Display display = wText.getDisplay();
+ if (display != null && !display.isDisposed()) {
wText.setBackground(
enabled
? GuiResource.getInstance().getColorWhite()
diff --git a/ui/src/main/java/org/apache/hop/ui/core/widget/TreeMemory.java
b/ui/src/main/java/org/apache/hop/ui/core/widget/TreeMemory.java
index 7698b814cd..d762498a40 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/widget/TreeMemory.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/TreeMemory.java
@@ -19,9 +19,12 @@ package org.apache.hop.ui.core.widget;
import java.util.Hashtable;
import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import org.apache.hop.ui.core.ConstUi;
+import org.eclipse.swt.SWT;
import org.eclipse.swt.events.TreeEvent;
import org.eclipse.swt.events.TreeListener;
+import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
@@ -32,19 +35,28 @@ import org.eclipse.swt.widgets.TreeItem;
public class TreeMemory {
private static TreeMemory treeMemory;
+ private static final Map<Display, TreeMemory> BY_DISPLAY = new
ConcurrentHashMap<>();
+
private Map<TreeMemoryEntry, Boolean> map;
public static final TreeMemory getInstance() {
+ Display display = Display.getCurrent();
+ if (display != null && !display.isDisposed()) {
+ return BY_DISPLAY.computeIfAbsent(
+ display,
+ d -> {
+ d.addListener(SWT.Dispose, e -> BY_DISPLAY.remove(d));
+ return new TreeMemory();
+ });
+ }
if (treeMemory != null) {
return treeMemory;
}
-
treeMemory = new TreeMemory();
-
return treeMemory;
}
- private TreeMemory() {
+ TreeMemory() {
map = new Hashtable<>(100);
}
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
index 4d37882f2e..3e76385473 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
@@ -29,9 +29,12 @@ import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Set;
import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
+import java.util.function.Supplier;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.io.output.TeeOutputStream;
@@ -350,6 +353,21 @@ public class HopGui
*/
@Setter private HopSecurityContext securityContext;
+ /**
+ * Per-HopGui (per RAP UISession) plugin/UI singletons that cannot use RAP
{@code SingletonUtil}
+ * because they live in plugin classloaders.
+ */
+ private final Map<Class<?>, Object> sessionSingletons = new
ConcurrentHashMap<>();
+
+ /**
+ * The file dialog currently open in this session, so toolbar plugins can
navigate it without a
+ * process-wide static.
+ */
+ @Getter @Setter private org.apache.hop.ui.core.vfs.HopVfsFileDialog
openVfsFileDialog;
+
+ /** Active namespace for this GUI session (project id, or {@link
#DEFAULT_HOP_GUI_NAMESPACE}). */
+ @Getter @Setter private String activeNamespace = DEFAULT_HOP_GUI_NAMESPACE;
+
protected HopGui() {
this(Display.getCurrent());
}
@@ -382,6 +400,7 @@ public class HopGui
updateMetadataManagers();
+ this.activeNamespace = DEFAULT_HOP_GUI_NAMESPACE;
HopNamespace.setNamespace(DEFAULT_HOP_GUI_NAMESPACE);
shell = new Shell(display, SWT.DIALOG_TRIM | SWT.RESIZE | SWT.MIN |
SWT.MAX);
}
@@ -405,6 +424,44 @@ public class HopGui
return (HopGui) PROVIDER.getInstanceInternal();
}
+ /**
+ * The HopGui of this process/session if it already exists. Does not
construct a GUI. Use this
+ * from {@code getInstance()} helpers that must stay inert in unit tests.
+ */
+ public static HopGui peekInstance() {
+ try {
+ return (HopGui) PROVIDER.peekInstanceInternal();
+ } catch (Throwable e) {
+ return null;
+ }
+ }
+
+ /**
+ * Returns a singleton owned by this HopGui / RAP UISession. Desktop has one
HopGui, so this is
+ * equivalent to a process-wide singleton there. Plugins cannot use RAP
{@code SingletonUtil}
+ * through {@link ImplementationLoader} because they load from a different
classloader.
+ */
+ @SuppressWarnings("unchecked")
+ public <T> T getSessionSingleton(Class<T> type, Supplier<T> factory) {
+ return (T) sessionSingletons.computeIfAbsent(type, key -> factory.get());
+ }
+
+ /**
+ * Looks up a perspective on the current session's HopGui. Returns null when
the GUI or its
+ * perspective manager is not ready (tests, disabled perspectives).
+ */
+ public static <T extends IHopPerspective> T findSessionPerspective(Class<T>
type) {
+ try {
+ HopGui hopGui = peekInstance();
+ if (hopGui == null || hopGui.getPerspectiveManager() == null) {
+ return null;
+ }
+ return hopGui.getPerspectiveManager().findPerspective(type);
+ } catch (Throwable e) {
+ return null;
+ }
+ }
+
public void setWebThemeRedirectCallback(Consumer<Boolean> callback) {
this.webThemeRedirectCallback = callback;
}
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGuiKeyHandler.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGuiKeyHandler.java
index f3e8bd8cae..2d8dd4b7f9 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGuiKeyHandler.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGuiKeyHandler.java
@@ -58,7 +58,25 @@ public class HopGuiKeyHandler extends KeyAdapter {
/** Widget classes that pass their key listeners on to a widget inside them.
*/
private static final Map<Class<?>, Boolean> DELEGATING_KEY_LISTENERS = new
ConcurrentHashMap<>();
- private static HopGuiKeyHandler singleton;
+ private static HopGuiKeyHandler fallback;
+
+ private static final ISingletonProvider PROVIDER = loadProvider();
+
+ private static ISingletonProvider loadProvider() {
+ try {
+ return (ISingletonProvider)
ImplementationLoader.newInstance(HopGuiKeyHandler.class);
+ } catch (Throwable e) {
+ // hop-ui unit tests have no rcp/rap *Impl on the classpath.
+ return () -> {
+ synchronized (HopGuiKeyHandler.class) {
+ if (fallback == null) {
+ fallback = new HopGuiKeyHandler();
+ }
+ return fallback;
+ }
+ };
+ }
+ }
public Set<Object> parentObjects;
@@ -69,20 +87,21 @@ public class HopGuiKeyHandler extends KeyAdapter {
private final Set<Shell> handledShells = new HashSet<>();
/**
- * Displays with a focus filter. This handler is a singleton for the whole
process while Hop Web
- * has a display per session, so the filter is installed once per display.
+ * Displays with a focus filter. Hop Web has one handler (and one display)
per RAP UISession; the
+ * desktop handler covers the single process display.
*/
private final Set<Display> filteredDisplays = new HashSet<>();
- private HopGuiKeyHandler() {
+ /**
+ * Public no-arg constructor so RAP {@code SingletonUtil.getSessionInstance}
can create a handler
+ * per UISession. Call {@link #getInstance()} rather than constructing this
yourself.
+ */
+ public HopGuiKeyHandler() {
this.parentObjects = new HashSet<>();
}
public static HopGuiKeyHandler getInstance() {
- if (singleton == null) {
- singleton = new HopGuiKeyHandler();
- }
- return singleton;
+ return (HopGuiKeyHandler) PROVIDER.getInstanceInternal();
}
public void addParentObjectToHandle(Object parentObject) {
@@ -214,7 +233,7 @@ public class HopGuiKeyHandler extends KeyAdapter {
return;
}
- List<Object> orderedParents = getParentObjectsInContextOrder(event.widget);
+ List<Object> orderedParents = getParentObjectsInContextOrder(event);
for (Object parentObject : orderedParents) {
List<KeyboardShortcut> shortcuts =
GuiRegistry.getInstance().getKeyboardShortcuts(parentObject.getClass().getName());
@@ -230,11 +249,15 @@ public class HopGuiKeyHandler extends KeyAdapter {
}
/** Order: parents whose window has focus (closest first), then active
perspectives, then rest. */
- private List<Object> getParentObjectsInContextOrder(Object focusedWidget) {
+ private List<Object> getParentObjectsInContextOrder(KeyEvent event) {
+ Object focusedWidget = event.widget;
List<Object> inFocus = new ArrayList<>();
List<Object> fallback = new ArrayList<>();
for (Object parent : parentObjects) {
Control control = parent instanceof Control c ? c :
parentToControl.get(parent);
+ if (control != null && !belongsToEventDisplay(control, event)) {
+ continue;
+ }
if (control != null && isWidgetInControlHierarchy(focusedWidget,
control)) {
inFocus.add(parent);
} else {
@@ -296,6 +319,9 @@ public class HopGuiKeyHandler extends KeyAdapter {
Object parentObject, KeyEvent event, KeyboardShortcut shortcut) {
if (parentObject instanceof Control control) {
try {
+ if (!belongsToEventDisplay(control, event)) {
+ return false;
+ }
if (!control.isVisible()) {
return shortcut.isGlobal();
}
@@ -379,8 +405,24 @@ public class HopGuiKeyHandler extends KeyAdapter {
return false;
}
+ /**
+ * RAP forbids touching a widget whose Display belongs to another UISession.
Skip those parents
+ * instead of walking their widget tree.
+ */
+ private boolean belongsToEventDisplay(Control control, KeyEvent event) {
+ if (control == null || control.isDisposed() || event == null) {
+ return false;
+ }
+ try {
+ Display controlDisplay = control.getDisplay();
+ return event.display != null && event.display.equals(controlDisplay);
+ } catch (SWTException e) {
+ return false;
+ }
+ }
+
private boolean isWidgetInControlHierarchy(Object widget, Control control) {
- if (!(widget instanceof Control)) {
+ if (!(widget instanceof Control) || control == null ||
control.isDisposed()) {
return false;
}
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/ISingletonProvider.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/ISingletonProvider.java
index 6d9c9d8c84..66e5a084f5 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/ISingletonProvider.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/ISingletonProvider.java
@@ -19,4 +19,12 @@ package org.apache.hop.ui.hopgui;
public interface ISingletonProvider {
Object getInstanceInternal();
+
+ /**
+ * Existing instance, or {@code null} if one has not been created. Must not
construct a GUI.
+ * Default is {@code null} so providers that only create on demand stay
inert during tests.
+ */
+ default Object peekInstanceInternal() {
+ return null;
+ }
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
index a8a00c0030..1f0e5e5bf2 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
@@ -5690,7 +5690,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
// Also make sure to clear the log entries in the central log store
& registry
//
if (pipeline != null) {
- DrillDownGuiPlugin.cleanupOnRunStart();
+ DrillDownGuiPlugin.cleanupOnRunStart(hopGui.getId());
HopLogStore.discardLines(pipeline.getLogChannelId(), true);
}
@@ -5714,6 +5714,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
variables.resolve(pipelineRunConfigurationName),
hopGui.getMetadataProvider(),
pipelineMeta);
+ DrillDownGuiPlugin.bindToHopGui(pipeline, hopGui.getId());
// Set the variables from the execution configuration
// These are values set by the user in the execution dialog
@@ -5895,7 +5896,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
// Do we have a previous execution to clean up in the logging registry?
//
if (pipeline != null) {
- DrillDownGuiPlugin.cleanupOnRunStart();
+ DrillDownGuiPlugin.cleanupOnRunStart(hopGui.getId());
HopLogStore.discardLines(pipeline.getLogChannelId(), false);
LoggingRegistry.getInstance().removeIncludingChildren(pipeline.getLogChannelId());
}
@@ -5904,6 +5905,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
//
pipelineMeta.clearCaches();
pipeline = new LocalPipelineEngine(pipelineMeta, variables,
hopGui.getLoggingObject());
+ DrillDownGuiPlugin.bindToHopGui(pipeline, hopGui.getId());
pipeline.setPreview(true);
pipeline.setVariable(IPipelineEngine.PIPELINE_IN_PREVIEW_MODE, "Y");
pipeline.setMetadataProvider(hopGui.getMetadataProvider());
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/DrillDownGuiPlugin.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/DrillDownGuiPlugin.java
index 35b4abb877..b9496f42f3 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/DrillDownGuiPlugin.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/DrillDownGuiPlugin.java
@@ -22,6 +22,7 @@ import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import org.apache.hop.core.IExtensionData;
import org.apache.hop.core.action.GuiContextAction;
import org.apache.hop.core.action.GuiContextActionFilter;
import org.apache.hop.core.exception.HopException;
@@ -59,68 +60,162 @@ public class DrillDownGuiPlugin {
private static final Class<?> PKG = DrillDownGuiPlugin.class;
- // Global registries to track running instances
- private static final Map<String, IPipelineEngine<PipelineMeta>>
runningPipelines =
- new ConcurrentHashMap<>();
- private static final Map<String, IWorkflowEngine<WorkflowMeta>>
runningWorkflows =
- new ConcurrentHashMap<>();
+ /**
+ * Extension-data key stamped onto engines started from the GUI so
registration on engine threads
+ * (no RAP UISession) still belongs to one Hop Web session.
+ */
+ public static final String HOP_GUI_ID = "HOP_GUI_ID";
+
+ // Per HopGui-id registries so one user's run does not clear or expose
another's engines.
+ private static final Map<String, Map<String, IPipelineEngine<PipelineMeta>>>
+ runningPipelinesByHopGui = new ConcurrentHashMap<>();
+ private static final Map<String, Map<String, IWorkflowEngine<WorkflowMeta>>>
+ runningWorkflowsByHopGui = new ConcurrentHashMap<>();
public static final String DRILL_DOWN_ERROR_OPENING_EXECUTION =
"DrillDown.Error.OpeningExecution";
public static final String DRILL_DOWN_ERROR_TITLE = "DrillDown.Error.Title";
+ public static void bindToHopGui(IExtensionData engine, String hopGuiId) {
+ if (engine == null || hopGuiId == null || engine.getExtensionDataMap() ==
null) {
+ return;
+ }
+ engine.getExtensionDataMap().put(HOP_GUI_ID, hopGuiId);
+ }
+
+ public static String hopGuiIdOf(Object engine) {
+ Object current = engine;
+ while (current != null) {
+ if (current instanceof IExtensionData ext && ext.getExtensionDataMap()
!= null) {
+ Object id = ext.getExtensionDataMap().get(HOP_GUI_ID);
+ if (id instanceof String hopGuiId && !hopGuiId.isEmpty()) {
+ return hopGuiId;
+ }
+ }
+ if (current instanceof ILoggingObject logging) {
+ current = logging.getParent();
+ } else {
+ break;
+ }
+ }
+ return null;
+ }
+
public static void registerRunningPipeline(
String logChannelId, IPipelineEngine<PipelineMeta> pipeline) {
- runningPipelines.put(logChannelId, pipeline);
+ String hopGuiId = ensureHopGuiId(pipeline);
+ if (hopGuiId == null || logChannelId == null || pipeline == null) {
+ return;
+ }
+ runningPipelinesByHopGui
+ .computeIfAbsent(hopGuiId, k -> new ConcurrentHashMap<>())
+ .put(logChannelId, pipeline);
}
public static void registerRunningWorkflow(
String logChannelId, IWorkflowEngine<WorkflowMeta> workflow) {
- runningWorkflows.put(logChannelId, workflow);
+ String hopGuiId = ensureHopGuiId(workflow);
+ if (hopGuiId == null || logChannelId == null || workflow == null) {
+ return;
+ }
+ runningWorkflowsByHopGui
+ .computeIfAbsent(hopGuiId, k -> new ConcurrentHashMap<>())
+ .put(logChannelId, workflow);
+ }
+
+ private static String ensureHopGuiId(IExtensionData engine) {
+ String hopGuiId = hopGuiIdOf(engine);
+ if (hopGuiId != null) {
+ return hopGuiId;
+ }
+ return null;
}
/**
- * Clears all drill-down and sample-row state. Call when starting a new run
to get a clean slate
- * and avoid leaking memory.
+ * Clears drill-down and sample-row state for this HopGui session. Call when
starting a new run.
*/
public static void cleanupOnRunStart() {
- runningPipelines.clear();
- runningWorkflows.clear();
- dataSnifferBuffersByLogChannelId.clear();
- dataSnifferHopBuffersByLogChannelId.clear();
+ HopGui hopGui = HopGui.peekInstance();
+ if (hopGui != null) {
+ cleanupOnRunStart(hopGui.getId());
+ }
+ }
+
+ public static void cleanupOnRunStart(String hopGuiId) {
+ if (hopGuiId == null) {
+ return;
+ }
+ Map<String, IPipelineEngine<PipelineMeta>> pipelines =
runningPipelinesByHopGui.get(hopGuiId);
+ if (pipelines != null) {
+ pipelines.clear();
+ }
+ Map<String, IWorkflowEngine<WorkflowMeta>> workflows =
runningWorkflowsByHopGui.get(hopGuiId);
+ if (workflows != null) {
+ workflows.clear();
+ }
+ Map<String, Map<String, RowBuffer>> sniffers =
dataSnifferBuffersByHopGui.get(hopGuiId);
+ if (sniffers != null) {
+ sniffers.clear();
+ }
+ Map<String, Map<String, RowBuffer>> hopSniffers =
dataSnifferHopBuffersByHopGui.get(hopGuiId);
+ if (hopSniffers != null) {
+ hopSniffers.clear();
+ }
+ }
+
+ /** Visible for tests. */
+ static IPipelineEngine<PipelineMeta> runningPipeline(String hopGuiId, String
logChannelId) {
+ Map<String, IPipelineEngine<PipelineMeta>> pipelines =
runningPipelinesByHopGui.get(hopGuiId);
+ return pipelines == null ? null : pipelines.get(logChannelId);
+ }
+
+ /** Drops every drill-down map for a UI session that is ending. */
+ public static void cleanupSession(String hopGuiId) {
+ if (hopGuiId == null) {
+ return;
+ }
+ runningPipelinesByHopGui.remove(hopGuiId);
+ runningWorkflowsByHopGui.remove(hopGuiId);
+ dataSnifferBuffersByHopGui.remove(hopGuiId);
+ dataSnifferHopBuffersByHopGui.remove(hopGuiId);
}
/**
- * Per-run data sniffer buffers (logChannelId -> transform name ->
RowBuffer). Filled when
- * pipelines start so we have row data even if the pipeline tab is never
opened. Only the latest
- * run's data is kept per execution (each run has its own logChannelId).
+ * Per-session, per-run data sniffer buffers (hopGuiId -> logChannelId ->
transform name ->
+ * RowBuffer).
*/
- private static final Map<String, Map<String, RowBuffer>>
dataSnifferBuffersByLogChannelId =
+ private static final Map<String, Map<String, Map<String, RowBuffer>>>
dataSnifferBuffersByHopGui =
new ConcurrentHashMap<>();
/**
- * Per-run hop-level sniffer buffers (logChannelId -> hop key -> RowBuffer)
for target hops
- * ({@code putRowTo}).
+ * Per-session hop-level sniffer buffers (hopGuiId -> logChannelId -> hop
key -> RowBuffer) for
+ * target hops ({@code putRowTo}).
*/
- private static final Map<String, Map<String, RowBuffer>>
dataSnifferHopBuffersByLogChannelId =
- new ConcurrentHashMap<>();
+ private static final Map<String, Map<String, Map<String, RowBuffer>>>
+ dataSnifferHopBuffersByHopGui = new ConcurrentHashMap<>();
/**
* Attach row listeners to a pipeline at start so we capture output rows for
debugging. Uses the
* pipeline's run configuration (sample type and size in GUI) when
available; otherwise no
- * sniffers are attached. Data is stored in {@link
#dataSnifferBuffersByLogChannelId} so it is
- * available when the user later opens the pipeline tab. Only applies to
local pipeline engine.
+ * sniffers are attached. Data is stored in the per-session sniffer buffers
so it is available
+ * when the user later opens the pipeline tab. Only applies to local
pipeline engine.
*/
public static void
attachDataSniffersToPipeline(IPipelineEngine<PipelineMeta> pipeline) {
if (!(pipeline instanceof LocalPipelineEngine)) {
return;
}
+ String hopGuiId = ensureHopGuiId(pipeline);
String logChannelId = pipeline.getLogChannelId();
+ if (hopGuiId == null || logChannelId == null) {
+ return;
+ }
Map<String, RowBuffer> buffers =
- dataSnifferBuffersByLogChannelId.computeIfAbsent(
- logChannelId, k -> new ConcurrentHashMap<>());
+ dataSnifferBuffersByHopGui
+ .computeIfAbsent(hopGuiId, k -> new ConcurrentHashMap<>())
+ .computeIfAbsent(logChannelId, k -> new ConcurrentHashMap<>());
Map<String, RowBuffer> hopBuffers =
- dataSnifferHopBuffersByLogChannelId.computeIfAbsent(
- logChannelId, k -> new ConcurrentHashMap<>());
+ dataSnifferHopBuffersByHopGui
+ .computeIfAbsent(hopGuiId, k -> new ConcurrentHashMap<>())
+ .computeIfAbsent(logChannelId, k -> new ConcurrentHashMap<>());
PipelineRowSamplerHelper.addRowSamplersToPipeline(pipeline, buffers,
hopBuffers);
}
@@ -129,7 +224,7 @@ public class DrillDownGuiPlugin {
* attaching to a running/finished pipeline so the UI can show the rows that
flowed through.
*/
public static Map<String, RowBuffer> getDataSnifferBuffersForPipeline(String
logChannelId) {
- return dataSnifferBuffersByLogChannelId.get(logChannelId);
+ return findSnifferBuffers(dataSnifferBuffersByHopGui, logChannelId);
}
/**
@@ -139,7 +234,39 @@ public class DrillDownGuiPlugin {
* @return hop key → RowBuffer, or null
*/
public static Map<String, RowBuffer>
getDataSnifferHopBuffersForPipeline(String logChannelId) {
- return dataSnifferHopBuffersByLogChannelId.get(logChannelId);
+ return findSnifferBuffers(dataSnifferHopBuffersByHopGui, logChannelId);
+ }
+
+ private static Map<String, RowBuffer> findSnifferBuffers(
+ Map<String, Map<String, Map<String, RowBuffer>>> byHopGui, String
logChannelId) {
+ if (logChannelId == null) {
+ return null;
+ }
+ for (Map<String, Map<String, RowBuffer>> sessionBuffers :
byHopGui.values()) {
+ Map<String, RowBuffer> buffers = sessionBuffers.get(logChannelId);
+ if (buffers != null) {
+ return buffers;
+ }
+ }
+ return null;
+ }
+
+ private static Map<String, IPipelineEngine<PipelineMeta>>
pipelinesFor(Object parentEngine) {
+ String hopGuiId = hopGuiIdOf(parentEngine);
+ if (hopGuiId == null) {
+ return Map.of();
+ }
+ Map<String, IPipelineEngine<PipelineMeta>> pipelines =
runningPipelinesByHopGui.get(hopGuiId);
+ return pipelines != null ? pipelines : Map.of();
+ }
+
+ private static Map<String, IWorkflowEngine<WorkflowMeta>>
workflowsFor(Object parentEngine) {
+ String hopGuiId = hopGuiIdOf(parentEngine);
+ if (hopGuiId == null) {
+ return Map.of();
+ }
+ Map<String, IWorkflowEngine<WorkflowMeta>> workflows =
runningWorkflowsByHopGui.get(hopGuiId);
+ return workflows != null ? workflows : Map.of();
}
// ==================== TRANSFORM CONTEXT ====================
@@ -253,22 +380,22 @@ public class DrillDownGuiPlugin {
if (child.pipeline != null) {
PipelineMeta meta = child.pipeline.getPipelineMeta();
if (meta != null) {
- display.asyncExec(
- () -> openTabAndAttachPipeline(hopGui, meta,
child.pipeline));
+ runOnDisplay(
+ display, () -> openTabAndAttachPipeline(hopGui,
meta, child.pipeline));
return;
}
} else {
WorkflowMeta meta = child.workflow.getWorkflowMeta();
if (meta != null) {
- display.asyncExec(
- () -> openTabAndAttachWorkflow(hopGui, meta,
child.workflow));
+ runOnDisplay(
+ display, () -> openTabAndAttachWorkflow(hopGui,
meta, child.workflow));
return;
}
}
}
Thread.sleep(100);
}
- display.asyncExec(() -> showNoRunningExecution(hopGui));
+ runOnDisplay(display, () -> showNoRunningExecution(hopGui));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Exception e) {
@@ -359,6 +486,19 @@ public class DrillDownGuiPlugin {
* Single scan over parent's children: looks up each child in both pipeline
and workflow maps,
* returns the one matching run (pipeline preferred over workflow, most
recent by start time).
*/
+ private static void runOnDisplay(Display display, Runnable runnable) {
+ if (display == null || display.isDisposed()) {
+ return;
+ }
+ display.asyncExec(
+ () -> {
+ if (display.isDisposed()) {
+ return;
+ }
+ runnable.run();
+ });
+ }
+
private RunningChild findRunningChild(Object parentEngine, String name) {
String parentLogChannelId;
if (parentEngine instanceof IPipelineEngine) {
@@ -372,6 +512,8 @@ public class DrillDownGuiPlugin {
LoggingRegistry registry = LoggingRegistry.getInstance();
List<String> childLogChannelIds =
registry.getLogChannelChildren(parentLogChannelId);
+ Map<String, IPipelineEngine<PipelineMeta>> runningPipelines =
pipelinesFor(parentEngine);
+ Map<String, IWorkflowEngine<WorkflowMeta>> runningWorkflows =
workflowsFor(parentEngine);
List<IPipelineEngine<PipelineMeta>> pipelines = new ArrayList<>();
List<IWorkflowEngine<WorkflowMeta>> workflows = new ArrayList<>();
for (String logChannelId : childLogChannelIds) {
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
index d276a92fb9..47441b3e0e 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
@@ -5196,7 +5196,7 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
// store & registry
//
if (workflow != null) {
- DrillDownGuiPlugin.cleanupOnRunStart();
+ DrillDownGuiPlugin.cleanupOnRunStart(hopGui.getId());
HopLogStore.discardLines(workflow.getLogChannelId(), true);
}
@@ -5258,6 +5258,7 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
// Pass specific extension points...
//
workflow.getExtensionDataMap().putAll(executionConfiguration.getExtensionOptions());
+ DrillDownGuiPlugin.bindToHopGui(workflow, hopGui.getId());
// Add action listeners
//
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/ConfigurationPerspective.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/ConfigurationPerspective.java
index 4f19a70ac5..4d2f9286db 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/ConfigurationPerspective.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/ConfigurationPerspective.java
@@ -23,7 +23,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import lombok.Getter;
import org.apache.hop.core.Props;
import org.apache.hop.core.gui.plugin.GuiPlugin;
import org.apache.hop.core.gui.plugin.GuiRegistry;
@@ -111,12 +110,25 @@ public class ConfigurationPerspective implements
IHopPerspective {
private SearchMatcher searchMatcher = new SearchMatcher("", false, false,
true);
private Color highlightColor; // Custom neutral highlight color
private Text searchBox;
- @Getter private static ConfigurationPerspective instance;
+ private static ConfigurationPerspective instance;
public ConfigurationPerspective() {
instance = this;
}
+ public static ConfigurationPerspective getInstance() {
+ try {
+ ConfigurationPerspective fromGui =
+ HopGui.findSessionPerspective(ConfigurationPerspective.class);
+ if (fromGui != null) {
+ return fromGui;
+ }
+ } catch (Throwable e) {
+ // No HopGuiImpl in unit tests
+ }
+ return instance;
+ }
+
@Override
public List<IGuiContextHandler> getContextHandlers() {
return new ArrayList<>();
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGeneralOptionsTab.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGeneralOptionsTab.java
index 0beedb8d99..6400eb0b01 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGeneralOptionsTab.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGeneralOptionsTab.java
@@ -520,7 +520,7 @@ public class ConfigGeneralOptionsTab {
expandBar.addListener(
SWT.Expand,
e ->
- Display.getDefault()
+ Display.getCurrent()
.asyncExec(
() -> {
if (!wGeneralComp.isDisposed() &&
!sGeneralComp.isDisposed()) {
@@ -532,7 +532,7 @@ public class ConfigGeneralOptionsTab {
expandBar.addListener(
SWT.Collapse,
e ->
- Display.getDefault()
+ Display.getCurrent()
.asyncExec(
() -> {
if (!wGeneralComp.isDisposed() &&
!sGeneralComp.isDisposed()) {
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGuiOptionsTab.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGuiOptionsTab.java
index 4ab32ada19..a69c6f5483 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGuiOptionsTab.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGuiOptionsTab.java
@@ -513,7 +513,7 @@ public class ConfigGuiOptionsTab {
appearanceExpandBar.addListener(
SWT.Expand,
e ->
- Display.getDefault()
+ Display.getCurrent()
.asyncExec(
() -> {
if (!wLookComp.isDisposed() && !sLookComp.isDisposed()) {
@@ -524,7 +524,7 @@ public class ConfigGuiOptionsTab {
appearanceExpandBar.addListener(
SWT.Collapse,
e ->
- Display.getDefault()
+ Display.getCurrent()
.asyncExec(
() -> {
if (!wLookComp.isDisposed() && !sLookComp.isDisposed()) {
@@ -596,7 +596,7 @@ public class ConfigGuiOptionsTab {
fontsExpandBar.addListener(
SWT.Expand,
e ->
- Display.getDefault()
+ Display.getCurrent()
.asyncExec(
() -> {
if (!wLookComp.isDisposed() && !sLookComp.isDisposed()) {
@@ -607,7 +607,7 @@ public class ConfigGuiOptionsTab {
fontsExpandBar.addListener(
SWT.Collapse,
e ->
- Display.getDefault()
+ Display.getCurrent()
.asyncExec(
() -> {
if (!wLookComp.isDisposed() && !sLookComp.isDisposed()) {
@@ -750,7 +750,7 @@ public class ConfigGuiOptionsTab {
canvasExpandBar.addListener(
SWT.Expand,
e ->
- Display.getDefault()
+ Display.getCurrent()
.asyncExec(
() -> {
if (!wLookComp.isDisposed() && !sLookComp.isDisposed()) {
@@ -761,7 +761,7 @@ public class ConfigGuiOptionsTab {
canvasExpandBar.addListener(
SWT.Collapse,
e ->
- Display.getDefault()
+ Display.getCurrent()
.asyncExec(
() -> {
if (!wLookComp.isDisposed() && !sLookComp.isDisposed()) {
@@ -876,7 +876,7 @@ public class ConfigGuiOptionsTab {
autoLayoutExpandBar.addListener(
SWT.Expand,
e ->
- Display.getDefault()
+ Display.getCurrent()
.asyncExec(
() -> {
if (!wLookComp.isDisposed() && !sLookComp.isDisposed()) {
@@ -887,7 +887,7 @@ public class ConfigGuiOptionsTab {
autoLayoutExpandBar.addListener(
SWT.Collapse,
e ->
- Display.getDefault()
+ Display.getCurrent()
.asyncExec(
() -> {
if (!wLookComp.isDisposed() && !sLookComp.isDisposed()) {
@@ -974,7 +974,7 @@ public class ConfigGuiOptionsTab {
tablesExpandBar.addListener(
SWT.Expand,
e ->
- Display.getDefault()
+ Display.getCurrent()
.asyncExec(
() -> {
if (!wLookComp.isDisposed() && !sLookComp.isDisposed()) {
@@ -985,7 +985,7 @@ public class ConfigGuiOptionsTab {
tablesExpandBar.addListener(
SWT.Collapse,
e ->
- Display.getDefault()
+ Display.getCurrent()
.asyncExec(
() -> {
if (!wLookComp.isDisposed() && !sLookComp.isDisposed()) {
@@ -1128,7 +1128,7 @@ public class ConfigGuiOptionsTab {
metricsPanelExpandBar.addListener(
SWT.Expand,
e ->
- Display.getDefault()
+ Display.getCurrent()
.asyncExec(
() -> {
if (!wLookComp.isDisposed() && !sLookComp.isDisposed()) {
@@ -1139,7 +1139,7 @@ public class ConfigGuiOptionsTab {
metricsPanelExpandBar.addListener(
SWT.Collapse,
e ->
- Display.getDefault()
+ Display.getCurrent()
.asyncExec(
() -> {
if (!wLookComp.isDisposed() && !sLookComp.isDisposed()) {
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigKeyboardShortcutsTab.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigKeyboardShortcutsTab.java
index 2ea93f1457..aca4bf5788 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigKeyboardShortcutsTab.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigKeyboardShortcutsTab.java
@@ -182,7 +182,7 @@ public class ConfigKeyboardShortcutsTab {
expandBar.addListener(
SWT.Expand,
e ->
- Display.getDefault()
+ Display.getCurrent()
.asyncExec(
() -> {
if (!contentComposite.isDisposed() &&
!scrolledComposite.isDisposed()) {
@@ -194,7 +194,7 @@ public class ConfigKeyboardShortcutsTab {
expandBar.addListener(
SWT.Collapse,
e ->
- Display.getDefault()
+ Display.getCurrent()
.asyncExec(
() -> {
if (!contentComposite.isDisposed() &&
!scrolledComposite.isDisposed()) {
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigPluginOptionsTab.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigPluginOptionsTab.java
index 93991041b4..f6f3018886 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigPluginOptionsTab.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigPluginOptionsTab.java
@@ -54,7 +54,6 @@ public class ConfigPluginOptionsTab {
public static final String GUI_WIDGETS_PARENT_ID =
"EnterOptionsDialog-GuiWidgetsParent";
- private static Map<String, Object> pluginDataMap = new HashMap<>();
private Composite wPluginComposite;
public ConfigPluginOptionsTab() {
@@ -96,7 +95,7 @@ public class ConfigPluginOptionsTab {
// Load all configuration plugins and store their data
//
- pluginDataMap.clear();
+ pluginDataMap().clear();
PluginRegistry pluginRegistry = PluginRegistry.getInstance();
List<String> disabledIds = GuiRegistry.getDisabledGuiElements();
java.util.List<IPlugin> configPlugins =
pluginRegistry.getPlugins(ConfigPluginType.class);
@@ -120,7 +119,7 @@ public class ConfigPluginOptionsTab {
Const.NVL(
TranslateUtil.translate(annotation.description(),
emptySourceData.getClass()),
"");
- pluginDataMap.put(name, sourceData);
+ pluginDataMap().put(name, sourceData);
}
} catch (Exception e) {
new ErrorDialog(
@@ -150,7 +149,7 @@ public class ConfigPluginOptionsTab {
* ConfigurationPerspective.
*/
public static void showConfigPluginSettings(String pluginName, Composite
targetComposite) {
- Object pluginSourceData = pluginDataMap.get(pluginName);
+ Object pluginSourceData = pluginDataMap().get(pluginName);
if (pluginSourceData == null) {
return;
}
@@ -204,7 +203,27 @@ public class ConfigPluginOptionsTab {
/** Get all available plugin names for tree population */
public static java.util.Set<String> getPluginNames() {
- return pluginDataMap.keySet();
+ return pluginDataMap().keySet();
+ }
+
+ private static PluginOptionsData fallbackData;
+
+ private static Map<String, Object> pluginDataMap() {
+ HopGui hopGui = HopGui.peekInstance();
+ if (hopGui != null) {
+ return hopGui.getSessionSingleton(PluginOptionsData.class,
PluginOptionsData::new).map;
+ }
+ synchronized (ConfigPluginOptionsTab.class) {
+ if (fallbackData == null) {
+ fallbackData = new PluginOptionsData();
+ }
+ return fallbackData.map;
+ }
+ }
+
+ /** Per-session store of config-plugin GUI objects. */
+ static final class PluginOptionsData {
+ final Map<String, Object> map = new HashMap<>();
}
/** Show instruction message when no plugin is selected */
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspective.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspective.java
index 2be21509b4..0feb1b3a69 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspective.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspective.java
@@ -181,7 +181,7 @@ public class ExecutionPerspective implements
IHopPerspective, TabClosable {
private static final String TAB_KEY_DELIMITER = "\t";
public static final String SNAP_ID_EIL_REFRESH = "EILRefresh";
- @Getter private static ExecutionPerspective instance;
+ private static ExecutionPerspective instance;
private boolean onlyShowingParents = true;
private boolean onlyShowingFailed;
@@ -227,6 +227,18 @@ public class ExecutionPerspective implements
IHopPerspective, TabClosable {
instance = this;
}
+ public static ExecutionPerspective getInstance() {
+ try {
+ ExecutionPerspective fromGui =
HopGui.findSessionPerspective(ExecutionPerspective.class);
+ if (fromGui != null) {
+ return fromGui;
+ }
+ } catch (Throwable e) {
+ // No HopGuiImpl in unit tests
+ }
+ return instance;
+ }
+
/**
* When this perspective is disabled (an exclusion in
disabledGuiElements.xml) HopGui skips it
* while loading the perspectives, so {@link #initialize(HopGui, Composite)}
never runs. The
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
index ae1696b0df..deafce40fb 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
@@ -352,13 +352,25 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
}
public static ExplorerPerspective getInstance() {
- // There can be only one
+ ExplorerPerspective fromGui = sessionPerspectiveOrNull();
+ if (fromGui != null) {
+ return fromGui;
+ }
+ // Fallback for tests and the disabled-perspective case (constructed,
never initialized).
if (instance == null) {
new ExplorerPerspective();
}
return instance;
}
+ private static ExplorerPerspective sessionPerspectiveOrNull() {
+ try {
+ return HopGui.findSessionPerspective(ExplorerPerspective.class);
+ } catch (Throwable e) {
+ return null;
+ }
+ }
+
@Override
public String getId() {
return "explorer-perspective";
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 fcb3580bf0..bcdc58f1c7 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
@@ -28,7 +28,6 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import lombok.Getter;
import org.apache.commons.vfs2.FileObject;
import org.apache.hop.core.Const;
import org.apache.hop.core.Props;
@@ -230,7 +229,7 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
private static final int FILTER_DEBOUNCE_MS = 250;
- @Getter private static MetadataPerspective instance;
+ private static MetadataPerspective instance;
private HopGui hopGui;
private SashForm sash;
@@ -277,6 +276,18 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
this.metadataFileType = new MetadataFileType();
}
+ public static MetadataPerspective getInstance() {
+ try {
+ MetadataPerspective fromGui =
HopGui.findSessionPerspective(MetadataPerspective.class);
+ if (fromGui != null) {
+ return fromGui;
+ }
+ } catch (Throwable e) {
+ // No HopGuiImpl in unit tests
+ }
+ return instance;
+ }
+
/**
* When this perspective is disabled (an exclusion in
disabledGuiElements.xml) HopGui skips it
* while loading the perspectives, so initialize() never runs. The singleton
still exists because
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/shared/SashFormMemory.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/shared/SashFormMemory.java
index 4c4b60fa3a..502f6ad4aa 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/shared/SashFormMemory.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/shared/SashFormMemory.java
@@ -18,9 +18,11 @@
package org.apache.hop.ui.hopgui.shared;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import org.apache.hop.core.logging.LogChannel;
import org.apache.hop.history.AuditList;
import org.apache.hop.history.AuditManager;
@@ -52,8 +54,12 @@ public final class SashFormMemory {
*/
private static final int[] DEFAULT_WEIGHTS = {20, 80};
- /** Every tracked sash form, keyed by its audit key, so {@link #resetAll()}
can reach them all. */
- private static final Map<String, Tracked> TRACKED = new LinkedHashMap<>();
+ /**
+ * Tracked sash forms of one Display, keyed by audit key. Hop Web has a
Display per UISession; a
+ * process-wide map would let {@link #resetAll()} touch another session's
widgets.
+ */
+ private static final Map<Display, Map<String, Tracked>> TRACKED_BY_DISPLAY =
+ new ConcurrentHashMap<>();
private record Tracked(SashForm sashForm, int[] defaultWeights) {}
@@ -76,7 +82,7 @@ public final class SashFormMemory {
}
int[] defaults = defaultsOrFallback(defaultWeights);
restore(sashForm, key, defaults);
- TRACKED.put(key, new Tracked(sashForm, defaults));
+ trackedFor(sashForm.getDisplay()).put(key, new Tracked(sashForm,
defaults));
Display display = sashForm.getDisplay();
Runnable saver = () -> save(sashForm, key);
@@ -149,10 +155,18 @@ public final class SashFormMemory {
* startup).
*/
public static void resetAll() {
- for (Map.Entry<String, Tracked> entry : TRACKED.entrySet()) {
- Tracked tracked = entry.getValue();
- SashForm sashForm = tracked.sashForm();
- int[] defaults = tracked.defaultWeights();
+ Display display = Display.getCurrent();
+ if (display == null || display.isDisposed()) {
+ return;
+ }
+ Map<String, Tracked> tracked = TRACKED_BY_DISPLAY.get(display);
+ if (tracked == null) {
+ return;
+ }
+ for (Map.Entry<String, Tracked> entry : tracked.entrySet()) {
+ Tracked item = entry.getValue();
+ SashForm sashForm = item.sashForm();
+ int[] defaults = item.defaultWeights();
if (sashForm != null && !sashForm.isDisposed() && defaults != null &&
defaults.length > 0) {
sashForm.setWeights(defaults);
}
@@ -160,6 +174,21 @@ public final class SashFormMemory {
}
}
+ private static Map<String, Tracked> trackedFor(Display display) {
+ return TRACKED_BY_DISPLAY.computeIfAbsent(
+ display,
+ d -> {
+ d.addListener(SWT.Dispose, e -> TRACKED_BY_DISPLAY.remove(d));
+ return Collections.synchronizedMap(new LinkedHashMap<>());
+ });
+ }
+
+ /** Visible for tests: how many sash forms this Display currently tracks. */
+ static int trackedCount(Display display) {
+ Map<String, Tracked> tracked = TRACKED_BY_DISPLAY.get(display);
+ return tracked == null ? 0 : tracked.size();
+ }
+
/** The caller's defaults, or the shared {@link #DEFAULT_WEIGHTS} when none
were supplied. */
private static int[] defaultsOrFallback(int[] defaultWeights) {
return (defaultWeights != null && defaultWeights.length > 0) ?
defaultWeights : DEFAULT_WEIGHTS;
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/shared/SwtGc.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/shared/SwtGc.java
index 253fb3e598..4ec4264b9a 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/shared/SwtGc.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/shared/SwtGc.java
@@ -55,12 +55,14 @@ public class SwtGc implements IGc {
* its SWT {@link Image} bitmaps) that is never disposed — a severe handle
leak on large graphs
* (e.g. Data Vault models with many table icons redrawn while dragging).
*
- * <p>Keyed by SVG filename and dark-mode flag so theme changes get a
correct rendering.
+ * <p>Keyed by Device (one Display per Hop Web session), then SVG filename
and dark-mode flag. A
+ * process-wide cache would share (and dispose) session-unique SWT images.
*/
- private static final Map<String, SwtUniversalImage> SVG_IMAGE_CACHE = new
ConcurrentHashMap<>();
+ private static final Map<Device, Map<String, SwtUniversalImage>>
SVG_IMAGE_CACHE_BY_DEVICE =
+ new ConcurrentHashMap<>();
- private static final Object SVG_CACHE_HOOK_LOCK = new Object();
- private static volatile boolean svgCacheDisposeHookRegistered;
+ private static final java.util.Set<Device> SVG_CACHE_DISPOSE_HOOKS =
+ ConcurrentHashMap.newKeySet();
protected Color background;
@@ -565,34 +567,41 @@ public class SwtGc implements IGc {
SvgCacheEntry cacheEntry = SvgCache.loadSvg(svgFile);
boolean darkMode = PropsUi.getInstance().isDarkMode();
String cacheKey = svgFile.getFilename() + (darkMode ? "|dark" : "|light");
- ensureSvgImageCacheDisposeHook(gc.getDevice());
- return SVG_IMAGE_CACHE.computeIfAbsent(
- cacheKey,
- key -> new SwtUniversalImageSvg(new
SvgImage(cacheEntry.getSvgDocument()), false));
+ Device device = gc.getDevice();
+ ensureSvgImageCacheDisposeHook(device);
+ return SVG_IMAGE_CACHE_BY_DEVICE
+ .computeIfAbsent(device, d -> new ConcurrentHashMap<>())
+ .computeIfAbsent(
+ cacheKey,
+ key -> new SwtUniversalImageSvg(new
SvgImage(cacheEntry.getSvgDocument()), false));
}
private static void ensureSvgImageCacheDisposeHook(Device device) {
- if (svgCacheDisposeHookRegistered || !(device instanceof Display display))
{
+ if (!(device instanceof Display display) ||
!SVG_CACHE_DISPOSE_HOOKS.add(device)) {
return;
}
- synchronized (SVG_CACHE_HOOK_LOCK) {
- if (svgCacheDisposeHookRegistered) {
- return;
- }
- display.addListener(
- SWT.Dispose,
- event -> {
- for (SwtUniversalImage image : SVG_IMAGE_CACHE.values()) {
- try {
- image.dispose();
- } catch (Exception ignored) {
- // best-effort cleanup at display shutdown
- }
+ display.addListener(
+ SWT.Dispose,
+ event -> {
+ SVG_CACHE_DISPOSE_HOOKS.remove(device);
+ Map<String, SwtUniversalImage> cache =
SVG_IMAGE_CACHE_BY_DEVICE.remove(device);
+ if (cache == null) {
+ return;
+ }
+ for (SwtUniversalImage image : cache.values()) {
+ try {
+ image.dispose();
+ } catch (Exception ignored) {
+ // best-effort cleanup at display shutdown
}
- SVG_IMAGE_CACHE.clear();
- });
- svgCacheDisposeHookRegistered = true;
- }
+ }
+ });
+ }
+
+ /** Visible for tests: number of cached SVG images for a Device. */
+ static int cachedSvgCount(Device device) {
+ Map<String, SwtUniversalImage> cache =
SVG_IMAGE_CACHE_BY_DEVICE.get(device);
+ return cache == null ? 0 : cache.size();
}
@Override
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/HopGuiKeyHandlerTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/HopGuiKeyHandlerTest.java
index 536003292f..7cab8c20df 100644
--- a/ui/src/test/java/org/apache/hop/ui/hopgui/HopGuiKeyHandlerTest.java
+++ b/ui/src/test/java/org/apache/hop/ui/hopgui/HopGuiKeyHandlerTest.java
@@ -226,6 +226,20 @@ class HopGuiKeyHandlerTest {
verify(terminalWidget, never()).addKeyListener(keyHandler);
}
+ @Test
+ void separateHandlersDoNotShareParentObjects() {
+ HopGuiKeyHandler first = new HopGuiKeyHandler();
+ HopGuiKeyHandler second = new HopGuiKeyHandler();
+ Object parent = new Object();
+
+ first.addParentObjectToHandle(parent);
+
+ assertTrue(first.parentObjects.contains(parent));
+ assertFalse(
+ second.parentObjects.contains(parent),
+ "Each RAP UISession must have its own key handler parent set");
+ }
+
@Test
void pipelineGraphBindsArrowKeys() {
assertGraphBindsArrowKeys(HopGuiPipelineGraph.class);
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/StaticSwtSessionResourceTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/StaticSwtSessionResourceTest.java
new file mode 100644
index 0000000000..0d28f27ff2
--- /dev/null
+++
b/ui/src/test/java/org/apache/hop/ui/hopgui/StaticSwtSessionResourceTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.assertTrue;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+import org.apache.hop.ui.hopgui.perspective.execution.ExecutionLogPanel;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Shell;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Static SWT resources are RAP-session-unique. Caching them in {@code static}
fields lets one Hop
+ * Web session dispose images/widgets still used by another.
+ */
+class StaticSwtSessionResourceTest {
+
+ private static final Set<Class<?>> SWT_RESOURCE_TYPES =
+ Set.of(
+ Image.class,
+ Color.class,
+ Font.class,
+ Control.class,
+ Shell.class,
+ SashForm.class,
+ GC.class);
+
+ @Test
+ void uiClassesDoNotKeepSwtResourcesInStaticFields() throws Exception {
+ List<String> violations = new ArrayList<>();
+ Path root =
+ Path.of(
+
ExecutionLogPanel.class.getProtectionDomain().getCodeSource().getLocation().toURI());
+ ClassLoader classLoader =
StaticSwtSessionResourceTest.class.getClassLoader();
+
+ try (Stream<Path> files = Files.walk(root)) {
+ for (Path file :
+ (Iterable<Path>)
files.filter(StaticSwtSessionResourceTest::isClassFile)::iterator) {
+ String className =
+ root.relativize(file)
+ .toString()
+ .replace(java.io.File.separatorChar, '.')
+ .replaceAll("\\.class$", "");
+ if (!className.startsWith("org.apache.hop.ui")) {
+ continue;
+ }
+ Class<?> clazz;
+ try {
+ clazz = Class.forName(className, false, classLoader);
+ } catch (Throwable e) {
+ continue;
+ }
+ for (Field field : clazz.getDeclaredFields()) {
+ if (!Modifier.isStatic(field.getModifiers()) || field.isSynthetic())
{
+ continue;
+ }
+ if (isSwtResource(field.getType())) {
+ violations.add(
+ clazz.getName() + "." + field.getName() + " : " +
field.getType().getName());
+ }
+ }
+ }
+ }
+
+ assertTrue(
+ violations.isEmpty(), "Static SWT resource fields leak RAP UISession
state: " + violations);
+ }
+
+ private static boolean isSwtResource(Class<?> type) {
+ for (Class<?> swtType : SWT_RESOURCE_TYPES) {
+ if (swtType.isAssignableFrom(type)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isClassFile(Path path) {
+ String name = path.getFileName().toString();
+ return name.endsWith(".class") && !name.contains("$");
+ }
+}
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/file/shared/DrillDownGuiPluginTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/file/shared/DrillDownGuiPluginTest.java
new file mode 100644
index 0000000000..cfef9a38bf
--- /dev/null
+++
b/ui/src/test/java/org/apache/hop/ui/hopgui/file/shared/DrillDownGuiPluginTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.file.shared;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.engine.IPipelineEngine;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+class DrillDownGuiPluginTest {
+
+ @AfterEach
+ void tearDown() {
+ DrillDownGuiPlugin.cleanupSession("gui-a");
+ DrillDownGuiPlugin.cleanupSession("gui-b");
+ DrillDownGuiPlugin.cleanupSession("session-1");
+ }
+
+ @Test
+ void cleanupOnRunStartDoesNotClearAnotherSession() {
+ IPipelineEngine<PipelineMeta> engineA = engine("gui-a", "log-a");
+ IPipelineEngine<PipelineMeta> engineB = engine("gui-b", "log-b");
+
+ DrillDownGuiPlugin.registerRunningPipeline("log-a", engineA);
+ DrillDownGuiPlugin.registerRunningPipeline("log-b", engineB);
+
+ DrillDownGuiPlugin.cleanupOnRunStart("gui-a");
+
+ assertNull(DrillDownGuiPlugin.runningPipeline("gui-a", "log-a"));
+ assertSame(engineB, DrillDownGuiPlugin.runningPipeline("gui-b", "log-b"));
+ }
+
+ @Test
+ void hopGuiIdWalksExtensionData() {
+ IPipelineEngine<PipelineMeta> engine = engine("session-1", "log-1");
+ assertEquals("session-1", DrillDownGuiPlugin.hopGuiIdOf(engine));
+ }
+
+ @SuppressWarnings("unchecked")
+ private static IPipelineEngine<PipelineMeta> engine(String hopGuiId, String
logChannelId) {
+ IPipelineEngine<PipelineMeta> pipeline = mock(IPipelineEngine.class);
+ Map<String, Object> data = new HashMap<>();
+ data.put(DrillDownGuiPlugin.HOP_GUI_ID, hopGuiId);
+ when(pipeline.getExtensionDataMap()).thenReturn(data);
+ when(pipeline.getLogChannelId()).thenReturn(logChannelId);
+ when(pipeline.getParent()).thenReturn(null);
+ return pipeline;
+ }
+}