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