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));
+  }
+}

Reply via email to