This is an automated email from the ASF dual-hosted git repository.

bamaer 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 1c3c1aa290 Issue #8728 : Report VFS explorer driver failures and stop 
the status line (#8842)
1c3c1aa290 is described below

commit 1c3c1aa290610e0afc13e6c0b2507633a9edb99b
Author: Matt Casters <[email protected]>
AuthorDate: Sat Oct 10 13:29:26 2026 +0200

    Issue #8728 : Report VFS explorer driver failures and stop the status line 
(#8842)
    
    A missing VFS library throws Error, which the explorer listings did not 
catch. The operation stayed running and the failure was only printed on the Hop 
GUI console.
---
 .../hopgui/vfs/explorer/VfsExplorerOperation.java  | 76 ++++++++++++++++++++++
 .../vfs/explorer/VfsFileExplorerLocation.java      | 35 +++++-----
 .../hop/ui/hopgui/vfs/explorer/VfsFileListing.java | 31 +++++++--
 .../hopgui/vfs/explorer/VfsExplorerStatusTest.java | 52 +++++++++++++++
 .../ui/hopgui/vfs/explorer/VfsFileListingTest.java | 76 ++++++++++++++++++++++
 5 files changed, 251 insertions(+), 19 deletions(-)

diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsExplorerOperation.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsExplorerOperation.java
index 42a737b256..00026fa99a 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsExplorerOperation.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsExplorerOperation.java
@@ -18,6 +18,7 @@
 package org.apache.hop.ui.hopgui.vfs.explorer;
 
 import lombok.Getter;
+import org.apache.hop.core.Const;
 
 /** One listing or file change. Stop interrupts the worker; it does not close 
the file system. */
 @Getter
@@ -79,6 +80,81 @@ public class VfsExplorerOperation {
     worker = null;
   }
 
+  /**
+   * Record {@code thrown} and stop the clock. A missing library is an {@link 
Error}, and {@link
+   * ExceptionInInitializerError} often has no message of its own, so the text 
includes the cause.
+   */
+  public void fail(Throwable thrown) {
+    fail(describe(thrown), Const.getClassicStackTrace(thrown));
+  }
+
+  /**
+   * Stop wins over the failure: the user already interrupted this operation. 
Otherwise the failure
+   * is recorded, including an {@link Error} from a VFS driver.
+   */
+  void failUnlessCancelled(Throwable thrown) {
+    if (isCancelled()) {
+      complete();
+      return;
+    }
+    fail(thrown);
+  }
+
+  /**
+   * Status text for a driver failure. Walks to the first cause that has a 
message, and names an
+   * {@link Error} or {@link ClassNotFoundException} so a bare missing-class 
name is readable.
+   */
+  static String describe(Throwable thrown) {
+    if (thrown == null) {
+      return "";
+    }
+    String message = messageOf(thrown);
+    Throwable cause = thrown.getCause();
+    if (cause != null && cause != thrown && isDriverFailure(cause)) {
+      String causeText = describe(cause);
+      if (!causeText.isEmpty() && !message.contains(causeText)) {
+        message = message + ": " + causeText;
+      }
+    }
+    return message;
+  }
+
+  private static String messageOf(Throwable thrown) {
+    Throwable reported = thrown;
+    while (isBlank(reported.getMessage())
+        && reported.getCause() != null
+        && reported.getCause() != reported) {
+      reported = reported.getCause();
+    }
+    String message = reported.getMessage();
+    if (isBlank(message)) {
+      message = reported.toString();
+    }
+    if (reported instanceof Error || reported instanceof 
ClassNotFoundException) {
+      String type = reported.getClass().getSimpleName();
+      if (!message.startsWith(type)) {
+        message = type + ": " + message;
+      }
+    }
+    return message;
+  }
+
+  private static boolean isDriverFailure(Throwable thrown) {
+    Throwable current = thrown;
+    while (current != null) {
+      if (current instanceof Error || current instanceof 
ClassNotFoundException) {
+        return true;
+      }
+      Throwable cause = current.getCause();
+      current = cause == current ? null : cause;
+    }
+    return false;
+  }
+
+  private static boolean isBlank(String text) {
+    return text == null || text.isBlank();
+  }
+
   public boolean isFinished() {
     return status != Status.RUNNING;
   }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsFileExplorerLocation.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsFileExplorerLocation.java
index 5785431082..f32960ed66 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsFileExplorerLocation.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsFileExplorerLocation.java
@@ -26,6 +26,8 @@ import org.apache.commons.vfs2.Selectors;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElement;
+import org.apache.hop.core.logging.HopLogStore;
+import org.apache.hop.core.logging.LogChannel;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.core.vfs.HopVfs;
 import org.apache.hop.i18n.BaseMessages;
@@ -393,7 +395,7 @@ public class VfsFileExplorerLocation extends Composite {
                       }
                       explorer.refreshOperations();
                     });
-              } catch (Exception e) {
+              } catch (Throwable e) {
                 fail(operation, e);
                 refreshPanel(display);
               }
@@ -508,7 +510,7 @@ public class VfsFileExplorerLocation extends Composite {
                       }
                       explorer.refreshOperations();
                     });
-              } catch (Exception e) {
+              } catch (Throwable e) {
                 fail(operation, e);
                 refreshPanel(display);
               }
@@ -791,7 +793,7 @@ public class VfsFileExplorerLocation extends Composite {
       if (archiveUri != null) {
         navigateTo(archiveUri, true);
       }
-    } catch (Exception e) {
+    } catch (Throwable e) {
       failOnUi(e);
     }
   }
@@ -1090,7 +1092,7 @@ public class VfsFileExplorerLocation extends Composite {
                       }
                       explorer.refreshOperations();
                     });
-              } catch (Exception e) {
+              } catch (Throwable e) {
                 fail(operation, e);
                 refreshPanel(display);
               }
@@ -1116,7 +1118,7 @@ public class VfsFileExplorerLocation extends Composite {
                 }
                 show = change.run();
                 operation.complete();
-              } catch (Exception e) {
+              } catch (Throwable e) {
                 fail(operation, e);
               }
               String folder = show;
@@ -1538,7 +1540,7 @@ public class VfsFileExplorerLocation extends Composite {
         return;
       }
       type.openFile(explorer.getHopGui(), row.getUri(), variables);
-    } catch (Exception e) {
+    } catch (Throwable e) {
       failOnUi(e);
     }
   }
@@ -1901,21 +1903,24 @@ public class VfsFileExplorerLocation extends Composite {
     return !isDisposed() && generation.isCurrent(token);
   }
 
-  private void fail(VfsExplorerOperation operation, Exception exception) {
-    if (operation.isCancelled() || Thread.currentThread().isInterrupted()) {
-      operation.complete();
-      return;
+  /**
+   * Finish {@code operation} after a driver failure. {@link Error} (a missing 
library, a broken
+   * static initializer) is not an {@link Exception}. Leaving it uncaught 
keeps the status line
+   * ticking and prints the stack only on the Hop GUI console.
+   */
+  private void fail(VfsExplorerOperation operation, Throwable thrown) {
+    operation.failUnlessCancelled(thrown);
+    if (operation.getStatus() == VfsExplorerOperation.Status.FAILED
+        && HopLogStore.isInitialized()) {
+      LogChannel.UI.logError(Const.NVL(operation.getDescription(), ""), 
thrown);
     }
-    operation.fail(
-        Const.NVL(exception.getMessage(), exception.toString()),
-        Const.getClassicStackTrace(exception));
   }
 
-  private void failOnUi(Exception exception) {
+  private void failOnUi(Throwable thrown) {
     VfsExplorerOperation operation =
         explorer.beginOperation(
             BaseMessages.getString(PKG, "VfsFileExplorer.Error.Title"), 
getLocationText());
-    fail(operation, exception);
+    fail(operation, thrown);
     explorer.refreshOperations();
   }
 
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsFileListing.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsFileListing.java
index 9e90c4dc43..2cbea700a4 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsFileListing.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsFileListing.java
@@ -20,7 +20,6 @@ package org.apache.hop.ui.hopgui.vfs.explorer;
 import java.util.ArrayList;
 import java.util.List;
 import org.apache.commons.vfs2.FileObject;
-import org.apache.commons.vfs2.FileSystemException;
 import org.apache.hop.core.search.SearchMatcher;
 import org.apache.hop.core.util.Utils;
 
@@ -29,25 +28,49 @@ public final class VfsFileListing {
 
   private VfsFileListing() {}
 
-  public static List<VfsFileRow> childrenOf(FileObject folder) throws 
FileSystemException {
+  public static List<VfsFileRow> childrenOf(FileObject folder) throws 
Exception {
     FileObject[] children = folder.getChildren();
     if (children == null || children.length == 0) {
       return List.of();
     }
     List<VfsFileRow> rows = new ArrayList<>(children.length);
+    Exception firstFailure = null;
     for (FileObject child : children) {
       if (child == null) {
         continue;
       }
       try {
         rows.add(VfsFileDetails.read(child));
-      } catch (Exception ignored) {
-        // One unreadable child does not hide the rest of the folder.
+      } catch (Exception e) {
+        // A missing library or a broken provider fails the whole folder. One 
unreadable child
+        // does not hide the rest.
+        if (isDriverFailure(e)) {
+          throw e;
+        }
+        if (firstFailure == null) {
+          firstFailure = e;
+        }
       }
     }
+    if (rows.isEmpty() && firstFailure != null) {
+      throw firstFailure;
+    }
     return rows;
   }
 
+  /** A linkage or class-loading failure means the provider itself is 
unusable. */
+  static boolean isDriverFailure(Throwable thrown) {
+    Throwable current = thrown;
+    while (current != null) {
+      if (current instanceof Error || current instanceof 
ClassNotFoundException) {
+        return true;
+      }
+      Throwable cause = current.getCause();
+      current = cause == current ? null : cause;
+    }
+    return false;
+  }
+
   /**
    * Folders for the left-hand tree, in case-insensitive name order. Hidden 
folders (names starting
    * with {@code .}) are omitted unless {@code showHiddenFolders} is set. 
Files are not included.
diff --git 
a/ui/src/test/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsExplorerStatusTest.java
 
b/ui/src/test/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsExplorerStatusTest.java
index 534f7b426b..c18b1fef04 100644
--- 
a/ui/src/test/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsExplorerStatusTest.java
+++ 
b/ui/src/test/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsExplorerStatusTest.java
@@ -18,9 +18,12 @@
 package org.apache.hop.ui.hopgui.vfs.explorer;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.util.List;
+import org.apache.commons.vfs2.FileSystemException;
 import org.junit.jupiter.api.Test;
 
 class VfsExplorerStatusTest {
@@ -45,6 +48,55 @@ class VfsExplorerStatusTest {
     assertTrue(line.contains("ms - 200 files (2.1GB) - 10 files selected 
(120MB)"));
   }
 
+  @Test
+  void missingLibraryFailsTheOperationAndStopsTheStatus() {
+    VfsExplorerOperation operation =
+        new VfsExplorerOperation("Listing smb://share/data", 
"smb://share/data");
+    assertFalse(operation.isFinished());
+
+    operation.failUnlessCancelled(
+        new ExceptionInInitializerError(new 
NoClassDefFoundError("com.example.Missing")));
+
+    assertEquals(VfsExplorerOperation.Status.FAILED, operation.getStatus());
+    assertTrue(operation.isFinished());
+    assertTrue(operation.getErrorMessage().contains("NoClassDefFoundError"));
+    assertTrue(operation.getErrorMessage().contains("com.example.Missing"));
+    assertTrue(operation.getDetail().contains("com.example.Missing"));
+    String line = VfsExplorerOperationsPanel.formatStatusLine(operation);
+    assertTrue(line.contains("Failed"));
+    assertTrue(line.contains("com.example.Missing"));
+    assertFalse(line.contains("Running"));
+  }
+
+  @Test
+  void wrappedMissingClassIsNamedOnTheStatus() throws Exception {
+    FileSystemException failure =
+        new FileSystemException("Could not list folder", new 
NoClassDefFoundError("org.acme.Lib"));
+    VfsExplorerOperation operation = new VfsExplorerOperation("Listing", 
"smb://share");
+    operation.fail(failure);
+
+    assertTrue(operation.getErrorMessage().contains("NoClassDefFoundError"));
+    assertTrue(operation.getErrorMessage().contains("org.acme.Lib"));
+    
assertTrue(VfsExplorerOperationsPanel.formatStatusLine(operation).contains("org.acme.Lib"));
+  }
+
+  @Test
+  void ordinaryFailureKeepsItsOwnMessage() {
+    VfsExplorerOperation operation = new VfsExplorerOperation("Listing", 
"file:///tmp");
+    operation.fail(new IllegalStateException("folder is gone"));
+    assertEquals("folder is gone", operation.getErrorMessage());
+  }
+
+  @Test
+  void stopWinsOverADriverFailure() {
+    VfsExplorerOperation operation = new VfsExplorerOperation("Listing", 
"smb://share");
+    operation.cancel();
+    operation.failUnlessCancelled(new 
NoClassDefFoundError("com.example.Missing"));
+    assertEquals(VfsExplorerOperation.Status.CANCELLED, operation.getStatus());
+    assertTrue(operation.isFinished());
+    assertNull(operation.getErrorMessage());
+  }
+
   @Test
   void oneRowUsesTheSingular() {
     VfsListingCounts counts = new VfsListingCounts(1, 5, 1, 5);
diff --git 
a/ui/src/test/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsFileListingTest.java
 
b/ui/src/test/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsFileListingTest.java
index 331f02938c..5e454aa6df 100644
--- 
a/ui/src/test/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsFileListingTest.java
+++ 
b/ui/src/test/java/org/apache/hop/ui/hopgui/vfs/explorer/VfsFileListingTest.java
@@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
@@ -31,7 +32,11 @@ import static org.mockito.Mockito.when;
 
 import java.io.OutputStream;
 import java.util.List;
+import java.util.Map;
+import org.apache.commons.vfs2.FileContent;
+import org.apache.commons.vfs2.FileName;
 import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystemException;
 import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.vfs.HopVfs;
 import org.apache.hop.ui.core.vfs.HopVfsFileDialog;
@@ -182,6 +187,77 @@ class VfsFileListingTest {
         HopVfsFileDialog.buildArchiveBrowseUri("jar", 
"zip:file:///tmp/outer.zip!/nested.jar"));
   }
 
+  @Test
+  void driverFailureFailsTheFolder() throws Exception {
+    FileObject folder = mock(FileObject.class);
+    FileObject child = namedChild("data.csv");
+    NoClassDefFoundError missing = new 
NoClassDefFoundError("com.example.Missing");
+    when(child.isFolder()).thenThrow(new FileSystemException("Could not read 
child", missing));
+    when(folder.getChildren()).thenReturn(new FileObject[] {child});
+
+    Exception thrown = assertThrows(Exception.class, () -> 
VfsFileListing.childrenOf(folder));
+    assertTrue(VfsFileListing.isDriverFailure(thrown));
+    assertSame(missing, thrown.getCause());
+  }
+
+  @Test
+  void missingClassOnAChildIsNotSwallowed() throws Exception {
+    FileObject folder = mock(FileObject.class);
+    FileObject child = namedChild("data.csv");
+    when(child.isFolder()).thenThrow(new 
NoClassDefFoundError("com.example.Missing"));
+    when(folder.getChildren()).thenReturn(new FileObject[] {child});
+
+    NoClassDefFoundError thrown =
+        assertThrows(NoClassDefFoundError.class, () -> 
VfsFileListing.childrenOf(folder));
+    assertEquals("com.example.Missing", thrown.getMessage());
+  }
+
+  @Test
+  void oneUnreadableChildDoesNotHideTheRest() throws Exception {
+    FileObject folder = mock(FileObject.class);
+    FileObject good = readableFile("notes.txt", "ram:///notes.txt");
+    FileObject bad = namedChild("locked.bin");
+    when(bad.isFolder()).thenThrow(new FileSystemException("permission 
denied"));
+    when(folder.getChildren()).thenReturn(new FileObject[] {good, bad});
+
+    List<VfsFileRow> rows = VfsFileListing.childrenOf(folder);
+    assertEquals(List.of("notes.txt"), 
rows.stream().map(VfsFileRow::getName).toList());
+  }
+
+  @Test
+  void folderThatCannotBeReadIsNotAnEmptyListing() throws Exception {
+    FileObject folder = mock(FileObject.class);
+    FileObject child = namedChild("locked.bin");
+    FileSystemException failure = new FileSystemException("permission denied");
+    when(child.isFolder()).thenThrow(failure);
+    when(folder.getChildren()).thenReturn(new FileObject[] {child});
+
+    Exception thrown = assertThrows(Exception.class, () -> 
VfsFileListing.childrenOf(folder));
+    assertSame(failure, thrown);
+  }
+
+  private static FileObject namedChild(String baseName) throws Exception {
+    FileObject child = mock(FileObject.class);
+    FileName name = mock(FileName.class);
+    when(child.getName()).thenReturn(name);
+    when(name.getBaseName()).thenReturn(baseName);
+    return child;
+  }
+
+  private static FileObject readableFile(String baseName, String uri) throws 
Exception {
+    FileObject file = namedChild(baseName);
+    FileName name = file.getName();
+    FileContent content = mock(FileContent.class);
+    when(name.getRootURI()).thenReturn("ram:///");
+    when(name.getURI()).thenReturn(uri);
+    when(file.isFolder()).thenReturn(false);
+    when(file.getContent()).thenReturn(content);
+    when(content.getSize()).thenReturn(4L);
+    when(content.getLastModifiedTime()).thenReturn(1_700_000_000_000L);
+    when(content.getAttributes()).thenReturn(Map.of());
+    return file;
+  }
+
   private static VfsFileRow find(List<VfsFileRow> rows, String name) {
     return rows.stream().filter(row -> 
name.equals(row.getName())).findFirst().orElseThrow();
   }

Reply via email to