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 c0cdaa12b7 Issue #8477 : Do not crash Hop Web project activation
before perspectives exist (#8484)
c0cdaa12b7 is described below
commit c0cdaa12b70db99f0da2b701cd3c6879faf8ee86
Author: Matt Casters <[email protected]>
AuthorDate: Mon Sep 21 13:33:01 2026 +0200
Issue #8477 : Do not crash Hop Web project activation before perspectives
exist (#8484)
---
.../apache/hop/projects/gui/ProjectsGuiPlugin.java | 40 ++++++++++++++++++----
.../ProjectsGuiPluginPerspectiveLookupTest.java | 39 +++++++++++++++++++++
.../main/java/org/apache/hop/ui/hopgui/HopGui.java | 17 +++++++--
.../ui/hopgui/delegates/HopGuiFileDelegate.java | 20 +++++++++--
.../execution/ExecutionPerspective.java | 6 ++++
.../perspective/explorer/ExplorerPerspective.java | 21 +++++++++---
.../ExecutionPerspectiveDisabledTest.java | 18 ++++++++++
.../perspective/explorer/ExplorerTabMenuTest.java | 31 +++++++++++++++++
8 files changed, 176 insertions(+), 16 deletions(-)
diff --git
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectsGuiPlugin.java
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectsGuiPlugin.java
index 310090d840..52c8d65a55 100644
---
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectsGuiPlugin.java
+++
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectsGuiPlugin.java
@@ -186,14 +186,21 @@ public class ProjectsGuiPlugin {
}
// Save execution perspective state (toolbar filters + open tabs) under
the current project
- // namespace before closing tabs / switching namespace.
+ // namespace before closing tabs / switching namespace. Hop Web can
enable a project before
+ // every perspective exists (issue #8477).
//
- ExecutionPerspective.getInstance().saveState();
+ ExecutionPerspective executionPerspective =
findExecutionPerspective(hopGui);
+ if (executionPerspective != null) {
+ executionPerspective.saveState();
+ }
// Save explorer layout (split panes, panel visibility) while tabs are
still open so the
// restored layout matches what the user had before the switch (issue
#7692 / #6708).
//
- ExplorerPerspective.getInstance().saveExplorerStateOnShutdown();
+ ExplorerPerspective explorerPerspective =
findExplorerPerspective(hopGui);
+ if (explorerPerspective != null) {
+ explorerPerspective.saveExplorerStateOnShutdown();
+ }
// Close's all (including execution information tabs)
//
@@ -310,9 +317,12 @@ public class ProjectsGuiPlugin {
// Restore the state of the execution perspective as well
//
- ExecutionPerspective.getInstance().restoreState();
- if (ExecutionPerspective.getInstance().isActive()) {
- ExecutionPerspective.getInstance().refresh();
+ executionPerspective = findExecutionPerspective(hopGui);
+ if (executionPerspective != null) {
+ executionPerspective.restoreState();
+ if (executionPerspective.isActive()) {
+ executionPerspective.refresh();
+ }
}
// Send out an event notifying that a new project is activated...
@@ -363,6 +373,24 @@ public class ProjectsGuiPlugin {
}
}
+ /**
+ * Session-scoped lookup so Hop Web does not NPE when a project is enabled
before perspectives
+ * exist (issue #8477).
+ */
+ static ExecutionPerspective findExecutionPerspective(HopGui hopGui) {
+ if (hopGui == null || hopGui.getPerspectiveManager() == null) {
+ return null;
+ }
+ return
hopGui.getPerspectiveManager().findPerspective(ExecutionPerspective.class);
+ }
+
+ static ExplorerPerspective findExplorerPerspective(HopGui hopGui) {
+ if (hopGui == null || hopGui.getPerspectiveManager() == null) {
+ return null;
+ }
+ return
hopGui.getPerspectiveManager().findPerspective(ExplorerPerspective.class);
+ }
+
/**
* Metadata elements which can't be loaded (a missing plugin, invalid JSON,
...) are ignored when
* we load a project. We check for those here and show one dialog listing
all of them so the user
diff --git
a/plugins/misc/projects/src/test/java/org/apache/hop/projects/gui/ProjectsGuiPluginPerspectiveLookupTest.java
b/plugins/misc/projects/src/test/java/org/apache/hop/projects/gui/ProjectsGuiPluginPerspectiveLookupTest.java
new file mode 100644
index 0000000000..96947680c1
--- /dev/null
+++
b/plugins/misc/projects/src/test/java/org/apache/hop/projects/gui/ProjectsGuiPluginPerspectiveLookupTest.java
@@ -0,0 +1,39 @@
+/*
+ * 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
+ *
+ * https://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.projects.gui;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Project activation must not assume the execution or explorer perspectives
exist yet (Hop Web init
+ * order, issue #8477).
+ */
+class ProjectsGuiPluginPerspectiveLookupTest {
+
+ @Test
+ void findExecutionPerspectiveReturnsNullWithoutHopGui() {
+ assertNull(ProjectsGuiPlugin.findExecutionPerspective(null));
+ }
+
+ @Test
+ void findExplorerPerspectiveReturnsNullWithoutHopGui() {
+ assertNull(ProjectsGuiPlugin.findExplorerPerspective(null));
+ }
+}
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 3f42992cbe..6722a2470d 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
@@ -806,10 +806,21 @@ public class HopGui
// Terminal restoration is handled by the Projects plugin
// Restore explorer perspective state (file explorer panel
visibility) for current
- // namespace (default or project set by extension point).
+ // namespace (default or project set by extension point). Skip when
a perspective was
+ // not loaded (disabled, or Hop Web init before loadPerspectives();
issue #8477).
//
- ExplorerPerspective.getInstance().applyRestoredState();
- ExecutionPerspective.getInstance().restoreState();
+ if (perspectiveManager != null) {
+ ExplorerPerspective explorerPerspective =
+ perspectiveManager.findPerspective(ExplorerPerspective.class);
+ if (explorerPerspective != null) {
+ explorerPerspective.applyRestoredState();
+ }
+ ExecutionPerspective executionPerspective =
+ perspectiveManager.findPerspective(ExecutionPerspective.class);
+ if (executionPerspective != null) {
+ executionPerspective.restoreState();
+ }
+ }
// We need to start tracking file history again.
//
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileDelegate.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileDelegate.java
index 79f7c4e854..8cbd1b7025 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileDelegate.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileDelegate.java
@@ -325,7 +325,7 @@ public class HopGuiFileDelegate {
// viewers from the previous project open. Callers that need to remember
tabs (project switch)
// must call ExecutionPerspective.saveState() first.
//
- ExecutionPerspective executionPerspective =
ExecutionPerspective.getInstance();
+ ExecutionPerspective executionPerspective = findExecutionPerspective();
if (executionPerspective != null) {
executionPerspective.closeAllTabs();
}
@@ -401,12 +401,12 @@ public class HopGuiFileDelegate {
// Save explorer perspective state (file explorer panel visibility)
//
- ExplorerPerspective explorerPerspective =
ExplorerPerspective.getInstance();
+ ExplorerPerspective explorerPerspective = findExplorerPerspective();
if (explorerPerspective != null) {
explorerPerspective.saveExplorerStateOnShutdown();
}
- ExecutionPerspective executionPerspective =
ExecutionPerspective.getInstance();
+ ExecutionPerspective executionPerspective = findExecutionPerspective();
if (executionPerspective != null) {
executionPerspective.saveState();
}
@@ -414,6 +414,20 @@ public class HopGuiFileDelegate {
return true;
}
+ private ExecutionPerspective findExecutionPerspective() {
+ if (hopGui.getPerspectiveManager() == null) {
+ return null;
+ }
+ return
hopGui.getPerspectiveManager().findPerspective(ExecutionPerspective.class);
+ }
+
+ private ExplorerPerspective findExplorerPerspective() {
+ if (hopGui.getPerspectiveManager() == null) {
+ return null;
+ }
+ return
hopGui.getPerspectiveManager().findPerspective(ExplorerPerspective.class);
+ }
+
/** Show all the recent files in a new dialog... */
public void fileOpenRecent() {
// Get the recent files for the active perspective...
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 0feb1b3a69..2bb85d595c 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
@@ -236,6 +236,12 @@ public class ExecutionPerspective implements
IHopPerspective, TabClosable {
} catch (Throwable e) {
// No HopGuiImpl in unit tests
}
+ // Fallback for tests and the disabled-perspective case (constructed,
never initialized).
+ // Hop Web project activation can reach us before loadPerspectives() has
run, or when this
+ // perspective is excluded; callers must not NPE on a null singleton
(issue #8477).
+ if (instance == null) {
+ new ExecutionPerspective();
+ }
return instance;
}
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 3b859237fa..6e1e9e3d01 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
@@ -2154,12 +2154,27 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
TabCloseHandler tabCloseHandler = new TabCloseHandler(this, folder);
new TabItemReorder(this, folder);
+ addTabSplitMenuItems(folder, tabCloseHandler);
+ return folder;
+ }
+
+ /**
+ * The tab popup is owned by {@link TabCloseHandler} and is not attached to
the folder. Hop Web
+ * layout restore can run before that menu exists (issue #8477).
+ */
+ static boolean isUsableTabMenu(Menu menu) {
+ return menu != null && !menu.isDisposed();
+ }
+
+ private void addTabSplitMenuItems(CTabFolder folder, TabCloseHandler
tabCloseHandler) {
+ Menu menu = tabCloseHandler.getMenu();
+ if (!isUsableTabMenu(menu)) {
+ return;
+ }
// Split ("Move to Right") works in both desktop and web since it operates
within the docked
// editor layout. Detach ("Move to New Window") depends on floating
windows, which don't work
// under RAP, so it is desktop-only.
- // The tab menu is not attached to the folder, so take it from the close
handler that owns it.
- Menu menu = tabCloseHandler.getMenu();
new MenuItem(menu, SWT.SEPARATOR);
MenuItem miSplitMove = new MenuItem(menu, SWT.NONE);
miSplitMove.setText(BaseMessages.getString(PKG,
"ExplorerPerspective.TabMenu.MoveToRight"));
@@ -2264,8 +2279,6 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
}
}
});
-
- return folder;
}
private CTabFolder getTargetTabFolder() {
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspectiveDisabledTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspectiveDisabledTest.java
index bc80b89ca5..218ecfc79f 100644
---
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspectiveDisabledTest.java
+++
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspectiveDisabledTest.java
@@ -19,8 +19,10 @@ package org.apache.hop.ui.hopgui.perspective.execution;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
+import java.lang.reflect.Field;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -89,4 +91,20 @@ class ExecutionPerspectiveDisabledTest {
void aDisabledPerspectiveIsNeverActive() {
assertFalse(disabledPerspective.isActive());
}
+
+ @Test
+ void getInstanceDoesNotReturnNullWhenNeverConstructed() throws Exception {
+ Field instanceField =
ExecutionPerspective.class.getDeclaredField("instance");
+ instanceField.setAccessible(true);
+ Object previous = instanceField.get(null);
+ instanceField.set(null, null);
+ try {
+ ExecutionPerspective perspective = ExecutionPerspective.getInstance();
+ assertNotNull(perspective);
+ assertDoesNotThrow(perspective::saveState);
+ assertDoesNotThrow(perspective::restoreState);
+ } finally {
+ instanceField.set(null, previous);
+ }
+ }
}
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerTabMenuTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerTabMenuTest.java
new file mode 100644
index 0000000000..fd87db3285
--- /dev/null
+++
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerTabMenuTest.java
@@ -0,0 +1,31 @@
+/*
+ * 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
+ *
+ * https://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.perspective.explorer;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import org.junit.jupiter.api.Test;
+
+/** Issue #8477: restoring explorer layout must not create MenuItems on a
missing popup. */
+class ExplorerTabMenuTest {
+
+ @Test
+ void aMissingTabMenuIsNotUsable() {
+ assertFalse(ExplorerPerspective.isUsableTabMenu(null));
+ }
+}