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 ae93af2252 add guarding for Display, fixes #8047
ae93af2252 is described below

commit ae93af22521c9a18edfa58ffcaaa96c3145788a9
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Thu Aug 27 09:36:25 2026 +0200

    add guarding for Display, fixes #8047
---
 .../org/apache/hop/ui/core/gui/HopNamespace.java   | 48 ++++++++++--
 .../hop/ui/core/gui/HopNamespaceHeadlessTest.java  | 89 ++++++++++++++++++++++
 2 files changed, 129 insertions(+), 8 deletions(-)

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 a507eace48..f3d71258fe 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
@@ -19,6 +19,7 @@ package org.apache.hop.ui.core.gui;
 
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import org.apache.hop.core.Const;
 import org.apache.hop.core.exception.HopRuntimeException;
 import org.apache.hop.core.util.Utils;
 import org.eclipse.swt.SWT;
@@ -56,9 +57,8 @@ 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 (hasUserInterface()) {
+      String sessionNamespace = namespaceOfCurrentDisplay();
       if (!Utils.isEmpty(sessionNamespace)) {
         return sessionNamespace;
       }
@@ -75,12 +75,44 @@ public class HopNamespace {
    */
   public static final void setNamespace(String namespace) {
     getInstance().namespace = namespace;
+    if (hasUserInterface()) {
+      rememberForCurrentDisplay(namespace);
+    }
+  }
+
+  /**
+   * Whether this process has a user interface at all.
+   *
+   * <p>Touching {@link Display} loads the SWT native libraries. A Hop Server 
has no reason to load
+   * them and in a container no way to: there is no GTK, so the attempt fails 
with an {@link
+   * UnsatisfiedLinkError} and the server never starts. It enables a project 
on startup like every
+   * other Hop tool, which is what brings it here.
+   *
+   * <p>The two methods below are kept apart from the ones above on purpose: 
it keeps every
+   * reference to {@link Display} out of the code path a headless process runs.
+   */
+  private static boolean hasUserInterface() {
+    return "GUI".equalsIgnoreCase(Const.getHopPlatformRuntime());
+  }
+
+  /** The namespace of the session on this thread, or null. Only call with a 
user interface. */
+  private static String namespaceOfCurrentDisplay() {
     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);
+    if (display == null || display.isDisposed()) {
+      return null;
+    }
+    return NAMESPACE_BY_DISPLAY.get(display);
+  }
+
+  /** Remember the namespace for the session on this thread. Only call with a 
user interface. */
+  private static void rememberForCurrentDisplay(String namespace) {
+    Display display = Display.getCurrent();
+    if (display == null || display.isDisposed()) {
+      return;
+    }
+    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/test/java/org/apache/hop/ui/core/gui/HopNamespaceHeadlessTest.java 
b/ui/src/test/java/org/apache/hop/ui/core/gui/HopNamespaceHeadlessTest.java
new file mode 100644
index 0000000000..ee116840e7
--- /dev/null
+++ b/ui/src/test/java/org/apache/hop/ui/core/gui/HopNamespaceHeadlessTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.core.gui;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.File;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.Const;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+/**
+ * A Hop Server enables a project on startup just like every other Hop tool, 
and that sets the
+ * namespace. It has no user interface, and in a container no GTK either, so 
anything on that path
+ * that touches SWT takes the whole server down with an {@link 
UnsatisfiedLinkError} before it
+ * serves a single request.
+ */
+class HopNamespaceHeadlessTest {
+
+  private String originalRuntime;
+
+  @BeforeEach
+  void rememberRuntime() {
+    originalRuntime = System.getProperty(Const.HOP_PLATFORM_RUNTIME);
+    // A server, hop-run or a worker: nothing sets this.
+    System.clearProperty(Const.HOP_PLATFORM_RUNTIME);
+  }
+
+  @AfterEach
+  void restoreRuntime() {
+    if (originalRuntime == null) {
+      System.clearProperty(Const.HOP_PLATFORM_RUNTIME);
+    } else {
+      System.setProperty(Const.HOP_PLATFORM_RUNTIME, originalRuntime);
+    }
+  }
+
+  @Test
+  @DisplayName("Setting and reading a namespace without a user interface loads 
no SWT")
+  void namespaceWorksWithoutAUserInterface() throws Exception {
+    // Loaded where SWT cannot be reached at all, because on a developer 
machine it loads happily
+    // and the test would pass either way - a container without GTK is where 
it bites.
+    try (URLClassLoader noSwt = classLoaderWithoutSwt()) {
+      Class<?> namespaceClass = noSwt.loadClass(HopNamespace.class.getName());
+      namespaceClass.getMethod("setNamespace", String.class).invoke(null, 
"headless-project");
+
+      assertEquals("headless-project", 
namespaceClass.getMethod("getNamespace").invoke(null));
+    }
+  }
+
+  /** The test classpath, with every SWT class refused the way a server 
without GTK refuses them. */
+  private URLClassLoader classLoaderWithoutSwt() throws Exception {
+    List<URL> classpath = new ArrayList<>();
+    for (String entry : 
System.getProperty("java.class.path").split(File.pathSeparator)) {
+      classpath.add(new File(entry).toURI().toURL());
+    }
+    return new URLClassLoader(classpath.toArray(new URL[0]), 
ClassLoader.getPlatformClassLoader()) {
+      @Override
+      protected Class<?> loadClass(String name, boolean resolve) throws 
ClassNotFoundException {
+        if (name.startsWith("org.eclipse.swt")) {
+          throw new ClassNotFoundException(
+              "SWT must not be loaded without a user interface: " + name);
+        }
+        return super.loadClass(name, resolve);
+      }
+    };
+  }
+}

Reply via email to