From 8179e7eb4d0b4a249d8babbaef899a8ab1353b3a Mon Sep 17 00:00:00 2001 From: Leo Leplat <60394504+Rylern@users.noreply.github.com> Date: Thu, 4 Dec 2025 16:20:15 +0000 Subject: [PATCH 1/2] Manually track jars installed with catalogs instead of using a watcher --- .../core/ExtensionCatalogManager.java | 197 +++++++++++------- .../core/ExtensionFolderManager.java | 178 +++++++--------- .../extensionmanager/gui/CatalogManager.java | 2 +- 3 files changed, 200 insertions(+), 177 deletions(-) diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/ExtensionCatalogManager.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/ExtensionCatalogManager.java index fd66d36..bc85e24 100644 --- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/ExtensionCatalogManager.java +++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/ExtensionCatalogManager.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URI; +import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; @@ -37,6 +38,7 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * A manager for catalogs and extensions. It can be used to get access to all saved catalogs, @@ -59,6 +61,8 @@ public class ExtensionCatalogManager implements AutoCloseable{ private final ObservableList savedCatalogs = FXCollections.observableList(new CopyOnWriteArrayList<>()); private final ObservableList savedCatalogsImmutable = FXCollections.unmodifiableObservableList(savedCatalogs); private final Map>> installedExtensions = new ConcurrentHashMap<>(); + private final ObservableList catalogManagedInstalledJars = FXCollections.observableList(new CopyOnWriteArrayList<>()); + private final ObservableList catalogManagedInstalledJarsImmutable = FXCollections.unmodifiableObservableList(catalogManagedInstalledJars); private final ExtensionFolderManager extensionFolderManager; private final ExtensionClassLoader extensionClassLoader; private final String version; @@ -78,6 +82,10 @@ public enum InstallationStep { */ EXTRACTING_ZIP } + private enum Operation { + ADD, + REMOVE + } /** * Create the extension catalog manager. @@ -120,6 +128,12 @@ public ExtensionCatalogManager( } }); + updateCatalogManagedInstalledJarsOfDirectory(extensionFolderManager.getCatalogsDirectoryPath().getValue(), Operation.ADD); + extensionFolderManager.getCatalogsDirectoryPath().addListener((p, o, n) -> { + updateCatalogManagedInstalledJarsOfDirectory(o, Operation.REMOVE); + updateCatalogManagedInstalledJarsOfDirectory(n, Operation.ADD); + }); + loadJars(); } @@ -130,53 +144,47 @@ public void close() throws Exception { } /** - * @return a read only property containing the path to the extension folder. - * It may be updated from any thread and the path (but not the property) can - * be null or invalid + * @return a read only property containing the path to the extension folder. It may be updated from any thread and the + * path (but not the property) canvbe null or invalid */ public ReadOnlyObjectProperty getExtensionDirectoryPath() { return extensionFolderManager.getExtensionDirectoryPath(); } /** - * @return a text describing the release of the current software with the form "v[MAJOR].[MINOR].[PATCH]" - * or "v[MAJOR].[MINOR].[PATCH]-rc[RELEASE_CANDIDATE]" + * @return a text describing the release of the current software with the form "v[MAJOR].[MINOR].[PATCH]" or + * "v[MAJOR].[MINOR].[PATCH]-rc[RELEASE_CANDIDATE]" */ public String getVersion() { return version; } /** - * Get the path to the directory containing the provided catalog. This will also create the - * directory containing all catalogs and installed extensions if it doesn't already exist - * (but the returned directory is not guaranteed to exist). + * Get the path to the directory containing the provided catalog. * * @param savedCatalog the catalog to retrieve * @return the path of the directory containing the provided catalog - * @throws IOException if an I/O error occurs while creating the directory * @throws InvalidPathException if the path cannot be created - * @throws SecurityException if the user doesn't have enough rights to create the directory * @throws NullPointerException if the provided catalog is null or if the path contained in - * {@link #getExtensionDirectoryPath()} is null + * {@link ExtensionFolderManager#getCatalogsDirectoryPath()} is null */ - public Path getCatalogDirectory(SavedCatalog savedCatalog) throws IOException { + public Path getCatalogDirectory(SavedCatalog savedCatalog) { return extensionFolderManager.getCatalogDirectoryPath(savedCatalog); } /** - * Add catalogs to the available list. This will save them to the registry. - * Catalogs with the same name as an already existing catalog will not be added. - * No check will be performed concerning whether the provided catalogs point to + * Add catalogs to the available list. This will save them to the registry. Catalogs with the same name as an already + * existing catalog will not be added. No check will be performed concerning whether the provided catalogs point to * valid catalogs. *

* If an exception occurs (see below), the provided catalogs are not added. * * @param savedCatalogs the catalogs to add. They must have different names - * @throws IOException if an I/O error occurs while saving the registry file. In that case, - * the provided catalogs are not added + * @throws IOException if an I/O error occurs while saving the registry file. In that case, the provided catalogs are + * not added * @throws SecurityException if the user doesn't have sufficient rights to save the registry file - * @throws NullPointerException if the path contained in {@link #getExtensionDirectoryPath()} is null, - * if the provided list of catalogs is null or if one of the provided catalog is null + * @throws NullPointerException if the path contained in {@link #getExtensionDirectoryPath()} is null, if the provided + * list of catalogs is null or if one of the provided catalog is null * @throws IllegalArgumentException if at least two of the provided catalogs have the same name */ public void addCatalog(List savedCatalogs) throws IOException { @@ -213,7 +221,7 @@ public void addCatalog(List savedCatalogs) throws IOException { try { extensionFolderManager.saveRegistry(new Registry(this.savedCatalogs)); - } catch (IOException | SecurityException | NullPointerException e) { + } catch (Exception e) { this.savedCatalogs.removeAll(catalogsToAdd); throw e; @@ -223,8 +231,8 @@ public void addCatalog(List savedCatalogs) throws IOException { } /** - * Get the catalogs added or removed with {@link #addCatalog(List)} and {@link #removeCatalogs(List, boolean)}. - * This list may be updated from any thread and won't contain null elements. + * Get the catalogs added or removed with {@link #addCatalog(List)} and {@link #removeCatalogs(List, boolean)}. This + * list may be updated from any thread and won't contain null elements. * * @return a read-only observable list of all saved catalogs */ @@ -233,24 +241,23 @@ public ObservableList getCatalogs() { } /** - * Remove catalogs from the available list. This will remove them from the - * saved registry and may delete any installed extension belonging to these catalogs. + * Remove catalogs from the available list. This will remove them from the saved registry and may delete any installed + * extension belonging to these catalogs. *

- * Catalogs that are not deletable (see {@link SavedCatalog#deletable()}) won't be - * deleted. + * Catalogs that are not deletable (see {@link SavedCatalog#deletable()}) won't be deleted. *

* If an exception occurs (see below), the provided catalogs are not added. *

- * Warning: this will move the directory returned by {@link #getCatalogDirectory(SavedCatalog)} to - * trash if supported by this platform or recursively delete it if extension are asked to be removed. + * Warning: this will move the directory returned by {@link #getCatalogDirectory(SavedCatalog)} to trash if supported + * by this platform or recursively delete it if extension are asked to be removed. * * @param savedCatalogs the catalogs to remove * @param removeExtensions whether to remove extensions belonging to the catalogs to remove - * @throws IOException if an I/O error occurs while saving the registry file. In that case, - * the provided catalogs are not added + * @throws IOException if an I/O error occurs while saving the registry file. In that case, the provided catalogs are + * not added * @throws SecurityException if the user doesn't have sufficient rights to save the registry file - * @throws NullPointerException if the path contained in {@link #getExtensionDirectoryPath()} is null, - * if the provided list of catalogs is null or if one of the provided catalog is null + * @throws NullPointerException if the path contained in {@link #getExtensionDirectoryPath()} is null, if the provided + * list of catalogs is null or if one of the provided catalog is null */ public void removeCatalogs(List savedCatalogs, boolean removeExtensions) throws IOException { if (getExtensionDirectoryPath().get() == null) { @@ -276,7 +283,7 @@ public void removeCatalogs(List savedCatalogs, boolean removeExten try { extensionFolderManager.saveRegistry(new Registry(this.savedCatalogs)); - } catch (IOException | SecurityException | NullPointerException e) { + } catch (Exception e) { this.savedCatalogs.addAll(catalogsToRemove); throw e; @@ -304,10 +311,9 @@ public void removeCatalogs(List savedCatalogs, boolean removeExten } /** - * Get the path to the directory containing the provided extension of the provided - * catalog. This will also create the directory containing all installed extensions - * of the provided catalog if it doesn't already exist (but the returned directory is - * not guaranteed to be created). + * Get the path to the directory containing the provided extension of the provided catalog. This will also create the + * directory containing all installed extensions of the provided catalog if it doesn't already exist (but the returned + * directory is not guaranteed to be created). * * @param savedCatalog the catalog owning the extension * @param extension the extension to retrieve @@ -323,19 +329,21 @@ public Path getExtensionDirectory(SavedCatalog savedCatalog, Extension extension } /** - * Get the list of links the {@link #installOrUpdateExtension(SavedCatalog, Extension, InstalledExtension, Consumer, BiConsumer)} function - * will download to install the provided extension. + * Get the list of links the {@link #installOrUpdateExtension(SavedCatalog, Extension, InstalledExtension, Consumer, BiConsumer)} + * function will download to install the provided extension. * * @param savedCatalog the catalog owning the extension to install * @param extension the extension to install * @param installationInformation what to install on the extension * @return the list URIs that will be downloaded to install the extension with the provided parameters - * @throws NullPointerException if one of the parameters is null or if the path contained in {@link #getExtensionDirectoryPath()} is null + * @throws NullPointerException if one of the parameters is null or if the path contained in {@link #getExtensionDirectoryPath()} + * is null * @throws IOException if an I/O error occurred while deleting, downloading or installing the extension - * @throws InvalidPathException if a path cannot be created, for example because the extensions folder path contain invalid characters + * @throws InvalidPathException if a path cannot be created, for example because the extensions folder path contain + * invalid characters * @throws SecurityException if the user doesn't have sufficient rights to install or update the extension - * @throws IllegalArgumentException if the release name of the provided installation information cannot be found in the releases - * of the provided extension + * @throws IllegalArgumentException if the release name of the provided installation information cannot be found in + * the releases of the provided extension */ public List getDownloadLinks(SavedCatalog savedCatalog, Extension extension, InstalledExtension installationInformation) throws IOException { return getDownloadUrlsToFilePaths(savedCatalog, extension, installationInformation, false).stream() @@ -344,8 +352,8 @@ public List getDownloadLinks(SavedCatalog savedCatalog, Extension extension } /** - * Install (or update if it already exists) an extension. This may take a lot of time depending on the - * internet connection and the size of the extension, but this operation is cancellable. + * Install (or update if it already exists) an extension. This may take a lot of time depending on the internet connection + * and the size of the extension, but this operation is cancellable. *

* If the extension already exists, it will be deleted before downloading the provided version of the extension. *

@@ -362,12 +370,14 @@ public List getDownloadLinks(SavedCatalog savedCatalog, Extension extension * will be the step currently happening, and its second parameter a text describing the resource * on which the step is happening (for example, a link if the step is a download). This function * will be called from the calling thread - * @throws NullPointerException if one of the parameters is null or if the path contained in {@link #getExtensionDirectoryPath()} is null + * @throws NullPointerException if one of the parameters is null or if the path contained in {@link #getExtensionDirectoryPath()} + * is null * @throws IOException if an I/O error occurred while deleting, downloading or installing the extension - * @throws InvalidPathException if a path cannot be created, for example because the extensions folder path contain invalid characters + * @throws InvalidPathException if a path cannot be created, for example because the extensions folder path contain + * invalid characters * @throws SecurityException if the user doesn't have sufficient rights to install or update the extension - * @throws IllegalArgumentException if the release name of the provided installation information cannot be found in the releases - * of the provided extension + * @throws IllegalArgumentException if the release name of the provided installation information cannot be found in + * the releases of the provided extension * @throws InterruptedException if the calling thread is interrupted */ public void installOrUpdateExtension( @@ -383,6 +393,10 @@ public void installOrUpdateExtension( ); logger.debug("Deleting files of {} before installing or updating it", extension.name()); + updateCatalogManagedInstalledJarsOfDirectory( + extensionFolderManager.getExtensionDirectoryPath(savedCatalog, extension), + Operation.REMOVE + ); extensionFolderManager.deleteExtension(savedCatalog, extension); synchronized (this) { extensionProperty.set(Optional.empty()); @@ -399,6 +413,10 @@ public void installOrUpdateExtension( extensionFolderManager.deleteExtension(savedCatalog, extension); throw e; } + updateCatalogManagedInstalledJarsOfDirectory( + extensionFolderManager.getExtensionDirectoryPath(savedCatalog, extension), + Operation.ADD + ); synchronized (this) { extensionProperty.set(Optional.of(installationInformation)); @@ -412,8 +430,8 @@ public void installOrUpdateExtension( * * @param savedCatalog the catalog owning the extension to find * @param extension the extension to get installed information on - * @return a read-only object property containing an Optional of an installed extension. If the Optional - * is empty, then it means the extension is not installed. This property may be updated from any thread + * @return a read-only object property containing an Optional of an installed extension. If the Optional is empty, + * then it means the extension is not installed. This property may be updated from any thread */ public ReadOnlyObjectProperty> getInstalledExtension(SavedCatalog savedCatalog, Extension extension) { return installedExtensions.computeIfAbsent( @@ -423,18 +441,16 @@ public ReadOnlyObjectProperty> getInstalledExtensio } /** - * @return a read-only observable list of paths pointing to JAR files that were - * added with catalogs to the extension directory. This list can be updated from - * any thread. Note that this list can take a few seconds to update when a JAR is - * added or removed + * @return a read-only observable list of paths pointing to JAR files that were added with catalogs to the extension + * directory. This list can be updated from any thread. Note that this list can take a few seconds to update when a + * JAR is added or removed */ public ObservableList getCatalogManagedInstalledJars() { - return extensionFolderManager.getCatalogManagedInstalledJars(); + return catalogManagedInstalledJarsImmutable; } /** - * Extension classes are automatically loaded with a custom class loader. - * This function returns it. + * Extension classes are automatically loaded with a custom class loader. This function returns it. * * @return the class loader user to load extensions */ @@ -444,8 +460,7 @@ public ClassLoader getExtensionClassLoader() { /** * Set a runnable to be called each time a JAR file is loaded by {@link #getExtensionClassLoader()}. The call may - * happen from any thread. - * Note that the runnable may be called a few seconds after a JAR is added. + * happen from any thread. Note that the runnable may be called a few seconds after a JAR is added. * * @param runnable the runnable to run when a JAR file is loaded * @throws NullPointerException if the provided path is null @@ -455,11 +470,11 @@ public void addOnJarLoadedRunnable(Runnable runnable) { } /** - * Get a list of updates available on the currently installed extensions (with catalogs, extensions - * manually installed are not considered). + * Get a list of updates available on the currently installed extensions (with catalogs, extensions manually installed + * are not considered). * - * @return a CompletableFuture with a list of available updates, or a failed CompletableFuture - * if the update query failed + * @return a CompletableFuture with a list of available updates, or a failed CompletableFuture if the update query + * failed */ public CompletableFuture> getAvailableUpdates() { return CompletableFuture.supplyAsync(() -> savedCatalogs.stream() @@ -473,20 +488,20 @@ public CompletableFuture> getAvailableUpdates() { } /** - * Uninstall an extension by removing its files. This can take some time depending on the number - * of files to delete and the speed of the disk. + * Uninstall an extension by removing its files. This can take some time depending on the number of files to delete + * and the speed of the disk. *

- * Warning: this will move the directory returned by {@link #getExtensionDirectory(SavedCatalog, Extension)} - * to trash or recursively delete it if moving files to trash is not supported by this platform. + * Warning: this will move the directory returned by {@link #getExtensionDirectory(SavedCatalog, Extension)} to + * trash or recursively delete it if moving files to trash is not supported by this platform. * * @param savedCatalog the catalog owning the extension to uninstall * @param extension the extension to uninstall * @throws IOException if an I/O error occurs while deleting the folder - * @throws java.nio.file.InvalidPathException if the path of the extension folder cannot be created, - * for example because the extension name contain invalid characters + * @throws InvalidPathException if the path of the extension folder cannot be created, for example because the extension + * name contain invalid characters * @throws SecurityException if the user doesn't have sufficient rights to delete the extension files - * @throws NullPointerException if the path contained in {@link #getExtensionDirectoryPath()} is null, - * or if one of the parameters is null + * @throws NullPointerException if the path contained in {@link #getExtensionDirectoryPath()} is null, or if one of + * the parameters is null */ public void removeExtension(SavedCatalog savedCatalog, Extension extension) throws IOException { var extensionProperty = installedExtensions.computeIfAbsent( @@ -494,6 +509,10 @@ public void removeExtension(SavedCatalog savedCatalog, Extension extension) thro e -> new SimpleObjectProperty<>() ); + updateCatalogManagedInstalledJarsOfDirectory( + extensionFolderManager.getExtensionDirectoryPath(savedCatalog, extension), + Operation.REMOVE + ); extensionFolderManager.deleteExtension(savedCatalog, extension); synchronized (this) { extensionProperty.set(Optional.empty()); @@ -503,10 +522,9 @@ public void removeExtension(SavedCatalog savedCatalog, Extension extension) thro } /** - * @return a read-only observable list of paths pointing to JAR files that were - * manually added (i.e. not with a catalog) to the extension directory. This list - * can be updated from any thread. Note that this list can take a few seconds to - * update when a JAR is added or removed + * @return a read-only observable list of paths pointing to JAR files that were manually added (i.e. not with a catalog) + * to the extension directory. This list can be updated from any thread. Note that this list can take a few seconds + * to update when a JAR is added or removed */ public ObservableList getManuallyInstalledJars() { return extensionFolderManager.getManuallyInstalledJars(); @@ -531,6 +549,29 @@ private synchronized void setCatalogsFromRegistry() { } } + private void updateCatalogManagedInstalledJarsOfDirectory(Path directory, Operation operation) { + if (directory != null) { + try (Stream files = Files.walk(directory)) { + List jars = files.filter(path -> path.toString().endsWith(".jar")).toList(); + + switch (operation) { + case ADD -> catalogManagedInstalledJars.addAll(jars); + case REMOVE -> catalogManagedInstalledJars.removeAll(jars); + } + } catch (IOException e) { + logger.debug( + "Error while finding jars located in {}. None will be {}", + directory, + switch (operation) { + case ADD -> "added"; + case REMOVE -> "removed"; + }, + e + ); + } + } + } + private Optional getInstalledExtension(CatalogExtension catalogExtension) { try { return extensionFolderManager.getInstalledExtension( @@ -553,8 +594,8 @@ private void loadJars() { change.reset(); }); - addJars(extensionFolderManager.getCatalogManagedInstalledJars()); - extensionFolderManager.getCatalogManagedInstalledJars().addListener((ListChangeListener) change -> { + addJars(catalogManagedInstalledJarsImmutable); + catalogManagedInstalledJarsImmutable.addListener((ListChangeListener) change -> { while (change.next()) { addJars(change.getAddedSubList()); removeJars(change.getRemoved()); diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/ExtensionFolderManager.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/ExtensionFolderManager.java index e90f119..f9c885f 100644 --- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/ExtensionFolderManager.java +++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/ExtensionFolderManager.java @@ -5,6 +5,7 @@ import com.google.gson.JsonSyntaxException; import com.google.gson.stream.JsonReader; import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -78,8 +79,8 @@ class ExtensionFolderManager implements AutoCloseable { private static final Predicate isJar = path -> path.toString().toLowerCase().endsWith(".jar"); private static final Gson gson = new Gson(); private final ReadOnlyObjectProperty extensionDirectoryPath; + private final ObservableValue catalogsDirectoryPath; private final FilesWatcher manuallyInstalledExtensionsWatcher; - private final FilesWatcher catalogManagedInstalledExtensionsWatcher; /** * A type of files to retrieve. */ @@ -121,6 +122,31 @@ public enum FileType { */ public ExtensionFolderManager(ReadOnlyObjectProperty extensionDirectoryPath) { this.extensionDirectoryPath = extensionDirectoryPath; + this.catalogsDirectoryPath = extensionDirectoryPath.map(path -> { + if (path == null) { + return null; + } + + Path catalogsFolder = null; + try { + catalogsFolder = path.resolve(CATALOGS_FOLDER); + + if (Files.isRegularFile(catalogsFolder)) { + logger.debug("Deleting {} because it should be a directory", catalogsFolder); + Files.deleteIfExists(catalogsFolder); + } + Files.createDirectories(catalogsFolder); + + return catalogsFolder; + } catch (IOException | InvalidPathException e) { + logger.debug( + "Error while resolving path of catalogs directory or while creating a corresponding directory", + e + ); + } + + return catalogsFolder; + }); this.manuallyInstalledExtensionsWatcher = new FilesWatcher( extensionDirectoryPath, @@ -134,49 +160,38 @@ public ExtensionFolderManager(ReadOnlyObjectProperty extensionDirectoryPat } } ); - - this.catalogManagedInstalledExtensionsWatcher = new FilesWatcher( - extensionDirectoryPath.map(path -> { - if (path == null) { - return null; - } else { - try { - return getAndCreateCatalogsDirectory(); - } catch (IOException | InvalidPathException | SecurityException | NullPointerException e) { - logger.debug("Error when getting catalog path from {}", path, e); - return null; - } - } - }), - isJar, - path -> false - ); } @Override public void close() throws Exception { manuallyInstalledExtensionsWatcher.close(); - catalogManagedInstalledExtensionsWatcher.close(); } /** - * @return a read only property containing the path to the extension folder. - * It may be updated from any thread and the path (but not the property) can be null - * or invalid + * @return a read only property containing the path to the extension folder. It may be updated from any thread and + * the path (but not the property) can be null or invalid */ public ReadOnlyObjectProperty getExtensionDirectoryPath() { return extensionDirectoryPath; } /** - * Save the provided registry to disk. It can later be retrieved with - * {@link #getSavedRegistry()}. + * @return an observable value containing the path to the "catalogs" directory in the extension folder. It may be + * updated from any thread and the path can be null or invalid. Note that if {@link #getExtensionDirectoryPath()} + * points to a valid directory, the path returned by this function should also point to a valid (i.e. existing) directory + */ + public ObservableValue getCatalogsDirectoryPath() { + return catalogsDirectoryPath; + } + + /** + * Save the provided registry to disk. It can later be retrieved with {@link #getSavedRegistry()}. * * @param registry the registry to save * @throws IOException if an I/O error occurs while writing the registry file - * @throws SecurityException if the user doesn't have sufficient rights to save the file - * @throws NullPointerException if the path contained in {@link #getExtensionDirectoryPath()} is null + * @throws NullPointerException if the path contained in {@link #getCatalogsDirectoryPath()} is null * or if the provided registry is null + * @throws InvalidPathException if the path to the registry cannot be created */ public synchronized void saveRegistry(Registry registry) throws IOException { try ( @@ -189,13 +204,14 @@ public synchronized void saveRegistry(Registry registry) throws IOException { } /** + * Read and return the registry that was last saved with {@link #saveRegistry(Registry)}. + * * @return the registry that was last saved with {@link #saveRegistry(Registry)} * @throws IOException if an I/O error occurs while reading the registry file * @throws java.io.FileNotFoundException if the registry file does not exist * @throws java.nio.file.InvalidPathException if the path to the registry cannot be created - * @throws SecurityException if the user doesn't have enough rights to read the registry - * @throws NullPointerException if the registry file exists but is empty or if the path contained - * in {@link #getExtensionDirectoryPath()} is null + * @throws NullPointerException if the registry file exists but is empty or if the path contained in + * {@link #getCatalogsDirectoryPath()} is null * @throws JsonSyntaxException if the registry file exists but contain a malformed JSON element * @throws JsonIOException if there was a problem reading from the registry file */ @@ -209,33 +225,28 @@ public synchronized Registry getSavedRegistry() throws IOException { } /** - * Get the path to the directory containing the provided catalog. This will create the - * "catalogs" directory (see the class description) if it doesn't already exist. + * Get the path to the directory containing the provided catalog. * * @param savedCatalog the catalog to retrieve * @return the path to the directory containing the provided catalog - * @throws IOException if an I/O error occurs while creating the directory * @throws InvalidPathException if the path cannot be created - * @throws SecurityException if the user doesn't have enough rights to create the directory * @throws NullPointerException if the provided catalog is null or if the path contained in - * {@link #getExtensionDirectoryPath()} is null + * {@link #getCatalogsDirectoryPath()} is null */ - public synchronized Path getCatalogDirectoryPath(SavedCatalog savedCatalog) throws IOException { - return getAndCreateCatalogsDirectory().resolve(FileTools.stripInvalidFilenameCharacters(savedCatalog.name())); + public synchronized Path getCatalogDirectoryPath(SavedCatalog savedCatalog) { + return catalogsDirectoryPath.getValue().resolve(FileTools.stripInvalidFilenameCharacters(savedCatalog.name())); } /** - * Delete all extensions belonging to the provided catalog. This will move the directory - * returned by {@link #getCatalogDirectoryPath(SavedCatalog)} to trash or recursively delete - * it if moving to trash is not supported by this platform. + * Delete all extensions belonging to the provided catalog. This will move the directory returned by + * {@link #getCatalogDirectoryPath(SavedCatalog)} to trash or recursively delete it if moving to trash is not supported + * by this platform. * * @param savedCatalog the catalog owning the extensions to delete * @throws IOException if an I/O error occur while deleting the files - * @throws SecurityException if the user doesn't have sufficient rights to delete - * the catalog - * @throws java.nio.file.InvalidPathException if the path to the catalog directory cannot be created - * @throws NullPointerException if the path contained in {@link #getExtensionDirectoryPath()} is null - * or if the provided catalog is null + * @throws InvalidPathException if the path to the catalog directory cannot be created + * @throws NullPointerException if the path contained in {@link #getCatalogsDirectoryPath()} is null or if the + * provided catalog is null */ public synchronized void deleteExtensionsFromCatalog(SavedCatalog savedCatalog) throws IOException { File catalogDirectory = getCatalogDirectoryPath(savedCatalog).toFile(); @@ -244,37 +255,32 @@ public synchronized void deleteExtensionsFromCatalog(SavedCatalog savedCatalog) } /** - * Get the path to the directory containing the provided extension of the provided - * catalog. This will create the directory containing all extensions of the provided - * catalog if it doesn't already exist. + * Get the path to the directory containing the provided extension of the provided catalog. * * @param savedCatalog the catalog owning the extension * @param extension the extension to retrieve * @return the path to the folder containing the provided extension - * @throws IOException if an I/O error occurs while creating the directory * @throws InvalidPathException if the path cannot be created - * @throws SecurityException if the user doesn't have enough rights to create the directory * @throws NullPointerException if one of the provided parameter is null or if the path contained in - * {@link #getExtensionDirectoryPath()} is null + * {@link #getCatalogsDirectoryPath()} is null */ - public synchronized Path getExtensionDirectoryPath(SavedCatalog savedCatalog, Extension extension) throws IOException { + public synchronized Path getExtensionDirectoryPath(SavedCatalog savedCatalog, Extension extension) { return getCatalogDirectoryPath(savedCatalog).resolve(FileTools.stripInvalidFilenameCharacters(extension.name())); } /** - * Indicate whether an extension belonging to a catalog is installed. If that's the - * case, installation information are returned. + * Indicate whether an extension belonging to a catalog is installed. If that's the case, installation information + * are returned. * * @param savedCatalog the catalog owning the extension to search * @param extension the extension to search * @return an empty Optional if the provided extension is not installed, or information * on the installed extension * @throws IOException if an I/O error occurs when searching for the extension - * @throws java.nio.file.InvalidPathException if the Path object of the extension cannot be created, for example - * because the extensions folder path contain invalid characters - * @throws SecurityException if the user doesn't have sufficient rights to search for extension files - * @throws NullPointerException if the path contained in {@link #getExtensionDirectoryPath()} is null or - * if one of the parameters is null + * @throws InvalidPathException if the Path object of the extension cannot be created, for example because the + * extensions folder path contain invalid characters + * @throws NullPointerException if the path contained in {@link #getCatalogsDirectoryPath()} is null or if one of + * the parameters is null */ public synchronized Optional getInstalledExtension(SavedCatalog savedCatalog, Extension extension) throws IOException { Path extensionPath = getExtensionDirectoryPath(savedCatalog, extension); @@ -331,17 +337,16 @@ public synchronized Optional getInstalledExtension(SavedCata /** * Delete all files of an extension belonging to a catalog. This will move the - * {@link #getExtensionDirectoryPath(SavedCatalog, Extension)} directory to trash or - * recursively delete it the platform doesn't support moving files to trash. + * {@link #getExtensionDirectoryPath(SavedCatalog, Extension)} directory to trash or recursively delete it the platform + * doesn't support moving files to trash. * * @param savedCatalog the catalog owning the extension to delete * @param extension the extension to delete * @throws IOException if an I/O error occurs while deleting the folder - * @throws java.nio.file.InvalidPathException if the Path object of the extension folder cannot be created, for example - * because the extensions folder path contain invalid characters - * @throws SecurityException if the user doesn't have sufficient rights to delete the folder - * @throws NullPointerException if the path contained in {@link #getExtensionDirectoryPath()} is null - * or if one of the provided parameter is null + * @throws InvalidPathException if the Path object of the extension folder cannot be created, for example because the + * extensions folder path contain invalid characters + * @throws NullPointerException if the path contained in {@link #getCatalogsDirectoryPath()} is null or if one of the + * provided parameter is null */ public synchronized void deleteExtension(SavedCatalog savedCatalog, Extension extension) throws IOException { FileTools.moveDirectoryToTrashOrDeleteRecursively(getExtensionDirectoryPath(savedCatalog, extension).toFile()); @@ -349,9 +354,8 @@ public synchronized void deleteExtension(SavedCatalog savedCatalog, Extension ex } /** - * Get (and create if asked and if it doesn't already exist) the path to the folder - * containing the specified files of the provided extension at the specified - * version belonging to the provided catalog. + * Get (and create if asked and if it doesn't already exist) the path to the folder containing the specified files of + * the provided extension at the specified version belonging to the provided catalog. * * @param savedCatalog the catalog owning the extension * @param extension the extension to find the folder to @@ -360,11 +364,10 @@ public synchronized void deleteExtension(SavedCatalog savedCatalog, Extension ex * @param createDirectory whether to create a folder on the returned path * @return the path to the folder containing the specified files of the provided extension * @throws IOException if an I/O error occurs while creating the folder - * @throws java.nio.file.InvalidPathException if the Path object cannot be created, for example - * because the extensions folder path contain invalid characters - * @throws SecurityException if the user doesn't have sufficient rights to create the folder - * @throws NullPointerException if the path contained in {@link #getExtensionDirectoryPath()} is null - * or if one of the provided parameters is null + * @throws java.nio.file.InvalidPathException if the Path object cannot be created, for example because the extensions + * folder path contain invalid characters + * @throws NullPointerException if the path contained in {@link #getCatalogsDirectoryPath()} is null or if one of the + * provided parameters is null */ public synchronized Path getExtensionPath( SavedCatalog savedCatalog, @@ -391,35 +394,14 @@ public synchronized Path getExtensionPath( } /** - * @return a read-only observable list of paths pointing to JAR files that were - * manually added (i.e. not with a catalog) to the extension directory. This list - * may be updated from any thread + * @return a read-only observable list of paths pointing to JAR files that were manually added (i.e. not with a catalog) + * to the extension directory. This list may be updated from any thread */ public ObservableList getManuallyInstalledJars() { return manuallyInstalledExtensionsWatcher.getFiles(); } - /** - * @return a read-only observable list of paths pointing to JAR files that were - * added with catalogs to the extension directory. This list may be updated from any thread - */ - public ObservableList getCatalogManagedInstalledJars() { - return catalogManagedInstalledExtensionsWatcher.getFiles(); - } - - private Path getRegistryPath() throws IOException { - return getAndCreateCatalogsDirectory().resolve(REGISTRY_NAME); - } - - private Path getAndCreateCatalogsDirectory() throws IOException { - Path catalogsFolder = extensionDirectoryPath.get().resolve(CATALOGS_FOLDER); - - if (Files.isRegularFile(catalogsFolder)) { - logger.debug("Deleting {} because it should be a directory", catalogsFolder); - Files.deleteIfExists(catalogsFolder); - } - Files.createDirectories(catalogsFolder); - - return catalogsFolder; + private Path getRegistryPath() { + return catalogsDirectoryPath.getValue().resolve(REGISTRY_NAME); } } diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/CatalogManager.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/CatalogManager.java index 3f51e10..3800c26 100644 --- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/CatalogManager.java +++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/CatalogManager.java @@ -332,7 +332,7 @@ private void deleteCatalogs(List catalogs) { .map(savedCatalog -> { try { return String.format("\"%s\"", extensionCatalogManager.getCatalogDirectory(savedCatalog)); - } catch (IOException | InvalidPathException | SecurityException | NullPointerException e) { + } catch (InvalidPathException | SecurityException | NullPointerException e) { logger.error("Cannot retrieve path of {}", savedCatalog.name(), e); return null; } From ad227d1c2d9f2580ec9a4d7865dcdeb047f65e95 Mon Sep 17 00:00:00 2001 From: lleplat Date: Fri, 5 Dec 2025 11:13:29 +0000 Subject: [PATCH 2/2] Adapt tests --- .../core/TestExtensionCatalogManager.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/TestExtensionCatalogManager.java b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/TestExtensionCatalogManager.java index d23ce09..d55be27 100644 --- a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/TestExtensionCatalogManager.java +++ b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/TestExtensionCatalogManager.java @@ -36,7 +36,8 @@ public class TestExtensionCatalogManager { private static final String CATALOG_NAME = "catalog.json"; - private static final int CHANGE_WAITING_TIME_MS = 10000; + private static final int CHANGE_WAITING_TIME_CATALOG_MS = 100; + private static final int CHANGE_WAITING_TIME_MANUAL_MS = 10000; private static SimpleServer server; @BeforeAll @@ -945,7 +946,7 @@ void Check_Installed_Jars_After_Extension_Installation() throws Exception { (step, resource) -> {} ); - Thread.sleep(CHANGE_WAITING_TIME_MS); // wait for list to update + Thread.sleep(CHANGE_WAITING_TIME_CATALOG_MS); // wait for list to update TestUtils.assertCollectionsEqualsWithoutOrder( expectedJarNames, extensionCatalogManager.getCatalogManagedInstalledJars().stream() @@ -1017,7 +1018,7 @@ void Check_Jar_Loaded_Runnable_Run_After_Extension_Installation() throws Excepti (step, resource) -> {} ); - Thread.sleep(CHANGE_WAITING_TIME_MS); // wait for list to update + Thread.sleep(CHANGE_WAITING_TIME_CATALOG_MS); // wait for list to update Assertions.assertEquals(expectedNumberOfCalls, numberOfJarLoaded.get()); } } @@ -1221,7 +1222,7 @@ void Check_Installed_Jars_After_Extension_Removal() throws Exception { extensionCatalogManager.removeExtension(catalog, extension); - Thread.sleep(CHANGE_WAITING_TIME_MS); // wait for list to update + Thread.sleep(CHANGE_WAITING_TIME_CATALOG_MS); // wait for list to update TestUtils.assertCollectionsEqualsWithoutOrder( expectedJarNames, extensionCatalogManager.getCatalogManagedInstalledJars().stream() @@ -1267,7 +1268,7 @@ void Check_Manually_Installed_Jars_When_Two_Jars_Added_After_Manager_Creation() List jars = extensionCatalogManager.getManuallyInstalledJars(); - Thread.sleep(CHANGE_WAITING_TIME_MS); // wait for list to update + Thread.sleep(CHANGE_WAITING_TIME_MANUAL_MS); // wait for list to update TestUtils.assertCollectionsEqualsWithoutOrder(expectedJars, jars); } }