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 2c3ea87b36 issue #7797: hard-refresh explorer tree so new files appear
(#7798)
2c3ea87b36 is described below
commit 2c3ea87b364a7711446c4038723748955f24b69a
Author: Matt Casters <[email protected]>
AuthorDate: Thu Aug 6 15:07:41 2026 +0200
issue #7797: hard-refresh explorer tree so new files appear (#7798)
Toolbar F5 always rebuilds the project tree (no scoped-only refresh),
forces VFS FileObject.refresh() before directory listings, and keeps
scoped refresh on a new context-menu action. Path equality is hardened
for mixed separators so locate/find works more reliably on Windows.
---
.../java/org/apache/hop/core/vfs/HopVfsTest.java | 37 ++++++++
.../perspective/explorer/ExplorerPathUtils.java | 56 ++++++++++++
.../perspective/explorer/ExplorerPerspective.java | 99 +++++++++++++++++-----
.../explorer/messages/messages_en_US.properties | 3 +-
.../explorer/ExplorerPathUtilsTest.java | 60 +++++++++++++
5 files changed, 234 insertions(+), 21 deletions(-)
diff --git a/core/src/test/java/org/apache/hop/core/vfs/HopVfsTest.java
b/core/src/test/java/org/apache/hop/core/vfs/HopVfsTest.java
index c732b72637..f7d692c3b0 100644
--- a/core/src/test/java/org/apache/hop/core/vfs/HopVfsTest.java
+++ b/core/src/test/java/org/apache/hop/core/vfs/HopVfsTest.java
@@ -17,11 +17,14 @@
package org.apache.hop.core.vfs;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
import org.apache.commons.vfs2.FileObject;
import org.apache.hop.core.variables.Variables;
import org.junit.jupiter.api.Test;
@@ -97,4 +100,38 @@ class HopVfsTest {
outputStream.write("Test-content".getBytes());
}
}
+
+ /**
+ * Commons VFS caches directory children until {@link FileObject#refresh()}.
Explorer hard-refresh
+ * relies on an explicit refresh before re-listing so externally created
files appear (issue
+ * #7797).
+ */
+ @Test
+ void testRefreshClearsChildrenCacheForExternalCreates() throws Exception {
+ Path dir = Files.createTempDirectory("hop-vfs-children-");
+ try {
+ FileObject folder =
HopVfs.getFileObject(dir.toAbsolutePath().toString());
+ assertEquals(0, folder.getChildren().length);
+
+ // Create a file outside this FileObject graph (same as a pipeline
writing to disk)
+ Files.writeString(dir.resolve("external.txt"), "created outside VFS");
+
+ // Cached children may still look empty until refresh
+ folder.refresh();
+ FileObject[] children = folder.getChildren();
+ assertEquals(1, children.length);
+ assertEquals("external.txt", children[0].getName().getBaseName());
+ } finally {
+ Files.walk(dir)
+ .sorted((a, b) -> b.compareTo(a))
+ .forEach(
+ p -> {
+ try {
+ Files.deleteIfExists(p);
+ } catch (Exception ignored) {
+ // best-effort cleanup
+ }
+ });
+ }
+ }
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPathUtils.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPathUtils.java
new file mode 100644
index 0000000000..0867577bac
--- /dev/null
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPathUtils.java
@@ -0,0 +1,56 @@
+/*
+ * 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.hopgui.perspective.explorer;
+
+/**
+ * Path helpers for the file explorer tree (separator normalization,
equality). Package-private so
+ * unit tests can cover Windows/Unix path friction without loading SWT.
+ */
+final class ExplorerPathUtils {
+
+ private ExplorerPathUtils() {}
+
+ /**
+ * True when both paths refer to the same location after normalizing
separators and trailing
+ * slashes. Null-safe ({@code null} equals only {@code null}).
+ */
+ static boolean pathsEqual(String a, String b) {
+ if (a == null || b == null) {
+ return a == b;
+ }
+ if (a.equals(b)) {
+ return true;
+ }
+ return normalizePath(a).equals(normalizePath(b));
+ }
+
+ /**
+ * Normalize path separators to {@code /} and strip trailing slashes (except
a single root {@code
+ * /}).
+ */
+ static String normalizePath(String path) {
+ if (path == null) {
+ return null;
+ }
+ String n = path.replace('\\', '/');
+ while (n.length() > 1 && n.endsWith("/")) {
+ n = n.substring(0, n.length() - 1);
+ }
+ return n;
+ }
+}
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 3dfe044525..43d52e2b8e 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
@@ -211,6 +211,8 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
"ExplorerPerspective-ContextMenu-10060-ExpandAll";
public static final String CONTEXT_MENU_COLLAPSE_ALL =
"ExplorerPerspective-ContextMenu-10070-CollapseAll";
+ public static final String CONTEXT_MENU_REFRESH_FOLDER =
+ "ExplorerPerspective-ContextMenu-10080-RefreshFolder";
public static final String CONTEXT_MENU_OPEN =
"ExplorerPerspective-ContextMenu-10100-Open";
public static final String CONTEXT_MENU_OPEN_AS_TEXT =
"ExplorerPerspective-ContextMenu-10101-OpenAsText";
@@ -768,6 +770,12 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
createFolderItem.setEnabled(selection.length == 1);
}
+ MenuItem refreshFolderItem =
menuWidgets.findMenuItem(CONTEXT_MENU_REFRESH_FOLDER);
+ if (refreshFolderItem != null) {
+ // Scoped refresh needs a single selection so
lastSelectedFolderPath is meaningful.
+ refreshFolderItem.setEnabled(selection.length == 1 && tif != null);
+ }
+
// Finding references is only meaningful for a single pipeline or
workflow file.
//
MenuItem findReferencesItem =
menuWidgets.findMenuItem(CONTEXT_MENU_FIND_REFERENCES);
@@ -1064,7 +1072,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
/** Depth passed to {@link #refreshFolder} when loading children of {@code
tif}. */
private int childDepthForFolder(TreeItemFolder tif) {
- return tif.path.equals(rootFolder) ? 0 : tif.depth + 1;
+ return ExplorerPathUtils.pathsEqual(tif.path, rootFolder) ? 0 : tif.depth
+ 1;
}
/**
@@ -2623,7 +2631,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
private boolean selectInTree(TreeItem item, String filename) {
TreeItemFolder tif = (TreeItemFolder) item.getData();
- if (tif != null && tif.path.equals(filename)) {
+ if (tif != null && ExplorerPathUtils.pathsEqual(tif.path, filename)) {
tree.setSelection(tif.treeItem);
tree.showItem(tif.treeItem);
return true;
@@ -3171,6 +3179,31 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
tree.setRedraw(true);
}
+ /**
+ * Context menu: refresh only the selected folder (or the parent of a
selected file). Cheaper than
+ * toolbar F5 full rebuild; use when you know which folder changed.
+ */
+ @GuiMenuElement(
+ root = GUI_PLUGIN_CONTEXT_MENU_PARENT_ID,
+ parentId = GUI_PLUGIN_CONTEXT_MENU_PARENT_ID,
+ id = CONTEXT_MENU_REFRESH_FOLDER,
+ label = "i18n::ExplorerPerspective.Menu.RefreshFolder",
+ image = "ui/images/refresh.svg",
+ separator = true)
+ public void refreshFolderFromMenu() {
+ TreeItem[] selection = tree.getSelection();
+ if (selection == null || selection.length != 1) {
+ return;
+ }
+ TreeItemFolder tif = (TreeItemFolder) selection[0].getData();
+ if (tif == null) {
+ return;
+ }
+ // Ensure lastSelectedFolderPath matches the context-clicked item
+ recordSelectedFolderPath(tif, selection[0]);
+ refreshSelectedFolder();
+ }
+
public void expandCollapse(TreeItem item, boolean expand) {
if (item == null || item.isDisposed()) {
return;
@@ -3383,22 +3416,29 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
}
}
+ /**
+ * Hard-refresh the explorer tree (toolbar / F5).
+ *
+ * <p>Always rebuilds from disk so files and folders created outside the
current selection (for
+ * example by a running pipeline) become visible. Clears the filter index
and re-lists folders via
+ * VFS with an explicit {@link FileObject#refresh()} so cached children do
not hide new entries.
+ * Expand/collapse state is restored from {@link TreeMemory}.
+ *
+ * <p>For a cheaper single-folder update use {@link
#refreshSelectedFolder()} (context menu).
+ */
public void refresh() {
determineRootFolderName(hopGui);
- // Structural refresh invalidates the filter index
+ // Structural refresh invalidates the filter index and discards lazy tree
snapshots
treeModel.clear();
if (!Utils.isEmpty(filterText)) {
- // F5 / structural refresh while filtering: update git status, then
re-index once
+ // F5 while filtering: update git status, then re-index the full project
once
for (IExplorerRefreshListener listener : refreshListeners) {
listener.beforeRefresh();
}
applyFilterFromModel();
return;
}
- if (!Utils.isEmpty(lastSelectedFolderPath) &&
isUnderCurrentRoot(lastSelectedFolderPath)) {
- refreshSelectedFolder();
- return;
- }
+ // Always full rebuild — scoped refresh left only for context menu /
internal ops
refreshEntireTree();
}
@@ -3523,14 +3563,13 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
private void loadModelChildren(ExplorerTreeModel.Node parent, String
metadataFolderName)
throws Exception {
- FileObject fileObject = HopVfs.getFileObject(parent.getPath());
FileObject[] children;
try {
- children = fileObject.getChildren();
+ children = listChildrenFresh(parent.getPath());
} catch (Exception e) {
return;
}
- if (children == null || children.length == 0) {
+ if (children.length == 0) {
return;
}
@@ -3635,13 +3674,16 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
if (Utils.isEmpty(path) || Utils.isEmpty(rootFolder)) {
return false;
}
- if (path.equals(rootFolder)) {
+ if (ExplorerPathUtils.pathsEqual(path, rootFolder)) {
return true;
}
return isDescendant(rootFolder, path);
}
- /** Refresh only the last selected folder, keeping the rest of the tree
intact. */
+ /**
+ * Refresh only the last selected folder, keeping the rest of the tree
intact. Used by the context
+ * menu and by internal operations (delete parent fix-up); toolbar F5 uses
{@link #refresh()}.
+ */
private void refreshSelectedFolder() {
String folderPath = lastSelectedFolderPath;
if (Utils.isEmpty(folderPath)
@@ -3759,7 +3801,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
*/
private void updateLastSelectedFolderAfterDelete(TreeItem treeItem) {
TreeItemFolder tif = (TreeItemFolder) treeItem.getData();
- if (tif == null || !tif.path.equals(lastSelectedFolderPath)) {
+ if (tif == null || !ExplorerPathUtils.pathsEqual(tif.path,
lastSelectedFolderPath)) {
return;
}
@@ -3804,7 +3846,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
}
private String getParentFolderPath(String path) {
- if (Utils.isEmpty(path) || path.equals(rootFolder)) {
+ if (Utils.isEmpty(path) || ExplorerPathUtils.pathsEqual(path, rootFolder))
{
return null;
}
@@ -3978,6 +4020,24 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
return emptyFileType;
}
+ /**
+ * Resolve a folder and force a VFS re-list so external creates (pipelines,
other processes) are
+ * visible. Commons VFS caches children on {@link FileObject} until {@link
FileObject#refresh()}.
+ */
+ private FileObject[] listChildrenFresh(String path) throws Exception {
+ FileObject folder = HopVfs.getFileObject(path);
+ try {
+ folder.refresh();
+ } catch (Exception e) {
+ // Still attempt to list; some providers throw on refresh for
missing/imaginary files
+ if (hopGui != null && hopGui.getLog() != null) {
+ hopGui.getLog().logDebug("VFS refresh failed for '" + path + "': " +
e.getMessage());
+ }
+ }
+ FileObject[] children = folder.getChildren();
+ return children != null ? children : new FileObject[0];
+ }
+
private void refreshFolder(TreeItem item, String path, int depth) {
try {
@@ -3987,9 +4047,8 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
child.dispose();
}
- FileObject fileObject = HopVfs.getFileObject(path);
- FileObject[] children = fileObject.getChildren();
- if (children == null || children.length == 0) {
+ FileObject[] children = listChildrenFresh(path);
+ if (children.length == 0) {
return;
}
@@ -4361,7 +4420,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
private TreeItem locateTreeItem(TreeItem item, String path) {
TreeItemFolder tif = (TreeItemFolder) item.getData();
- if (tif != null && tif.path.equals(path)) {
+ if (tif != null && ExplorerPathUtils.pathsEqual(tif.path, path)) {
return item;
}
@@ -4381,7 +4440,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
private TreeItem findTreeItem(TreeItem item, String filename) {
TreeItemFolder tif = (TreeItemFolder) item.getData();
- if (tif != null && tif.path.equals(filename)) {
+ if (tif != null && ExplorerPathUtils.pathsEqual(tif.path, filename)) {
return tif.treeItem;
}
for (TreeItem child : item.getItems()) {
diff --git
a/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/messages/messages_en_US.properties
b/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/messages/messages_en_US.properties
index ab2c289386..1c87791da8 100644
---
a/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/messages/messages_en_US.properties
+++
b/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/messages/messages_en_US.properties
@@ -47,13 +47,14 @@ ExplorerPerspective.Menu.OpenInExplorer=Open in explorer
ExplorerPerspective.Menu.Rename=Rename
ExplorerPerspective.Menu.ExpandAll=Expand All
ExplorerPerspective.Menu.CollapseAll=Collapse All
+ExplorerPerspective.Menu.RefreshFolder=Refresh folder
ExplorerPerspective.Name=File Explorer
ExplorerPerspective.Search.Placeholder=Search files and folders (min. 3
characters)...
ExplorerPerspective.Tooltip.Active=Click the folder to show/hide the project
tree
ExplorerPerspective.ToolbarElement.CreateFolder.Tooltip=Create folder
ExplorerPerspective.ToolbarElement.Delete.Tooltip=Delete selected file
ExplorerPerspective.ToolbarElement.Open.Tooltip=Open selected file
-ExplorerPerspective.ToolbarElement.Refresh.Tooltip=Refresh
+ExplorerPerspective.ToolbarElement.Refresh.Tooltip=Refresh the entire project
tree (clears caches)
ExplorerPerspective.ToolbarElement.Rename.Tooltip=Rename the selected file
ExplorerPerspective.ToolbarElement.ExpandAll.Tooltip=Expand all folders
ExplorerPerspective.ToolbarElement.CollapseAll.Tooltip=Collapse all folders
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPathUtilsTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPathUtilsTest.java
new file mode 100644
index 0000000000..c9cb7a23ba
--- /dev/null
+++
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPathUtilsTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.hopgui.perspective.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 org.junit.jupiter.api.Test;
+
+class ExplorerPathUtilsTest {
+
+ @Test
+ void pathsEqualExactMatch() {
+ assertTrue(ExplorerPathUtils.pathsEqual("/project/a", "/project/a"));
+ assertTrue(ExplorerPathUtils.pathsEqual(null, null));
+ assertFalse(ExplorerPathUtils.pathsEqual(null, "/x"));
+ assertFalse(ExplorerPathUtils.pathsEqual("/x", null));
+ }
+
+ @Test
+ void pathsEqualNormalizesSeparators() {
+ assertTrue(ExplorerPathUtils.pathsEqual("C:\\project\\a", "C:/project/a"));
+ assertTrue(ExplorerPathUtils.pathsEqual("C:/project/a/b",
"C:\\project\\a\\b"));
+ assertFalse(ExplorerPathUtils.pathsEqual("C:/project/a", "C:/project/b"));
+ }
+
+ @Test
+ void pathsEqualIgnoresTrailingSlash() {
+ assertTrue(ExplorerPathUtils.pathsEqual("/project/a", "/project/a/"));
+ assertTrue(ExplorerPathUtils.pathsEqual("/project/a/", "/project/a"));
+ assertTrue(ExplorerPathUtils.pathsEqual("C:\\project\\a\\",
"C:/project/a"));
+ // Root slash stays
+ assertTrue(ExplorerPathUtils.pathsEqual("/", "/"));
+ }
+
+ @Test
+ void normalizePath() {
+ assertEquals("/project/a", ExplorerPathUtils.normalizePath("/project/a/"));
+ assertEquals("C:/project/a",
ExplorerPathUtils.normalizePath("C:\\project\\a\\"));
+ assertEquals("/", ExplorerPathUtils.normalizePath("/"));
+ assertNull(ExplorerPathUtils.normalizePath(null));
+ }
+}