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 da7c91e54c collapse the 2 FileSystemManagers, fixes #7688 (#7759)
da7c91e54c is described below
commit da7c91e54cb2ef4c8b043f06141f21456fbbce97
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Mon Aug 3 12:52:42 2026 +0200
collapse the 2 FileSystemManagers, fixes #7688 (#7759)
do not register empty providers
---
.../main/java/org/apache/hop/core/vfs/HopVfs.java | 470 +++++++++++----------
.../java/org/apache/hop/core/vfs/plugin/IVfs.java | 20 +
.../hop/core/vfs/HopVfsSingleManagerTest.java | 271 ++++++++++++
.../org/apache/hop/projects/util/ProjectsUtil.java | 6 +
.../hop/vfs/databricks/DatabricksVfsPlugin.java | 2 +-
.../vfs/databricks/DatabricksVfsPluginTest.java | 9 +-
.../org/apache/hop/vfs/minio/MinioVfsPlugin.java | 2 +-
.../apache/hop/vfs/minio/MinioVfsPluginTest.java | 28 +-
8 files changed, 553 insertions(+), 255 deletions(-)
diff --git a/core/src/main/java/org/apache/hop/core/vfs/HopVfs.java
b/core/src/main/java/org/apache/hop/core/vfs/HopVfs.java
index fee0d224e7..de7b547e30 100644
--- a/core/src/main/java/org/apache/hop/core/vfs/HopVfs.java
+++ b/core/src/main/java/org/apache/hop/core/vfs/HopVfs.java
@@ -29,7 +29,6 @@ import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.vfs2.CacheStrategy;
import org.apache.commons.vfs2.FileContent;
@@ -47,6 +46,7 @@ import org.apache.hop.core.Const;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.exception.HopFileException;
import org.apache.hop.core.exception.HopRuntimeException;
+import org.apache.hop.core.logging.LogChannel;
import org.apache.hop.core.plugins.IPlugin;
import org.apache.hop.core.plugins.PluginRegistry;
import org.apache.hop.core.variables.IVariables;
@@ -59,66 +59,152 @@ public class HopVfs {
public static final String TEMP_DIR = System.getProperty("java.io.tmpdir");
+ /** The one and only file system manager. */
private static DefaultFileSystemManager fsm;
- private static DefaultFileSystemManager extendedFsm;
- private static final ReentrantReadWriteLock lock = new
ReentrantReadWriteLock();
+ /**
+ * The variables used to bootstrap the metadata driven providers (the named
VFS connections).
+ * They're only needed while those providers are being registered, not to
resolve individual
+ * files.
+ */
+ private static IVariables bootstrapVariables;
- public static DefaultFileSystemManager getFileSystemManager() {
- lock.readLock().lock();
- try {
- if (fsm == null) {
- try {
- fsm = createFileSystemManager();
- fsm.init();
- } catch (Exception e) {
- throw new HopRuntimeException("Error initializing file system
manager : ", e);
- }
+ /** Set once the metadata driven providers have been registered on {@link
#fsm}. */
+ private static boolean namedProvidersRegistered;
+
+ /**
+ * Guards against re-entrant registration: reading the VFS connection
metadata resolves files
+ * through this very class.
+ */
+ private static boolean registeringNamedProviders;
+
+ /**
+ * Set the variables to look up the metadata driven VFS providers (the named
VFS connections)
+ * with. Call this when a project is loaded: its variables are what point at
the metadata holding
+ * those connections. The named connections of that project are registered
the next time the file
+ * system manager is used.
+ *
+ * @param variables the variables to bootstrap the VFS providers with
+ */
+ public static synchronized void setBootstrapVariables(IVariables variables) {
+ if (variables == bootstrapVariables) {
+ return;
+ }
+ bootstrapVariables = variables;
+
+ // Connections of a previous project are registered on the manager and
there's no unregistering
+ // them, so start over. Nothing registered yet means we can keep the
manager as it is: phase 2
+ // simply runs with these variables the next time around.
+ //
+ if (namedProvidersRegistered) {
+ reset();
+ }
+ }
+
+ /**
+ * Get the file system manager. There is only one: it knows both the fixed
schemes (file, zip, s3,
+ * ...) and the schemes of the named VFS connections in the metadata.
+ *
+ * @return the file system manager
+ */
+ public static synchronized DefaultFileSystemManager getFileSystemManager() {
+ if (fsm == null) {
+ try {
+ // Phase 1 : the standard schemes and the fixed schemes of the VFS
plugins.
+ //
+ DefaultFileSystemManager manager = createFileSystemManager();
+ manager.init();
+
+ // Publish before phase 2 : registering the metadata driven providers
below reads the
+ // metadata, and reading metadata resolves files through this very
manager.
+ //
+ fsm = manager;
+ } catch (Exception e) {
+ throw new HopRuntimeException("Error initializing file system manager
: ", e);
}
- return fsm;
- } finally {
- lock.readLock().unlock();
}
+
+ // Phase 2 : the providers of the named VFS connections in the metadata.
Only once a project
+ // handed us its variables: they're what points us at the metadata holding
those connections,
+ // and what the providers resolve their credentials with. Everything
resolving files before
+ // that (reading the configuration, loading the images of the GUI) wants a
local file and is
+ // served by phase 1 alone.
+ //
+ if (bootstrapVariables != null && !namedProvidersRegistered &&
!registeringNamedProviders) {
+ // Set before anything else: registering reads metadata, which resolves
files, which lands
+ // right back here.
+ //
+ registeringNamedProviders = true;
+ try {
+ registerNamedProviders(fsm, bootstrapVariables);
+ } finally {
+ registeringNamedProviders = false;
+ namedProvidersRegistered = true;
+ }
+ }
+
+ return fsm;
}
- public static DefaultFileSystemManager getFileSystemManager(IVariables
variables) {
- lock.readLock().lock();
- try {
- if (extendedFsm == null) {
- try {
- extendedFsm = createFileSystemManager();
- // Here are extra VFS plugins to register
- //
- PluginRegistry registry = PluginRegistry.getInstance();
- List<IPlugin> plugins = registry.getPlugins(VfsPluginType.class);
- for (IPlugin plugin : plugins) {
- IVfs iVfs = registry.loadClass(plugin, IVfs.class);
- try {
- Map<String, FileProvider> fileProviderMap =
iVfs.getProviders(variables);
- if (fileProviderMap != null) {
- for (Map.Entry<String, FileProvider> entry :
fileProviderMap.entrySet()) {
- extendedFsm.addProvider(entry.getKey(), entry.getValue());
- }
- }
- } catch (Exception e) {
- throw new HopException(
- "Error registering provider for VFS plugin "
- + plugin.getIds()[0]
- + " : "
- + plugin.getName()
- + " : ",
- e);
- }
- }
+ /**
+ * @param variables the variables to bootstrap the metadata driven providers
with, in case nothing
+ * did that yet
+ * @return the one and only file system manager
+ * @see #getFileSystemManager()
+ */
+ public static synchronized DefaultFileSystemManager
getFileSystemManager(IVariables variables) {
+ bootstrapWith(variables);
+ return getFileSystemManager();
+ }
- extendedFsm.init();
- } catch (Exception e) {
- throw new HopRuntimeException("Error initializing file system
manager : ", e);
+ /**
+ * Remember the variables to bootstrap the metadata driven providers with,
for as long as nothing
+ * bootstrapped them yet. These are the first variables we get to see, so
the named connections
+ * are looked up in the metadata they point at, the next time the manager is
used. An explicit
+ * {@link #setBootstrapVariables(IVariables)} always wins.
+ */
+ private static synchronized void bootstrapWith(IVariables variables) {
+ if (variables != null && bootstrapVariables == null) {
+ bootstrapVariables = variables;
+ }
+ }
+
+ /**
+ * Register a provider for every named VFS connection in the metadata. A
connection which can't be
+ * registered is skipped: a single bad connection should never take the
whole file system manager
+ * down with it.
+ */
+ private static void registerNamedProviders(
+ DefaultFileSystemManager manager, IVariables variables) {
+ PluginRegistry registry = PluginRegistry.getInstance();
+ for (IPlugin plugin : registry.getPlugins(VfsPluginType.class)) {
+ try {
+ IVfs iVfs = registry.loadClass(plugin, IVfs.class);
+ Map<String, FileProvider> fileProviderMap =
iVfs.getProviders(variables);
+ if (fileProviderMap == null) {
+ continue;
+ }
+ for (Map.Entry<String, FileProvider> entry :
fileProviderMap.entrySet()) {
+ String scheme = entry.getKey();
+ if (manager.hasProvider(scheme)) {
+ LogChannel.GENERAL.logError(
+ "The VFS connection '"
+ + scheme
+ + "' of plugin "
+ + plugin.getIds()[0]
+ + " is ignored: a provider is already registered for that
scheme.");
+ continue;
+ }
+ manager.addProvider(scheme, entry.getValue());
}
+ } catch (Exception e) {
+ LogChannel.GENERAL.logError(
+ "Error registering provider for VFS plugin "
+ + plugin.getIds()[0]
+ + " : "
+ + plugin.getName(),
+ e);
}
- return extendedFsm;
- } finally {
- lock.readLock().unlock();
}
}
@@ -182,7 +268,15 @@ public class HopVfs {
for (IPlugin plugin : plugins) {
IVfs iVfs = registry.loadClass(plugin, IVfs.class);
try {
- fsm.addProvider(iVfs.getUrlSchemes(), iVfs.getProvider());
+ String[] urlSchemes = iVfs.getUrlSchemes();
+ FileProvider provider = iVfs.getProvider();
+
+ // Skip plugins with no fixed scheme (Minio, Databricks): a provider
the manager has no
+ // scheme for is never reached, and never closed. Phase 2 registers
those by name.
+ if (urlSchemes == null || urlSchemes.length == 0 || provider ==
null) {
+ continue;
+ }
+ fsm.addProvider(urlSchemes, provider);
} catch (Exception e) {
throw new HopException(
"Error registering provider for VFS plugin "
@@ -199,100 +293,59 @@ public class HopVfs {
}
}
- public static synchronized FileObject getFileObject(String vfsFilename,
IVariables variables)
+ /**
+ * @param vfsFilename the name of the file to resolve
+ * @param variables the variables to bootstrap the metadata driven providers
with, in case nothing
+ * did that yet. They play no role in resolving the file itself.
+ * @return the file object
+ * @see #getFileObject(String)
+ */
+ public static FileObject getFileObject(String vfsFilename, IVariables
variables)
throws HopFileException {
- lock.readLock().lock();
- try {
- DefaultFileSystemManager fsManager = getFileSystemManager(variables);
-
- try {
- // We have one problem with VFS: if the file is in a subdirectory of
the current one:
- // somedir/somefile
- // In that case, VFS doesn't parse the file correctly.
- // We need to put file: in front of it to make it work.
- // However, how are we going to verify this?
- //
- // We are going to see if the filename starts with one of the known
protocols like file:
- // zip: ram: smb: jar: etc.
- // If not, we are going to assume it's a file.
- //
- boolean relativeFilename = true;
- String[] initialSchemes = fsManager.getSchemes();
-
- relativeFilename = checkForScheme(initialSchemes, relativeFilename,
vfsFilename);
-
- String filename;
- if (vfsFilename.startsWith("\\\\")) {
- File file = new File(vfsFilename);
- filename = file.toURI().toString();
- } else {
- if (relativeFilename) {
- File file = new File(vfsFilename);
- filename = file.getAbsolutePath();
- } else {
- filename = vfsFilename;
- }
- }
-
- return fsManager.resolveFile(filename);
- } catch (Exception e) {
- throw new HopFileException(
- "Unable to get VFS File object for filename '"
- + cleanseFilename(vfsFilename)
- + "' : "
- + e.getMessage(),
- e);
- }
- } finally {
- lock.readLock().unlock();
- }
+ bootstrapWith(variables);
+ return getFileObject(vfsFilename);
}
public static synchronized FileObject getFileObject(String vfsFilename)
throws HopFileException {
- lock.readLock().lock();
- try {
- DefaultFileSystemManager fsManager = getFileSystemManager();
+ DefaultFileSystemManager fsManager = getFileSystemManager();
- try {
- // We have one problem with VFS: if the file is in a subdirectory of
the current one:
- // somedir/somefile
- // In that case, VFS doesn't parse the file correctly.
- // We need to put file: in front of it to make it work.
- // However, how are we going to verify this?
- //
- // We are going to see if the filename starts with one of the known
protocols like file:
- // zip: ram: smb: jar: etc.
- // If not, we are going to assume it's a file.
- //
- boolean relativeFilename = true;
- String[] initialSchemes = fsManager.getSchemes();
+ try {
+ // We have one problem with VFS: if the file is in a subdirectory of the
current one:
+ // somedir/somefile
+ // In that case, VFS doesn't parse the file correctly.
+ // We need to put file: in front of it to make it work.
+ // However, how are we going to verify this?
+ //
+ // We are going to see if the filename starts with one of the known
protocols like file:
+ // zip: ram: smb: jar: etc.
+ // If not, we are going to assume it's a file.
+ //
+ boolean relativeFilename = true;
+ String[] initialSchemes = fsManager.getSchemes();
- relativeFilename = checkForScheme(initialSchemes, relativeFilename,
vfsFilename);
+ relativeFilename = checkForScheme(initialSchemes, relativeFilename,
vfsFilename);
- String filename;
- if (vfsFilename.startsWith("\\\\")) {
+ String filename;
+ if (vfsFilename.startsWith("\\\\")) {
+ File file = new File(vfsFilename);
+ filename = file.toURI().toString();
+ } else {
+ if (relativeFilename) {
File file = new File(vfsFilename);
- filename = file.toURI().toString();
+ filename = file.getAbsolutePath();
} else {
- if (relativeFilename) {
- File file = new File(vfsFilename);
- filename = file.getAbsolutePath();
- } else {
- filename = vfsFilename;
- }
+ filename = vfsFilename;
}
-
- return fsManager.resolveFile(filename);
- } catch (Exception e) {
- throw new HopFileException(
- "Unable to get VFS File object for filename '"
- + cleanseFilename(vfsFilename)
- + "' : "
- + e.getMessage(),
- e);
}
- } finally {
- lock.readLock().unlock();
+
+ return fsManager.resolveFile(filename);
+ } catch (Exception e) {
+ throw new HopFileException(
+ "Unable to get VFS File object for filename '"
+ + cleanseFilename(vfsFilename)
+ + "' : "
+ + e.getMessage(),
+ e);
}
}
@@ -362,23 +415,13 @@ public class HopVfs {
}
}
+ /**
+ * @see #fileExists(String)
+ */
public static boolean fileExists(String vfsFilename, IVariables variables)
throws HopFileException {
- FileObject fileObject = null;
- try {
- fileObject = getFileObject(vfsFilename, variables);
- return fileObject.exists();
- } catch (IOException e) {
- throw new HopFileException(e);
- } finally {
- if (fileObject != null) {
- try {
- fileObject.close();
- } catch (Exception e) {
- /* Ignore */
- }
- }
- }
+ bootstrapWith(variables);
+ return fileExists(vfsFilename);
}
public static boolean isLocalFileSystem(String path) {
@@ -405,15 +448,13 @@ public class HopVfs {
}
}
+ /**
+ * @see #getInputStream(String)
+ */
public static InputStream getInputStream(String vfsFilename, IVariables
variables)
throws HopFileException {
- try {
- FileObject fileObject = getFileObject(vfsFilename, variables);
-
- return getInputStream(fileObject);
- } catch (IOException e) {
- throw new HopFileException(e);
- }
+ bootstrapWith(variables);
+ return getInputStream(vfsFilename);
}
public static OutputStream getOutputStream(FileObject fileObject, boolean
append)
@@ -453,14 +494,13 @@ public class HopVfs {
}
}
+ /**
+ * @see #getOutputStream(String, boolean)
+ */
public static OutputStream getOutputStream(
String vfsFilename, boolean append, IVariables variables) throws
HopFileException {
- try {
- FileObject fileObject = getFileObject(vfsFilename, variables);
- return getOutputStream(fileObject, append);
- } catch (IOException e) {
- throw new HopFileException(e);
- }
+ bootstrapWith(variables);
+ return getOutputStream(vfsFilename, append);
}
public static OutputStream getOutputStream(String vfsFilename, boolean
append)
@@ -519,19 +559,12 @@ public class HopVfs {
return friendlyName;
}
+ /**
+ * @see #getFriendlyURI(String)
+ */
public static String getFriendlyURI(String filename, IVariables variables) {
- if (filename == null) {
- return null;
- }
- String friendlyName;
- try {
- friendlyName = getFriendlyURI(HopVfs.getFileObject(filename, variables));
- } catch (Exception e) {
- // unable to get a friendly name from VFS object.
- // Cleanse name of pwd before returning
- friendlyName = cleanseFilename(filename);
- }
- return friendlyName;
+ bootstrapWith(variables);
+ return getFriendlyURI(filename);
}
public static String getFriendlyURI(FileObject fileObject) {
@@ -593,40 +626,17 @@ public class HopVfs {
* @param prefix - file name
* @param suffix - file extension
* @param directory - directory where file will be created
+ * @param variables the variables to bootstrap the metadata driven providers
with, in case nothing
+ * did that yet
* @return FileObject
* @throws HopFileException
+ * @see #createTempFile(String, String, String)
*/
- public static synchronized FileObject createTempFile(
+ public static FileObject createTempFile(
String prefix, String suffix, String directory, IVariables variables)
throws HopFileException {
- try {
- FileObject fileObject;
- do {
- // Temporary files are always stored locally.
- // No other schemes besides file:// make sense
- //
- String baseUrl;
- if (directory.contains("://")) {
- baseUrl = directory;
- } else {
- File directoryFile = new File(directory);
- baseUrl = "file://" + directoryFile.getAbsolutePath();
- }
-
- // Build temporary file name using UUID to ensure uniqueness. Old
mechanism would fail using
- // Sort Rows (for example)
- // when there multiple nodes with multiple JVMs on each node. In this
case, the temp file
- // names would end up being
- // duplicated which would cause the sort to fail.
- //
- String filename = baseUrl + "/" + prefix + "_" + UUID.randomUUID() +
suffix;
-
- fileObject = getFileObject(filename, variables);
- } while (fileObject.exists());
- return fileObject;
- } catch (IOException e) {
- throw new HopFileException(e);
- }
+ bootstrapWith(variables);
+ return createTempFile(prefix, suffix, directory);
}
public static Comparator<FileObject> getComparator() {
@@ -645,24 +655,30 @@ public class HopVfs {
* @return boolean
*/
public static boolean startsWithScheme(String vfsFileName, IVariables
variables) {
- lock.readLock().lock();
- try {
-
- DefaultFileSystemManager fsManager = getFileSystemManager(variables);
+ bootstrapWith(variables);
+ return startsWithScheme(vfsFileName);
+ }
- boolean found = false;
- String[] schemes = fsManager.getSchemes();
- for (String scheme : schemes) {
- if (vfsFileName.startsWith(scheme + ":")) {
- found = true;
- break;
- }
+ /**
+ * Check if filename starts with one of the known protocols like file: zip:
ram: smb: jar: etc. If
+ * yes, return true otherwise return false
+ *
+ * @param vfsFileName
+ * @return boolean
+ */
+ public static boolean startsWithScheme(String vfsFileName) {
+ DefaultFileSystemManager fsManager = getFileSystemManager();
+
+ boolean found = false;
+ String[] schemes = fsManager.getSchemes();
+ for (String scheme : schemes) {
+ if (vfsFileName.startsWith(scheme + ":")) {
+ found = true;
+ break;
}
-
- return found;
- } finally {
- lock.readLock().unlock();
}
+
+ return found;
}
/**
@@ -728,23 +744,25 @@ public class HopVfs {
/**
* @see StandardFileSystemManager#freeUnusedResources()
*/
- public static void freeUnusedResources() {
+ public static synchronized void freeUnusedResources() {
if (fsm != null) {
fsm.freeUnusedResources();
}
}
- public static void reset() {
+ /**
+ * Drop the file system manager so it's rebuilt, providers of the named VFS
connections included,
+ * the next time it's used. The bootstrap variables are kept: use {@link
+ * #setBootstrapVariables(IVariables)} to change those.
+ */
+ public static synchronized void reset() {
if (fsm != null) {
fsm.freeUnusedResources();
fsm.close();
fsm = null;
}
- if (extendedFsm != null) {
- extendedFsm.freeUnusedResources();
- extendedFsm.close();
- extendedFsm = null;
- }
+ namedProvidersRegistered = false;
+ registeringNamedProviders = false;
}
public enum Suffix {
diff --git a/core/src/main/java/org/apache/hop/core/vfs/plugin/IVfs.java
b/core/src/main/java/org/apache/hop/core/vfs/plugin/IVfs.java
index 387f14c425..a6ccf71680 100644
--- a/core/src/main/java/org/apache/hop/core/vfs/plugin/IVfs.java
+++ b/core/src/main/java/org/apache/hop/core/vfs/plugin/IVfs.java
@@ -22,9 +22,29 @@ import org.apache.commons.vfs2.provider.FileProvider;
import org.apache.hop.core.variables.IVariables;
public interface IVfs {
+ /**
+ * The fixed URL schemes served by {@link #getProvider()}, empty for a
plugin serving named
+ * connections from the metadata only.
+ *
+ * @return the fixed URL schemes of this plugin
+ */
String[] getUrlSchemes();
+ /**
+ * The provider serving the fixed {@link #getUrlSchemes()}. Return {@code
null} when there are no
+ * fixed schemes: a provider registered under no scheme at all can never be
reached, and the file
+ * system manager can't close what it doesn't know a scheme for.
+ *
+ * @return the provider for the fixed schemes, or null if this plugin has
none
+ */
FileProvider getProvider();
+ /**
+ * The providers of the named connections in the metadata, keyed by
connection name : the name is
+ * the scheme these are registered under.
+ *
+ * @param variables the variables pointing at the metadata to load the
connections from
+ * @return a provider per named connection, empty or null if there are none
+ */
Map<String, FileProvider> getProviders(IVariables variables);
}
diff --git
a/core/src/test/java/org/apache/hop/core/vfs/HopVfsSingleManagerTest.java
b/core/src/test/java/org/apache/hop/core/vfs/HopVfsSingleManagerTest.java
new file mode 100644
index 0000000000..dddeeef810
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/core/vfs/HopVfsSingleManagerTest.java
@@ -0,0 +1,271 @@
+/*
+ * 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.core.vfs;
+
+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 static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import org.apache.commons.logging.Log;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.impl.DefaultFileSystemManager;
+import org.apache.commons.vfs2.provider.FileProvider;
+import org.apache.commons.vfs2.provider.ram.RamFileProvider;
+import org.apache.hop.core.extension.IPluginMock;
+import org.apache.hop.core.plugins.IPlugin;
+import org.apache.hop.core.plugins.PluginRegistry;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.core.vfs.plugin.IVfs;
+import org.apache.hop.core.vfs.plugin.VfsPluginType;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.ArgumentCaptor;
+
+/**
+ * {@link HopVfs} keeps one file system manager, knowing both the fixed
schemes and the schemes of
+ * the named VFS connections in the metadata. The variables of {@code
getFileObject(path,variables)}
+ * only bootstrap the metadata driven providers; they play no role in
resolving a file, so both
+ * entry points have to end up on that one manager.
+ */
+class HopVfsSingleManagerTest {
+
+ /** Stands in for HOP_METADATA_FOLDER: only variables carrying it see the
project metadata. */
+ private static final String PROJECT_MARKER = "TEST_PROJECT_METADATA";
+
+ private IPlugin registeredPlugin;
+
+ @AfterEach
+ void tearDown() {
+ if (registeredPlugin != null) {
+ PluginRegistry.getInstance().removePlugin(VfsPluginType.class,
registeredPlugin);
+ registeredPlugin = null;
+ }
+ // Drop the manager built with the test providers, and the variables it
was built with, so
+ // other tests start clean.
+ HopVfs.setBootstrapVariables(null);
+ HopVfs.reset();
+ }
+
+ @Test
+ @DisplayName("Both entry points return the very same file system manager")
+ void bothEntryPointsShareOneManager() {
+ DefaultFileSystemManager plain = HopVfs.getFileSystemManager();
+ DefaultFileSystemManager withVariables =
+ HopVfs.getFileSystemManager(Variables.getADefaultVariableSpace());
+
+ assertSame(plain, withVariables);
+ assertSame(withVariables, HopVfs.getFileSystemManager());
+ }
+
+ @Test
+ @DisplayName("Without variables nothing goes looking for metadata")
+ void noMetadataLookupBeforeVariablesArrive(@TempDir Path tmp) throws
Exception {
+ // Reading the Hop configuration resolves a file, and building a default
variable space reads
+ // the Hop configuration. Going looking for VFS connections here loops
forever, so resolving a
+ // file without variables must not touch the metadata at all.
+ boolean[] wentLooking = {false};
+ registerTestVfsPlugin("too-early", variables -> wentLooking[0] = true);
+
+ try (FileObject file =
+
HopVfs.getFileObject(tmp.resolve("hop-config.json").toUri().toString())) {
+ assertNotNull(file);
+ }
+
+ assertFalse(wentLooking[0], "The VFS connections were looked up before a
project was loaded");
+ }
+
+ @Test
+ @DisplayName("Files resolved with and without variables come off the same
manager")
+ void resolvedFilesShareOneManager(@TempDir Path tmp) throws Exception {
+ String url = tmp.resolve("single-manager.txt").toUri().toString();
+
+ try (FileObject withoutVariables = HopVfs.getFileObject(url);
+ FileObject withVariables =
+ HopVfs.getFileObject(url, Variables.getADefaultVariableSpace())) {
+ assertSame(
+ withoutVariables.getFileSystem().getFileSystemManager(),
+ withVariables.getFileSystem().getFileSystemManager());
+ assertSame(
+ HopVfs.getFileSystemManager(),
withoutVariables.getFileSystem().getFileSystemManager());
+ }
+ }
+
+ @Test
+ @DisplayName("Named connection providers land on the manager the no-argument
getter hands out")
+ void namedProvidersAreVisibleToTheNoArgumentGetter() {
+ registerTestVfsPlugin("named-connection-test", variables -> {});
+
+ // Loading a project is what hands over the variables to find the
connections with.
+ HopVfs.setBootstrapVariables(Variables.getADefaultVariableSpace());
+
+ // There is one manager, so the connections are there for callers passing
no variables too.
+ assertTrue(
+ HopVfs.getFileSystemManager().hasProvider("named-connection-test"),
+ "The named VFS connection is missing from the file system manager");
+
assertTrue(HopVfs.startsWithScheme("named-connection-test://some/file.txt"));
+ }
+
+ @Test
+ @DisplayName("Resolving a file while the named providers register does not
recurse")
+ void reEntrantResolutionDuringRegistration(@TempDir Path tmp) {
+ String url = tmp.resolve("metadata.json").toUri().toString();
+ FileObject[] resolvedDuringRegistration = new FileObject[1];
+
+ // Reading the VFS connection metadata resolves files through HopVfs, so
registering the
+ // providers re-enters getFileSystemManager(). That has to hand back the
manager under
+ // construction instead of building a second one or recursing forever.
+ registerTestVfsPlugin(
+ "reentrant-test",
+ variables -> {
+ try {
+ resolvedDuringRegistration[0] = HopVfs.getFileObject(url);
+ } catch (Exception e) {
+ throw new IllegalStateException("Re-entrant file resolution
failed", e);
+ }
+ });
+
+ HopVfs.setBootstrapVariables(Variables.getADefaultVariableSpace());
+ DefaultFileSystemManager manager = HopVfs.getFileSystemManager();
+
+ assertNotNull(resolvedDuringRegistration[0], "Nothing was resolved during
registration");
+ assertSame(manager,
resolvedDuringRegistration[0].getFileSystem().getFileSystemManager());
+ assertTrue(manager.hasProvider("reentrant-test"));
+ }
+
+ @Test
+ @DisplayName("Resolving a file before a project is enabled does not lock out
the named providers")
+ void earlyResolutionDoesNotLockOutNamedProviders(@TempDir Path tmp) throws
Exception {
+ // A named connection only visible to variables pointing at the metadata
of a project, the way
+ // HOP_METADATA_FOLDER points the VFS plugins at the connections of the
project being opened.
+ registerTestVfsPlugin(
+ "project-connection",
+ variables -> {},
+ variables -> "true".equals(variables.getVariable(PROJECT_MARKER)));
+
+ // HopGui resolves files long before any project is enabled: it loads its
images that way.
+ try (FileObject startup =
HopVfs.getFileObject(tmp.resolve("icon.svg").toUri().toString())) {
+ assertNotNull(startup);
+ }
+ assertFalse(
+ HopVfs.getFileSystemManager().hasProvider("project-connection"),
+ "Nothing pointed at the project metadata yet, so there is nothing to
find");
+
+ // Enabling the project hands us variables that do see its metadata.
+ IVariables projectVariables = Variables.getADefaultVariableSpace();
+ projectVariables.setVariable(PROJECT_MARKER, "true");
+
+ assertTrue(
+
HopVfs.getFileSystemManager(projectVariables).hasProvider("project-connection"),
+ "The named VFS connection of the project was never registered");
+
assertTrue(HopVfs.startsWithScheme("project-connection://container/file.txt"));
+ }
+
+ @Test
+ @DisplayName("A plugin without fixed schemes leaves no unclosed component
behind")
+ void schemelessPluginLeavesNothingUnclosed() {
+ // A provider registered under no scheme at all is adopted as a component
of the manager but
+ // never lands in its providers map, so close() can't reach it and
complains :
+ // "DefaultFilesystemManager.close: not all components are closed".
+ registerTestVfsPlugin("closes-clean", variables -> {});
+ HopVfs.setBootstrapVariables(Variables.getADefaultVariableSpace());
+
+ DefaultFileSystemManager manager = HopVfs.getFileSystemManager();
+ assertTrue(manager.hasProvider("closes-clean"));
+
+ Log log = mock(Log.class);
+ manager.setLogger(log);
+
+ HopVfs.reset();
+
+ ArgumentCaptor<Object> warnings = ArgumentCaptor.forClass(Object.class);
+ verify(log, atLeast(0)).warn(warnings.capture());
+ assertTrue(
+ warnings.getAllValues().stream()
+ .noneMatch(
+ warning -> String.valueOf(warning).contains("not all
components are closed")),
+ "The file system manager was left with components it can't close : "
+ + warnings.getAllValues());
+ }
+
+ /** Register a VFS plugin serving one named connection, and reset HopVfs so
it's picked up. */
+ private void registerTestVfsPlugin(String connectionName,
Consumer<IVariables> whileRegistering) {
+ registerTestVfsPlugin(connectionName, whileRegistering, variables -> true);
+ }
+
+ /**
+ * Register a VFS plugin serving one named connection, visible only to
variables accepted by
+ * {@code visibleTo} the way a connection is only visible to variables
pointing at the metadata
+ * folder holding it.
+ */
+ private void registerTestVfsPlugin(
+ String connectionName,
+ Consumer<IVariables> whileRegistering,
+ Predicate<IVariables> visibleTo) {
+ IVfs vfs =
+ new IVfs() {
+ @Override
+ public String[] getUrlSchemes() {
+ // Like the Minio and Databricks plugins: all schemes come from
the metadata.
+ return new String[] {};
+ }
+
+ @Override
+ public FileProvider getProvider() {
+ return new RamFileProvider();
+ }
+
+ @Override
+ public Map<String, FileProvider> getProviders(IVariables variables) {
+ whileRegistering.accept(variables);
+ Map<String, FileProvider> providers = new HashMap<>();
+ if (visibleTo.test(variables)) {
+ providers.put(connectionName, new RamFileProvider());
+ }
+ return providers;
+ }
+ };
+
+ IPluginMock plugin = mock(IPluginMock.class);
+ when(plugin.getIds()).thenReturn(new String[] {"test-vfs-plugin"});
+ when(plugin.getName()).thenReturn("Test VFS plugin");
+ when(plugin.getMainType()).thenReturn((Class) IVfs.class);
+ when(plugin.loadClass(IVfs.class)).thenReturn(vfs);
+
+ try {
+ PluginRegistry.getInstance().registerPlugin(VfsPluginType.class, plugin);
+ } catch (Exception e) {
+ throw new IllegalStateException("Unable to register the test VFS
plugin", e);
+ }
+ registeredPlugin = plugin;
+
+ // Forget any manager built before the test plugin existed.
+ HopVfs.reset();
+ }
+}
diff --git
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/util/ProjectsUtil.java
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/util/ProjectsUtil.java
index 64b24d775c..8fabc7cd80 100644
---
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/util/ProjectsUtil.java
+++
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/util/ProjectsUtil.java
@@ -133,6 +133,12 @@ public class ProjectsUtil {
project.setMetadataProvider(metadataProvider);
}
+ // The named VFS connections live in the metadata of this project, so hand
HopVfs the variables
+ // to find them with. This also resets the file system manager: the
providers of the previous
+ // project are gone and those of this one are registered the next time VFS
is used.
+ //
+ HopVfs.setBootstrapVariables(variables);
+
// We store the project in the namespace singleton (used mainly in the GUI)
//
HopNamespace.setNamespace(projectName);
diff --git
a/plugins/tech/databricks/src/main/java/org/apache/hop/vfs/databricks/DatabricksVfsPlugin.java
b/plugins/tech/databricks/src/main/java/org/apache/hop/vfs/databricks/DatabricksVfsPlugin.java
index dd6b7a4329..2df9328b9d 100644
---
a/plugins/tech/databricks/src/main/java/org/apache/hop/vfs/databricks/DatabricksVfsPlugin.java
+++
b/plugins/tech/databricks/src/main/java/org/apache/hop/vfs/databricks/DatabricksVfsPlugin.java
@@ -50,7 +50,7 @@ public class DatabricksVfsPlugin implements IVfs {
@Override
public FileProvider getProvider() {
- return new DatabricksFileProvider();
+ return null;
}
@Override
diff --git
a/plugins/tech/databricks/src/test/java/org/apache/hop/vfs/databricks/DatabricksVfsPluginTest.java
b/plugins/tech/databricks/src/test/java/org/apache/hop/vfs/databricks/DatabricksVfsPluginTest.java
index 120d4192c3..841f6747b2 100644
---
a/plugins/tech/databricks/src/test/java/org/apache/hop/vfs/databricks/DatabricksVfsPluginTest.java
+++
b/plugins/tech/databricks/src/test/java/org/apache/hop/vfs/databricks/DatabricksVfsPluginTest.java
@@ -19,6 +19,7 @@ package org.apache.hop.vfs.databricks;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Map;
@@ -36,11 +37,11 @@ class DatabricksVfsPluginTest {
}
@Test
- void getProviderReturnsDatabricksProvider() {
+ void getProviderIsNullWithoutFixedSchemes() {
+ // Nothing to register a provider under, and a provider the manager has no
scheme for is never
+ // closed : "DefaultFilesystemManager.close: not all components are
closed".
DatabricksVfsPlugin plugin = new DatabricksVfsPlugin();
- FileProvider provider = plugin.getProvider();
- assertNotNull(provider);
- assertTrue(provider instanceof DatabricksFileProvider);
+ assertNull(plugin.getProvider());
}
@Test
diff --git
a/plugins/tech/minio/src/main/java/org/apache/hop/vfs/minio/MinioVfsPlugin.java
b/plugins/tech/minio/src/main/java/org/apache/hop/vfs/minio/MinioVfsPlugin.java
index 4dbba9ae6f..573437a110 100644
---
a/plugins/tech/minio/src/main/java/org/apache/hop/vfs/minio/MinioVfsPlugin.java
+++
b/plugins/tech/minio/src/main/java/org/apache/hop/vfs/minio/MinioVfsPlugin.java
@@ -39,7 +39,7 @@ public class MinioVfsPlugin implements IVfs {
@Override
public FileProvider getProvider() {
- return new MinioFileProvider();
+ return null;
}
@Override
diff --git
a/plugins/tech/minio/src/test/java/org/apache/hop/vfs/minio/MinioVfsPluginTest.java
b/plugins/tech/minio/src/test/java/org/apache/hop/vfs/minio/MinioVfsPluginTest.java
index 8edb68f7e3..4544f8f748 100644
---
a/plugins/tech/minio/src/test/java/org/apache/hop/vfs/minio/MinioVfsPluginTest.java
+++
b/plugins/tech/minio/src/test/java/org/apache/hop/vfs/minio/MinioVfsPluginTest.java
@@ -19,6 +19,7 @@ package org.apache.hop.vfs.minio;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Map;
@@ -50,9 +51,10 @@ class MinioVfsPluginTest {
@Test
void testGetProvider() {
- FileProvider provider = plugin.getProvider();
- assertNotNull(provider, "Provider should not be null");
- assertTrue(provider instanceof MinioFileProvider, "Provider should be
MinioFileProvider");
+ // There are no fixed schemes, so there is nothing to register a provider
under. Handing one
+ // over anyway leaves it unreachable and unclosed on the file system
manager :
+ // "DefaultFilesystemManager.close: not all components are closed".
+ assertNull(plugin.getProvider(), "MinIO has no fixed scheme, so it has no
fixed provider");
}
@Test
@@ -81,17 +83,6 @@ class MinioVfsPluginTest {
assertNotNull(providers, "Providers map should not be null");
}
- @Test
- void testMultipleGetProviderCalls() {
- FileProvider provider1 = plugin.getProvider();
- FileProvider provider2 = plugin.getProvider();
-
- assertNotNull(provider1, "First provider should not be null");
- assertNotNull(provider2, "Second provider should not be null");
- assertTrue(provider1 instanceof MinioFileProvider, "Should be
MinioFileProvider");
- assertTrue(provider2 instanceof MinioFileProvider, "Should be
MinioFileProvider");
- }
-
@Test
void testPluginAnnotation() {
// Verify the plugin has the VfsPlugin annotation by checking class
annotations
@@ -122,15 +113,6 @@ class MinioVfsPluginTest {
assertNotNull(providers, "Should handle errors and return empty map");
}
- @Test
- void testProviderCreation() {
- // Test that provider is properly created each time
- FileProvider provider = plugin.getProvider();
-
- assertNotNull(provider, "Provider should be created");
- assertTrue(provider instanceof MinioFileProvider, "Should create
MinioFileProvider");
- }
-
@Test
void testGetProvidersReturnsMap() {
IVariables variables = new Variables();