From a6f0b6eba2f331bdfb6e0e2a91ca8386eef9bbd8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?L=C3=A9o=20Leplat?=
<60394504+Rylern@users.noreply.github.com>
Date: Fri, 12 Dec 2025 09:54:54 +0000
Subject: [PATCH 1/8] Extensions in json
---
.../app/ExtensionManagerApp.java | 15 +-
.../extensionmanager/app/package-info.java | 3 +-
.../core/ExtensionCatalogManager.java | 697 ++++++++----------
.../core/ExtensionClassLoader.java | 17 +-
.../core/ExtensionFolderManager.java | 224 ++----
.../ext/extensionmanager/core/Version.java | 6 +-
.../core/catalog/Catalog.java | 192 ++++-
.../core/catalog/Extension.java | 231 ++++--
.../core/catalog/Release.java | 169 +++--
.../core/catalog/UpdateAvailable.java | 12 +
.../core/catalog/package-info.java | 11 +-
.../core/model/CatalogModel.java | 56 ++
.../CatalogModelFetcher.java} | 16 +-
.../core/model/ExtensionModel.java | 61 ++
.../core/model/ReleaseModel.java | 110 +++
.../core/{catalog => model}/Utils.java | 3 +-
.../VersionRangeModel.java} | 32 +-
.../core/model/package-info.java | 11 +
.../core/registry/Registry.java | 54 ++
.../core/registry/RegistryCatalog.java | 34 +
.../core/registry/RegistryExtension.java | 20 +
.../core/registry/package-info.java | 5 +
.../savedentities/InstalledExtension.java | 10 -
.../core/savedentities/Registry.java | 22 -
.../core/savedentities/SavedCatalog.java | 17 -
.../core/savedentities/UpdateAvailable.java | 10 -
.../core/savedentities/package-info.java | 4 -
.../core/tools/FileDownloader.java | 16 +-
.../core/tools/FileTools.java | 22 +-
.../core/tools/FilesWatcher.java | 16 +-
.../core/tools/GitHubRawLinkFinder.java | 11 +-
.../core/tools/RecursiveDirectoryWatcher.java | 26 +-
.../core/tools/ZipExtractor.java | 26 +-
.../core/tools/package-info.java | 3 +-
.../extensionmanager/gui/CatalogManager.java | 125 ++--
.../gui/ExtensionCatalogModel.java | 68 +-
.../gui/ExtensionManager.java | 81 +-
.../gui/ManuallyInstalledExtensionLine.java | 11 +-
.../extensionmanager/gui/ProgressWindow.java | 10 +-
.../ext/extensionmanager/gui/UiUtils.java | 62 +-
.../gui/catalog/CatalogPane.java | 106 +--
.../gui/catalog/ExtensionDetails.java | 9 +-
.../gui/catalog/ExtensionLine.java | 116 ++-
.../gui/catalog/ExtensionModel.java | 38 +
.../catalog/ExtensionModificationWindow.java | 147 ++--
.../extensionmanager/gui/package-info.java | 4 +-
.../ext/extensionmanager/package-info.java | 8 +-
.../ext/extensionmanager/strings.properties | 3 +-
.../extensionmanager/strings_fr.properties | 3 +-
...estExtensionModelCatalogModelManager.java} | 210 +++---
.../TestCatalogModel.java} | 42 +-
.../TestCatalogModelFetcher.java} | 28 +-
.../TestExtensionModel.java} | 142 ++--
.../TestReleaseModel.java} | 88 +--
.../TestVersionRangeModel.java} | 72 +-
.../{catalog => model}/invalid_catalog.json | 0
.../{catalog => model}/valid_catalog.json | 0
57 files changed, 1942 insertions(+), 1593 deletions(-)
create mode 100644 extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/UpdateAvailable.java
create mode 100644 extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/CatalogModel.java
rename extensionmanager/src/main/java/qupath/ext/extensionmanager/core/{catalog/CatalogFetcher.java => model/CatalogModelFetcher.java} (87%)
create mode 100644 extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/ExtensionModel.java
create mode 100644 extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/ReleaseModel.java
rename extensionmanager/src/main/java/qupath/ext/extensionmanager/core/{catalog => model}/Utils.java (96%)
rename extensionmanager/src/main/java/qupath/ext/extensionmanager/core/{catalog/VersionRange.java => model/VersionRangeModel.java} (82%)
create mode 100644 extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/package-info.java
create mode 100644 extensionmanager/src/main/java/qupath/ext/extensionmanager/core/registry/Registry.java
create mode 100644 extensionmanager/src/main/java/qupath/ext/extensionmanager/core/registry/RegistryCatalog.java
create mode 100644 extensionmanager/src/main/java/qupath/ext/extensionmanager/core/registry/RegistryExtension.java
create mode 100644 extensionmanager/src/main/java/qupath/ext/extensionmanager/core/registry/package-info.java
delete mode 100644 extensionmanager/src/main/java/qupath/ext/extensionmanager/core/savedentities/InstalledExtension.java
delete mode 100644 extensionmanager/src/main/java/qupath/ext/extensionmanager/core/savedentities/Registry.java
delete mode 100644 extensionmanager/src/main/java/qupath/ext/extensionmanager/core/savedentities/SavedCatalog.java
delete mode 100644 extensionmanager/src/main/java/qupath/ext/extensionmanager/core/savedentities/UpdateAvailable.java
delete mode 100644 extensionmanager/src/main/java/qupath/ext/extensionmanager/core/savedentities/package-info.java
create mode 100644 extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/catalog/ExtensionModel.java
rename extensionmanager/src/test/java/qupath/ext/extensionmanager/core/{TestExtensionCatalogManager.java => TestExtensionModelCatalogModelManager.java} (87%)
rename extensionmanager/src/test/java/qupath/ext/extensionmanager/core/{catalog/TestCatalog.java => model/TestCatalogModel.java} (80%)
rename extensionmanager/src/test/java/qupath/ext/extensionmanager/core/{catalog/TestCatalogFetcher.java => model/TestCatalogModelFetcher.java} (73%)
rename extensionmanager/src/test/java/qupath/ext/extensionmanager/core/{catalog/TestExtension.java => model/TestExtensionModel.java} (81%)
rename extensionmanager/src/test/java/qupath/ext/extensionmanager/core/{catalog/TestRelease.java => model/TestReleaseModel.java} (84%)
rename extensionmanager/src/test/java/qupath/ext/extensionmanager/core/{catalog/TestVersionRange.java => model/TestVersionRangeModel.java} (84%)
rename extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/{catalog => model}/invalid_catalog.json (100%)
rename extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/{catalog => model}/valid_catalog.json (100%)
diff --git a/extensionmanager-app/src/main/java/qupath/ext/extensionmanager/app/ExtensionManagerApp.java b/extensionmanager-app/src/main/java/qupath/ext/extensionmanager/app/ExtensionManagerApp.java
index 6788a31..dcca058 100644
--- a/extensionmanager-app/src/main/java/qupath/ext/extensionmanager/app/ExtensionManagerApp.java
+++ b/extensionmanager-app/src/main/java/qupath/ext/extensionmanager/app/ExtensionManagerApp.java
@@ -4,8 +4,7 @@
import javafx.beans.property.SimpleObjectProperty;
import javafx.stage.Stage;
import qupath.ext.extensionmanager.core.ExtensionCatalogManager;
-import qupath.ext.extensionmanager.core.savedentities.Registry;
-import qupath.ext.extensionmanager.core.savedentities.SavedCatalog;
+import qupath.ext.extensionmanager.core.catalog.Catalog;
import qupath.ext.extensionmanager.gui.ExtensionManager;
import java.io.IOException;
@@ -15,9 +14,8 @@
import java.util.List;
/**
- * An application that launches a {@link ExtensionManager}. A temporary directory (with
- * an empty extension JAR file inside) is used as the extension directory.
- * This catalog is used.
+ * An application that launches a {@link ExtensionManager}. A temporary directory (with an empty extension JAR file inside)
+ * is used as the extension directory. This catalog is used.
*/
public class ExtensionManagerApp extends Application {
@@ -38,13 +36,12 @@ public void start(Stage stage) throws IOException {
new SimpleObjectProperty<>(createExtensionDirectory()),
ExtensionManagerApp.class.getClassLoader(),
"v0.6.0",
- new Registry(List.of(new SavedCatalog(
+ List.of(new Catalog(
"QuPath catalog",
"Extensions maintained by the QuPath team",
URI.create("https://github.com/qupath/qupath-catalog"),
- URI.create("https://raw.githubusercontent.com/qupath/qupath-catalog/refs/heads/main/catalog.json"),
- false
- )))
+ URI.create("https://raw.githubusercontent.com/qupath/qupath-catalog/refs/heads/main/catalog.json")
+ ))
);
new ExtensionManager(extensionCatalogManager, () -> {}).show();
diff --git a/extensionmanager-app/src/main/java/qupath/ext/extensionmanager/app/package-info.java b/extensionmanager-app/src/main/java/qupath/ext/extensionmanager/app/package-info.java
index 288d46e..99571a1 100644
--- a/extensionmanager-app/src/main/java/qupath/ext/extensionmanager/app/package-info.java
+++ b/extensionmanager-app/src/main/java/qupath/ext/extensionmanager/app/package-info.java
@@ -1,5 +1,4 @@
/**
- * This package contains an application to start an
- * {@link qupath.ext.extensionmanager.gui.ExtensionManager ExtensionManager}.
+ * This package contains an application to start an {@link qupath.ext.extensionmanager.gui.ExtensionManager ExtensionManager}.
*/
package qupath.ext.extensionmanager.app;
\ No newline at end of file
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 bc85e24..8d0ae5e 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/ExtensionCatalogManager.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/ExtensionCatalogManager.java
@@ -1,23 +1,20 @@
package qupath.ext.extensionmanager.core;
-import javafx.beans.property.ObjectProperty;
-import javafx.beans.property.ReadOnlyObjectProperty;
-import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import qupath.ext.extensionmanager.core.catalog.CatalogFetcher;
-import qupath.ext.extensionmanager.core.savedentities.SavedCatalog;
-import qupath.ext.extensionmanager.core.savedentities.UpdateAvailable;
+import qupath.ext.extensionmanager.core.catalog.Catalog;
+import qupath.ext.extensionmanager.core.catalog.Extension;
+import qupath.ext.extensionmanager.core.catalog.Release;
+import qupath.ext.extensionmanager.core.registry.Registry;
+import qupath.ext.extensionmanager.core.catalog.UpdateAvailable;
+import qupath.ext.extensionmanager.core.registry.RegistryCatalog;
import qupath.ext.extensionmanager.core.tools.FileDownloader;
import qupath.ext.extensionmanager.core.tools.FileTools;
import qupath.ext.extensionmanager.core.tools.ZipExtractor;
-import qupath.ext.extensionmanager.core.catalog.Extension;
-import qupath.ext.extensionmanager.core.catalog.Release;
-import qupath.ext.extensionmanager.core.savedentities.InstalledExtension;
-import qupath.ext.extensionmanager.core.savedentities.Registry;
import java.io.IOError;
import java.io.IOException;
@@ -29,27 +26,25 @@
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
-import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutionException;
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,
- * add or remove a catalog, get access to all installed extensions, and install or delete an extension.
- * Manually installed extensions are automatically detected.
+ * A manager for catalogs and extensions. It can be used to get access to all saved catalogs, add or remove a catalog, get
+ * access to all installed extensions, and install or delete an extension. Manually installed extensions are automatically
+ * detected.
*
- * It also automatically loads extension classes with a custom ClassLoader (see {@link #getExtensionClassLoader()}).
- * Note that removed extensions are not unloaded from the class loader.
+ * It also automatically loads extension classes with a custom ClassLoader (see {@link #getExtensionClassLoader()}). Note
+ * that removed extensions are not unloaded from the class loader.
*
- * The list of active catalogs and installed extensions is determined by this class. It is internally saved
- * in a registry JSON file located in the extension directory (see {@link Registry}).
+ * The list of active catalogs and installed extensions is determined by this class. It is internally saved in a registry
+ * JSON file located in the extension directory.
*
* This class is thread-safe.
*
@@ -58,16 +53,13 @@
public class ExtensionCatalogManager implements AutoCloseable{
private static final Logger logger = LoggerFactory.getLogger(ExtensionCatalogManager.class);
- private final ObservableList savedCatalogs = FXCollections.observableList(new CopyOnWriteArrayList<>());
- private final ObservableList savedCatalogsImmutable = FXCollections.unmodifiableObservableList(savedCatalogs);
- private final Map>> installedExtensions = new ConcurrentHashMap<>();
+ private final ObservableList catalogs = FXCollections.observableList(new CopyOnWriteArrayList<>());
+ private final ObservableList catalogsImmutable = FXCollections.unmodifiableObservableList(catalogs);
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;
- private final Registry defaultRegistry;
- private record CatalogExtension(SavedCatalog savedCatalog, Extension extension) {}
+ private final Version version;
private record UriFileName(URI uri, Path filePath) {}
/**
* Indicate an extension installation step
@@ -90,354 +82,276 @@ private enum Operation {
/**
* Create the extension catalog manager.
*
- * @param extensionDirectoryPath a read-only property pointing to the path the extension directory should have. The
- * path can be null or invalid (but not the property). If this property is changed,
- * catalogs and extensions will be set to the content of the new value of the property
+ * @param extensionDirectoryPath an observable value pointing to the path the extension directory should have. The
+ * path can be null or invalid (but not the observable). If this observable is changed,
+ * catalogs and extensions will be set to the content of the new value of the observable
* (so will be reset if the new path is empty)
* @param parentClassLoader the class loader that should be the parent of the extension class loader. Can be null to use
* the bootstrap class loader
- * @param version a text describing the release of the current software with the form "v[MAJOR].[MINOR].[PATCH]"
- * or "v[MAJOR].[MINOR].[PATCH]-rc[RELEASE_CANDIDATE]". It will determine which extensions are
- * compatible
- * @param defaultRegistry the default registry to use when the saved one cannot be used. Can be null
+ * @param version a text describing the release of the current software with the form "v[MAJOR].[MINOR].[PATCH]" or
+ * "v[MAJOR].[MINOR].[PATCH]-rc[RELEASE_CANDIDATE]". It will determine which extensions are compatible
+ * @param defaultCatalogs a list of catalogs this manager should use by default, i.e. when no catalog or extension is
+ * installed
* @throws IllegalArgumentException if the provided version doesn't meet the specified requirements
- * @throws SecurityException if the user doesn't have enough rights to create the extension class loader
- * @throws NullPointerException if extensionDirectoryPath or version is null
+ * @throws NullPointerException if one of the parameters (except the class loader) is null
*/
public ExtensionCatalogManager(
- ReadOnlyObjectProperty extensionDirectoryPath,
+ ObservableValue extensionDirectoryPath,
ClassLoader parentClassLoader,
String version,
- Registry defaultRegistry
+ List defaultCatalogs
) {
- Version.isValid(version, true);
-
this.extensionFolderManager = new ExtensionFolderManager(extensionDirectoryPath);
this.extensionClassLoader = new ExtensionClassLoader(parentClassLoader);
- this.version = version;
- this.defaultRegistry = defaultRegistry;
-
- setCatalogsFromRegistry();
- extensionDirectoryPath.addListener((p, o, n) -> {
- setCatalogsFromRegistry();
+ this.version = new Version(version);
- synchronized (this) {
- for (CatalogExtension catalogExtension : installedExtensions.keySet()) {
- installedExtensions.get(catalogExtension).set(getInstalledExtension(catalogExtension));
- }
- }
- });
-
- updateCatalogManagedInstalledJarsOfDirectory(extensionFolderManager.getCatalogsDirectoryPath().getValue(), Operation.ADD);
- extensionFolderManager.getCatalogsDirectoryPath().addListener((p, o, n) -> {
- updateCatalogManagedInstalledJarsOfDirectory(o, Operation.REMOVE);
- updateCatalogManagedInstalledJarsOfDirectory(n, Operation.ADD);
- });
+ resetCatalogsAndJars(defaultCatalogs);
+ extensionFolderManager.getCatalogsDirectoryPath().addListener((p, o, n) ->
+ resetCatalogsAndJars(defaultCatalogs)
+ );
loadJars();
}
@Override
public void close() throws Exception {
- this.extensionFolderManager.close();
this.extensionClassLoader.close();
+ this.extensionFolderManager.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) canvbe null or invalid
+ * @return the version of the current software, as given in {@link #ExtensionCatalogManager(ObservableValue, ClassLoader, String, List)}
*/
- public ReadOnlyObjectProperty getExtensionDirectoryPath() {
- return extensionFolderManager.getExtensionDirectoryPath();
+ public Version getVersion() {
+ return version;
}
/**
- * @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 an observable value containing the path to the extension folder. It may be updated from any thread and the
+ * path (but not the observable) can be null or invalid
*/
- public String getVersion() {
- return version;
+ public ObservableValue getExtensionDirectory() {
+ return extensionFolderManager.getExtensionDirectoryPath();
}
/**
* Get the path to the directory containing the provided catalog.
*
- * @param savedCatalog the catalog to retrieve
+ * @param catalogName the name of the catalog to retrieve
* @return the path of the directory containing the provided catalog
* @throws InvalidPathException if the path cannot be created
- * @throws NullPointerException if the provided catalog is null or if the path contained in
- * {@link ExtensionFolderManager#getCatalogsDirectoryPath()} is null
+ * @throws NullPointerException if the provided catalog is null or if the path contained in {@link #getExtensionDirectory()}
+ * is null
*/
- public Path getCatalogDirectory(SavedCatalog savedCatalog) {
- return extensionFolderManager.getCatalogDirectoryPath(savedCatalog);
+ public Path getCatalogDirectory(String catalogName) {
+ return extensionFolderManager.getCatalogDirectoryPath(catalogName);
}
/**
- * 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.
+ * Add and save a catalog.
*
- * If an exception occurs (see below), the provided catalogs are not added.
+ * This operation may take some time, but can be interrupted.
*
- * @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 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 IllegalArgumentException if at least two of the provided catalogs have the same name
+ * @param catalog the catalog to add. It must have a different name from the ones returned by {@link #getCatalogs()}
+ * @throws IllegalArgumentException if a catalog with the same name already exists
+ * @throws IOException if an I/O error occurs while saving the catalogs to disk
+ * @throws NullPointerException if the path contained in {@link #getExtensionDirectory()} is null or if the provided
+ * catalog is null
+ * @throws InvalidPathException if the path to the registry containing the list of catalogs cannot be created
+ * @throws ExecutionException if an error occurred while saving the registry
+ * @throws InterruptedException if the calling thread is interrupted
*/
- public void addCatalog(List savedCatalogs) throws IOException {
- if (savedCatalogs.stream().map(SavedCatalog::name).collect(Collectors.toSet()).size() < savedCatalogs.size()) {
+ public synchronized void addCatalog(Catalog catalog) throws IOException, ExecutionException, InterruptedException {
+ Objects.requireNonNull(catalog);
+
+ if (catalogs.stream().map(Catalog::getName).anyMatch(catalogName -> catalogName.equals(catalog.getName()))) {
throw new IllegalArgumentException(String.format(
- "Two of the provided catalogs %s have the same name",
- savedCatalogs
+ "Cannot add %s: a catalog with the same name already exists",
+ catalog
));
}
- if (getExtensionDirectoryPath().get() == null) {
- throw new NullPointerException("The extension directory path is null");
- }
-
- List catalogsToAdd;
- synchronized (this) {
- catalogsToAdd = savedCatalogs.stream()
- .filter(savedCatalog -> {
- if (this.savedCatalogs.stream().noneMatch(catalog -> catalog.name().equals(savedCatalog.name()))) {
- return true;
- } else {
- logger.warn("{} has the same name as an existing catalog and will not be added", savedCatalog.name());
- return false;
- }
- })
- .toList();
-
- if (catalogsToAdd.isEmpty()) {
- logger.debug("No catalog to add");
- return;
- }
-
- this.savedCatalogs.addAll(catalogsToAdd);
- }
+ catalogs.add(catalog);
try {
- extensionFolderManager.saveRegistry(new Registry(this.savedCatalogs));
+ extensionFolderManager.saveRegistry(Registry.createFromCatalogs(catalogs).get());
} catch (Exception e) {
- this.savedCatalogs.removeAll(catalogsToAdd);
-
+ catalogs.remove(catalog);
throw e;
}
- logger.info("Catalogs {} added", catalogsToAdd.stream().map(SavedCatalog::name).toList());
+ logger.info("Catalog {} added", catalog);
}
/**
- * 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(Catalog)} and {@link #removeCatalog(Catalog)}. This list
+ * may be updated from any thread and won't contain null elements.
*
* @return a read-only observable list of all saved catalogs
*/
- public ObservableList getCatalogs() {
- return savedCatalogsImmutable;
+ public ObservableList getCatalogs() {
+ return catalogsImmutable;
}
/**
- * 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.
+ * Remove the provided catalog from the list of saved catalogs.
*
- * If an exception occurs (see below), the provided catalogs are not added.
+ * Warning: this will attempt to move the directory returned by {@link #getCatalogDirectory(String)} to trash if supported
+ * by this platform or recursively delete it if extension are asked to be removed. If this operation fails, no exception
+ * is thrown.
*
- * 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.
+ * This operation may take some time, but can be interrupted.
*
- * @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 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
+ * @param catalog the catalog to remove
+ * @throws IllegalArgumentException if the provided catalog is not {@link RegistryCatalog#deletable()}
+ * @throws IOException if an I/O error occurs while removing the catalog from disk
+ * @throws NullPointerException if the path contained in {@link #getExtensionDirectory()} is null or if the provided
+ * catalog is null
+ * @throws InvalidPathException if the path to the registry containing the list of catalogs cannot be created
+ * @throws ExecutionException if an error occurred while saving the registry
+ * @throws InterruptedException if the calling thread is interrupted
*/
- public void removeCatalogs(List savedCatalogs, boolean removeExtensions) throws IOException {
- if (getExtensionDirectoryPath().get() == null) {
- throw new NullPointerException("The extension directory path is null");
+ public synchronized void removeCatalog(Catalog catalog) throws IOException, ExecutionException, InterruptedException {
+ if (!catalog.isDeletable()) {
+ throw new IllegalArgumentException(String.format("Cannot delete %s: this catalog is not deletable", catalog));
}
- List catalogsToRemove = savedCatalogs.stream()
- .filter(savedCatalog -> {
- if (savedCatalog.deletable()) {
- return true;
- } else {
- logger.warn("{} is not deletable and won't be deleted", savedCatalog.name());
- return false;
- }
- })
- .toList();
- if (catalogsToRemove.isEmpty()) {
- logger.debug("No catalog to remove");
- return;
- }
-
- this.savedCatalogs.removeAll(catalogsToRemove);
-
+ catalogs.remove(catalog);
try {
- extensionFolderManager.saveRegistry(new Registry(this.savedCatalogs));
+ extensionFolderManager.saveRegistry(Registry.createFromCatalogs(catalogs).get());
} catch (Exception e) {
- this.savedCatalogs.addAll(catalogsToRemove);
-
+ catalogs.add(catalog);
throw e;
}
- if (removeExtensions) {
- for (SavedCatalog savedCatalog : catalogsToRemove) {
- try {
- extensionFolderManager.deleteExtensionsFromCatalog(savedCatalog);
- } catch (IOException | SecurityException | InvalidPathException | NullPointerException e) {
- logger.debug("Could not delete {}", savedCatalog.name(), e);
- }
- }
+ updateCatalogManagedInstalledJarsOfDirectory(
+ extensionFolderManager.getCatalogDirectoryPath(catalog.getName()),
+ Operation.REMOVE
+ );
- for (var entry: installedExtensions.entrySet()) {
- if (catalogsToRemove.contains(entry.getKey().savedCatalog)) {
- synchronized (this) {
- entry.getValue().set(Optional.empty());
- }
- }
- }
+ try {
+ extensionFolderManager.deleteExtensionsFromCatalog(catalog.getName());
+ } catch (IOException | InvalidPathException | NullPointerException e) {
+ logger.debug("Could not delete {}", catalog, e);
}
- logger.info("Catalogs {} removed", catalogsToRemove.stream().map(SavedCatalog::name).toList());
+ logger.info("Catalog {} removed", catalog);
}
/**
- * 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.
*
- * @param savedCatalog the catalog owning the extension
- * @param extension the extension to retrieve
+ * @param catalogName the name of the catalog owning the extension
+ * @param extensionName the name of 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 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 #getExtensionDirectory()}
+ * is null
*/
- public Path getExtensionDirectory(SavedCatalog savedCatalog, Extension extension) throws IOException {
- return extensionFolderManager.getExtensionDirectoryPath(savedCatalog, extension);
+ public Path getExtensionDirectory(String catalogName, String extensionName) {
+ return extensionFolderManager.getExtensionDirectoryPath(catalogName, extensionName);
}
/**
- * Get the list of links the {@link #installOrUpdateExtension(SavedCatalog, Extension, InstalledExtension, Consumer, BiConsumer)}
+ * Get the list of links the {@link #installOrUpdateExtension(Catalog, Extension, Release, boolean, 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
+ * @param catalogName the name of the catalog owning the extension to install
+ * @param extensionName the name of the extension to install
+ * @param release the release of the extension to install
+ * @param installOptionalDependencies whether to install optional dependencies
* @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 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 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 NullPointerException if one of the parameters is null or if the path contained in {@link #getExtensionDirectory()}
+ * is null
*/
- public List getDownloadLinks(SavedCatalog savedCatalog, Extension extension, InstalledExtension installationInformation) throws IOException {
- return getDownloadUrlsToFilePaths(savedCatalog, extension, installationInformation, false).stream()
- .map(UriFileName::uri)
- .toList();
+ public List getDownloadLinks(String catalogName, String extensionName, Release release, boolean installOptionalDependencies) {
+ try {
+ return getDownloadUrlsToFilePaths(catalogName, extensionName, release, installOptionalDependencies, false).stream()
+ .map(UriFileName::uri)
+ .toList();
+ } catch (IOException e) {
+ // IOException only occurs if directories are created, which is not the case here, so this should never be called
+ throw new RuntimeException(e);
+ }
}
/**
* 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.
+ * and the size of the extension, but this operation can be interrupted.
*
* If the extension already exists, it will be deleted before downloading the provided version of the extension.
*
- * Warning: this will move to trash the directory returned by {@link #getExtensionDirectory(SavedCatalog, Extension)}
- * or recursively delete it if moving files to trash is not supported.
+ * Warning: If the extension already exists, this function will attempt to move to trash the directory returned by
+ * {@link #getExtensionDirectory(String, String)} or recursively delete it if moving files to trash is not supported.
+ * If this operation fails, no exception is thrown.
*
- * @param savedCatalog the catalog owning the extension to install/update
- * @param extension the extension to install/update
- * @param installationInformation what to install/update on the extension
- * @param onProgress a function that will be called at different steps during the installation. Its parameter
- * will be a float between 0 and 1 indicating the progress of the installation (0: beginning,
- * 1: finished). This function will be called from the calling thread
+ * @param catalog the catalog owning the extension to install/update. It must be one of {@link #getCatalogs()}
+ * @param extension the extension to install/update. It must belong to the provided catalog
+ * @param release the release to install. It must belong to the provided extension
+ * @param installOptionalDependencies whether to install optional dependencies
+ * @param onProgress a function that will be called at different steps during the installation. Its parameter will be
+ * a float between 0 and 1 indicating the progress of the installation (0: beginning, 1: finished).
+ * This function will be called from the calling thread
* @param onStatusChanged a function that will be called at different steps during the installation. Its first parameter
* 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()}
+ * @throws IllegalArgumentException if the provided catalog does not belong to {@link #getCatalogs()}, if the provided
+ * extension does not belong to the provided catalog, or if the provided release does not belong to the provided extension
+ * @throws NullPointerException if one of the parameters is null or if the path contained in {@link #getExtensionDirectory()}
* 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 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 InterruptedException if the calling thread is interrupted
+ * @throws ExecutionException if an error occurs while retrieving the extensions of the provided catalog
*/
- public void installOrUpdateExtension(
- SavedCatalog savedCatalog,
+ public synchronized void installOrUpdateExtension(
+ Catalog catalog,
Extension extension,
- InstalledExtension installationInformation,
+ Release release,
+ boolean installOptionalDependencies,
Consumer onProgress,
BiConsumer onStatusChanged
- ) throws IOException, InterruptedException {
- var extensionProperty = installedExtensions.computeIfAbsent(
- new CatalogExtension(savedCatalog, extension),
- e -> new SimpleObjectProperty<>()
- );
+ ) throws IOException, InterruptedException, ExecutionException {
+ if (extension.getReleases().stream().noneMatch(release::equals)) {
+ throw new IllegalArgumentException(String.format(
+ "The provided release %s does not belong to the provided extension %s",
+ release,
+ extension
+ ));
+ }
- logger.debug("Deleting files of {} before installing or updating it", extension.name());
- updateCatalogManagedInstalledJarsOfDirectory(
- extensionFolderManager.getExtensionDirectoryPath(savedCatalog, extension),
- Operation.REMOVE
+ removeExtension(catalog, extension);
+
+ downloadAndExtractLinks(
+ getDownloadUrlsToFilePaths(
+ catalog.getName(),
+ extension.getName(),
+ release,
+ installOptionalDependencies,
+ true
+ ),
+ onProgress,
+ onStatusChanged
);
- extensionFolderManager.deleteExtension(savedCatalog, extension);
- synchronized (this) {
- extensionProperty.set(Optional.empty());
- }
+
+ extension.installRelease(release, installOptionalDependencies);
try {
- downloadAndExtractLinks(
- getDownloadUrlsToFilePaths(savedCatalog, extension, installationInformation, true),
- onProgress,
- onStatusChanged
- );
+ extensionFolderManager.saveRegistry(Registry.createFromCatalogs(catalogs).get());
} catch (Exception e) {
- logger.debug("Installation of {} failed. Clearing extension files", extension.name());
- extensionFolderManager.deleteExtension(savedCatalog, extension);
+ extension.uninstallRelease();
throw e;
}
+
updateCatalogManagedInstalledJarsOfDirectory(
- extensionFolderManager.getExtensionDirectoryPath(savedCatalog, extension),
+ extensionFolderManager.getExtensionDirectoryPath(catalog.getName(), extension.getName(), release.getVersion().toString()),
Operation.ADD
);
- synchronized (this) {
- extensionProperty.set(Optional.of(installationInformation));
- }
-
- logger.info("{} of {} installed", extension.name(), savedCatalog.name());
- }
-
- /**
- * Indicate whether an extension belonging to a catalog is installed.
- *
- * @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
- */
- public ReadOnlyObjectProperty> getInstalledExtension(SavedCatalog savedCatalog, Extension extension) {
- return installedExtensions.computeIfAbsent(
- new CatalogExtension(savedCatalog, extension),
- catalogExtension -> new SimpleObjectProperty<>(getInstalledExtension(catalogExtension))
- );
+ logger.info("{} of {} installed", extension, catalog);
}
/**
@@ -476,49 +390,81 @@ public void addOnJarLoadedRunnable(Runnable runnable) {
* @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()
- .map(savedCatalog -> CatalogFetcher.getCatalog(savedCatalog.rawUri()).join().extensions().stream()
- .map(extension -> getUpdateAvailable(savedCatalog, extension))
- .filter(Objects::nonNull)
+ public synchronized CompletableFuture> getAvailableUpdates() {
+ return CompletableFuture.supplyAsync(() -> catalogs.stream()
+ .map(catalog -> catalog.getExtensions().join().stream()
+ .map(extension -> extension.getUpdateAvailable(version))
+ .flatMap(Optional::stream)
.toList()
)
.flatMap(List::stream)
- .toList());
+ .toList()
+ );
}
/**
- * 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 and attempt to remove its files. This can take some time but the operation can be interrupted.
*
- * 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 attempt to move the directory returned by {@link #getExtensionDirectory(String, String)} to
+ * trash or recursively delete it if moving files to trash is not supported by this platform. If this operation fails,
+ * the function doesn't throw any exceptions.
*
- * @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 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
+ * @param catalog the catalog owning the extension to uninstall. It must be one of {@link #getCatalogs()}
+ * @param extension the extension to uninstall. It must belong to the provided catalog
+ * @throws IllegalArgumentException if the provided catalog does not belong to {@link #getCatalogs()}, or if the provided
+ * extension does not belong to the provided catalog
+ * @throws NullPointerException if one of the parameters is null or if the path contained in {@link #getExtensionDirectory()}
+ * is null
+ * @throws IOException if an I/O error occurred while deleting the extension
+ * @throws InvalidPathException if a path cannot be created, for example because the extensions folder path contain
+ * invalid characters
+ * @throws InterruptedException if the calling thread is interrupted
+ * @throws ExecutionException if an error occurs while retrieving the extensions of the provided catalog
*/
- public void removeExtension(SavedCatalog savedCatalog, Extension extension) throws IOException {
- var extensionProperty = installedExtensions.computeIfAbsent(
- new CatalogExtension(savedCatalog, extension),
- e -> new SimpleObjectProperty<>()
- );
+ public synchronized void removeExtension(Catalog catalog, Extension extension) throws IOException, ExecutionException, InterruptedException {
+ if (catalogs.stream().noneMatch(catalog::equals)) {
+ throw new IllegalArgumentException(String.format(
+ "The provided catalog %s is not among the internal list %s",
+ catalog,
+ catalogs
+ ));
+ }
+ if (catalog.getExtensions().get().stream().noneMatch(extension::equals)) {
+ throw new IllegalArgumentException(String.format(
+ "The provided extension %s does not belong to the provided catalog %s",
+ extension,
+ catalog
+ ));
+ }
+
+ if (extension.getInstalledRelease().getValue().isEmpty()) {
+ logger.debug("{} is not installed. Skipping deletion of it", extension);
+ return;
+ }
+
+ extension.uninstallRelease();
+ try {
+ extensionFolderManager.saveRegistry(Registry.createFromCatalogs(catalogs).get());
+ } catch (Exception e) {
+ extension.installRelease(
+ extension.getInstalledRelease().getValue().get(),
+ extension.areOptionalDependenciesInstalled().get()
+ );
+ throw e;
+ }
updateCatalogManagedInstalledJarsOfDirectory(
- extensionFolderManager.getExtensionDirectoryPath(savedCatalog, extension),
+ extensionFolderManager.getExtensionDirectoryPath(catalog.getName(), extension.getName()),
Operation.REMOVE
);
- extensionFolderManager.deleteExtension(savedCatalog, extension);
- synchronized (this) {
- extensionProperty.set(Optional.empty());
+
+ try {
+ extensionFolderManager.deleteExtension(catalog.getName(), extension.getName());
+ } catch (Exception e) {
+ logger.debug("Error while removing files of {}. They won't be deleted", extension, e);
}
- logger.info("{} of {} removed", extension.name(), savedCatalog.name());
+ logger.info("{} of {} removed", extension, catalog);
}
/**
@@ -530,25 +476,54 @@ public ObservableList getManuallyInstalledJars() {
return extensionFolderManager.getManuallyInstalledJars();
}
- private synchronized void setCatalogsFromRegistry() {
- this.savedCatalogs.clear();
-
+ private void resetCatalogsAndJars(List defaultCatalogs) {
try {
- this.savedCatalogs.addAll(extensionFolderManager.getSavedRegistry().catalogs());
- logger.debug("Catalogs set from saved registry");
- } catch (Exception e) {
- logger.debug("Error while retrieving saved registry. Using default one", e);
+ List catalogs = extensionFolderManager.getSavedRegistry().catalogs();
- if (defaultRegistry != null) {
- this.savedCatalogs.addAll(defaultRegistry.catalogs());
- logger.debug(
- "Catalogs {} set from default registry",
- defaultRegistry.catalogs().stream().map(SavedCatalog::name).toList()
- );
+ this.catalogs.setAll(catalogs.stream()
+ .map(Catalog::new)
+ .toList()
+ );
+
+ catalogManagedInstalledJars.clear();
+ List releasePaths = catalogs.stream()
+ .flatMap(catalog -> catalog.extensions().stream()
+ .map(extension -> extensionFolderManager.getExtensionDirectoryPath(
+ catalog.name(),
+ extension.name(),
+ extension.installedVersion()))
+ )
+ .toList();
+ for (Path releasePath: releasePaths) {
+ updateCatalogManagedInstalledJarsOfDirectory(releasePath, Operation.ADD);
}
+
+ } catch (Exception e) {
+ logger.debug("Cannot retrieve saved registry. Using default catalogs {}", defaultCatalogs, e);
+ this.catalogs.setAll(defaultCatalogs);
}
}
+ private void loadJars() {
+ addJars(extensionFolderManager.getManuallyInstalledJars());
+ extensionFolderManager.getManuallyInstalledJars().addListener((ListChangeListener super Path>) change -> {
+ while (change.next()) {
+ addJars(change.getAddedSubList());
+ removeJars(change.getRemoved());
+ }
+ change.reset();
+ });
+
+ addJars(catalogManagedInstalledJarsImmutable);
+ catalogManagedInstalledJarsImmutable.addListener((ListChangeListener super Path>) change -> {
+ while (change.next()) {
+ addJars(change.getAddedSubList());
+ removeJars(change.getRemoved());
+ }
+ change.reset();
+ });
+ }
+
private void updateCatalogManagedInstalledJarsOfDirectory(Path directory, Operation operation) {
if (directory != null) {
try (Stream files = Files.walk(directory)) {
@@ -572,115 +547,72 @@ private void updateCatalogManagedInstalledJarsOfDirectory(Path directory, Operat
}
}
- private Optional getInstalledExtension(CatalogExtension catalogExtension) {
- try {
- return extensionFolderManager.getInstalledExtension(
- catalogExtension.savedCatalog,
- catalogExtension.extension
- );
- } catch (IOException | InvalidPathException | SecurityException | NullPointerException e) {
- logger.debug("Error while retrieving {} installation information", catalogExtension.extension.name(), e);
- return Optional.empty();
- }
- }
-
- private void loadJars() {
- addJars(extensionFolderManager.getManuallyInstalledJars());
- extensionFolderManager.getManuallyInstalledJars().addListener((ListChangeListener super Path>) change -> {
- while (change.next()) {
- addJars(change.getAddedSubList());
- removeJars(change.getRemoved());
- }
- change.reset();
- });
-
- addJars(catalogManagedInstalledJarsImmutable);
- catalogManagedInstalledJarsImmutable.addListener((ListChangeListener super Path>) change -> {
- while (change.next()) {
- addJars(change.getAddedSubList());
- removeJars(change.getRemoved());
- }
- change.reset();
- });
- }
-
private List getDownloadUrlsToFilePaths(
- SavedCatalog savedCatalog,
- Extension extension,
- InstalledExtension installationInformation,
- boolean createFolders
+ String catalogName,
+ String extensionName,
+ Release release,
+ boolean installOptionalDependencies,
+ boolean createDirectories
) throws IOException {
List downloadUrlToFilePaths = new ArrayList<>();
- Optional release = extension.releases().stream()
- .filter(r -> r.name().equals(installationInformation.releaseName()))
- .findAny();
-
- if (release.isEmpty()) {
- throw new IllegalArgumentException(String.format(
- "The provided release name %s is not present in the extension releases %s",
- installationInformation.releaseName(),
- extension.releases()
- ));
- }
-
downloadUrlToFilePaths.add(new UriFileName(
- release.get().mainUrl(),
+ release.getMainUrl(),
Paths.get(
extensionFolderManager.getExtensionPath(
- savedCatalog,
- extension,
- release.get().name(),
+ catalogName,
+ extensionName,
+ release.getVersion().toString(),
ExtensionFolderManager.FileType.MAIN_JAR,
- createFolders
+ createDirectories
).toString(),
- FileTools.getFileNameFromURI(release.get().mainUrl())
+ FileTools.getFileNameFromURI(release.getMainUrl())
)
));
- for (URI javadocUri: release.get().javadocUrls()) {
+ for (URI javadocUri: release.getJavadocUrls()) {
downloadUrlToFilePaths.add(new UriFileName(
javadocUri,
Paths.get(
extensionFolderManager.getExtensionPath(
- savedCatalog,
- extension,
- release.get().name(),
+ catalogName,
+ extensionName,
+ release.getVersion().toString(),
ExtensionFolderManager.FileType.JAVADOCS,
- createFolders
+ createDirectories
).toString(),
FileTools.getFileNameFromURI(javadocUri)
)
));
}
- for (URI requiredDependencyUri: release.get().requiredDependencyUrls()) {
+ for (URI requiredDependencyUri: release.getRequiredDependencyUrls()) {
downloadUrlToFilePaths.add(new UriFileName(
requiredDependencyUri,
Paths.get(
extensionFolderManager.getExtensionPath(
- savedCatalog,
- extension,
- release.get().name(),
+ catalogName,
+ extensionName,
+ release.getVersion().toString(),
ExtensionFolderManager.FileType.REQUIRED_DEPENDENCIES,
- createFolders
+ createDirectories
).toString(),
FileTools.getFileNameFromURI(requiredDependencyUri)
)
));
}
- if (installationInformation.optionalDependenciesInstalled()) {
- for (URI optionalDependencyUri: release.get().optionalDependencyUrls()) {
+ if (installOptionalDependencies) {
+ for (URI optionalDependencyUri: release.getOptionalDependencyUrls()) {
downloadUrlToFilePaths.add(new UriFileName(
optionalDependencyUri,
Paths.get(
extensionFolderManager.getExtensionPath(
- savedCatalog,
- extension,
- release.get().name(),
+ catalogName,
+ extensionName,
+ release.getVersion().toString(),
ExtensionFolderManager.FileType.OPTIONAL_DEPENDENCIES,
- createFolders
+ createDirectories
).toString(),
FileTools.getFileNameFromURI(optionalDependencyUri)
)
@@ -731,46 +663,11 @@ private void downloadAndExtractLinks(
}
}
- private UpdateAvailable getUpdateAvailable(SavedCatalog savedCatalog, Extension extension) {
- Optional installedExtension = getInstalledExtension(
- new CatalogExtension(savedCatalog, extension)
- );
-
- if (installedExtension.isPresent()) {
- String installedRelease = installedExtension.get().releaseName();
- Optional maxCompatibleRelease = extension.getMaxCompatibleRelease(version);
-
- if (maxCompatibleRelease.isPresent() &&
- new Version(maxCompatibleRelease.get().name()).compareTo(new Version(installedRelease)) > 0
- ) {
- logger.debug(
- "{} installed and updatable to {}",
- extension.name(),
- maxCompatibleRelease.get().name()
- );
- return new UpdateAvailable(
- extension.name(),
- installedExtension.get().releaseName(),
- maxCompatibleRelease.get().name()
- );
- } else {
- logger.debug(
- "{} installed but no compatible update found",
- extension.name()
- );
- return null;
- }
- } else {
- logger.debug("{} not installed, so no update available", extension.name());
- return null;
- }
- }
-
private void addJars(List extends Path> jarPaths) {
for (Path path: jarPaths) {
try {
extensionClassLoader.addJar(path);
- } catch (IOError | SecurityException | MalformedURLException e) {
+ } catch (IOError | MalformedURLException e) {
logger.error("Cannot load extension {}", path, e);
}
}
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/ExtensionClassLoader.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/ExtensionClassLoader.java
index d635417..3e99048 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/ExtensionClassLoader.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/ExtensionClassLoader.java
@@ -27,9 +27,7 @@ class ExtensionClassLoader extends URLClassLoader {
/**
* Create the extension class loader.
*
- * @param parent the class loader that should be the parent of this
- * class loader
- * @throws SecurityException if the user doesn't have enough rights to create the class loader
+ * @param parent the class loader that should be the parent of this class loader
*/
public ExtensionClassLoader(ClassLoader parent) {
super(new URL[0], parent);
@@ -39,9 +37,7 @@ public ExtensionClassLoader(ClassLoader parent) {
* Load a JAR file located on the provided path.
*
* @param jarPath the path of the JAR file to load
- * @throws java.io.IOError if an I/O error occurs while obtaining the absolute path of the
- * provided path
- * @throws SecurityException if the user doesn't have read rights on the provided path
+ * @throws java.io.IOError if an I/O error occurs while obtaining the absolute path of the provided path
* @throws MalformedURLException if an error occurred while converting the provided path to a URL
* @throws NullPointerException if the provided path is null
*/
@@ -77,9 +73,9 @@ public void addJar(Path jarPath) throws MalformedURLException {
/**
* Indicate that a JAR file should be unloaded.
- * While this function doesn't currently unload the JAR, it is recommended
- * to call it when a JAR file given to {@link #addJar(Path)} should not be
- * loaded anymore. A future implementation may actually unload the JAR
+ *
+ * While this function doesn't currently unload the JAR, it is recommended to call it when a JAR file given to
+ * {@link #addJar(Path)} should not be loaded anymore. A future implementation may actually unload the JAR.
*
* @param jarPath the path of the JAR file to unload
* @throws NullPointerException if the provided path is null
@@ -89,8 +85,7 @@ public synchronized void removeJar(Path jarPath) {
}
/**
- * Set a runnable to be called each time a JAR file is loaded by this class loader. The call may
- * happen from any thread.
+ * Set a runnable to be called each time a JAR file is loaded by this class loader. The call may happen from any thread.
*
* @param runnable the runnable to run when a JAR file is loaded
* @throws NullPointerException if the provided path is null
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 f9c885f..7599903 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/ExtensionFolderManager.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/ExtensionFolderManager.java
@@ -4,15 +4,11 @@
import com.google.gson.JsonIOException;
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;
-import qupath.ext.extensionmanager.core.catalog.Extension;
-import qupath.ext.extensionmanager.core.savedentities.InstalledExtension;
-import qupath.ext.extensionmanager.core.savedentities.Registry;
-import qupath.ext.extensionmanager.core.savedentities.SavedCatalog;
+import qupath.ext.extensionmanager.core.registry.Registry;
import qupath.ext.extensionmanager.core.tools.FilesWatcher;
import qupath.ext.extensionmanager.core.tools.FileTools;
@@ -24,11 +20,8 @@
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.Objects;
-import java.util.Optional;
import java.util.function.Predicate;
-import java.util.stream.Stream;
/**
* A class to manage the extension folder (containing extension installation
@@ -78,7 +71,7 @@ class ExtensionFolderManager implements AutoCloseable {
private static final String REGISTRY_NAME = "registry.json";
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 extensionDirectoryPath;
private final ObservableValue catalogsDirectoryPath;
private final FilesWatcher manuallyInstalledExtensionsWatcher;
/**
@@ -114,13 +107,13 @@ public enum FileType {
/**
* Create the extension folder manager.
*
- * @param extensionDirectoryPath a read-only property pointing to the path the extension directory should have. The
- * path can be null or invalid (but not the property). If this property is changed,
- * catalogs and extensions will be set to the content of the new value of the property
+ * @param extensionDirectoryPath an observable value pointing to the path the extension directory should have. The
+ * path can be null or invalid (but not the observable). If this observable is changed,
+ * catalogs and extensions will be set to the content of the new value of the observable
* (so will be reset if the new path is empty)
* @throws NullPointerException if the parameter is null
*/
- public ExtensionFolderManager(ReadOnlyObjectProperty extensionDirectoryPath) {
+ public ExtensionFolderManager(ObservableValue extensionDirectoryPath) {
this.extensionDirectoryPath = extensionDirectoryPath;
this.catalogsDirectoryPath = extensionDirectoryPath.map(path -> {
if (path == null) {
@@ -153,7 +146,7 @@ public ExtensionFolderManager(ReadOnlyObjectProperty extensionDirectoryPat
isJar,
path -> {
try {
- return path.equals(extensionDirectoryPath.get().resolve(CATALOGS_FOLDER));
+ return path.equals(extensionDirectoryPath.getValue().resolve(CATALOGS_FOLDER));
} catch (InvalidPathException | NullPointerException e) {
logger.debug("Error when trying to assess if {} should be watched", path, e);
return true;
@@ -168,20 +161,23 @@ 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
- */
- public ReadOnlyObjectProperty getExtensionDirectoryPath() {
- return extensionDirectoryPath;
- }
-
- /**
- * @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
+ * 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 NullPointerException if the registry file exists but is empty or if the path contained in {@link #getCatalogsDirectoryPath()}
+ * is null
+ * @throws java.nio.file.InvalidPathException if the path to the registry cannot be created
+ * @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
*/
- public ObservableValue getCatalogsDirectoryPath() {
- return catalogsDirectoryPath;
+ public synchronized Registry getSavedRegistry() throws IOException {
+ try(
+ FileReader fileReader = new FileReader(catalogsDirectoryPath.getValue().resolve(REGISTRY_NAME).toFile());
+ JsonReader jsonReader = new JsonReader(fileReader)
+ ) {
+ return Objects.requireNonNull(gson.fromJson(jsonReader, Registry.class));
+ }
}
/**
@@ -189,177 +185,123 @@ public ObservableValue getCatalogsDirectoryPath() {
*
* @param registry the registry to save
* @throws IOException if an I/O error occurs while writing the registry file
- * @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
+ * @throws NullPointerException if the path contained in {@link #getCatalogsDirectoryPath()} is null or if the provided
+ * registry is null
+ * @throws java.nio.file.InvalidPathException if the path to the registry cannot be created
*/
public synchronized void saveRegistry(Registry registry) throws IOException {
try (
- FileWriter fileWriter = new FileWriter(getRegistryPath().toFile());
+ FileWriter fileWriter = new FileWriter(catalogsDirectoryPath.getValue().resolve(REGISTRY_NAME).toFile());
BufferedWriter writer = new BufferedWriter(fileWriter)
) {
writer.write(gson.toJson(Objects.requireNonNull(registry)));
- logger.debug("Registry containing {} saved", registry.catalogs().stream().map(SavedCatalog::name).toList());
}
}
/**
- * 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 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
+ * @return an observable value containing the path to the extension folder. It may be updated from any thread and
+ * the path (but not the observable) can be null or invalid
*/
- public synchronized Registry getSavedRegistry() throws IOException {
- try(
- FileReader fileReader = new FileReader(getRegistryPath().toFile());
- JsonReader jsonReader = new JsonReader(fileReader)
- ) {
- return Objects.requireNonNull(gson.fromJson(jsonReader, Registry.class));
- }
+ public ObservableValue getExtensionDirectoryPath() {
+ return extensionDirectoryPath;
+ }
+
+ /**
+ * @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;
}
/**
* Get the path to the directory containing the provided catalog.
*
- * @param savedCatalog the catalog to retrieve
+ * @param catalogName the name of the catalog to retrieve
* @return the path to the directory containing the provided catalog
* @throws InvalidPathException if the path cannot be created
* @throws NullPointerException if the provided catalog is null or if the path contained in
* {@link #getCatalogsDirectoryPath()} is null
*/
- public synchronized Path getCatalogDirectoryPath(SavedCatalog savedCatalog) {
- return catalogsDirectoryPath.getValue().resolve(FileTools.stripInvalidFilenameCharacters(savedCatalog.name()));
+ public synchronized Path getCatalogDirectoryPath(String catalogName) {
+ return catalogsDirectoryPath.getValue().resolve(FileTools.stripInvalidFilenameCharacters(catalogName));
}
/**
* 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
+ * {@link #getCatalogDirectoryPath(String)} 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
+ * @param catalogName the name of the catalog owning the extensions to delete
* @throws IOException if an I/O error occur while deleting the files
* @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();
+ public synchronized void deleteExtensionsFromCatalog(String catalogName) throws IOException {
+ File catalogDirectory = getCatalogDirectoryPath(catalogName).toFile();
FileTools.moveDirectoryToTrashOrDeleteRecursively(catalogDirectory);
- logger.debug("The extension files of {} located in {} have been deleted", savedCatalog.name(), catalogDirectory);
+ logger.debug("The extension files of {} located in {} have been deleted", catalogName, catalogDirectory);
}
/**
* 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
+ * @param catalogName the name of the catalog owning the extension
+ * @param extensionName the name of the extension to retrieve
* @return the path to the folder containing the provided extension
* @throws InvalidPathException if the path cannot be created
* @throws NullPointerException if one of the provided parameter is null or if the path contained in
* {@link #getCatalogsDirectoryPath()} is null
*/
- public synchronized Path getExtensionDirectoryPath(SavedCatalog savedCatalog, Extension extension) {
- return getCatalogDirectoryPath(savedCatalog).resolve(FileTools.stripInvalidFilenameCharacters(extension.name()));
+ public synchronized Path getExtensionDirectoryPath(String catalogName, String extensionName) {
+ return getCatalogDirectoryPath(catalogName).resolve(FileTools.stripInvalidFilenameCharacters(extensionName));
}
/**
- * Indicate whether an extension belonging to a catalog is installed. If that's the case, installation information
- * are returned.
+ * Get the path to the directory containing the provided release of the provided extension of the provided catalog.
+ * It may not exist.
*
- * @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 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
+ * @param catalogName the name of the catalog owning the extension
+ * @param extensionName the name of the extension to retrieve
+ * @param releaseName the name of the release to retrieve
+ * @return the path to the folder containing the provided extension
+ * @throws InvalidPathException if the path cannot be created
+ * @throws NullPointerException if one of the provided parameter is null or if the path contained in
+ * {@link #getCatalogsDirectoryPath()} is null
*/
- public synchronized Optional getInstalledExtension(SavedCatalog savedCatalog, Extension extension) throws IOException {
- Path extensionPath = getExtensionDirectoryPath(savedCatalog, extension);
-
- Path versionPath = null;
- if (Files.isDirectory(extensionPath)) {
- try (Stream stream = Files.list(extensionPath)) {
- versionPath = stream
- .filter(Files::isDirectory)
- .findAny()
- .orElse(null);
- }
- }
- if (versionPath == null) {
- logger.debug("No folder found in {}. Guessing {} is not installed", extensionPath, extension.name());
- return Optional.empty();
- }
-
- Path mainJarFolderPath = Paths.get(
- versionPath.toString(),
- FileType.MAIN_JAR.name
- );
- if (!FileTools.isDirectoryNotEmpty(mainJarFolderPath)) {
- logger.debug(
- "The folder at {} is not a non-empty directory. Guessing {} is not installed",
- mainJarFolderPath,
- extension.name()
- );
- return Optional.empty();
- }
- logger.debug("{} detected at {}", extension.name(), mainJarFolderPath);
-
- Path optionalDependenciesFolderPath = Paths.get(
- versionPath.toString(),
- FileType.OPTIONAL_DEPENDENCIES.name
- );
- boolean optionalDependenciesInstalled = FileTools.isDirectoryNotEmpty(optionalDependenciesFolderPath);
- if (optionalDependenciesInstalled) {
- logger.debug(
- "Optional dependencies of {} detected because {} is a non-empty directory",
- extension.name(),
- optionalDependenciesFolderPath
- );
- } else {
- logger.debug(
- "Optional dependencies of {} not detected because {} is not a non-empty directory",
- extension.name(),
- optionalDependenciesFolderPath
- );
- }
-
- return Optional.of(new InstalledExtension(versionPath.toFile().getName(), optionalDependenciesInstalled));
+ public synchronized Path getExtensionDirectoryPath(String catalogName, String extensionName, String releaseName) {
+ return getCatalogDirectoryPath(catalogName)
+ .resolve(FileTools.stripInvalidFilenameCharacters(extensionName))
+ .resolve(FileTools.stripInvalidFilenameCharacters(releaseName));
}
/**
* 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
+ * {@link #getExtensionDirectoryPath(String, String)} 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
+ * @param catalogName the name of the catalog owning the extension to delete
+ * @param extensionName the name of the extension to delete
* @throws IOException if an I/O error occurs while deleting the folder
- * @throws InvalidPathException if the Path object of the extension folder cannot be created, for example because the
- * extensions folder path contain invalid characters
+ * @throws InvalidPathException if the path of the extension folder cannot be created, for example if 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());
- logger.debug("The extension files of {} belonging to {} have been deleted", extension.name(), savedCatalog.name());
+ public synchronized void deleteExtension(String catalogName, String extensionName) throws IOException {
+ FileTools.moveDirectoryToTrashOrDeleteRecursively(getExtensionDirectoryPath(catalogName, extensionName).toFile());
+ logger.debug("The extension files of {} belonging to {} have been deleted", extensionName, catalogName);
}
/**
* 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
- * @param releaseName the version name of the extension to retrieve
+ * @param catalogName the name of the catalog owning the extension
+ * @param extensionName the name of the extension to find the folder to
+ * @param releaseName the name of the version to retrieve
* @param fileType the type of files to retrieve
* @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
@@ -370,17 +312,13 @@ public synchronized void deleteExtension(SavedCatalog savedCatalog, Extension ex
* provided parameters is null
*/
public synchronized Path getExtensionPath(
- SavedCatalog savedCatalog,
- Extension extension,
+ String catalogName,
+ String extensionName,
String releaseName,
FileType fileType,
boolean createDirectory
) throws IOException {
- Path folderPath = Paths.get(
- getExtensionDirectoryPath(savedCatalog, extension).toString(),
- releaseName,
- fileType.name
- );
+ Path folderPath = getExtensionDirectoryPath(catalogName, extensionName, releaseName).resolve(fileType.name);
if (createDirectory) {
if (Files.isRegularFile(folderPath)) {
@@ -400,8 +338,4 @@ public synchronized Path getExtensionPath(
public ObservableList getManuallyInstalledJars() {
return manuallyInstalledExtensionsWatcher.getFiles();
}
-
- private Path getRegistryPath() {
- return catalogsDirectoryPath.getValue().resolve(REGISTRY_NAME);
- }
}
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/Version.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/Version.java
index 7360d1f..e6cd3d4 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/Version.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/Version.java
@@ -26,10 +26,8 @@ public class Version implements Comparable {
/**
* Create a release from a text.
*
- * @param version the text containing the release to parse. It must correspond to the
- * specifications of this class.
- * @throws IllegalArgumentException if the provided text doesn't correspond to the
- * specifications of this class
+ * @param version the text containing the release to parse. It must correspond to the specifications of this class.
+ * @throws IllegalArgumentException if the provided text doesn't correspond to the specifications of this class
* @throws NullPointerException if the provided version is null
*/
public Version(String version) {
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/Catalog.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/Catalog.java
index 28c5eee..50aecab 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/Catalog.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/Catalog.java
@@ -1,56 +1,180 @@
package qupath.ext.extensionmanager.core.catalog;
+import qupath.ext.extensionmanager.core.model.CatalogModel;
+import qupath.ext.extensionmanager.core.model.CatalogModelFetcher;
+import qupath.ext.extensionmanager.core.registry.RegistryCatalog;
+import qupath.ext.extensionmanager.core.registry.RegistryExtension;
+
import java.net.URI;
-import java.util.Collections;
import java.util.List;
-import java.util.stream.Collectors;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
/**
- * A catalog describing a collection of extensions.
- *
- * @param name the name of the catalog
- * @param description a short (one sentence or so) description of what the catalog contains and what its purpose is
- * @param extensions the collection of extensions that the catalog describes. This list is immutable
+ * A catalog containing a collection of extensions.
*/
-public record Catalog(String name, String description, List extensions) {
+public class Catalog {
+
+ private final String name;
+ private final String description;
+ private final URI uri;
+ private final URI rawUri;
+ private final boolean deletable;
+ private final List registryExtensions;
+ private CompletableFuture> extensions;
/**
- * Create a catalog.
- *
- * It must respect the following requirements:
- *
- * - The 'name', 'description', and 'extensions' fields must be defined (but can be empty).
- * -
- * Each extension of the 'extensions' list must be a valid object
- * (see {@link Extension#Extension(String, String, String, URI, boolean, List)}).
- *
- * - Two extensions of the 'extensions' list cannot have the same name.
- *
+ * Create a non-deletable catalog from a list of attributes.
*
* @param name the name of the catalog
* @param description a short (one sentence or so) description of what the catalog contains and what its purpose is
- * @param extensions the collection of extensions that the catalog describes
- * @throws IllegalArgumentException when the created catalog is not valid (see the requirements above)
+ * @param uri a URI pointing to the raw content of the catalog, or to a GitHub repository where the catalog can be found
+ * @param rawUri the URI pointing to the raw content of the catalog (can be the same as the provided uri)
*/
- public Catalog(String name, String description, List extensions) {
+ public Catalog(String name, String description, URI uri, URI rawUri) {
this.name = name;
this.description = description;
- this.extensions = extensions == null ? null : Collections.unmodifiableList(extensions);
+ this.uri = uri;
+ this.rawUri = rawUri;
+ this.deletable = false;
+ this.registryExtensions = List.of();
+ }
+
+ /**
+ * Create a catalog from a {@link CatalogModel}. This will directly populate the extensions.
+ *
+ * @param catalogModel information on the catalog
+ * @param uri a URI pointing to the raw content of the catalog, or to a GitHub repository where the catalog can be found
+ * @param rawUri the URI pointing to the raw content of the catalog (can be same as {@link #uri})
+ * @param deletable whether this catalog can be deleted
+ */
+ public Catalog(CatalogModel catalogModel, URI uri, URI rawUri, boolean deletable) {
+ this.name = catalogModel.name();
+ this.description = catalogModel.description();
+ this.uri = Objects.requireNonNull(uri);
+ this.rawUri = Objects.requireNonNull(rawUri);
+ this.deletable = deletable;
+ this.registryExtensions = List.of();
+ this.extensions = CompletableFuture.completedFuture(createExtensionsFromCatalog(catalogModel));
+ }
+
+ /**
+ * Create a catalog from a {@link RegistryCatalog}. This will not populate the extensions.
+ *
+ * @param registryCatalog information on the catalog
+ */
+ public Catalog(RegistryCatalog registryCatalog) {
+ this.name = registryCatalog.name();
+ this.description = registryCatalog.description();
+ this.uri = registryCatalog.uri();
+ this.rawUri = registryCatalog.rawUri();
+ this.deletable = registryCatalog.deletable();
+ this.registryExtensions = registryCatalog.extensions();
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) return false;
- checkValidity();
+ Catalog catalog = (Catalog) o;
+ return deletable == catalog.deletable && name.equals(catalog.name) && description.equals(catalog.description) &&
+ uri.equals(catalog.uri) && rawUri.equals(catalog.rawUri);
}
- private void checkValidity() {
- Utils.checkField(name, "name", "Catalog");
- Utils.checkField(description, "description", "Catalog");
- Utils.checkField(extensions, "extensions", "Catalog");
+ @Override
+ public int hashCode() {
+ int result = name.hashCode();
+ result = 31 * result + description.hashCode();
+ result = 31 * result + uri.hashCode();
+ result = 31 * result + rawUri.hashCode();
+ result = 31 * result + Boolean.hashCode(deletable);
+ return result;
+ }
- if (extensions.stream().map(Extension::name).collect(Collectors.toSet()).size() < extensions.size()) {
- throw new IllegalArgumentException(String.format(
- "At least two extensions of %s have the same name",
- extensions
- ));
+ /**
+ * @return the name of the catalog
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @return a short (one sentence or so) description of what the catalog contains and what its purpose is
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * @return a URI pointing to the raw content of the catalog, or to a GitHub repository where the catalog can be found
+ */
+ public URI getUri() {
+ return uri;
+ }
+
+ /**
+ * @return the URI pointing to the raw content of the catalog (can be same as {@link #uri})
+ */
+ public URI getRawUri() {
+ return rawUri;
+ }
+
+ /**
+ * @return whether this metadata can be deleted
+ */
+ public boolean isDeletable() {
+ return deletable;
+ }
+
+ /**
+ * Compute and return the extensions this catalog owns.
+ *
+ * Depending on which constructor was used to create this catalog, this function may act differently:
+ *
+ * - If {@link #Catalog(CatalogModel, URI, URI, boolean)} was used, the extensions are already determined.
+ * -
+ * If {@link #Catalog(RegistryCatalog)} was used, this function will call {@link CatalogModelFetcher#getCatalog(URI)}
+ * to determine the extensions, which might take some time.
+ *
+ *
+ * At most one call to {@link CatalogModelFetcher#getCatalog(URI)} is made, because the results are cached.
+ *
+ * Note that exception handling is left to the caller (the returned CompletableFuture may complete exceptionally
+ * if the request made in {@link CatalogModelFetcher#getCatalog(URI)} failed).
+ *
+ * @return a CompletableFuture with the list of extensions this catalog owns (that completes exceptionally if the
+ * operation failed)
+ */
+ public synchronized CompletableFuture> getExtensions() {
+ if (extensions == null) {
+ extensions = CatalogModelFetcher.getCatalog(rawUri).thenApply(this::createExtensionsFromCatalog);
}
+ return extensions;
}
-}
+ private List createExtensionsFromCatalog(CatalogModel catalog) {
+ return catalog.extensions().stream()
+ .map(extension -> {
+ Optional registryExtension = registryExtensions.stream()
+ .filter(e -> e.name().equals(extension.name()))
+ .findAny();
+
+ return new Extension(
+ extension,
+ registryExtension.flatMap(e -> extension.releases().stream()
+ .filter(release -> release.name().equals(e.installedVersion()))
+ .map(Release::new)
+ .findAny()
+ ).orElse(null),
+ registryExtension.isPresent() && registryExtension.get().optionalDependenciesInstalled()
+ );
+ })
+ .toList();
+ }
+}
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/Extension.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/Extension.java
index 6d672a5..81f29b2 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/Extension.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/Extension.java
@@ -1,75 +1,197 @@
package qupath.ext.extensionmanager.core.catalog;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ObservableBooleanValue;
+import javafx.beans.value.ObservableValue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import qupath.ext.extensionmanager.core.Version;
+import qupath.ext.extensionmanager.core.model.ExtensionModel;
import java.net.URI;
-import java.util.Collections;
import java.util.List;
+import java.util.Objects;
import java.util.Optional;
/**
- * A description of an extension.
- *
- * @param name the extension's name
- * @param description a short (one sentence or so) description of what the extension is and what it does
- * @param author the author or group responsible for the extension
- * @param homepage a link to the GitHub repository associated with the extension
- * @param starred whether the extension is generally useful or recommended for most users
- * @param releases a list of available releases of the extension. This list is immutable
+ * An optionally installed extension.
*/
-public record Extension(String name, String description, String author, URI homepage, boolean starred, List releases) {
+public class Extension {
+
+ private static final Logger logger = LoggerFactory.getLogger(Extension.class);
+ private final String name;
+ private final String description;
+ private final List releases;
+ private final URI homepage;
+ private final boolean starred;
+ private final ObjectProperty> installedRelease;
+ private final BooleanProperty optionalDependenciesInstalled;
+
+ /**
+ * Create an extension from a {@link ExtensionModel}.
+ *
+ * @param extensionModel information on the extension
+ * @param installedRelease the release of the extension that is currently installed. Can be null to indicate that the
+ * extension is not installed
+ * @param optionalDependenciesInstalled whether optional dependencies of the extension are currently installed
+ */
+ public Extension(ExtensionModel extensionModel, Release installedRelease, boolean optionalDependenciesInstalled) {
+ this.name = extensionModel.name();
+ this.description = extensionModel.description();
+ this.releases = extensionModel.releases().stream().map(Release::new).toList();
+ this.homepage = extensionModel.homepage();
+ this.starred = extensionModel.starred();
+ this.installedRelease = new SimpleObjectProperty<>(Optional.ofNullable(installedRelease));
+ this.optionalDependenciesInstalled = new SimpleBooleanProperty(optionalDependenciesInstalled);
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Extension extension = (Extension) o;
+ return starred == extension.starred && name.equals(extension.name) && description.equals(extension.description) &&
+ releases.equals(extension.releases) && homepage.equals(extension.homepage);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = name.hashCode();
+ result = 31 * result + description.hashCode();
+ result = 31 * result + releases.hashCode();
+ result = 31 * result + homepage.hashCode();
+ result = 31 * result + Boolean.hashCode(starred);
+ return result;
+ }
/**
- * Create an Extension.
+ * @return the name of the extension
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @return a short (one sentence or so) description of what the extension is and what it does
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * @return a list of available releases of the extension. This list is immutable
+ */
+ public List getReleases() {
+ return releases;
+ }
+
+ /**
+ * @return a link to the GitHub repository associated with the extension
+ */
+ public URI getHomepage() {
+ return homepage;
+ }
+
+ /**
+ * @return whether the extension is generally useful or recommended for most users
+ */
+ public boolean isStarred() {
+ return starred;
+ }
+
+ /**
+ * Get an observable value showing the currently installed release of this extension. An empty Optional indicates
+ * that the extension is not installed.
+ *
+ * This observable may be updated from any thread.
+ *
+ * @return an observable value showing the currently installed release of this extension
+ */
+ public ObservableValue> getInstalledRelease() {
+ return installedRelease;
+ }
+
+ /**
+ * Get an observable boolean value showing whether optional dependencies are currently installed.
*
- * It must respect the following requirements:
- *
- * - The 'name', 'description', 'author', 'homepage', and 'releases' fields must be defined (but can be empty).
- * -
- * Each release of the 'version' list must be a valid object
- * (see {@link Release#Release(String, URI, List, List, List, VersionRange)}).
- *
- * - The 'homepage' field must be a GitHub URL.
- *
+ * This observable may be updated from any thread.
*
- * @param name the extension's name
- * @param description a short (one sentence or so) description of what the extension is and what it does
- * @param author the author or group responsible for the extension
- * @param homepage a link to the GitHub repository associated with the extension
- * @param starred whether the extension is generally useful or recommended for most users
- * @param releases a list of available releases of the extension
- * @throws IllegalArgumentException when the created extension is not valid (see the requirements above)
+ * @return observable boolean value showing whether optional dependencies are currently installed
+ */
+ public ObservableBooleanValue areOptionalDependenciesInstalled() {
+ return optionalDependenciesInstalled;
+ }
+
+ /**
+ * Indicate that this extension is now installed with the provided release.
+ *
+ * @param release the installed release
+ * @param optionalDependenciesInstalled whether optional dependencies have been installed
+ * @throws NullPointerException if the provided release is null
+ */
+ public synchronized void installRelease(Release release, boolean optionalDependenciesInstalled) {
+ this.installedRelease.set(Optional.of(Objects.requireNonNull(release)));
+ this.optionalDependenciesInstalled.set(optionalDependenciesInstalled);
+ }
+
+ /**
+ * Indicate that this extension is now uninstalled.
*/
- public Extension(String name, String description, String author, URI homepage, boolean starred, List releases) {
- this.name = name;
- this.description = description;
- this.author = author;
- this.homepage = homepage;
- this.starred = starred;
- this.releases = releases == null ? null : Collections.unmodifiableList(releases);
-
- checkValidity();
+ public synchronized void uninstallRelease() {
+ installedRelease.set(Optional.empty());
+ optionalDependenciesInstalled.set(false);
}
/**
- * Provide the most up-to-date release compatible with the provided version.
+ * Indicate whether this extension is installed, and if the installed version can be updated to a newer release that
+ * is compatible with the provided version.
*
- * @param version the version that the release should be compatible with. It
- * must be specified in the form "v[MAJOR].[MINOR].[PATCH]" or
- * "v[MAJOR].[MINOR].[PATCH]-rc[RELEASE_CANDIDATE]"
- * @return the most up-to-date release compatible with the provided version, or
- * an empty Optional if no release is compatible with the provided version
- * @throws IllegalArgumentException if this extension contains at least one release and
- * the provided version doesn't match the required form
- * @throws NullPointerException if this extension contains at least one release and
- * the provided version is null
+ * @param version the version that the new release should be compatible to
+ * @return a more up-to-date version of this extension, or an empty Optional if this extension is not installed or
+ * is already installed with the latest compatible release
*/
- public Optional getMaxCompatibleRelease(String version) {
+ public Optional getUpdateAvailable(Version version) {
+ Optional installedRelease = getInstalledRelease().getValue();
+
+ if (installedRelease.isEmpty()) {
+ logger.debug("{} not installed, so no update available", this);
+ return Optional.empty();
+ }
+
+ Optional maxCompatibleRelease = getMaxCompatibleRelease(version);
+ if (maxCompatibleRelease.isEmpty()) {
+ logger.debug("{} installed but no compatible release with {} found", this, version);
+ return Optional.empty();
+ }
+
+ if (maxCompatibleRelease.get().getVersion().compareTo(installedRelease.get().getVersion()) <= 0) {
+ logger.debug("{} installed but corresponds to the latest compatible version", this);
+ return Optional.empty();
+ }
+
+ logger.debug("{} installed and updatable to {}", this, maxCompatibleRelease.get());
+ return Optional.of(new UpdateAvailable(
+ name,
+ installedRelease.get().getVersion(),
+ maxCompatibleRelease.get().getVersion()
+ ));
+ }
+
+ private Optional getMaxCompatibleRelease(Version version) {
Release maxCompatibleRelease = null;
for (Release release: releases) {
- if (release.versionRange().isCompatible(version) &&
- (maxCompatibleRelease == null || new Version(release.name()).compareTo(new Version(maxCompatibleRelease.name())) > 0)
+ if (
+ release.isCompatible(version) &&
+ (maxCompatibleRelease == null || release.getVersion().compareTo(maxCompatibleRelease.getVersion()) > 0)
) {
maxCompatibleRelease = release;
}
@@ -77,15 +199,4 @@ public Optional getMaxCompatibleRelease(String version) {
return Optional.ofNullable(maxCompatibleRelease);
}
-
- private void checkValidity() {
- Utils.checkField(name, "name", "Extension");
- Utils.checkField(description, "description", "Extension");
- Utils.checkField(author, "author", "Extension");
- Utils.checkField(homepage, "homepage", "Extension");
- Utils.checkField(releases, "releases", "Extension");
-
- Utils.checkGithubURI(homepage);
- }
}
-
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/Release.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/Release.java
index 687283b..de1315d 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/Release.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/Release.java
@@ -1,110 +1,107 @@
package qupath.ext.extensionmanager.core.catalog;
import qupath.ext.extensionmanager.core.Version;
+import qupath.ext.extensionmanager.core.model.ReleaseModel;
+import qupath.ext.extensionmanager.core.model.VersionRangeModel;
import java.net.URI;
-import java.util.Collections;
import java.util.List;
/**
- * A description of an extension release hosted on GitHub.
- *
- * @param name the name of this release in the form "v[MAJOR].[MINOR].[PATCH]" or "v[MAJOR].[MINOR].[PATCH]-rc[RELEASE_CANDIDATE]"
- * @param mainUrl the GitHub URL where the main extension jar can be downloaded
- * @param requiredDependencyUrls SciJava Maven, Maven Central, or GitHub URLs where required dependency jars can be downloaded.
- * This list is immutable and won't be null
- * @param optionalDependencyUrls SciJava Maven, Maven Central, or GitHub URLs where optional dependency jars can be downloaded.
- * This list is immutable and won't be null
- * @param javadocUrls SciJava Maven, Maven Central, or GitHub URLs where javadoc jars for the main extension
- * jar and for dependencies can be downloaded. This list is immutable and won't be null
- * @param versionRange a specification of minimum and maximum compatible versions
+ * An extension release hosted on GitHub.
*/
-public record Release(
- String name,
- URI mainUrl,
- List requiredDependencyUrls,
- List optionalDependencyUrls,
- List javadocUrls,
- VersionRange versionRange
-) {
- private static final List VALID_HOSTS = List.of("github.com", "maven.scijava.org", "repo1.maven.org");
- private static final String VALID_SCHEME = "https";
+public class Release {
+
+ private final Version version;
+ private final URI mainUrl;
+ private final List javadocUrls;
+ private final List requiredDependencyUrls;
+ private final List optionalDependencyUrls;
+ private final VersionRangeModel versionRange;
/**
- * Create a Release.
- *
- * It must respect the following requirements:
- *
- * - The 'name', 'mainUrl', and 'versionRange' fields must be defined.
- * -
- * 'name' must be specified in the form "v[MAJOR].[MINOR].[PATCH]" corresponding to semantic versions,
- * although trailing release candidate qualifiers (eg, "-rc1") are also allowed.
- *
- * - The 'versions' object must be valid (see {@link VersionRange#VersionRange(String, String, List)}).
- * - The 'mainURL' field must be a GitHub URL. All other URLs must be SciJava Maven, Maven Central, or GitHub URLs.
- *
+ * Create a release from a {@link ReleaseModel}.
*
- * @param name the name of this release in the form "v[MAJOR].[MINOR].[PATCH]" or "v[MAJOR].[MINOR].[PATCH]-rc[RELEASE_CANDIDATE]"
- * @param mainUrl the GitHub URL where the main extension jar can be downloaded
- * @param requiredDependencyUrls SciJava Maven, Maven Central, or GitHub URLs where required dependency jars can be downloaded.
- * Can be null
- * @param optionalDependencyUrls SciJava Maven, Maven Central, or GitHub URLs where optional dependency jars can be downloaded.
- * Can be null
- * @param javadocUrls SciJava Maven, Maven Central, or GitHub URLs where javadoc jars for the main extension
- * jar and for dependencies can be downloaded. Can be null
- * @param versionRange a specification of minimum and maximum compatible versions
- * @throws IllegalArgumentException when the created release is not valid (see the requirements above)
+ * @param releaseModel information on the release
*/
- public Release(
- String name,
- URI mainUrl,
- List requiredDependencyUrls,
- List optionalDependencyUrls,
- List javadocUrls,
- VersionRange versionRange
- ) {
- this.name = name;
- this.mainUrl = mainUrl;
- this.requiredDependencyUrls = requiredDependencyUrls == null ? List.of() : Collections.unmodifiableList(requiredDependencyUrls);
- this.optionalDependencyUrls = optionalDependencyUrls == null ? List.of() : Collections.unmodifiableList(optionalDependencyUrls);
- this.javadocUrls = javadocUrls == null ? List.of() : Collections.unmodifiableList(javadocUrls);
- this.versionRange = versionRange;
+ public Release(ReleaseModel releaseModel) {
+ this.version = new Version(releaseModel.name());
+ this.mainUrl = releaseModel.mainUrl();
+ this.javadocUrls = releaseModel.javadocUrls();
+ this.requiredDependencyUrls = releaseModel.requiredDependencyUrls();
+ this.optionalDependencyUrls = releaseModel.optionalDependencyUrls();
+ this.versionRange = releaseModel.versionRange();
+ }
- checkValidity();
+ @Override
+ public String toString() {
+ return version.toString();
}
- private void checkValidity() {
- Utils.checkField(name, "name", "Release");
- Utils.checkField(mainUrl, "mainUrl", "Release");
- Utils.checkField(versionRange, "versionRange", "Release");
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) return false;
- Version.isValid(name, true);
+ Release release = (Release) o;
+ return version.equals(release.version) && mainUrl.equals(release.mainUrl) && javadocUrls.equals(release.javadocUrls) &&
+ requiredDependencyUrls.equals(release.requiredDependencyUrls) && optionalDependencyUrls.equals(release.optionalDependencyUrls) &&
+ versionRange.equals(release.versionRange);
+ }
- Utils.checkGithubURI(mainUrl);
+ @Override
+ public int hashCode() {
+ int result = version.hashCode();
+ result = 31 * result + mainUrl.hashCode();
+ result = 31 * result + javadocUrls.hashCode();
+ result = 31 * result + requiredDependencyUrls.hashCode();
+ result = 31 * result + optionalDependencyUrls.hashCode();
+ result = 31 * result + versionRange.hashCode();
+ return result;
+ }
- checkURIHostValidity(requiredDependencyUrls);
- checkURIHostValidity(optionalDependencyUrls);
- checkURIHostValidity(javadocUrls);
+ /**
+ * @return the version of this release
+ */
+ public Version getVersion() {
+ return version;
}
- private static void checkURIHostValidity(List uris) {
- if (uris != null) {
- for (URI uri: uris) {
- if (!VALID_SCHEME.equalsIgnoreCase(uri.getScheme())) {
- throw new IllegalArgumentException(String.format(
- "The URL %s must use %s",
- uri,
- VALID_SCHEME
- ));
- }
+ /**
+ * Indicate whether the provided version is compatible with this release.
+ *
+ * @param version the version that may be compatible with this release
+ * @return whether the provided version is compatible with this release
+ */
+ public boolean isCompatible(Version version) {
+ return versionRange.isCompatible(version);
+ }
- if (!VALID_HOSTS.contains(uri.getHost())) {
- throw new IllegalArgumentException(String.format(
- "The host part of %s is not among %s", uri, VALID_HOSTS
- ));
- }
- }
- }
+ /**
+ * @return the GitHub URL where the main extension jar can be downloaded
+ */
+ public URI getMainUrl() {
+ return mainUrl;
}
-}
+ /**
+ * @return SciJava Maven, Maven Central, or GitHub URLs where javadoc jars for the main extension jar and for dependencies
+ * can be downloaded
+ */
+ public List getJavadocUrls() {
+ return javadocUrls;
+ }
+
+ /**
+ * @return SciJava Maven, Maven Central, or GitHub URLs where required dependency jars can be downloaded
+ */
+ public List getRequiredDependencyUrls() {
+ return requiredDependencyUrls;
+ }
+
+ /**
+ * @return SciJava Maven, Maven Central, or GitHub URLs where optional dependency jars can be downloaded
+ */
+ public List getOptionalDependencyUrls() {
+ return optionalDependencyUrls;
+ }
+}
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/UpdateAvailable.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/UpdateAvailable.java
new file mode 100644
index 0000000..afbf9c5
--- /dev/null
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/UpdateAvailable.java
@@ -0,0 +1,12 @@
+package qupath.ext.extensionmanager.core.catalog;
+
+import qupath.ext.extensionmanager.core.Version;
+
+/**
+ * An object indicating an update available.
+ *
+ * @param extensionName the name of the updatable extension
+ * @param currentVersion the current version of the updatable extension
+ * @param newVersion the most recent and compatible version of the updatable extension
+ */
+public record UpdateAvailable(String extensionName, Version currentVersion, Version newVersion) {}
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/package-info.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/package-info.java
index de6318a..21700a6 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/package-info.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/package-info.java
@@ -1,13 +1,4 @@
/**
- * This package contains the model of a catalog as described on
- * this
- * Pydantic model.
- *
- * However, there is a small difference between the Pydantic model and this package:
- * classes of this package use the camel case naming convention while the Pydantic
- * model uses the snake case naming convention.
- *
- * A class ({@link qupath.ext.extensionmanager.core.catalog.CatalogFetcher CatalogFetcher}) is
- * also provided to fetch such catalog.
+ * This package contains classes to work with catalogs, extensions, and releases.
*/
package qupath.ext.extensionmanager.core.catalog;
\ No newline at end of file
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/CatalogModel.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/CatalogModel.java
new file mode 100644
index 0000000..ea3c792
--- /dev/null
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/CatalogModel.java
@@ -0,0 +1,56 @@
+package qupath.ext.extensionmanager.core.model;
+
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * A catalog describing a collection of extensions.
+ *
+ * @param name the name of the catalog
+ * @param description a short (one sentence or so) description of what the catalog contains and what its purpose is
+ * @param extensions the collection of extensions that the catalog describes. This list is immutable
+ */
+public record CatalogModel(String name, String description, List extensions) {
+
+ /**
+ * Create a catalog.
+ *
+ * It must respect the following requirements:
+ *
+ * - The 'name', 'description', and 'extensions' fields must be defined (but can be empty).
+ * -
+ * Each extension of the 'extensions' list must be a valid object
+ * (see {@link ExtensionModel#ExtensionModel(String, String, String, URI, boolean, List)}).
+ *
+ * - Two extensions of the 'extensions' list cannot have the same name.
+ *
+ *
+ * @param name the name of the catalog
+ * @param description a short (one sentence or so) description of what the catalog contains and what its purpose is
+ * @param extensions the collection of extensions that the catalog describes
+ * @throws IllegalArgumentException when the created catalog is not valid (see the requirements above)
+ */
+ public CatalogModel(String name, String description, List extensions) {
+ this.name = name;
+ this.description = description;
+ this.extensions = extensions == null ? null : Collections.unmodifiableList(extensions);
+
+ checkValidity();
+ }
+
+ private void checkValidity() {
+ Utils.checkField(name, "name", "Catalog");
+ Utils.checkField(description, "description", "Catalog");
+ Utils.checkField(extensions, "extensions", "Catalog");
+
+ if (extensions.stream().map(ExtensionModel::name).collect(Collectors.toSet()).size() < extensions.size()) {
+ throw new IllegalArgumentException(String.format(
+ "At least two extensions of %s have the same name",
+ extensions
+ ));
+ }
+ }
+}
+
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/CatalogFetcher.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/CatalogModelFetcher.java
similarity index 87%
rename from extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/CatalogFetcher.java
rename to extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/CatalogModelFetcher.java
index 9ac0477..8adae25 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/CatalogFetcher.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/CatalogModelFetcher.java
@@ -1,4 +1,4 @@
-package qupath.ext.extensionmanager.core.catalog;
+package qupath.ext.extensionmanager.core.model;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
@@ -16,15 +16,15 @@
/**
* A class to fetch a catalog.
*/
-public class CatalogFetcher {
+public class CatalogModelFetcher {
- private static final Logger logger = LoggerFactory.getLogger(CatalogFetcher.class);
+ private static final Logger logger = LoggerFactory.getLogger(CatalogModelFetcher.class);
private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(10);
private static final Gson gson = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) // convert snake case to camel case
.create();
- private CatalogFetcher() {
+ private CatalogModelFetcher() {
throw new AssertionError("This class is not instantiable.");
}
@@ -32,10 +32,10 @@ private CatalogFetcher() {
* Attempt to get a catalog from the provided URL.
*
* @param uri the URI pointing to the raw content of the catalog. It must contain "http" or "https"
- * @return a CompletableFuture with the catalog or a failed CompletableFuture if the provided URL doesn't point to
- * a valid catalog
+ * @return a CompletableFuture with the catalog or a failed CompletableFuture if the provided URL doesn't point to a
+ * valid catalog
*/
- public static CompletableFuture getCatalog(URI uri) {
+ public static CompletableFuture getCatalog(URI uri) {
if (uri == null) {
return CompletableFuture.failedFuture(new NullPointerException("The provided URI is null"));
}
@@ -66,7 +66,7 @@ public static CompletableFuture getCatalog(URI uri) {
}
logger.debug("Got response from {} with status 200:\n{}", uri, response.body());
- Catalog catalog = gson.fromJson(response.body(), Catalog.class);
+ CatalogModel catalog = gson.fromJson(response.body(), CatalogModel.class);
if (catalog == null) {
throw new RuntimeException(String.format("The response to %s is empty.", uri));
}
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/ExtensionModel.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/ExtensionModel.java
new file mode 100644
index 0000000..b824a07
--- /dev/null
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/ExtensionModel.java
@@ -0,0 +1,61 @@
+package qupath.ext.extensionmanager.core.model;
+
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A description of an extension.
+ *
+ * @param name the extension's name
+ * @param description a short (one sentence or so) description of what the extension is and what it does
+ * @param author the author or group responsible for the extension
+ * @param homepage a link to the GitHub repository associated with the extension
+ * @param starred whether the extension is generally useful or recommended for most users
+ * @param releases a list of available releases of the extension. This list is immutable
+ */
+public record ExtensionModel(String name, String description, String author, URI homepage, boolean starred, List releases) {
+
+ /**
+ * Create an Extension.
+ *
+ * It must respect the following requirements:
+ *
+ * - The 'name', 'description', 'author', 'homepage', and 'releases' fields must be defined (but can be empty).
+ * -
+ * Each release of the 'version' list must be a valid object
+ * (see {@link ReleaseModel#ReleaseModel(String, URI, List, List, List, VersionRangeModel)}).
+ *
+ * - The 'homepage' field must be a GitHub URL.
+ *
+ *
+ * @param name the extension's name
+ * @param description a short (one sentence or so) description of what the extension is and what it does
+ * @param author the author or group responsible for the extension
+ * @param homepage a link to the GitHub repository associated with the extension
+ * @param starred whether the extension is generally useful or recommended for most users
+ * @param releases a list of available releases of the extension
+ * @throws IllegalArgumentException when the created extension is not valid (see the requirements above)
+ */
+ public ExtensionModel(String name, String description, String author, URI homepage, boolean starred, List releases) {
+ this.name = name;
+ this.description = description;
+ this.author = author;
+ this.homepage = homepage;
+ this.starred = starred;
+ this.releases = releases == null ? null : Collections.unmodifiableList(releases);
+
+ checkValidity();
+ }
+
+ private void checkValidity() {
+ Utils.checkField(name, "name", "Extension");
+ Utils.checkField(description, "description", "Extension");
+ Utils.checkField(author, "author", "Extension");
+ Utils.checkField(homepage, "homepage", "Extension");
+ Utils.checkField(releases, "releases", "Extension");
+
+ Utils.checkGithubURI(homepage);
+ }
+}
+
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/ReleaseModel.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/ReleaseModel.java
new file mode 100644
index 0000000..3dfc18d
--- /dev/null
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/ReleaseModel.java
@@ -0,0 +1,110 @@
+package qupath.ext.extensionmanager.core.model;
+
+import qupath.ext.extensionmanager.core.Version;
+
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A description of an extension release hosted on GitHub.
+ *
+ * @param name the name of this release in the form "v[MAJOR].[MINOR].[PATCH]" or "v[MAJOR].[MINOR].[PATCH]-rc[RELEASE_CANDIDATE]"
+ * @param mainUrl the GitHub URL where the main extension jar can be downloaded
+ * @param requiredDependencyUrls SciJava Maven, Maven Central, or GitHub URLs where required dependency jars can be downloaded.
+ * This list is immutable and won't be null
+ * @param optionalDependencyUrls SciJava Maven, Maven Central, or GitHub URLs where optional dependency jars can be downloaded.
+ * This list is immutable and won't be null
+ * @param javadocUrls SciJava Maven, Maven Central, or GitHub URLs where javadoc jars for the main extension jar and for
+ * dependencies can be downloaded. This list is immutable and won't be null
+ * @param versionRange a specification of minimum and maximum compatible versions
+ */
+public record ReleaseModel(
+ String name,
+ URI mainUrl,
+ List requiredDependencyUrls,
+ List optionalDependencyUrls,
+ List javadocUrls,
+ VersionRangeModel versionRange
+) {
+ private static final List VALID_HOSTS = List.of("github.com", "maven.scijava.org", "repo1.maven.org");
+ private static final String VALID_SCHEME = "https";
+
+ /**
+ * Create a Release.
+ *
+ * It must respect the following requirements:
+ *
+ * - The 'name', 'mainUrl', and 'versionRange' fields must be defined.
+ * -
+ * 'name' must be specified in the form "v[MAJOR].[MINOR].[PATCH]" corresponding to semantic versions,
+ * although trailing release candidate qualifiers (eg, "-rc1") are also allowed.
+ *
+ * - The 'versions' object must be valid (see {@link VersionRangeModel#VersionRangeModel(String, String, List)}).
+ * - The 'mainURL' field must be a GitHub URL. All other URLs must be SciJava Maven, Maven Central, or GitHub URLs.
+ *
+ *
+ * @param name the name of this release in the form "v[MAJOR].[MINOR].[PATCH]" or "v[MAJOR].[MINOR].[PATCH]-rc[RELEASE_CANDIDATE]"
+ * @param mainUrl the GitHub URL where the main extension jar can be downloaded
+ * @param requiredDependencyUrls SciJava Maven, Maven Central, or GitHub URLs where required dependency jars can be
+ * downloaded. Can be null
+ * @param optionalDependencyUrls SciJava Maven, Maven Central, or GitHub URLs where optional dependency jars can be
+ * downloaded. Can be null
+ * @param javadocUrls SciJava Maven, Maven Central, or GitHub URLs where javadoc jars for the main extension jar and
+ * for dependencies can be downloaded. Can be null
+ * @param versionRange a specification of minimum and maximum compatible versions
+ * @throws IllegalArgumentException when the created release is not valid (see the requirements above)
+ */
+ public ReleaseModel(
+ String name,
+ URI mainUrl,
+ List requiredDependencyUrls,
+ List optionalDependencyUrls,
+ List javadocUrls,
+ VersionRangeModel versionRange
+ ) {
+ this.name = name;
+ this.mainUrl = mainUrl;
+ this.requiredDependencyUrls = requiredDependencyUrls == null ? List.of() : Collections.unmodifiableList(requiredDependencyUrls);
+ this.optionalDependencyUrls = optionalDependencyUrls == null ? List.of() : Collections.unmodifiableList(optionalDependencyUrls);
+ this.javadocUrls = javadocUrls == null ? List.of() : Collections.unmodifiableList(javadocUrls);
+ this.versionRange = versionRange;
+
+ checkValidity();
+ }
+
+ private void checkValidity() {
+ Utils.checkField(name, "name", "Release");
+ Utils.checkField(mainUrl, "mainUrl", "Release");
+ Utils.checkField(versionRange, "versionRange", "Release");
+
+ Version.isValid(name, true);
+
+ Utils.checkGithubURI(mainUrl);
+
+ checkURIHostValidity(requiredDependencyUrls);
+ checkURIHostValidity(optionalDependencyUrls);
+ checkURIHostValidity(javadocUrls);
+ }
+
+ private static void checkURIHostValidity(List uris) {
+ if (uris != null) {
+ for (URI uri: uris) {
+ if (!VALID_SCHEME.equalsIgnoreCase(uri.getScheme())) {
+ throw new IllegalArgumentException(String.format(
+ "The URL %s must use %s",
+ uri,
+ VALID_SCHEME
+ ));
+ }
+
+ if (!VALID_HOSTS.contains(uri.getHost())) {
+ throw new IllegalArgumentException(String.format(
+ "The host part of %s is not among %s", uri, VALID_HOSTS
+ ));
+ }
+ }
+ }
+ }
+}
+
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/Utils.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/Utils.java
similarity index 96%
rename from extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/Utils.java
rename to extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/Utils.java
index e2e0dcf..f655d3c 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/Utils.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/Utils.java
@@ -1,4 +1,4 @@
-package qupath.ext.extensionmanager.core.catalog;
+package qupath.ext.extensionmanager.core.model;
import java.net.URI;
@@ -8,6 +8,7 @@
class Utils {
private static final String GITHUB_HOST = "github.com";
+
private Utils() {
throw new AssertionError("This class is not instantiable.");
}
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/VersionRange.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/VersionRangeModel.java
similarity index 82%
rename from extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/VersionRange.java
rename to extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/VersionRangeModel.java
index f85c493..3e5f1e4 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/VersionRange.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/VersionRangeModel.java
@@ -1,4 +1,4 @@
-package qupath.ext.extensionmanager.core.catalog;
+package qupath.ext.extensionmanager.core.model;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -15,9 +15,9 @@
* @param max the maximum/highest version that this extension is known to be compatible with. Can be null
* @param excludes any specific versions that are not compatible. This list is immutable and won't be null
*/
-public record VersionRange(String min, String max, List excludes) {
+public record VersionRangeModel(String min, String max, List excludes) {
- private static final Logger logger = LoggerFactory.getLogger(VersionRange.class);
+ private static final Logger logger = LoggerFactory.getLogger(VersionRangeModel.class);
/**
* Create a version range.
@@ -26,8 +26,10 @@ public record VersionRange(String min, String max, List excludes) {
*
* - The 'min' field must be defined.
* - If 'max' is specified, it must correspond to a version higher than or equal to 'min'.
- * - If 'excludes' is specified, each of its element must correspond to a version higher than or equal to
- * 'min', and lower than or equal to 'max' if 'max' is defined.
+ * -
+ * If 'excludes' is specified, each of its element must correspond to a version higher than or equal to 'min',
+ * and lower than or equal to 'max' if 'max' is defined.
+ *
* -
* All versions must be specified in the form "v[MAJOR].[MINOR].[PATCH]" corresponding to
* semantic versions, although trailing release candidate qualifiers (eg, "-rc1") are also allowed.
@@ -39,7 +41,7 @@ public record VersionRange(String min, String max, List excludes) {
* @param excludes any specific versions that are not compatible. Can be null
* @throws IllegalArgumentException when the created version range is not valid (see the requirements above)
*/
- public VersionRange(String min, String max, List excludes) {
+ public VersionRangeModel(String min, String max, List excludes) {
this.min = min;
this.max = max;
this.excludes = excludes == null ? List.of() : Collections.unmodifiableList(excludes);
@@ -50,18 +52,14 @@ public VersionRange(String min, String max, List excludes) {
/**
* Indicate if this release range is compatible with the provided version.
*
- * @param version the version to check if this release range is compatible with. It
- * must be specified in the form "v[MAJOR].[MINOR].[PATCH]", although
- * trailing release candidate qualifiers (eg, "-rc1") are also allowed.
+ * @param version the version to check if this release range is compatible with
* @return a boolean indicating if the provided version is compatible with this release range
- * @throws IllegalArgumentException if the provided version doesn't match the required form
- * or if this release range is not valid
+ * @throws IllegalArgumentException if the provided version doesn't match the required form or if this release range
+ * is not valid
* @throws NullPointerException if the provided version is null
*/
- public boolean isCompatible(String version) {
- Version versionObject = new Version(version);
-
- if (new Version(min).compareTo(versionObject) > 0) {
+ public boolean isCompatible(Version version) {
+ if (new Version(min).compareTo(version) > 0) {
logger.debug(
"This version range {} is not compatible with {} because of the minimum compatible version",
this,
@@ -70,7 +68,7 @@ public boolean isCompatible(String version) {
return false;
}
- if (max != null && versionObject.compareTo(new Version(max)) > 0) {
+ if (max != null && version.compareTo(new Version(max)) > 0) {
logger.debug(
"This version range {} is not compatible with {} because of the maximum compatible version",
this,
@@ -79,7 +77,7 @@ public boolean isCompatible(String version) {
return false;
}
- if (excludes != null && excludes.stream().map(Version::new).anyMatch(versionObject::equals)) {
+ if (excludes != null && excludes.stream().map(Version::new).anyMatch(version::equals)) {
logger.debug(
"This version range {} is not compatible with {} because of the excluded versions",
this,
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/package-info.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/package-info.java
new file mode 100644
index 0000000..c06adb3
--- /dev/null
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/model/package-info.java
@@ -0,0 +1,11 @@
+/**
+ * This package contains the model of a catalog as described on this
+ * Pydantic model.
+ *
+ * However, there is a small difference between the Pydantic model and this package: classes of this package use the camel
+ * case naming convention while the Pydantic model uses the snake case naming convention.
+ *
+ * A class ({@link qupath.ext.extensionmanager.core.model.CatalogModelFetcher CatalogFetcher}) is also provided to fetch
+ * such catalog.
+ */
+package qupath.ext.extensionmanager.core.model;
\ No newline at end of file
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/registry/Registry.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/registry/Registry.java
new file mode 100644
index 0000000..17f7363
--- /dev/null
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/registry/Registry.java
@@ -0,0 +1,54 @@
+package qupath.ext.extensionmanager.core.registry;
+
+import qupath.ext.extensionmanager.core.catalog.Catalog;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * A registry contains a list of {@link RegistryCatalog catalogs}.
+ *
+ * A {@link RuntimeException} is thrown if one parameter is null.
+ *
+ * @param catalogs the catalogs this registry owns
+ */
+public record Registry(List catalogs) {
+
+ public Registry {
+ Objects.requireNonNull(catalogs);
+ }
+
+ /**
+ * Create a registry from a list of {@link Catalog catalogs}. As this function requires to call {@link Catalog#getExtensions()},
+ * it may take some time to complete.
+ *
+ * Note that exception handling is left to the caller (the returned CompletableFuture may complete exceptionally).
+ *
+ * @param catalogs the catalogs to create the registry from
+ * @return a CompletableFuture with the created registry (that completes exceptionally if the operation failed)
+ */
+ public static CompletableFuture createFromCatalogs(List catalogs) {
+ return CompletableFuture.supplyAsync(() -> new Registry(catalogs.stream()
+ .map(catalog -> new RegistryCatalog(
+ catalog.getName(),
+ catalog.getDescription(),
+ catalog.getUri(),
+ catalog.getRawUri(),
+ catalog.isDeletable(),
+ catalog.getExtensions().join().stream()
+ .map(extension -> extension.getInstalledRelease().getValue()
+ .map(release -> new RegistryExtension(
+ extension.getName(),
+ release.getVersion().toString(),
+ extension.areOptionalDependenciesInstalled().get()
+ ))
+ .orElse(null)
+ )
+ .filter(Objects::nonNull)
+ .toList()
+ ))
+ .toList()
+ ));
+ }
+}
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/registry/RegistryCatalog.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/registry/RegistryCatalog.java
new file mode 100644
index 0000000..bfec765
--- /dev/null
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/registry/RegistryCatalog.java
@@ -0,0 +1,34 @@
+package qupath.ext.extensionmanager.core.registry;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A catalog containing a collection of extensions.
+ *
+ * A {@link RuntimeException} is thrown if one parameter is null.
+ *
+ * @param name the name of the catalog
+ * @param description a short (one sentence or so) description of what the catalog contains and what its purpose is
+ * @param uri a URI pointing to the raw content of the catalog, or to a GitHub repository where the catalog can be found
+ * @param rawUri the URI pointing to the raw content of the catalog (can be same as {@link #uri})
+ * @param deletable whether this metadata can be deleted
+ * @param extensions a list of installed extensions this catalogs owns
+ */
+public record RegistryCatalog(
+ String name,
+ String description,
+ URI uri,
+ URI rawUri,
+ boolean deletable,
+ List extensions
+) {
+ public RegistryCatalog {
+ Objects.requireNonNull(name);
+ Objects.requireNonNull(description);
+ Objects.requireNonNull(uri);
+ Objects.requireNonNull(rawUri);
+ Objects.requireNonNull(extensions);
+ }
+}
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/registry/RegistryExtension.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/registry/RegistryExtension.java
new file mode 100644
index 0000000..371638a
--- /dev/null
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/registry/RegistryExtension.java
@@ -0,0 +1,20 @@
+package qupath.ext.extensionmanager.core.registry;
+
+import java.util.Objects;
+
+/**
+ * An installed extension.
+ *
+ * A {@link RuntimeException} is thrown if one parameter is null.
+ *
+ * @param name the name of the extension
+ * @param installedVersion the name of the version of this extension that is currently installed
+ * @param optionalDependenciesInstalled whether optional dependencies are currently installed
+ */
+public record RegistryExtension(String name, String installedVersion, boolean optionalDependenciesInstalled) {
+
+ public RegistryExtension {
+ Objects.requireNonNull(name);
+ Objects.requireNonNull(installedVersion);
+ }
+}
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/registry/package-info.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/registry/package-info.java
new file mode 100644
index 0000000..16f048a
--- /dev/null
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/registry/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * This package contains a {@link qupath.ext.extensionmanager.core.registry.Registry}, which can be used to store a list
+ * of catalogs and extensions in a JSON-friendly format.
+ */
+package qupath.ext.extensionmanager.core.registry;
\ No newline at end of file
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/savedentities/InstalledExtension.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/savedentities/InstalledExtension.java
deleted file mode 100644
index f050fb5..0000000
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/savedentities/InstalledExtension.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package qupath.ext.extensionmanager.core.savedentities;
-
-/**
- * Installation information on an extension.
- *
- * @param releaseName the name of the installed release that should follow the
- * specifications of {@link qupath.ext.extensionmanager.core.Version}
- * @param optionalDependenciesInstalled whether optional dependencies are installed
- */
-public record InstalledExtension(String releaseName, boolean optionalDependenciesInstalled) {}
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/savedentities/Registry.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/savedentities/Registry.java
deleted file mode 100644
index 75d2cfe..0000000
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/savedentities/Registry.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package qupath.ext.extensionmanager.core.savedentities;
-
-import java.util.Collections;
-import java.util.List;
-
-/**
- * A class containing information regarding a list of saved catalogs.
- *
- * @param catalogs the saved catalogs. This list is immutable and won't be null
- */
-public record Registry(List catalogs) {
-
- /**
- * Create a registry containing the provided list of catalogs.
- *
- * @param catalogs the catalogs this registry should contain
- * @throws NullPointerException if the provided list is null
- */
- public Registry(List catalogs) {
- this.catalogs = Collections.unmodifiableList(catalogs);
- }
-}
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/savedentities/SavedCatalog.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/savedentities/SavedCatalog.java
deleted file mode 100644
index 1edc600..0000000
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/savedentities/SavedCatalog.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package qupath.ext.extensionmanager.core.savedentities;
-
-import qupath.ext.extensionmanager.core.catalog.Catalog;
-
-import java.net.URI;
-
-/**
- * Basic metadata on a catalog.
- *
- * @param name see {@link Catalog#name()}
- * @param description see {@link Catalog#description()}
- * @param uri a URI pointing to the raw content of the catalog, or to a GitHub repository where the
- * catalog can be found
- * @param rawUri the URI pointing to the raw content of the catalog (can be same as {@link #uri})
- * @param deletable whether this metadata can be deleted
- */
-public record SavedCatalog(String name, String description, URI uri, URI rawUri, boolean deletable) {}
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/savedentities/UpdateAvailable.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/savedentities/UpdateAvailable.java
deleted file mode 100644
index 190b751..0000000
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/savedentities/UpdateAvailable.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package qupath.ext.extensionmanager.core.savedentities;
-
-/**
- * An object indicating an update available.
- *
- * @param extensionName the name of the updatable extension
- * @param currentVersion a text describing the current version of the updatable extension
- * @param newVersion a text describing the most recent and compatible version of the updatable extension
- */
-public record UpdateAvailable(String extensionName, String currentVersion, String newVersion) {}
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/savedentities/package-info.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/savedentities/package-info.java
deleted file mode 100644
index 3d57cd1..0000000
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/savedentities/package-info.java
+++ /dev/null
@@ -1,4 +0,0 @@
-/**
- * Some models used to save / retrieve information on catalogs and extensions.
- */
-package qupath.ext.extensionmanager.core.savedentities;
\ No newline at end of file
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/FileDownloader.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/FileDownloader.java
index fcd8d8e..6c4963f 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/FileDownloader.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/FileDownloader.java
@@ -31,23 +31,21 @@ private FileDownloader() {
}
/**
- * Download a file located at the provided URI and place it on the provided path.
- * This function may take a lot of time depending on the internet connection and the
- * size of the download, but is cancellable.
+ * Download a file located at the provided URI and place it on the provided path. This function may take a lot of time
+ * depending on the internet connection and the size of the download, but can be interrupted.
*
* @param uri the URI pointing to the file to download. It must contain "http" or "https"
* @param outputPath the path the downloaded file should have. It will be overridden if it already exists
- * @param onProgress a function that will be called at different steps during the download. Its parameter
- * will be a float between 0 and 1 indicating the progress of the download (0: beginning,
- * 1: finished). This function will be called from the calling thread
+ * @param onProgress a function that will be called at different steps during the download. Its parameter will be a
+ * float between 0 and 1 indicating the progress of the download (0: beginning, 1: finished). This
+ * function will be called from the calling thread
* @throws NullPointerException if one of the provided parameter is null
- * @throws IOException if an I/O error occurs when sending the request or receiving the file. It can also occur
- * when this function is interrupted, in which case its {@link Exception#getCause() cause} will be an {@link InterruptedException}
+ * @throws IOException if an I/O error occurs when sending the request or receiving the file. It can also occur if this
+ * function is interrupted, in which case its {@link Exception#getCause() cause} will be an {@link InterruptedException}
* @throws InterruptedException if this function is interrupted
* @throws IllegalArgumentException if the provided URI does not contain a valid scheme ("http" or "https")
* @throws java.io.FileNotFoundException if the downloaded file already exists but is a directory rather than a regular
* file, does not exist but cannot be created, or cannot be opened for any other reason
- * @throws SecurityException if the user doesn't have enough rights to write the output file
*/
public static void downloadFile(URI uri, Path outputPath, Consumer onProgress) throws IOException, InterruptedException {
if (!"http".equals(uri.getScheme()) && !"https".equals(uri.getScheme())) {
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/FileTools.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/FileTools.java
index e843c4d..9c4ce0b 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/FileTools.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/FileTools.java
@@ -30,7 +30,6 @@ private FileTools() {
* @param path the path to check
* @return whether the provided path is a directory and is not empty
* @throws IOException if an I/O error occurs
- * @throws SecurityException if the user doesn't have sufficient rights to read the file
* @throws NullPointerException if the provided path is null
*/
public static boolean isDirectoryNotEmpty(Path path) throws IOException {
@@ -51,15 +50,13 @@ public static boolean isDirectoryNotEmpty(Path path) throws IOException {
}
/**
- * Attempt to move the provided file to trash, or delete it (and all its children
- * recursively if it's a directory) if the current platform does not support moving
- * files to trash.
+ * Attempt to move the provided file to trash, or delete it (and all its children recursively if it's a directory) if
+ * the current platform does not support moving files to trash.
+ *
* This won't do anything if the provided file doesn't exist.
*
* @param directoryToDelete the file or directory to delete
* @throws IOException if an I/O error occurs
- * @throws SecurityException if the user doesn't have sufficient rights to move or
- * delete some files
* @throws NullPointerException if the provided directory is null
*/
public static void moveDirectoryToTrashOrDeleteRecursively(File directoryToDelete) throws IOException {
@@ -82,8 +79,8 @@ public static void moveDirectoryToTrashOrDeleteRecursively(File directoryToDelet
}
/**
- * Strip the provided name from characters that would be invalid
- * in a file name. This is not guaranteed to work for any character.
+ * Strip the provided name from characters that would be invalid in a file name. This is not guaranteed to work for
+ * any character.
*
* @param name the name to strip characters from
* @return the provided name without characters that would be invalid in a file name
@@ -94,22 +91,19 @@ public static String stripInvalidFilenameCharacters(String name) {
}
/**
- * Get the name of the file or directory denoted by the path contained in the
- * provided URI.
+ * Get the name of the file or directory denoted by the path contained in the provided URI.
*
* @param uri the URI containing the file name to retrieve
* @return the file name denoted by the path contained in the provided URI
* @throws NullPointerException if the path of the provided URI is undefined
- * @throws java.nio.file.InvalidPathException if the path of the provided URI cannot be
- * converted to a Path
+ * @throws java.nio.file.InvalidPathException if the path of the provided URI cannot be converted to a Path
*/
public static String getFileNameFromURI(URI uri) {
return Paths.get(uri.getPath()).getFileName().toString();
}
/**
- * Indicate whether a file is a (direct or not) parent of another file. The provided
- * files don't have to exist.
+ * Indicate whether a file is a (direct or not) parent of another file. The provided files don't have to exist.
*
* @param possibleParent the file that may be a parent of the other file
* @param possibleChild the file that may be a child of the other file
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/FilesWatcher.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/FilesWatcher.java
index db0d1a9..3718539 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/FilesWatcher.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/FilesWatcher.java
@@ -12,12 +12,11 @@
import java.util.function.Predicate;
/**
- * A class that returns an ObservableList of paths to certain files that
- * are contained within a specific directory (recursively but with a maximal
- * depth of 8).
- * The list is automatically updated based on changes to the specified directory
- * (or one of its descendant).
- * Note that changes may take a few seconds to be detected.
+ * A class that returns an ObservableList of paths to certain files that are contained within a specific directory (recursively
+ * but with a maximal depth of 8).
+ *
+ * The list is automatically updated based on changes to the specified directory (or one of its descendant). Note that
+ * changes may take a few seconds to be detected.
*
* This watcher must be {@link #close() closed} once no longer used.
*
@@ -72,9 +71,8 @@ public void close() throws Exception {
}
/**
- * @return a read-only list of files that are currently present according to the parameters
- * specified in {@link #FilesWatcher(ObservableValue, Predicate, Predicate)}. This list may be
- * updated from any thread
+ * @return a read-only list of files that are currently present according to the parameters specified in
+ * {@link #FilesWatcher(ObservableValue, Predicate, Predicate)}. This list may be updated from any thread
*/
public ObservableList getFiles() {
return filesImmutable;
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/GitHubRawLinkFinder.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/GitHubRawLinkFinder.java
index 83d6f49..d0df090 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/GitHubRawLinkFinder.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/GitHubRawLinkFinder.java
@@ -43,13 +43,12 @@ private GitHubRawLinkFinder() {
* Get the link pointing to the raw content of the provided file within the provided GitHub
* repository.
*
- * @param url the URL pointing to the GitHub repository containing the file to find. It can be a link
- * to any directory or file within the repository. If it's a link to a directory, all direct
- * children of this directory will be searched. If it's a link to a file, only this file will
- * be searched. It must contain "http" or "https"
+ * @param url the URL pointing to the GitHub repository containing the file to find. It can be a link to any directory
+ * or file within the repository. If it's a link to a directory, all direct children of this directory will
+ * be searched. If it's a link to a file, only this file will be searched. It must contain "http" or "https"
* @param filePredicate a predicate on the name of the file to find
- * @return a CompletableFuture with the link pointing to the raw content of the desired file, or a failed
- * CompletableFuture if it couldn't be retrieved
+ * @return a CompletableFuture with the link pointing to the raw content of the desired file, or a failed CompletableFuture
+ * if it couldn't be retrieved
*/
public static CompletableFuture getRawLinkOfFileInRepository(String url, Predicate filePredicate) {
if (url == null) {
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/RecursiveDirectoryWatcher.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/RecursiveDirectoryWatcher.java
index 2ae8962..edf80a7 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/RecursiveDirectoryWatcher.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/RecursiveDirectoryWatcher.java
@@ -45,28 +45,24 @@ class RecursiveDirectoryWatcher implements AutoCloseable {
private final Consumer onFileAdded;
/**
- * Set up some listeners to be called when files are added or removed
- * from the provided directory or one of its children.
- * The listeners may be called only a few seconds after a file was added
- * or removed.
+ * Set up some listeners to be called when files are added or removed from the provided directory or one of its children.
+ *
+ * The listeners may be called only a few seconds after a file was added or removed.
*
* @param directoryToWatch the path of the root directory to watch
* @param depth the maximum number of directory levels to watch
- * @param filesToFind a predicate indicating files to consider. Listeners won't be called for
- * files that don't match this predicate
+ * @param filesToFind a predicate indicating files to consider. Listeners won't be called for files that don't match
+ * this predicate
* @param directoriesToSkip a predicate indicating directories not to watch
- * @param onFileAdded a function that will be called when a new file is added to one of the watched directory.
- * Its parameter will be the path of the new file. This function may be called from
- * any thread. If files already exist in the provided directory to watch, this function
- * will be called on them
- * @param onFileDeleted a function that will be called when a file is removed from one of the watched directory.
- * Its parameter will be the path of the deleted file. This function may be called
- * from any thread. If a folder containing a file to consider is deleted, this function will be
- * called on this file
+ * @param onFileAdded a function that will be called when a new file is added to one of the watched directory. Its
+ * parameter will be the path of the new file. This function may be called from any thread. If
+ * files already exist in the provided directory to watch, this function will be called on them
+ * @param onFileDeleted a function that will be called when a file is removed from one of the watched directory. Its
+ * parameter will be the path of the deleted file. This function may be called from any thread.
+ * If a folder containing a file to consider is deleted, this function will be called on this file
* @throws IOException if an I/O error occurs
* @throws UnsupportedOperationException if watching file system is not supported by this system
* @throws java.nio.file.NotDirectoryException if there is no directory on the provided path
- * @throws SecurityException if the user doesn't have enough rights to read the provided directory
* @throws NullPointerException if one of the parameter is used and null
*/
public RecursiveDirectoryWatcher(
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/ZipExtractor.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/ZipExtractor.java
index 58a7229..369a55b 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/ZipExtractor.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/ZipExtractor.java
@@ -16,8 +16,6 @@
/**
* A class to extract a ZIP file and get information on the extraction progress.
- *
- * This class was inspired by this guide.
*/
public class ZipExtractor {
@@ -29,20 +27,18 @@ private ZipExtractor() {
}
/**
- * Extract files from the provided ZIP file path and place them in the provided output folder.
- * This function may take a lot of time depending on the ZIP file size, but is cancellable.
+ * Extract files from the provided ZIP file path and place them in the provided output folder. This function may take
+ * a lot of time depending on the ZIP file size, but is cancellable.
*
* @param inputZipPath the path of the ZIP file to extract
* @param outputFolderPath the path of a folder that should contain the extracted files
- * @param onProgress a function that will be called at different steps during the extraction. Its parameter
- * will be a float between 0 and 1 indicating the progress of the extraction (0: beginning,
- * 1: finished). This function will be called from the calling thread
- * @throws IOException if an I/O error has occurred while opening the ZIP file or extracting the files,
- * or if the output directory cannot be created
+ * @param onProgress a function that will be called at different steps during the extraction. Its parameter will be a
+ * float between 0 and 1 indicating the progress of the extraction (0: beginning, 1: finished). This
+ * function will be called from the calling thread
+ * @throws IOException if an I/O error has occurred while opening the ZIP file or extracting the files, or if the
+ * output directory cannot be created
* @throws InterruptedException if the running thread is interrupted
* @throws java.util.zip.ZipException if a ZIP format error has occurred when opening the ZIP file
- * @throws SecurityException if the user doesn't have sufficient rights to open the ZIP file or
- * write files to the output folder
* @throws NullPointerException if one of the provided parameter is null
*/
public static void extractZipToFolder(Path inputZipPath, Path outputFolderPath, Consumer onProgress) throws IOException, InterruptedException {
@@ -100,16 +96,16 @@ private static int getNumberOfFilesInZip(Path zipPath) throws IOException {
}
/**
- * Create a file object by adding the name of the provided ZIP entry
- * to the provided destination directory.
+ * Create a file object by adding the name of the provided ZIP entry to the provided destination directory.
+ *
* This method guards against writing files to the file system outside the target folder
* (Zip Slip vulnerability).
*
* @param destinationDirectory the parent directory of the file to create
* @param zipEntry the zip entry containing the name of the file to create
* @return the created file
- * @throws IOException if an I/O error has occurred creating the file or when the created file
- * was outside the target folder
+ * @throws IOException if an I/O error has occurred creating the file or when the created file was outside the target
+ * folder
*/
private static File createFile(File destinationDirectory, ZipEntry zipEntry) throws IOException {
File destFile = new File(destinationDirectory, zipEntry.getName());
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/package-info.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/package-info.java
index 270f35c..f222a72 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/package-info.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/tools/package-info.java
@@ -1,5 +1,4 @@
/**
- * A set of tools to work with files, directories, GitHub repositories, and ZIP
- * archives.
+ * A set of tools to work with files, directories, GitHub repositories, and ZIP archives.
*/
package qupath.ext.extensionmanager.core.tools;
\ No newline at end of file
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 3800c26..fbe0071 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/CatalogManager.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/CatalogManager.java
@@ -27,17 +27,16 @@
import org.slf4j.LoggerFactory;
import qupath.ext.extensionmanager.core.ExtensionCatalogManager;
import qupath.ext.extensionmanager.core.catalog.Catalog;
-import qupath.ext.extensionmanager.core.catalog.CatalogFetcher;
-import qupath.ext.extensionmanager.core.savedentities.SavedCatalog;
+import qupath.ext.extensionmanager.core.model.CatalogModel;
+import qupath.ext.extensionmanager.core.model.CatalogModelFetcher;
import qupath.ext.extensionmanager.core.tools.GitHubRawLinkFinder;
import qupath.fx.dialogs.Dialogs;
import java.io.IOException;
import java.net.URI;
-import java.nio.file.InvalidPathException;
import java.text.MessageFormat;
+import java.util.ArrayList;
import java.util.List;
-import java.util.Objects;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
@@ -55,15 +54,15 @@ class CatalogManager extends Stage {
private final ExtensionCatalogManager extensionCatalogManager;
private final Runnable onInvalidExtensionDirectory;
@FXML
- private TableView catalogTable;
+ private TableView catalogTable;
@FXML
- private TableColumn nameColumn;
+ private TableColumn nameColumn;
@FXML
- private TableColumn urlColumn;
+ private TableColumn urlColumn;
@FXML
- private TableColumn descriptionColumn;
+ private TableColumn descriptionColumn;
@FXML
- private TableColumn removeColumn;
+ private TableColumn removeColumn;
@FXML
private TextField catalogUrl;
@@ -73,12 +72,12 @@ class CatalogManager extends Stage {
* @param extensionCatalogManager the extension catalog manager this window should use
* @param model the model to use when accessing data
* @param onInvalidExtensionDirectory a function that will be called if an operation needs to access the extension
- * directory (see {@link ExtensionCatalogManager#getExtensionDirectoryPath()})
- * but this directory is currently invalid. It lets the possibility to the user to
- * define and create a valid directory before performing the operation (which would
- * fail if the directory is invalid). This function is guaranteed to be called from
- * the JavaFX Application Thread
- * @throws IOException when an error occurs while creating the window
+ * directory (see {@link ExtensionCatalogManager#getExtensionDirectory()}) but this
+ * directory is currently invalid. It lets the possibility to the user to define
+ * and create a valid directory before performing the operation (which would fail
+ * if the directory is invalid). This function is guaranteed to be called from the
+ * JavaFX Application Thread
+ * @throws IOException if an error occurs while creating the window
*/
public CatalogManager(
ExtensionCatalogManager extensionCatalogManager,
@@ -99,7 +98,7 @@ public CatalogManager(
@FXML
private void onAddClicked(ActionEvent ignored) {
- UiUtils.promptExtensionDirectory(extensionCatalogManager.getExtensionDirectoryPath(), onInvalidExtensionDirectory);
+ UiUtils.promptExtensionDirectory(extensionCatalogManager.getExtensionDirectory(), onInvalidExtensionDirectory);
String catalogUrl = this.catalogUrl.getText();
if (catalogUrl == null || catalogUrl.isBlank()) {
@@ -147,10 +146,10 @@ private void onAddClicked(ActionEvent ignored) {
finalUri.toString()
));
});
- Catalog catalog = CatalogFetcher.getCatalog(uri).get();
+ CatalogModel catalog = CatalogModelFetcher.getCatalog(uri).get();
Platform.runLater(() -> progressWindow.setProgress(1));
- if (extensionCatalogManager.getCatalogs().stream().anyMatch(savedCatalog -> savedCatalog.name().equals(catalog.name()))) {
+ if (extensionCatalogManager.getCatalogs().stream().anyMatch(c -> c.getName().equals(catalog.name()))) {
displayErrorMessage(
resources.getString("CatalogManager.cannotAddCatalog"),
MessageFormat.format(
@@ -161,13 +160,7 @@ private void onAddClicked(ActionEvent ignored) {
return;
}
- extensionCatalogManager.addCatalog(List.of(new SavedCatalog(
- catalog.name(),
- catalog.description(),
- new URI(catalogUrl),
- uri,
- true
- )));
+ extensionCatalogManager.addCatalog(new Catalog(catalog, new URI(catalogUrl), uri, true));
} catch (Exception e) {
logger.debug("Error when fetching catalog at {}", catalogUrl, e);
displayErrorMessage(
@@ -182,23 +175,25 @@ private void onAddClicked(ActionEvent ignored) {
}
private void setColumns() {
- nameColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().name()));
- urlColumn.setCellValueFactory(cellData -> new SimpleObjectProperty<>(cellData.getValue().uri()));
- descriptionColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().description()));
+ nameColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getName()));
+ urlColumn.setCellValueFactory(cellData -> new SimpleObjectProperty<>(cellData.getValue().getUri()));
+ descriptionColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getDescription()));
removeColumn.setCellValueFactory(cellData -> {
Button button = new Button(resources.getString("CatalogManager.remove"));
- button.setDisable(!cellData.getValue().deletable());
+
+ button.setDisable(!cellData.getValue().isDeletable());
button.setOnAction(event -> {
- UiUtils.promptExtensionDirectory(extensionCatalogManager.getExtensionDirectoryPath(), onInvalidExtensionDirectory);
+ UiUtils.promptExtensionDirectory(extensionCatalogManager.getExtensionDirectory(), onInvalidExtensionDirectory);
deleteCatalogs(List.of(cellData.getValue()));
});
+
return new SimpleObjectProperty<>(button);
});
nameColumn.setCellFactory(getStringCellFactory());
urlColumn.setCellFactory(column -> {
- TableCell tableCell = new TableCell<>() {
+ TableCell tableCell = new TableCell<>() {
@Override
protected void updateItem(URI item, boolean empty) {
super.updateItem(item, empty);
@@ -213,12 +208,14 @@ protected void updateItem(URI item, boolean empty) {
}
}
};
+
tableCell.setAlignment(Pos.CENTER_LEFT);
+
return tableCell;
});
descriptionColumn.setCellFactory(getStringCellFactory());
removeColumn.setCellFactory(column -> {
- TableCell tableCell = new TableCell<>() {
+ TableCell tableCell = new TableCell<>() {
@Override
protected void updateItem(Button item, boolean empty) {
super.updateItem(item, empty);
@@ -230,17 +227,20 @@ protected void updateItem(Button item, boolean empty) {
}
}
};
+
tableCell.setAlignment(Pos.CENTER);
+
return tableCell;
});
}
private void setDoubleClickHandler() {
catalogTable.setRowFactory(tv -> {
- TableRow row = new TableRow<>();
+ TableRow row = new TableRow<>();
+
row.setOnMouseClicked(event -> {
if (event.getClickCount() == 2 && (!row.isEmpty())) {
- String url = row.getItem().uri().toString();
+ String url = row.getItem().getUri().toString();
UiUtils.openLinkInWebBrowser(url).exceptionally(error -> {
logger.error("Error when opening {} in browser", url, error);
@@ -258,6 +258,7 @@ private void setDoubleClickHandler() {
});
}
});
+
return row;
});
}
@@ -271,18 +272,18 @@ private void setRightClickHandler() {
MenuItem copyItem = new MenuItem(resources.getString("CatalogManager.copyUrl"));
copyItem.setOnAction(ignored -> {
ClipboardContent content = new ClipboardContent();
- content.putString(catalogTable.getSelectionModel().getSelectedItem().uri().toString());
+ content.putString(catalogTable.getSelectionModel().getSelectedItem().getUri().toString());
Clipboard.getSystemClipboard().setContent(content);
});
menu.getItems().add(copyItem);
MenuItem removeItem = new MenuItem(resources.getString("CatalogManager.remove"));
removeItem.setOnAction(ignored -> {
- UiUtils.promptExtensionDirectory(extensionCatalogManager.getExtensionDirectoryPath(), onInvalidExtensionDirectory);
+ UiUtils.promptExtensionDirectory(extensionCatalogManager.getExtensionDirectory(), onInvalidExtensionDirectory);
List nonDeletableCatalogs = catalogTable.getSelectionModel().getSelectedItems().stream()
- .filter(savedCatalog -> !savedCatalog.deletable())
- .map(SavedCatalog::name)
+ .filter(catalog -> !catalog.isDeletable())
+ .map(Catalog::getName)
.toList();
if (!nonDeletableCatalogs.isEmpty()) {
displayErrorMessage(
@@ -316,53 +317,39 @@ private void displayErrorMessage(String title, String text) {
.show();
}
- private void deleteCatalogs(List catalogs) {
- List catalogsToDelete = catalogs.stream()
- .filter(SavedCatalog::deletable)
+ private void deleteCatalogs(List catalogs) {
+ List catalogsToDelete = catalogs.stream()
+ .filter(Catalog::isDeletable)
.toList();
if (catalogsToDelete.isEmpty()) {
return;
}
- boolean deleteExtensions = Dialogs.showConfirmDialog(
- resources.getString("CatalogManager.deleteCatalog"),
- MessageFormat.format(
- resources.getString("CatalogManager.deleteExtensions"),
- catalogsToDelete.stream()
- .map(savedCatalog -> {
- try {
- return String.format("\"%s\"", extensionCatalogManager.getCatalogDirectory(savedCatalog));
- } catch (InvalidPathException | SecurityException | NullPointerException e) {
- logger.error("Cannot retrieve path of {}", savedCatalog.name(), e);
- return null;
- }
- })
- .filter(Objects::nonNull)
- .collect(Collectors.joining("\n"))
- )
- );
+ List errors = new ArrayList<>();
+ for (Catalog catalog: catalogsToDelete) {
+ try {
+ extensionCatalogManager.removeCatalog(catalog);
+ } catch (Exception e) {
+ logger.error("Error when removing {}", catalog, e);
- try {
- extensionCatalogManager.removeCatalogs(
- catalogsToDelete,
- deleteExtensions
- );
- } catch (IOException | SecurityException | NullPointerException e) {
- logger.error("Error when removing {}", catalogsToDelete.stream().map(SavedCatalog::name).toList(), e);
+ errors.add(e);
+ }
+ }
+ if (!errors.isEmpty()) {
displayErrorMessage(
resources.getString("CatalogManager.deleteCatalog"),
MessageFormat.format(
resources.getString("CatalogManager.cannotRemoveSelectedCatalogs"),
- e.getLocalizedMessage()
+ errors.stream().map(Throwable::getLocalizedMessage).collect(Collectors.joining("\n"))
)
);
}
}
- private static Callback, TableCell> getStringCellFactory() {
+ private static Callback, TableCell> getStringCellFactory() {
return column -> {
- TableCell tableCell = new TableCell<>() {
+ TableCell tableCell = new TableCell<>() {
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
@@ -376,7 +363,9 @@ protected void updateItem(String item, boolean empty) {
}
}
};
+
tableCell.setAlignment(Pos.CENTER_LEFT);
+
return tableCell;
};
}
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/ExtensionCatalogModel.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/ExtensionCatalogModel.java
index 2df24ae..b3300e1 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/ExtensionCatalogModel.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/ExtensionCatalogModel.java
@@ -1,33 +1,28 @@
package qupath.ext.extensionmanager.gui;
-import javafx.beans.property.ObjectProperty;
-import javafx.beans.property.ReadOnlyObjectProperty;
-import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import qupath.ext.extensionmanager.core.ExtensionCatalogManager;
-import qupath.ext.extensionmanager.core.catalog.Extension;
-import qupath.ext.extensionmanager.core.savedentities.InstalledExtension;
-import qupath.ext.extensionmanager.core.savedentities.SavedCatalog;
+import qupath.ext.extensionmanager.core.catalog.Catalog;
import java.nio.file.Path;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
/**
- * The model of the UI elements of this project. This is basically a wrapper around {@link ExtensionCatalogManager}
- * where listenable properties are propagated to the JavaFX Application Thread.
+ * The model of the UI elements of this project. This is basically a wrapper around {@link ExtensionCatalogManager} where
+ * listenable properties are propagated to the JavaFX Application Thread.
+ *
+ * An instance of this class must be {@link #close() closed} once no longer used.
*/
-public class ExtensionCatalogModel {
+class ExtensionCatalogModel implements AutoCloseable {
- private final ObservableList savedIndices = FXCollections.observableArrayList();
- private final ObservableList catalogsImmutable = FXCollections.unmodifiableObservableList(savedIndices);
- private static final Map>> installedExtensions = new HashMap<>();
+ private final ObservableList catalogs = FXCollections.observableArrayList();
+ private final ObservableList catalogsImmutable = FXCollections.unmodifiableObservableList(catalogs);
private final ObservableList manuallyInstalledJars = FXCollections.observableArrayList();
private final ObservableList manuallyInstalledJarsImmutable = FXCollections.unmodifiableObservableList(manuallyInstalledJars);
private final ExtensionCatalogManager extensionCatalogManager;
- private record CatalogExtension(SavedCatalog savedCatalog, Extension extension) {}
+ private final ListChangeListener super Catalog> catalogsListener;
+ private final ListChangeListener super Path> manuallyInstalledJarsListener;
/**
* Create the model.
@@ -37,44 +32,27 @@ private record CatalogExtension(SavedCatalog savedCatalog, Extension extension)
public ExtensionCatalogModel(ExtensionCatalogManager extensionCatalogManager) {
this.extensionCatalogManager = extensionCatalogManager;
- UiUtils.bindListInUIThread(savedIndices, extensionCatalogManager.getCatalogs());
- UiUtils.bindListInUIThread(manuallyInstalledJars, extensionCatalogManager.getManuallyInstalledJars());
+ this.catalogsListener = UiUtils.bindListInUIThread(catalogs, extensionCatalogManager.getCatalogs());
+ this.manuallyInstalledJarsListener = UiUtils.bindListInUIThread(manuallyInstalledJars, extensionCatalogManager.getManuallyInstalledJars());
}
- /**
- * @return a read-only observable list of all saved catalogs. This list will always be updated from the JavaFX
- * Application Thread
- */
- public ObservableList getCatalogs() {
- return catalogsImmutable;
+ @Override
+ public void close() {
+ extensionCatalogManager.getCatalogs().removeListener(catalogsListener);
+ extensionCatalogManager.getManuallyInstalledJars().removeListener(manuallyInstalledJarsListener);
}
/**
- * Get installed information of an extension.
- *
- * @param savedCatalog the catalog owning the extension
- * @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
- * will always be updated from the JavaFX Application Thread
+ * @return a read-only observable list of all catalogs. This list will always be updated from the JavaFX Application
+ * Thread
*/
- public ReadOnlyObjectProperty> getInstalledExtension(SavedCatalog savedCatalog, Extension extension) {
- return installedExtensions.computeIfAbsent(
- new CatalogExtension(savedCatalog, extension),
- e -> {
- SimpleObjectProperty> installedExtension = new SimpleObjectProperty<>(Optional.empty());
-
- UiUtils.bindPropertyInUIThread(installedExtension, extensionCatalogManager.getInstalledExtension(e.savedCatalog, e.extension));
-
- return installedExtension;
- }
- );
+ public ObservableList getCatalogs() {
+ return catalogsImmutable;
}
/**
- * @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 will always be updated from the
- * JavaFX Application 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 will always be updated from the JavaFX Application Thread
*/
public ObservableList getManuallyInstalledJars() {
return manuallyInstalledJarsImmutable;
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/ExtensionManager.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/ExtensionManager.java
index c21745c..8427cd2 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/ExtensionManager.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/ExtensionManager.java
@@ -1,10 +1,11 @@
package qupath.ext.extensionmanager.gui;
import javafx.beans.binding.Bindings;
-import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
+import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.TitledPane;
import javafx.scene.layout.VBox;
@@ -12,7 +13,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import qupath.ext.extensionmanager.core.ExtensionCatalogManager;
-import qupath.ext.extensionmanager.core.savedentities.SavedCatalog;
+import qupath.ext.extensionmanager.core.catalog.Catalog;
import qupath.ext.extensionmanager.gui.catalog.CatalogPane;
import qupath.fx.dialogs.Dialogs;
@@ -34,8 +35,10 @@
/**
* A window that displays information and controls regarding catalogs and their extensions.
+ *
+ * An instance of this class must be {@link #close() closed} once no longer used.
*/
-public class ExtensionManager extends Stage {
+public class ExtensionManager extends Stage implements AutoCloseable {
private static final Logger logger = LoggerFactory.getLogger(ExtensionManager.class);
private static final ResourceBundle resources = UiUtils.getResources();
@@ -57,12 +60,12 @@ public class ExtensionManager extends Stage {
*
* @param extensionCatalogManager the extension catalog manager this window should use
* @param onInvalidExtensionDirectory a function that will be called if an operation needs to access the extension
- * directory (see {@link ExtensionCatalogManager#getExtensionDirectoryPath()})
- * but this directory is currently invalid. It lets the possibility to the user to
- * define and create a valid directory before performing the operation (which would
- * fail if the directory is invalid). This function is guaranteed to be called from
- * the JavaFX Application Thread
- * @throws IOException when an error occurs while creating the container
+ * directory (see {@link ExtensionCatalogManager#getExtensionDirectory()}) but this
+ * directory is currently invalid. It lets the possibility to the user to define
+ * and create a valid directory before performing the operation (which would fail
+ * if the directory is invalid). This function is guaranteed to be called from the
+ * JavaFX Application Thread
+ * @throws IOException if an error occurs while creating the window
*/
public ExtensionManager(
ExtensionCatalogManager extensionCatalogManager,
@@ -74,10 +77,10 @@ public ExtensionManager(
UiUtils.loadFXML(this, ExtensionManager.class.getResource("extension_manager.fxml"));
- UiUtils.promptExtensionDirectory(extensionCatalogManager.getExtensionDirectoryPath(), onInvalidExtensionDirectory);
+ UiUtils.promptExtensionDirectory(extensionCatalogManager.getExtensionDirectory(), onInvalidExtensionDirectory);
setCatalogs();
- model.getCatalogs().addListener((ListChangeListener super SavedCatalog>) change ->
+ model.getCatalogs().addListener((ListChangeListener super Catalog>) change ->
setCatalogs()
);
@@ -96,26 +99,39 @@ public ExtensionManager(
noCatalogOrExtension.managedProperty().bind(noCatalogOrExtension.visibleProperty());
}
+ @Override
+ public void close() {
+ super.close();
+
+ for (Node child: catalogs.getChildren()) {
+ if (child instanceof CatalogPane catalogPane) {
+ catalogPane.close();
+ }
+ }
+ model.close();
+ }
+
/**
- * Copy the provided files to the provided extension directory. Some error dialogs will be shown to
- * the user if some errors occurs.
- * If at least a destination file already exists, the user is prompted for confirmation.
- * No confirmation dialog is prompted on success.
+ * Copy the provided files to the provided extension directory. Some error dialogs will be shown to the user if some
+ * errors occurs.
+ *
+ * If at least a destination file already exists, the user is prompted for confirmation. No confirmation dialog is
+ * prompted on success.
*
* @param filesToCopy the files to copy. No check is performed on those files
* @param extensionDirectoryPath the path to the extension directory
- * @param onInvalidExtensionDirectory a function that will be called if the provided extension
- * directory is invalid. It lets the possibility to the user to
- * define and create a valid directory before copying the files
+ * @param onInvalidExtensionDirectory a function that will be called if the provided extension directory is invalid.
+ * It lets the possibility to the user to define and create a valid directory before
+ * copying the files
*/
public static void promptToCopyFilesToExtensionDirectory(
List filesToCopy,
- ReadOnlyObjectProperty extensionDirectoryPath,
+ ObservableValue extensionDirectoryPath,
Runnable onInvalidExtensionDirectory
) {
UiUtils.promptExtensionDirectory(extensionDirectoryPath, onInvalidExtensionDirectory);
- Path extensionFolder = extensionDirectoryPath.get();
+ Path extensionFolder = extensionDirectoryPath.getValue();
if (extensionFolder == null) {
Dialogs.showErrorMessage(
resources.getString("ExtensionManager.error"),
@@ -142,17 +158,10 @@ public static void promptToCopyFilesToExtensionDirectory(
}
List destinationFilesAlreadyExisting = sourceToDestinationFiles.values().stream()
- .filter(file -> {
- try {
- return file.exists();
- } catch (SecurityException e) {
- logger.debug("Cannot check if {} exists", file, e);
- return false;
- }
- })
+ .filter(File::exists)
.toList();
if (!destinationFilesAlreadyExisting.isEmpty()) {
- var confirmation = Dialogs.showConfirmDialog(
+ boolean confirmation = Dialogs.showConfirmDialog(
resources.getString("ExtensionManager.copyFiles"),
MessageFormat.format(
resources.getString("ExtensionManager.alreadyExist"),
@@ -192,9 +201,9 @@ public static void promptToCopyFilesToExtensionDirectory(
@FXML
private void onOpenExtensionDirectory(ActionEvent ignored) {
- UiUtils.promptExtensionDirectory(extensionCatalogManager.getExtensionDirectoryPath(), onInvalidExtensionDirectory);
+ UiUtils.promptExtensionDirectory(extensionCatalogManager.getExtensionDirectory(), onInvalidExtensionDirectory);
- Path extensionDirectory = extensionCatalogManager.getExtensionDirectoryPath().get();
+ Path extensionDirectory = extensionCatalogManager.getExtensionDirectory().getValue();
if (extensionDirectory == null) {
Dialogs.showErrorMessage(
resources.getString("ExtensionManager.error"),
@@ -221,7 +230,7 @@ private void onOpenExtensionDirectory(ActionEvent ignored) {
@FXML
private void onManageCatalogsClicked(ActionEvent ignored) {
- UiUtils.promptExtensionDirectory(extensionCatalogManager.getExtensionDirectoryPath(), onInvalidExtensionDirectory);
+ UiUtils.promptExtensionDirectory(extensionCatalogManager.getExtensionDirectory(), onInvalidExtensionDirectory);
if (catalogManager == null) {
try {
@@ -240,10 +249,16 @@ private void onManageCatalogsClicked(ActionEvent ignored) {
}
private void setCatalogs() {
+ for (Node child: catalogs.getChildren()) {
+ if (child instanceof CatalogPane catalogPane) {
+ catalogPane.close();
+ }
+ }
+
catalogs.getChildren().setAll(model.getCatalogs().stream()
.map(catalog -> {
try {
- return new CatalogPane(extensionCatalogManager, catalog, model, onInvalidExtensionDirectory);
+ return new CatalogPane(extensionCatalogManager, catalog, onInvalidExtensionDirectory);
} catch (IOException e) {
logger.error("Error while creating catalog pane", e);
return null;
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/ManuallyInstalledExtensionLine.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/ManuallyInstalledExtensionLine.java
index 12a7daf..9391622 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/ManuallyInstalledExtensionLine.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/ManuallyInstalledExtensionLine.java
@@ -17,8 +17,7 @@
import java.util.ResourceBundle;
/**
- * A container displaying the name of a manually installed extension as well as a
- * button to delete it.
+ * A container displaying the name of a manually installed extension as well as a button to delete it.
*/
class ManuallyInstalledExtensionLine extends HBox {
@@ -34,7 +33,7 @@ class ManuallyInstalledExtensionLine extends HBox {
* Create the container.
*
* @param jarPath the path of the manually installed extension to display
- * @throws IOException when an error occurs while creating the container
+ * @throws IOException if an error occurs while creating the container
*/
public ManuallyInstalledExtensionLine(Path jarPath) throws IOException {
this.jarPath = jarPath;
@@ -51,14 +50,14 @@ public ManuallyInstalledExtensionLine(Path jarPath) throws IOException {
@FXML
private void onDeleteClicked(ActionEvent ignored) {
- var confirmation = Dialogs.showConfirmDialog(
+ boolean confirmation = Dialogs.showConfirmDialog(
resources.getString("ManuallyInstalledExtensionLine.deleteExtension"),
MessageFormat.format(
resources.getString("ManuallyInstalledExtensionLine.remove"),
jarPath.getFileName()
)
);
- if(!confirmation) {
+ if (!confirmation) {
return;
}
@@ -72,7 +71,7 @@ private void onDeleteClicked(ActionEvent ignored) {
jarPath.getFileName()
)
);
- } catch (IOException | SecurityException e) {
+ } catch (IOException e) {
logger.error("Cannot delete extension located at {}", jarPath, e);
Dialogs.showErrorMessage(
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/ProgressWindow.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/ProgressWindow.java
index 53ce341..f2ce167 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/ProgressWindow.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/ProgressWindow.java
@@ -11,6 +11,7 @@
/**
* A window that displays the progress of a long-running and cancellable operation.
+ *
* It is modal to its owning window.
*/
public class ProgressWindow extends Stage {
@@ -25,9 +26,9 @@ public class ProgressWindow extends Stage {
* Create the window.
*
* @param label a text describing the operation
- * @param onCancelClicked a function that will be called when the user cancel the operation. This window is
- * already automatically closed when this happens
- * @throws IOException when an error occurs while creating the window
+ * @param onCancelClicked a function that will be called when the user cancel the operation. This window is automatically
+ * closed when this happens
+ * @throws IOException if an error occurs while creating the window
*/
public ProgressWindow(String label, Runnable onCancelClicked) throws IOException {
this.onCancelClicked = onCancelClicked;
@@ -48,8 +49,7 @@ private void onCancelClicked(ActionEvent ignored) {
/**
* Set the progress displayed by the window.
*
- * @param progress a number between 0 and 1, where 0 means the beginning and 1 the end of
- * the operation
+ * @param progress a number between 0 and 1, where 0 means the beginning and 1 the end of the operation
*/
public void setProgress(float progress) {
progressBar.setProgress(progress);
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/UiUtils.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/UiUtils.java
index 9cf7be6..edc5c8f 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/UiUtils.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/UiUtils.java
@@ -1,7 +1,7 @@
package qupath.ext.extensionmanager.gui;
import javafx.application.Platform;
-import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WritableValue;
import javafx.collections.ListChangeListener;
@@ -11,8 +11,6 @@
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.glyphfont.GlyphFont;
import org.controlsfx.glyphfont.GlyphFontRegistry;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import java.awt.Desktop;
import java.io.IOException;
@@ -21,6 +19,7 @@
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.List;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
@@ -29,7 +28,6 @@
*/
public class UiUtils {
- private static final Logger logger = LoggerFactory.getLogger(UiUtils.class);
private static final ResourceBundle resources = ResourceBundle.getBundle("qupath.ext.extensionmanager.strings");
private static final GlyphFont fontAwesome = GlyphFontRegistry.font("FontAwesome");
@@ -93,12 +91,10 @@ public static String getClassName(CssClass cssClass) {
}
/**
- * Open the provided link in a web browser. This won't do anything if
- * browsing is not supported by the computer.
+ * Open the provided link in a web browser. This won't do anything if browsing is not supported by the computer.
*
* @param url the link to open
- * @return a CompletableFuture that will complete exceptionally if an error occurs while
- * browsing the provided link
+ * @return a CompletableFuture that will complete exceptionally if an error occurs while browsing the provided link
*/
public static CompletableFuture openLinkInWebBrowser(String url) {
return CompletableFuture.runAsync(() -> {
@@ -115,12 +111,11 @@ public static CompletableFuture openLinkInWebBrowser(String url) {
}
/**
- * Open the provided directory with the platform's file explorer. This won't do anything if
- * browsing files is not supported by the computer.
+ * Open the provided directory with the platform's file explorer. This won't do anything if browsing files is not
+ * supported by the computer.
*
* @param directory the path to the directory to browse
- * @return a CompletableFuture that will complete exceptionally if an error occurs while
- * browsing the provided directory
+ * @return a CompletableFuture that will complete exceptionally if an error occurs while browsing the provided directory
*/
public static CompletableFuture openFolderInFileExplorer(Path directory) {
return CompletableFuture.runAsync(() -> {
@@ -137,24 +132,20 @@ public static CompletableFuture openFolderInFileExplorer(Path directory) {
}
/**
- * Run the provided Runnable if the provided extension directory property doesn't point to a valid directory.
- * The provided Runnable can be for example used to prompt the user for a new extension directory path.
+ * Run the provided Runnable if the provided extension directory property doesn't point to a valid directory. The
+ * provided Runnable can be for example used to prompt the user for a new extension directory path.
*
* @param extensionDirectoryProperty the property to check
* @param onInvalidExtensionDirectory the Runnable to call if the provided path is not a valid directory
*/
public static void promptExtensionDirectory(
- ReadOnlyObjectProperty extensionDirectoryProperty,
+ ObservableValue extensionDirectoryProperty,
Runnable onInvalidExtensionDirectory
) {
- Path extensionDirectory = extensionDirectoryProperty.get();
+ Path extensionDirectory = extensionDirectoryProperty.getValue();
- try {
- if (extensionDirectory == null || !Files.isDirectory(extensionDirectory)) {
- onInvalidExtensionDirectory.run();
- }
- } catch (SecurityException e) {
- logger.debug("Cannot check if {} is invalid", extensionDirectory, e);
+ if (extensionDirectory == null || !Files.isDirectory(extensionDirectory)) {
+ onInvalidExtensionDirectory.run();
}
}
@@ -169,17 +160,18 @@ public static Glyph getFontAwesomeIcon(FontAwesome.Glyph glyph) {
}
/**
- * Propagates changes made to an observable list to another observable list.
- * The listening list is updated in the JavaFX Application Thread.
+ * Propagates changes made to an observable list to another observable list. The listening list is updated in the
+ * JavaFX Application Thread.
*
* @param listToUpdate the list to update
* @param listToListen the list to listen
+ * @return the listener that was added to the list to listen, so that it can be removed when needed
* @param the type of the elements of the lists
*/
- public static void bindListInUIThread(ObservableList listToUpdate, ObservableList listToListen) {
+ public static ListChangeListener super T> bindListInUIThread(List listToUpdate, ObservableList listToListen) {
listToUpdate.addAll(listToListen);
- listToListen.addListener((ListChangeListener super T>) change -> Platform.runLater(() -> {
+ ListChangeListener super T> listener = change -> Platform.runLater(() -> {
if (Platform.isFxApplicationThread()) {
while (change.next()) {
if (change.wasAdded()) {
@@ -203,26 +195,34 @@ public static void bindListInUIThread(ObservableList listToUpdate, Observ
change.reset();
});
}
- }));
+ });
+ listToListen.addListener(listener);
+
+ return listener;
}
/**
* Propagates changes made to a property to another property.
- * The listening property is updated in the JavaFX Application Thread.
+ *
+ * The listening property is updated in the UI thread.
*
* @param propertyToUpdate the property to update
* @param propertyToListen the property to listen
+ * @return the listener that was added to the property to listen, so that it can be removed when needed
* @param the type of the property
*/
- public static void bindPropertyInUIThread(WritableValue propertyToUpdate, ObservableValue propertyToListen) {
+ public static ChangeListener super T> bindPropertyInUIThread(WritableValue propertyToUpdate, ObservableValue propertyToListen) {
propertyToUpdate.setValue(propertyToListen.getValue());
- propertyToListen.addListener((p, o, n) -> {
+ ChangeListener super T> listener = (p, o, n) -> {
if (Platform.isFxApplicationThread()) {
propertyToUpdate.setValue(n);
} else {
Platform.runLater(() -> propertyToUpdate.setValue(n));
}
- });
+ };
+ propertyToListen.addListener(listener);
+
+ return listener;
}
}
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/catalog/CatalogPane.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/catalog/CatalogPane.java
index 7c5553b..650b369 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/catalog/CatalogPane.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/catalog/CatalogPane.java
@@ -2,26 +2,31 @@
import javafx.application.Platform;
import javafx.fxml.FXML;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
import javafx.scene.control.TitledPane;
import javafx.scene.layout.VBox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import qupath.ext.extensionmanager.core.ExtensionCatalogManager;
-import qupath.ext.extensionmanager.core.catalog.CatalogFetcher;
-import qupath.ext.extensionmanager.core.savedentities.SavedCatalog;
-import qupath.ext.extensionmanager.gui.ExtensionCatalogModel;
+import qupath.ext.extensionmanager.core.catalog.Catalog;
import qupath.ext.extensionmanager.gui.UiUtils;
import java.io.IOException;
+import java.text.MessageFormat;
import java.util.Objects;
+import java.util.ResourceBundle;
import java.util.stream.IntStream;
/**
* A pane that displays information and controls regarding a catalog and its extensions.
+ *
+ * An instance of this class must be {@link #close() closed} once no longer used.
*/
-public class CatalogPane extends TitledPane {
+public class CatalogPane extends TitledPane implements AutoCloseable {
private static final Logger logger = LoggerFactory.getLogger(CatalogPane.class);
+ private static final ResourceBundle resources = UiUtils.getResources();
@FXML
private VBox extensions;
@@ -29,60 +34,77 @@ public class CatalogPane extends TitledPane {
* Create the pane.
*
* @param extensionCatalogManager the extension catalog manager this pane should use
- * @param savedCatalog the catalog to display
- * @param model the model to use when accessing data
+ * @param catalog the catalog to display
* @param onInvalidExtensionDirectory a function that will be called if an operation needs to access the extension
- * directory (see {@link ExtensionCatalogManager#getExtensionDirectoryPath()})
- * but this directory is currently invalid. It lets the possibility to the user to
- * define and create a valid directory before performing the operation (which would
- * fail if the directory is invalid). This function is guaranteed to be called from
- * the JavaFX Application Thread
- * @throws IOException when an error occurs while loading a FXML file
+ * directory (see {@link ExtensionCatalogManager#getExtensionDirectory()}) but this
+ * directory is currently invalid. It lets the possibility to the user to define
+ * and create a valid directory before performing the operation (which would fail
+ * if the directory is invalid). This function is guaranteed to be called from the
+ * JavaFX Application Thread
+ * @throws IOException if an error occurs while creating the pane
*/
public CatalogPane(
ExtensionCatalogManager extensionCatalogManager,
- SavedCatalog savedCatalog,
- ExtensionCatalogModel model,
+ Catalog catalog,
Runnable onInvalidExtensionDirectory
) throws IOException {
UiUtils.loadFXML(this, CatalogPane.class.getResource("catalog_pane.fxml"));
- setText(savedCatalog.name());
+ setText(catalog.getName());
+
+ catalog.getExtensions().handle((extensions, error) -> {
+ if (error != null) {
+ logger.error("Error when getting extensions of {}", catalog, error);
- CatalogFetcher.getCatalog(savedCatalog.rawUri()).handle((fetchedCatalog, error) -> {
- if (error == null) {
Platform.runLater(() -> {
setExpanded(true);
- extensions.getChildren().addAll(IntStream.range(0, fetchedCatalog.extensions().size())
- .mapToObj(i -> {
- try {
- ExtensionLine extensionLine = new ExtensionLine(
- extensionCatalogManager,
- model,
- savedCatalog,
- fetchedCatalog.extensions().get(i),
- onInvalidExtensionDirectory
- );
+ this.extensions.getChildren().add(new Label(MessageFormat.format(
+ resources.getString("Catalog.CatalogPane.errorFetchingExtensions"),
+ error.getLocalizedMessage()
+ )));
+ });
+
+ return null;
+ }
+
+ Platform.runLater(() -> {
+ setExpanded(true);
- if (i % 2 == 0) {
- extensionLine.getStyleClass().add(UiUtils.getClassName(UiUtils.CssClass.ODD_ROW));
- }
+ this.extensions.getChildren().addAll(IntStream.range(0, extensions.size())
+ .mapToObj(i -> {
+ try {
+ ExtensionLine extensionLine = new ExtensionLine(
+ extensionCatalogManager,
+ catalog,
+ extensions.get(i),
+ onInvalidExtensionDirectory
+ );
- return extensionLine;
- } catch (IOException e) {
- logger.error("Error while creating extension line", e);
- return null;
+ if (i % 2 == 0) {
+ extensionLine.getStyleClass().add(UiUtils.getClassName(UiUtils.CssClass.ODD_ROW));
}
- })
- .filter(Objects::nonNull)
- .toList()
- );
- });
- } else {
- logger.warn("Error when fetching catalog at {}", savedCatalog.rawUri(), error);
- }
+
+ return extensionLine;
+ } catch (IOException e) {
+ logger.error("Error while creating extension line", e);
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .toList()
+ );
+ });
return null;
});
}
+
+ @Override
+ public void close() {
+ for (Node child: extensions.getChildren()) {
+ if (child instanceof ExtensionLine extensionLine) {
+ extensionLine.close();
+ }
+ }
+ }
}
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/catalog/ExtensionDetails.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/catalog/ExtensionDetails.java
index 25ffac8..e3bd4f8 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/catalog/ExtensionDetails.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/catalog/ExtensionDetails.java
@@ -19,6 +19,7 @@
/**
* A window displaying the description and the clickable homepage of an extension.
+ *
* It is modal to its owning window and can be easily closed with shortcuts.
*/
class ExtensionDetails extends Stage {
@@ -37,7 +38,7 @@ class ExtensionDetails extends Stage {
*
* @param extension the extension whose information should be displayed
* @param noAvailableRelease whether no release of this extension can be installed
- * @throws IOException when an error occurs while creating the container
+ * @throws IOException if an error occurs while creating the container
*/
public ExtensionDetails(Extension extension, boolean noAvailableRelease) throws IOException {
UiUtils.loadFXML(this, ExtensionDetails.class.getResource("extension_details.fxml"));
@@ -45,11 +46,11 @@ public ExtensionDetails(Extension extension, boolean noAvailableRelease) throws
FXUtils.addCloseWindowShortcuts(this);
initModality(Modality.WINDOW_MODAL);
- setTitle(extension.name());
+ setTitle(extension.getName());
- description.setText(extension.description());
+ description.setText(extension.getDescription());
- homepage.setText(extension.homepage().toString());
+ homepage.setText(extension.getHomepage().toString());
notCompatible.setVisible(noAvailableRelease);
notCompatible.setManaged(notCompatible.isVisible());
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/catalog/ExtensionLine.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/catalog/ExtensionLine.java
index bca22fd..7b71734 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/catalog/ExtensionLine.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/catalog/ExtensionLine.java
@@ -1,7 +1,7 @@
package qupath.ext.extensionmanager.gui.catalog;
import javafx.beans.binding.Bindings;
-import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
@@ -17,34 +17,34 @@
import org.slf4j.LoggerFactory;
import qupath.ext.extensionmanager.core.ExtensionCatalogManager;
import qupath.ext.extensionmanager.core.Version;
+import qupath.ext.extensionmanager.core.catalog.Catalog;
import qupath.ext.extensionmanager.core.catalog.Extension;
import qupath.ext.extensionmanager.core.catalog.Release;
-import qupath.ext.extensionmanager.core.savedentities.InstalledExtension;
-import qupath.ext.extensionmanager.core.savedentities.SavedCatalog;
-import qupath.ext.extensionmanager.gui.ExtensionCatalogModel;
import qupath.ext.extensionmanager.gui.UiUtils;
import qupath.fx.dialogs.Dialogs;
import java.io.IOException;
-import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
/**
* A container that displays information and controls of an extension.
+ *
+ * An instance of this class must be {@link #close() closed} once no longer used.
*/
-class ExtensionLine extends HBox {
+class ExtensionLine extends HBox implements AutoCloseable {
private static final Logger logger = LoggerFactory.getLogger(ExtensionLine.class);
private static final ResourceBundle resources = UiUtils.getResources();
private final ExtensionCatalogManager extensionCatalogManager;
- private final ExtensionCatalogModel model;
- private final SavedCatalog savedCatalog;
+ private final Catalog catalog;
private final Extension extension;
private final Runnable onInvalidExtensionDirectory;
+ private final ExtensionModel extensionModel;
@FXML
private Label name;
@FXML
@@ -70,59 +70,54 @@ class ExtensionLine extends HBox {
* Create the container.
*
* @param extensionCatalogManager the extension catalog manager this window should use
- * @param model the model to use when accessing data
- * @param savedCatalog the catalog owning the extension to display
+ * @param catalog the catalog owning the extension to display
* @param extension the extension to display
* @param onInvalidExtensionDirectory a function that will be called if an operation needs to access the extension
- * directory (see {@link ExtensionCatalogManager#getExtensionDirectoryPath()})
- * but this directory is currently invalid. It lets the possibility to the user to
- * define and create a valid directory before performing the operation (which would
- * fail if the directory is invalid). This function is guaranteed to be called from
- * the JavaFX Application Thread
+ * directory (see {@link ExtensionCatalogManager#getExtensionDirectory()}) but this
+ * directory is currently invalid. It lets the possibility to the user to define
+ * and create a valid directory before performing the operation (which would fail
+ * if the directory is invalid). This function is guaranteed to be called from the
+ * JavaFX Application Thread
* @throws IOException when an error occurs while creating the container
*/
public ExtensionLine(
ExtensionCatalogManager extensionCatalogManager,
- ExtensionCatalogModel model,
- SavedCatalog savedCatalog,
+ Catalog catalog,
Extension extension,
Runnable onInvalidExtensionDirectory
) throws IOException {
this.extensionCatalogManager = extensionCatalogManager;
- this.model = model;
- this.savedCatalog = savedCatalog;
+ this.catalog = catalog;
this.extension = extension;
this.onInvalidExtensionDirectory = onInvalidExtensionDirectory;
+ this.extensionModel = new ExtensionModel(extension);
UiUtils.loadFXML(this, ExtensionLine.class.getResource("extension_line.fxml"));
- ReadOnlyObjectProperty> installedExtension = model.getInstalledExtension(
- savedCatalog,
- extension
- );
- if (installedExtension.get().isPresent()) {
- name.setText(String.format("%s %s", extension.name(), installedExtension.get().get().releaseName()));
+ ObservableValue> installedRelease = extensionModel.getInstalledRelease();
+ if (installedRelease.getValue().isPresent()) {
+ name.setText(String.format("%s %s", extension.getName(), installedRelease.getValue().get().getVersion().toString()));
} else {
- name.setText(extension.name());
+ name.setText(extension.getName());
}
- installedExtension.addListener((p, o, n) -> {
+ installedRelease.addListener((p, o, n) -> {
if (n.isPresent()) {
- name.setText(String.format("%s %s", extension.name(), n.get().releaseName()));
+ name.setText(String.format("%s %s", extension.getName(), n.get().getVersion().toString()));
} else {
- name.setText(extension.name());
+ name.setText(extension.getName());
}
});
Glyph star = UiUtils.getFontAwesomeIcon(FontAwesome.Glyph.STAR);
- star.getStyleClass().add(extension.starred() ?
+ star.getStyleClass().add(extension.isStarred() ?
UiUtils.getClassName(UiUtils.CssClass.STAR) :
UiUtils.getClassName(UiUtils.CssClass.INVISIBLE)
);
name.setContentDisplay(ContentDisplay.LEFT);
name.setGraphic(star);
- StringBuilder descriptionText = new StringBuilder(extension.description());
- if (extension.starred()) {
+ StringBuilder descriptionText = new StringBuilder(extension.getDescription());
+ if (extension.isStarred()) {
descriptionText.append("\n");
descriptionText.append(resources.getString("Catalog.ExtensionLine.starredExtension"));
}
@@ -135,16 +130,17 @@ public ExtensionLine(
updateAvailable.visibleProperty().bind(Bindings.createBooleanBinding(
() -> {
- if (installedExtension.get().isEmpty()) {
+ if (installedRelease.getValue().isEmpty()) {
return false;
}
- Version installedVersion = new Version(installedExtension.get().get().releaseName());
+ Version installedVersion = installedRelease.getValue().get().getVersion();
- Optional availableVersion = extension.releases().stream()
- .filter(release -> release.versionRange().isCompatible(extensionCatalogManager.getVersion()))
- .map(Release::name)
- .filter(named -> new Version(named).compareTo(installedVersion) > 0)
+ Optional availableVersion = extension.getReleases().stream()
+ .filter(release -> release.isCompatible(extensionCatalogManager.getVersion()))
+ .filter(release -> release.getVersion().compareTo(installedVersion) > 0)
+ .map(release -> release.getVersion().toString())
.findAny();
+
availableVersion.ifPresent(version -> updateAvailableTooltip.setText(MessageFormat.format(
resources.getString("Catalog.ExtensionLine.updateAvailableDetails"),
version
@@ -152,7 +148,7 @@ public ExtensionLine(
return availableVersion.isPresent();
},
- installedExtension
+ installedRelease
));
updateAvailable.managedProperty().bind(updateAvailable.visibleProperty());
@@ -167,16 +163,16 @@ public ExtensionLine(
info.getGraphic().getStyleClass().add("other-buttons");
add.visibleProperty().bind(Bindings.createBooleanBinding(
- () -> installedExtension.get().isEmpty(),
- installedExtension
+ () -> installedRelease.getValue().isEmpty(),
+ installedRelease
));
settings.visibleProperty().bind(Bindings.createBooleanBinding(
- () -> installedExtension.get().isPresent(),
- installedExtension
+ () -> installedRelease.getValue().isPresent(),
+ installedRelease
));
delete.visibleProperty().bind(Bindings.createBooleanBinding(
- () -> installedExtension.get().isPresent(),
- installedExtension
+ () -> installedRelease.getValue().isPresent(),
+ installedRelease
));
add.managedProperty().bind(add.visibleProperty());
@@ -185,7 +181,12 @@ public ExtensionLine(
add.setDisable(noAvailableRelease());
- infoTooltip.setText(String.format("%s\n%s", extension.description(), extension.homepage()));
+ infoTooltip.setText(String.format("%s\n%s", extension.getDescription(), extension.getHomepage()));
+ }
+
+ @Override
+ public void close() {
+ extensionModel.close();
}
@FXML
@@ -198,9 +199,8 @@ private void onAddClicked(ActionEvent ignored) {
try {
ExtensionModificationWindow extensionModificationWindow = new ExtensionModificationWindow(
extensionCatalogManager,
- savedCatalog,
+ catalog,
extension,
- model.getInstalledExtension(savedCatalog, extension).get().orElse(null),
onInvalidExtensionDirectory
);
extensionModificationWindow.initOwner(getScene().getWindow());
@@ -215,9 +215,8 @@ private void onSettingsClicked(ActionEvent ignored) {
try {
ExtensionModificationWindow extensionModificationWindow = new ExtensionModificationWindow(
extensionCatalogManager,
- savedCatalog,
+ catalog,
extension,
- model.getInstalledExtension(savedCatalog, extension).get().orElse(null),
onInvalidExtensionDirectory
);
extensionModificationWindow.initOwner(getScene().getWindow());
@@ -231,8 +230,8 @@ private void onSettingsClicked(ActionEvent ignored) {
private void onDeleteClicked(ActionEvent ignored) {
Path directoryToDelete;
try {
- directoryToDelete = extensionCatalogManager.getExtensionDirectory(savedCatalog, extension);
- } catch (IOException | InvalidPathException | SecurityException | NullPointerException e) {
+ directoryToDelete = extensionCatalogManager.getExtensionDirectory(catalog.getName(), extension.getName());
+ } catch (Exception e) {
logger.error("Cannot retrieve directory containing the files of the extension to delete", e);
Dialogs.showErrorMessage(
@@ -245,11 +244,11 @@ private void onDeleteClicked(ActionEvent ignored) {
return;
}
- var confirmation = Dialogs.showConfirmDialog(
+ boolean confirmation = Dialogs.showConfirmDialog(
resources.getString("Catalog.ExtensionLine.removeExtension"),
MessageFormat.format(
resources.getString("Catalog.ExtensionLine.remove"),
- extension.name(),
+ extension.getName(),
directoryToDelete
)
);
@@ -259,8 +258,8 @@ private void onDeleteClicked(ActionEvent ignored) {
CompletableFuture.runAsync(() -> {
try {
- extensionCatalogManager.removeExtension(savedCatalog, extension);
- } catch (IOException e) {
+ extensionCatalogManager.removeExtension(catalog, extension);
+ } catch (IOException | ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
}).handle((v, error) -> {
@@ -269,7 +268,7 @@ private void onDeleteClicked(ActionEvent ignored) {
resources.getString("Catalog.ExtensionLine.extensionManager"),
MessageFormat.format(
resources.getString("Catalog.ExtensionLine.removed"),
- extension.name()
+ extension.getName()
)
);
} else {
@@ -299,7 +298,6 @@ private void onInfoClicked(ActionEvent ignored) {
}
private boolean noAvailableRelease() {
- return extension.releases().stream()
- .noneMatch(release -> release.versionRange().isCompatible(extensionCatalogManager.getVersion()));
+ return extension.getReleases().stream().noneMatch(release -> release.isCompatible(extensionCatalogManager.getVersion()));
}
}
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/catalog/ExtensionModel.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/catalog/ExtensionModel.java
new file mode 100644
index 0000000..6bcf8e5
--- /dev/null
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/catalog/ExtensionModel.java
@@ -0,0 +1,38 @@
+package qupath.ext.extensionmanager.gui.catalog;
+
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import qupath.ext.extensionmanager.core.catalog.Extension;
+import qupath.ext.extensionmanager.core.catalog.Release;
+import qupath.ext.extensionmanager.gui.UiUtils;
+
+import java.util.Optional;
+
+/**
+ * The model of UI elements representing an {@link Extension}. This is basically a wrapper around an {@link Extension}
+ * where listenable properties are propagated to the JavaFX Application Thread.
+ *
+ * An instance of this class must be {@link #close() closed} once no longer used.
+ */
+class ExtensionModel implements AutoCloseable {
+
+ private final ObjectProperty> installedRelease = new SimpleObjectProperty<>();
+ private final Extension extension;
+ private final ChangeListener super Optional> installedReleaseListener;
+
+ public ExtensionModel(Extension extension) {
+ this.extension = extension;
+ this.installedReleaseListener = UiUtils.bindPropertyInUIThread(installedRelease, extension.getInstalledRelease());
+ }
+
+ @Override
+ public void close() {
+ extension.getInstalledRelease().removeListener(installedReleaseListener);
+ }
+
+ public ObservableValue> getInstalledRelease() {
+ return installedRelease;
+ }
+}
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/catalog/ExtensionModificationWindow.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/catalog/ExtensionModificationWindow.java
index 8f40d34..4cbaae1 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/catalog/ExtensionModificationWindow.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/catalog/ExtensionModificationWindow.java
@@ -21,10 +21,9 @@
import org.slf4j.LoggerFactory;
import qupath.ext.extensionmanager.core.ExtensionCatalogManager;
import qupath.ext.extensionmanager.core.Version;
+import qupath.ext.extensionmanager.core.catalog.Catalog;
import qupath.ext.extensionmanager.core.catalog.Extension;
import qupath.ext.extensionmanager.core.catalog.Release;
-import qupath.ext.extensionmanager.core.savedentities.InstalledExtension;
-import qupath.ext.extensionmanager.core.savedentities.SavedCatalog;
import qupath.ext.extensionmanager.core.tools.FileTools;
import qupath.ext.extensionmanager.gui.ProgressWindow;
import qupath.ext.extensionmanager.gui.UiUtils;
@@ -32,11 +31,11 @@
import java.io.IOException;
import java.net.URI;
-import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.MessageFormat;
import java.util.Objects;
+import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -45,6 +44,7 @@
/**
* A window that provide choices to install or modify the installation of an extension.
+ *
* It is modal to its owning window.
*/
class ExtensionModificationWindow extends Stage {
@@ -52,9 +52,8 @@ class ExtensionModificationWindow extends Stage {
private static final Logger logger = LoggerFactory.getLogger(ExtensionModificationWindow.class);
private static final ResourceBundle resources = UiUtils.getResources();
private final ExtensionCatalogManager extensionCatalogManager;
- private final SavedCatalog savedCatalog;
+ private final Catalog catalog;
private final Extension extension;
- private final InstalledExtension installedExtension;
private final Runnable onInvalidExtensionDirectory;
@FXML
private Label name;
@@ -79,62 +78,60 @@ class ExtensionModificationWindow extends Stage {
* Create the window.
*
* @param extensionCatalogManager the extension catalog manager this window should use
- * @param savedCatalog the catalog owning the extension to modify
+ * @param catalog the catalog owning the extension to modify
* @param extension the extension to modify
- * @param installedExtension information on the already installed extension if this window should allow modifying it,
- * or null if the extension is to be installed by this window
* @param onInvalidExtensionDirectory a function that will be called if an operation needs to access the extension
- * directory (see {@link ExtensionCatalogManager#getExtensionDirectoryPath()})
- * but this directory is currently invalid. It lets the possibility to the user to
- * define and create a valid directory before performing the operation (which would
- * fail if the directory is invalid). This function is guaranteed to be called from
- * the JavaFX Application Thread
+ * directory (see {@link ExtensionCatalogManager#getExtensionDirectory()}) but this
+ * directory is currently invalid. It lets the possibility to the user to define
+ * and create a valid directory before performing the operation (which would fail
+ * if the directory is invalid). This function is guaranteed to be called from the
+ * JavaFX Application Thread
* @throws IOException when an error occurs while creating the window
*/
public ExtensionModificationWindow(
ExtensionCatalogManager extensionCatalogManager,
- SavedCatalog savedCatalog,
+ Catalog catalog,
Extension extension,
- InstalledExtension installedExtension,
Runnable onInvalidExtensionDirectory
) throws IOException {
this.extensionCatalogManager = extensionCatalogManager;
- this.savedCatalog = savedCatalog;
+ this.catalog = catalog;
this.extension = extension;
- this.installedExtension = installedExtension;
this.onInvalidExtensionDirectory = onInvalidExtensionDirectory;
UiUtils.loadFXML(this, ExtensionModificationWindow.class.getResource("extension_modification_window.fxml"));
- UiUtils.promptExtensionDirectory(extensionCatalogManager.getExtensionDirectoryPath(), onInvalidExtensionDirectory);
+ UiUtils.promptExtensionDirectory(extensionCatalogManager.getExtensionDirectory(), onInvalidExtensionDirectory);
initModality(Modality.WINDOW_MODAL);
- setTitle(resources.getString(installedExtension == null ?
- "Catalog.ExtensionModificationWindow.installExtension" :
- "Catalog.ExtensionModificationWindow.editExtension"
+ Optional installedRelease = extension.getInstalledRelease().getValue();
+
+ setTitle(resources.getString(installedRelease.isPresent() ?
+ "Catalog.ExtensionModificationWindow.editExtension" :
+ "Catalog.ExtensionModificationWindow.installExtension"
));
- name.setText(extension.name());
+ name.setText(extension.getName());
- if (installedExtension == null) {
- currentVersion.setVisible(false);
- currentVersion.setManaged(false);
- } else {
+ if (installedRelease.isPresent()) {
currentVersion.setText(MessageFormat.format(
resources.getString("Catalog.ExtensionModificationWindow.currentVersion"),
- installedExtension.releaseName()
+ installedRelease.get().getVersion().toString()
));
+ } else {
+ currentVersion.setVisible(false);
+ currentVersion.setManaged(false);
}
- release.getItems().addAll(extension.releases().stream()
- .filter(release -> release.versionRange().isCompatible(extensionCatalogManager.getVersion()))
+ release.getItems().addAll(extension.getReleases().stream()
+ .filter(release -> release.isCompatible(extensionCatalogManager.getVersion()))
.toList()
);
release.setConverter(new StringConverter<>() {
@Override
public String toString(Release object) {
- return object == null ? null : object.name();
+ return object == null ? null : object.getVersion().toString();
}
@Override
@@ -146,26 +143,24 @@ public Release fromString(String string) {
optionalDependencies.visibleProperty().bind(release.getSelectionModel()
.selectedItemProperty()
- .map(release -> !release.optionalDependencyUrls().isEmpty())
+ .map(release -> !release.getOptionalDependencyUrls().isEmpty())
);
optionalDependencies.managedProperty().bind(optionalDependencies.visibleProperty());
- optionalDependencies.setSelected(installedExtension != null && installedExtension.optionalDependenciesInstalled());
+ optionalDependencies.setSelected(extension.areOptionalDependenciesInstalled().get());
filesToDownload.textProperty().bind(Bindings.createStringBinding(
() -> {
try {
return extensionCatalogManager.getDownloadLinks(
- savedCatalog,
- extension,
- new InstalledExtension(
- release.getSelectionModel().getSelectedItem().name(),
- optionalDependencies.isSelected()
- )
+ catalog.getName(),
+ extension.getName(),
+ release.getSelectionModel().getSelectedItem(),
+ optionalDependencies.isSelected()
)
.stream()
.map(URI::toString)
.collect(Collectors.joining("\n"));
- } catch (NullPointerException | SecurityException | IllegalArgumentException e) {
+ } catch (Exception e) {
logger.error("Error while retrieving download links", e);
return resources.getString("Catalog.ExtensionModificationWindow.cannotRetrieveLinks");
}
@@ -175,10 +170,7 @@ public Release fromString(String string) {
));
try {
- Path extensionDirectory = extensionCatalogManager.getExtensionDirectory(
- savedCatalog,
- extension
- );
+ Path extensionDirectory = extensionCatalogManager.getExtensionDirectory(catalog.getName(), extension.getName());
if (FileTools.isDirectoryNotEmpty(extensionDirectory)) {
replaceDirectoryLabel.setText(resources.getString("Catalog.ExtensionModificationWindow.replaceDirectory"));
@@ -187,7 +179,7 @@ public Release fromString(String string) {
replaceDirectory.setVisible(false);
replaceDirectory.setManaged(false);
}
- } catch (IOException | InvalidPathException | SecurityException | NullPointerException e) {
+ } catch (Exception e) {
logger.error("Cannot see if extension directory is not empty", e);
replaceDirectoryLabel.setText(resources.getString("Catalog.ExtensionModificationWindow.extensionDirectoryNotRetrieved"));
@@ -213,11 +205,11 @@ private void onSubmitClicked(ActionEvent ignored) {
}
Release selectedRelease = release.getSelectionModel().getSelectedItem();
- UiUtils.promptExtensionDirectory(extensionCatalogManager.getExtensionDirectoryPath(), onInvalidExtensionDirectory);
+ UiUtils.promptExtensionDirectory(extensionCatalogManager.getExtensionDirectory(), onInvalidExtensionDirectory);
try {
- if (installedExtension == null && isJarAlreadyDownloaded(selectedRelease)) {
- var confirmation = new Dialogs.Builder()
+ if (extension.getInstalledRelease().getValue().isEmpty() && isJarAlreadyDownloaded(selectedRelease)) {
+ boolean confirmation = new Dialogs.Builder()
.alertType(Alert.AlertType.CONFIRMATION)
.buttons(
new ButtonType(resources.getString("Catalog.ExtensionModificationWindow.continueAnyway"), ButtonBar.ButtonData.OK_DONE),
@@ -233,12 +225,12 @@ private void onSubmitClicked(ActionEvent ignored) {
return;
}
}
- } catch (NullPointerException | InvalidPathException e) {
+ } catch (Exception e) {
logger.debug(
"Cannot get file name from {}. Assuming {} with release {} is not already installed",
- selectedRelease.mainUrl(),
- extension.name(),
- selectedRelease.name()
+ selectedRelease.getMainUrl(),
+ extension,
+ selectedRelease
);
}
@@ -247,12 +239,12 @@ private void onSubmitClicked(ActionEvent ignored) {
try {
progressWindow = new ProgressWindow(
MessageFormat.format(
- resources.getString(installedExtension == null ?
- "Catalog.ExtensionModificationWindow.installing" :
- "Catalog.ExtensionModificationWindow.updating"
+ resources.getString(extension.getInstalledRelease().getValue().isPresent() ?
+ "Catalog.ExtensionModificationWindow.updating" :
+ "Catalog.ExtensionModificationWindow.installing"
),
- extension.name(),
- selectedRelease.name()
+ extension.getName(),
+ selectedRelease.getVersion().toString()
),
executor::shutdownNow
);
@@ -267,12 +259,10 @@ private void onSubmitClicked(ActionEvent ignored) {
executor.execute(() -> {
try {
extensionCatalogManager.installOrUpdateExtension(
- savedCatalog,
+ catalog,
extension,
- new InstalledExtension(
- selectedRelease.name(),
- optionalDependencies.isSelected()
- ),
+ selectedRelease,
+ optionalDependencies.isSelected(),
progress -> Platform.runLater(() -> progressWindow.setProgress(progress)),
(step, resource) -> Platform.runLater(() -> progressWindow.setStatus(MessageFormat.format(
resources.getString(switch (step) {
@@ -289,8 +279,8 @@ private void onSubmitClicked(ActionEvent ignored) {
resources.getString("Catalog.ExtensionModificationWindow.extensionManager"),
MessageFormat.format(
resources.getString("Catalog.ExtensionModificationWindow.installed"),
- extension.name(),
- release.getSelectionModel().getSelectedItem().name()
+ extension.getName(),
+ release.getSelectionModel().getSelectedItem().getVersion().toString()
)
);
close();
@@ -299,16 +289,16 @@ private void onSubmitClicked(ActionEvent ignored) {
Platform.runLater(progressWindow::close);
if (e instanceof InterruptedException || e.getCause() instanceof InterruptedException) {
- logger.debug("Installation of {} interrupted", extension.name(), e);
+ logger.debug("Installation of {} interrupted", extension, e);
} else {
- logger.error("Error while installing {}", extension.name(), e);
+ logger.error("Error while installing {}", extension, e);
Platform.runLater(() -> Dialogs.showErrorMessage(
resources.getString("Catalog.ExtensionModificationWindow.installationError"),
MessageFormat.format(
resources.getString("Catalog.ExtensionModificationWindow.notInstalled"),
- extension.name(),
- release.getSelectionModel().getSelectedItem().name(),
+ extension.getName(),
+ release.getSelectionModel().getSelectedItem().getVersion().toString(),
e.getLocalizedMessage()
)
));
@@ -319,25 +309,20 @@ private void onSubmitClicked(ActionEvent ignored) {
}
private String getSubmitText() {
- if (installedExtension == null) {
+ if (extension.getInstalledRelease().getValue().isEmpty()) {
return "Catalog.ExtensionModificationWindow.install";
} else if (release.getSelectionModel().getSelectedItem() == null) {
return "Catalog.ExtensionModificationWindow.update";
} else {
- try {
- Version selectedVersion = new Version(release.getSelectionModel().getSelectedItem().name());
- Version installedVersion = new Version(installedExtension.releaseName());
+ Version selectedVersion = release.getSelectionModel().getSelectedItem().getVersion();
+ Version installedVersion = extension.getInstalledRelease().getValue().get().getVersion();
- if (selectedVersion.compareTo(installedVersion) < 0) {
- return "Catalog.ExtensionModificationWindow.downgrade";
- } else if (selectedVersion.compareTo(installedVersion) > 0) {
- return "Catalog.ExtensionModificationWindow.update";
- } else {
- return "Catalog.ExtensionModificationWindow.reinstall";
- }
- } catch (IllegalArgumentException e) {
- logger.debug("Cannot create version from selected item or installed release", e);
+ if (selectedVersion.compareTo(installedVersion) < 0) {
+ return "Catalog.ExtensionModificationWindow.downgrade";
+ } else if (selectedVersion.compareTo(installedVersion) > 0) {
return "Catalog.ExtensionModificationWindow.update";
+ } else {
+ return "Catalog.ExtensionModificationWindow.reinstall";
}
}
}
@@ -350,6 +335,6 @@ private boolean isJarAlreadyDownloaded(Release release) {
.map(Path::getFileName)
.filter(Objects::nonNull)
.map(Path::toString)
- .anyMatch(path -> path.equalsIgnoreCase(FileTools.getFileNameFromURI(release.mainUrl())));
+ .anyMatch(path -> path.equalsIgnoreCase(FileTools.getFileNameFromURI(release.getMainUrl())));
}
}
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/package-info.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/package-info.java
index f206572..3a810e6 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/package-info.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/package-info.java
@@ -1,5 +1,5 @@
/**
- * This package contains the {@link qupath.ext.extensionmanager.gui.ExtensionManager ExtensionManager}
- * window, as well as other UI elements internally used by the ExtensionManager.
+ * This package contains the {@link qupath.ext.extensionmanager.gui.ExtensionManager ExtensionManager} window, as well as
+ * other UI elements internally used by the ExtensionManager.
*/
package qupath.ext.extensionmanager.gui;
\ No newline at end of file
diff --git a/extensionmanager/src/main/java/qupath/ext/extensionmanager/package-info.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/package-info.java
index 878955b..6b79c3b 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/package-info.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/package-info.java
@@ -3,13 +3,13 @@
*
* -
* The {@link qupath.ext.extensionmanager.core} package contains the
- * {@link qupath.ext.extensionmanager.core.ExtensionCatalogManager ExtensionCatalogManager}.
- * Other classes are only meant to be used internally by the ExtensionCatalogManager.
+ * {@link qupath.ext.extensionmanager.core.ExtensionCatalogManager ExtensionCatalogManager}. Other classes are
+ * only meant to be used internally by the ExtensionCatalogManager.
*
* -
* The {@link qupath.ext.extensionmanager.gui} package contains the
- * {@link qupath.ext.extensionmanager.gui.ExtensionManager ExtensionManager}.
- * Other classes are only meant to be used internally by the ExtensionManager.
+ * {@link qupath.ext.extensionmanager.gui.ExtensionManager ExtensionManager}. Other classes are only meant to be
+ * used internally by the ExtensionManager.
*
*
*/
diff --git a/extensionmanager/src/main/resources/qupath/ext/extensionmanager/strings.properties b/extensionmanager/src/main/resources/qupath/ext/extensionmanager/strings.properties
index e23f033..42f4e62 100644
--- a/extensionmanager/src/main/resources/qupath/ext/extensionmanager/strings.properties
+++ b/extensionmanager/src/main/resources/qupath/ext/extensionmanager/strings.properties
@@ -32,7 +32,6 @@ CatalogManager.copyUrl = Copy URL
CatalogManager.remove = Remove
CatalogManager.cannotBeDeleted = {0} cannot be deleted.
CatalogManager.deleteCatalog = Delete catalog
-CatalogManager.deleteExtensions = Also delete extensions belonging to the catalogs to remove?\nThis will remove the following directories and their content:\n\n{0}
CatalogManager.cannotRemoveSelectedCatalogs = Cannot remove selected catalogs:\n{0}
ManuallyInstalledExtensionLine.deleteExtension = Delete extension
@@ -43,6 +42,8 @@ ManuallyInstalledExtensionLine.cannotDeleteExtension = Cannot delete extension:\
ProgressWindow.cancel = Cancel
+Catalog.CatalogPane.errorFetchingExtensions = Error while fetching extensions:\n{0}
+
Catalog.ExtensionDetails.close = Close
Catalog.ExtensionDetails.extensionNotCompatible = This extension is not compatible with the current version of the software.
Catalog.ExtensionDetails.browserError = Browser error
diff --git a/extensionmanager/src/main/resources/qupath/ext/extensionmanager/strings_fr.properties b/extensionmanager/src/main/resources/qupath/ext/extensionmanager/strings_fr.properties
index d027224..1d783ed 100644
--- a/extensionmanager/src/main/resources/qupath/ext/extensionmanager/strings_fr.properties
+++ b/extensionmanager/src/main/resources/qupath/ext/extensionmanager/strings_fr.properties
@@ -32,7 +32,6 @@ CatalogManager.copyUrl = Copier l'URL
CatalogManager.remove = Supprimer
CatalogManager.cannotBeDeleted = {0} ne peut pas être supprimé.
CatalogManager.deleteCatalog = Suppression du catalogue
-CatalogManager.deleteExtensions = Supprimer également les extensions appartenant à ces catalogues à supprimer ?\nCela supprimera les dossier suivants:\n\n{0}
CatalogManager.cannotRemoveSelectedCatalogs = Impossible de supprimer les catalogues sélectionnés:\n{0}
ManuallyInstalledExtensionLine.deleteExtension = Supprimer l'extension
@@ -43,6 +42,8 @@ ManuallyInstalledExtensionLine.cannotDeleteExtension = Impossible de supprimer l
ProgressWindow.cancel = Annuler
+Catalog.CatalogPane.errorFetchingExtensions = Erreur lors de la récupération des extensions:\n{0}
+
Catalog.ExtensionDetails.close = Fermer
Catalog.ExtensionDetails.extensionNotCompatible = Cette extension n'est pas compatible avec la version actuelle du logiciel.
Catalog.ExtensionDetails.browserError = Erreur de navigateur
diff --git a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/TestExtensionCatalogManager.java b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/TestExtensionModelCatalogModelManager.java
similarity index 87%
rename from extensionmanager/src/test/java/qupath/ext/extensionmanager/core/TestExtensionCatalogManager.java
rename to extensionmanager/src/test/java/qupath/ext/extensionmanager/core/TestExtensionModelCatalogModelManager.java
index d55be27..8c2d1a1 100644
--- a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/TestExtensionCatalogManager.java
+++ b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/TestExtensionModelCatalogModelManager.java
@@ -8,14 +8,14 @@
import org.junit.jupiter.api.Test;
import qupath.ext.extensionmanager.SimpleServer;
import qupath.ext.extensionmanager.TestUtils;
-import qupath.ext.extensionmanager.core.catalog.CatalogFetcher;
-import qupath.ext.extensionmanager.core.catalog.Extension;
-import qupath.ext.extensionmanager.core.catalog.Release;
-import qupath.ext.extensionmanager.core.catalog.VersionRange;
+import qupath.ext.extensionmanager.core.model.CatalogModelFetcher;
+import qupath.ext.extensionmanager.core.model.ExtensionModel;
+import qupath.ext.extensionmanager.core.model.ReleaseModel;
+import qupath.ext.extensionmanager.core.model.VersionRangeModel;
import qupath.ext.extensionmanager.core.savedentities.InstalledExtension;
import qupath.ext.extensionmanager.core.savedentities.Registry;
import qupath.ext.extensionmanager.core.savedentities.SavedCatalog;
-import qupath.ext.extensionmanager.core.savedentities.UpdateAvailable;
+import qupath.ext.extensionmanager.core.catalog.UpdateAvailable;
import java.io.IOException;
import java.net.URI;
@@ -33,7 +33,7 @@
* is used because the repository was archived and unlikely to be changed / removed. If that were to happen, some tests and the
* catalog in the resources directory would need to be changed.
*/
-public class TestExtensionCatalogManager {
+public class TestExtensionModelCatalogModelManager {
private static final String CATALOG_NAME = "catalog.json";
private static final int CHANGE_WAITING_TIME_CATALOG_MS = 100;
@@ -44,7 +44,7 @@ public class TestExtensionCatalogManager {
static void setupServer() throws IOException {
server = new SimpleServer(List.of(new SimpleServer.FileToServe(
CATALOG_NAME,
- Objects.requireNonNull(TestExtensionCatalogManager.class.getResourceAsStream(CATALOG_NAME))
+ Objects.requireNonNull(TestExtensionModelCatalogModelManager.class.getResourceAsStream(CATALOG_NAME))
)));
}
@@ -60,7 +60,7 @@ void Check_Creation_When_Extension_Directory_Null() {
Assertions.assertDoesNotThrow(() -> {
ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(null),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
);
@@ -75,7 +75,7 @@ void Check_Creation_When_Extension_Directory_Property_Null() {
() -> {
ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
null,
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
);
@@ -104,7 +104,7 @@ void Check_Creation_When_Version_Null() {
() -> {
ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
null,
createSampleRegistry()
);
@@ -120,7 +120,7 @@ void Check_Creation_When_Version_Not_Valid() {
() -> {
ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"invalid_version",
createSampleRegistry()
);
@@ -134,7 +134,7 @@ void Check_Creation_When_Default_Registry_Null() {
Assertions.assertDoesNotThrow(() -> {
ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
null
);
@@ -147,11 +147,11 @@ void Check_Extension_Directory_Path() throws Exception {
Path expectedExtensionDirectory = Files.createTempDirectory(null);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(expectedExtensionDirectory),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
- Path extensionDirectory = extensionCatalogManager.getExtensionDirectoryPath().get();
+ Path extensionDirectory = extensionCatalogManager.getExtensionDirectory().get();
Assertions.assertEquals(expectedExtensionDirectory, extensionDirectory);
}
@@ -164,12 +164,12 @@ void Check_Extension_Directory_Path_After_Changed() throws Exception {
ObjectProperty extensionDirectoryProperty = new SimpleObjectProperty<>(firstExtensionDirectory);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
extensionDirectoryProperty,
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
extensionDirectoryProperty.set(expectedExtensionDirectory);
- Path extensionDirectory = extensionCatalogManager.getExtensionDirectoryPath().get();
+ Path extensionDirectory = extensionCatalogManager.getExtensionDirectory().get();
Assertions.assertEquals(expectedExtensionDirectory, extensionDirectory);
}
@@ -180,7 +180,7 @@ void Check_Version() throws Exception {
String expectedVersion = "v1.2.4";
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
expectedVersion,
createSampleRegistry()
)) {
@@ -194,7 +194,7 @@ void Check_Version() throws Exception {
void Check_Catalog_Directory_When_Catalog_Is_Null() throws Exception {
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -209,7 +209,7 @@ void Check_Catalog_Directory_When_Catalog_Is_Null() throws Exception {
void Check_Catalog_Directory_Not_Null() throws Exception {
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -229,7 +229,7 @@ void Check_Catalog_Directory_Not_Null() throws Exception {
void Check_Catalog_Addition_When_Extension_Directory_Null() throws Exception {
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(null),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -244,7 +244,7 @@ void Check_Catalog_Addition_When_Extension_Directory_Null() throws Exception {
void Check_Catalog_Addition_When_List_Null() throws Exception {
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -268,7 +268,7 @@ void Check_Catalog_Addition_When_One_Null() throws Exception {
catalogsToAdd.add(null);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -295,7 +295,7 @@ void Check_Catalog_Added() throws Exception {
).toList();
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
defaultRegistry
)) {
@@ -323,7 +323,7 @@ void Check_Catalog_Addition_When_Same_Name() throws Exception {
);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
null
)) {
@@ -353,7 +353,7 @@ void Check_Catalog_With_Existing_Name_Not_Added() throws Exception {
List expectedCatalogs = List.of(firstCatalog);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
null
)) {
@@ -378,7 +378,7 @@ void Check_Only_Catalogs_From_Default_Registry_When_Extension_Directory_Changed(
List expectedCatalogs = defaultRegistry.catalogs();
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
extensionDirectory,
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
defaultRegistry
)) {
@@ -394,13 +394,13 @@ void Check_Only_Catalogs_From_Default_Registry_When_Extension_Directory_Changed(
void Check_Catalog_Deletion_When_Extension_Directory_Null() throws Exception {
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(null),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
Assertions.assertThrows(
NullPointerException.class,
- () -> extensionCatalogManager.removeCatalogs(List.of(), true)
+ () -> extensionCatalogManager.removeCatalog(List.of(), true)
);
}
}
@@ -409,13 +409,13 @@ void Check_Catalog_Deletion_When_Extension_Directory_Null() throws Exception {
void Check_Catalog_Deletion_When_List_Null() throws Exception {
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
Assertions.assertThrows(
NullPointerException.class,
- () -> extensionCatalogManager.removeCatalogs(null, true)
+ () -> extensionCatalogManager.removeCatalog(null, true)
);
}
}
@@ -433,13 +433,13 @@ void Check_Catalog_Deletion_When_One_Null() throws Exception {
catalogsToRemove.add(null);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
Assertions.assertThrows(
NullPointerException.class,
- () -> extensionCatalogManager.removeCatalogs(catalogsToRemove, true)
+ () -> extensionCatalogManager.removeCatalog(catalogsToRemove, true)
);
}
}
@@ -457,12 +457,12 @@ void Check_Catalog_Removed() throws Exception {
List expectedCatalogs = defaultRegistry.catalogs();
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
defaultRegistry
)) {
extensionCatalogManager.addCatalog(List.of(catalogToAdd));
- extensionCatalogManager.removeCatalogs(List.of(catalogToAdd), true);
+ extensionCatalogManager.removeCatalog(List.of(catalogToAdd), true);
TestUtils.assertCollectionsEqualsWithoutOrder(expectedCatalogs, extensionCatalogManager.getCatalogs());
}
@@ -484,12 +484,12 @@ void Check_Undeletable_Catalog_Not_Removed() throws Exception {
).toList();
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
defaultRegistry
)) {
extensionCatalogManager.addCatalog(List.of(catalogToAdd));
- extensionCatalogManager.removeCatalogs(List.of(catalogToAdd), true);
+ extensionCatalogManager.removeCatalog(List.of(catalogToAdd), true);
TestUtils.assertCollectionsEqualsWithoutOrder(expectedCatalogs, extensionCatalogManager.getCatalogs());
}
@@ -499,7 +499,7 @@ void Check_Undeletable_Catalog_Not_Removed() throws Exception {
void Check_Extension_Directory_When_Catalog_Is_Null() throws Exception {
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -507,7 +507,7 @@ void Check_Extension_Directory_When_Catalog_Is_Null() throws Exception {
NullPointerException.class,
() -> extensionCatalogManager.getExtensionDirectory(
null,
- new Extension("", "", "", URI.create("http://github.com"), false, List.of())
+ new ExtensionModel("", "", "", URI.create("http://github.com"), false, List.of())
)
);
}
@@ -517,7 +517,7 @@ void Check_Extension_Directory_When_Catalog_Is_Null() throws Exception {
void Check_Extension_Directory_When_Extension_Is_Null() throws Exception {
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -541,7 +541,7 @@ void Check_Extension_Directory_When_Extension_Is_Null() throws Exception {
void Check_Extension_Directory_Not_Null() throws Exception {
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -553,7 +553,7 @@ void Check_Extension_Directory_Not_Null() throws Exception {
URI.create("http://test"),
true
),
- new Extension("", "", "", URI.create("http://github.com"), false, List.of())
+ new ExtensionModel("", "", "", URI.create("http://github.com"), false, List.of())
);
Assertions.assertNotNull(extensionDirectory);
@@ -573,16 +573,16 @@ void Check_Download_Links() throws Exception {
URI.create("http://test"),
true
);
- Release release = new Release(
+ ReleaseModel release = new ReleaseModel(
"v0.1.2",
URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar"),
null,
List.of(URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar")),
null,
- new VersionRange("v0.0.0", null, null)
+ new VersionRangeModel("v0.0.0", null, null)
);
InstalledExtension installationInformation = new InstalledExtension(release.name(), true);
- Extension extension = new Extension(
+ ExtensionModel extension = new ExtensionModel(
"name",
"description",
"author",
@@ -592,7 +592,7 @@ void Check_Download_Links() throws Exception {
);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -615,7 +615,7 @@ void Check_Download_Links_Cannot_Be_Retrieved_When_Desired_Release_Does_Not_Exis
true
);
InstalledExtension installationInformation = new InstalledExtension("invalid_release", false);
- Extension extension = new Extension(
+ ExtensionModel extension = new ExtensionModel(
"name",
"description",
"author",
@@ -625,7 +625,7 @@ void Check_Download_Links_Cannot_Be_Retrieved_When_Desired_Release_Does_Not_Exis
);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -645,16 +645,16 @@ void Check_Download_Links_Cannot_Be_Retrieved_When_Extension_Directory_Null() th
URI.create("http://test"),
true
);
- Release release = new Release(
+ ReleaseModel release = new ReleaseModel(
"v0.1.2",
URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar"),
null,
List.of(URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar")),
null,
- new VersionRange("v0.0.0", null, null)
+ new VersionRangeModel("v0.0.0", null, null)
);
InstalledExtension installationInformation = new InstalledExtension(release.name(), true);
- Extension extension = new Extension(
+ ExtensionModel extension = new ExtensionModel(
"name",
"description",
"author",
@@ -664,7 +664,7 @@ void Check_Download_Links_Cannot_Be_Retrieved_When_Extension_Directory_Null() th
);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(null),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -684,16 +684,16 @@ void Check_Extension_Installed() throws Exception {
URI.create("http://test"),
true
);
- Release release = new Release(
+ ReleaseModel release = new ReleaseModel(
"v0.1.2",
URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar"),
null,
List.of(URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar")),
null,
- new VersionRange("v0.0.0", null, null)
+ new VersionRangeModel("v0.0.0", null, null)
);
InstalledExtension installationInformation = new InstalledExtension(release.name(), true);
- Extension extension = new Extension(
+ ExtensionModel extension = new ExtensionModel(
"name",
"description",
"author",
@@ -703,7 +703,7 @@ void Check_Extension_Installed() throws Exception {
);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -731,17 +731,17 @@ void Check_Extension_Reinstalled_When_Already_Installed() throws Exception {
URI.create("http://test"),
true
);
- Release release = new Release(
+ ReleaseModel release = new ReleaseModel(
"v0.1.2",
URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar"),
null,
List.of(URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar")),
null,
- new VersionRange("v0.0.0", null, null)
+ new VersionRangeModel("v0.0.0", null, null)
);
InstalledExtension firstInstallationInformation = new InstalledExtension(release.name(), true);
InstalledExtension secondInstallationInformation = new InstalledExtension(release.name(), false);
- Extension extension = new Extension(
+ ExtensionModel extension = new ExtensionModel(
"name",
"description",
"author",
@@ -751,7 +751,7 @@ void Check_Extension_Reinstalled_When_Already_Installed() throws Exception {
);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -787,7 +787,7 @@ void Check_Extension_Installation_Fails_When_Desired_Release_Does_Not_Exist() th
true
);
InstalledExtension installationInformation = new InstalledExtension("invalid_release", false);
- Extension extension = new Extension(
+ ExtensionModel extension = new ExtensionModel(
"name",
"description",
"author",
@@ -797,7 +797,7 @@ void Check_Extension_Installation_Fails_When_Desired_Release_Does_Not_Exist() th
);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -823,16 +823,16 @@ void Check_Extension_Installation_Fails_When_Extension_Directory_Null() throws E
URI.create("http://test"),
true
);
- Release release = new Release(
+ ReleaseModel release = new ReleaseModel(
"v0.1.2",
URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar"),
null,
List.of(URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar")),
null,
- new VersionRange("v0.0.0", null, null)
+ new VersionRangeModel("v0.0.0", null, null)
);
InstalledExtension installationInformation = new InstalledExtension(release.name(), true);
- Extension extension = new Extension(
+ ExtensionModel extension = new ExtensionModel(
"name",
"description",
"author",
@@ -842,7 +842,7 @@ void Check_Extension_Installation_Fails_When_Extension_Directory_Null() throws E
);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(null),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -869,15 +869,15 @@ void Check_Extension_Removed_After_Changing_Extension_Directory() throws Excepti
URI.create("http://test"),
true
);
- Release release = new Release(
+ ReleaseModel release = new ReleaseModel(
"v0.1.2",
URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar"),
null,
List.of(URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar")),
null,
- new VersionRange("v0.0.0", null, null)
+ new VersionRangeModel("v0.0.0", null, null)
);
- Extension extension = new Extension(
+ ExtensionModel extension = new ExtensionModel(
"name",
"description",
"author",
@@ -887,7 +887,7 @@ void Check_Extension_Removed_After_Changing_Extension_Directory() throws Excepti
);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
extensionDirectory,
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -914,17 +914,17 @@ void Check_Installed_Jars_After_Extension_Installation() throws Exception {
URI.create("http://test"),
true
);
- Release release = new Release(
+ ReleaseModel release = new ReleaseModel(
"v0.1.2",
URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar"),
null,
List.of(URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar")),
null,
- new VersionRange("v0.0.0", null, null)
+ new VersionRangeModel("v0.0.0", null, null)
);
List expectedJarNames = List.of("qupath-extension-macos.jar", "qupath-extension-macos.jar");
InstalledExtension installationInformation = new InstalledExtension(release.name(), true);
- Extension extension = new Extension(
+ ExtensionModel extension = new ExtensionModel(
"name",
"description",
"author",
@@ -934,7 +934,7 @@ void Check_Installed_Jars_After_Extension_Installation() throws Exception {
);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -964,7 +964,7 @@ void Check_Installed_Jars_After_Extension_Manually_Installed() throws Exception
List expectedJars = List.of();
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(extensionDirectory),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -984,17 +984,17 @@ void Check_Jar_Loaded_Runnable_Run_After_Extension_Installation() throws Excepti
URI.create("http://test"),
true
);
- Release release = new Release(
+ ReleaseModel release = new ReleaseModel(
"v0.1.2",
URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar"),
null,
List.of(URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar")),
null,
- new VersionRange("v0.0.0", null, null)
+ new VersionRangeModel("v0.0.0", null, null)
);
int expectedNumberOfCalls = 2;
InstalledExtension installationInformation = new InstalledExtension(release.name(), true);
- Extension extension = new Extension(
+ ExtensionModel extension = new ExtensionModel(
"name",
"description",
"author",
@@ -1004,7 +1004,7 @@ void Check_Jar_Loaded_Runnable_Run_After_Extension_Installation() throws Excepti
);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -1028,7 +1028,7 @@ void Check_No_Available_Updates_When_No_Extension_Installed() throws Exception {
List expectedUpdates = List.of();
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -1042,12 +1042,12 @@ void Check_No_Available_Updates_When_No_Extension_Installed() throws Exception {
void Check_No_Available_Updates_When_Incompatible_Version() throws Exception {
Registry defaultRegistry = createSampleRegistry();
SavedCatalog catalog = defaultRegistry.catalogs().getFirst();
- Extension extension = CatalogFetcher.getCatalog(catalog.rawUri()).get().extensions().getFirst();
- Release release = extension.releases().getFirst();
+ ExtensionModel extension = CatalogModelFetcher.getCatalog(catalog.rawUri()).get().extensions().getFirst();
+ ReleaseModel release = extension.releases().getFirst();
List expectedUpdates = List.of();
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
defaultRegistry
)) {
@@ -1069,8 +1069,8 @@ void Check_No_Available_Updates_When_Incompatible_Version() throws Exception {
void Check_Update_Available() throws Exception {
Registry defaultRegistry = createSampleRegistry();
SavedCatalog catalog = defaultRegistry.catalogs().getFirst();
- Extension extension = CatalogFetcher.getCatalog(catalog.rawUri()).get().extensions().getFirst();
- Release release = extension.releases().getFirst();
+ ExtensionModel extension = CatalogModelFetcher.getCatalog(catalog.rawUri()).get().extensions().getFirst();
+ ReleaseModel release = extension.releases().getFirst();
List expectedUpdates = List.of(new UpdateAvailable(
"Some extension",
"v0.1.0",
@@ -1078,7 +1078,7 @@ void Check_Update_Available() throws Exception {
)); // see catalog.json in resources
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v2.2.3",
defaultRegistry
)) {
@@ -1105,16 +1105,16 @@ void Check_Extension_Removed() throws Exception {
URI.create("http://test"),
true
);
- Release release = new Release(
+ ReleaseModel release = new ReleaseModel(
"v0.1.2",
URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar"),
null,
List.of(URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar")),
null,
- new VersionRange("v0.0.0", null, null)
+ new VersionRangeModel("v0.0.0", null, null)
);
InstalledExtension installationInformation = new InstalledExtension(release.name(), true);
- Extension extension = new Extension(
+ ExtensionModel extension = new ExtensionModel(
"name",
"description",
"author",
@@ -1124,7 +1124,7 @@ void Check_Extension_Removed() throws Exception {
);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -1151,15 +1151,15 @@ void Check_Extension_Removal_When_Not_Installed() throws Exception {
URI.create("http://test"),
true
);
- Release release = new Release(
+ ReleaseModel release = new ReleaseModel(
"v0.1.2",
URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar"),
null,
List.of(URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar")),
null,
- new VersionRange("v0.0.0", null, null)
+ new VersionRangeModel("v0.0.0", null, null)
);
- Extension extension = new Extension(
+ ExtensionModel extension = new ExtensionModel(
"name",
"description",
"author",
@@ -1169,7 +1169,7 @@ void Check_Extension_Removal_When_Not_Installed() throws Exception {
);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -1188,17 +1188,17 @@ void Check_Installed_Jars_After_Extension_Removal() throws Exception {
URI.create("http://test"),
true
);
- Release release = new Release(
+ ReleaseModel release = new ReleaseModel(
"v0.1.2",
URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar"),
null,
List.of(URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar")),
null,
- new VersionRange("v0.0.0", null, null)
+ new VersionRangeModel("v0.0.0", null, null)
);
List expectedJarNames = List.of();
InstalledExtension installationInformation = new InstalledExtension(release.name(), true);
- Extension extension = new Extension(
+ ExtensionModel extension = new ExtensionModel(
"name",
"description",
"author",
@@ -1208,7 +1208,7 @@ void Check_Installed_Jars_After_Extension_Removal() throws Exception {
);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(Files.createTempDirectory(null)),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -1242,7 +1242,7 @@ void Check_Manually_Installed_Jars_When_Two_Jars_Added_Before_Manager_Creation()
);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(extensionDirectory),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -1257,7 +1257,7 @@ void Check_Manually_Installed_Jars_When_Two_Jars_Added_After_Manager_Creation()
Path extensionDirectory = Files.createTempDirectory(null);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(extensionDirectory),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -1283,7 +1283,7 @@ void Check_Manually_Installed_Jars_When_Non_Jar_File_Added() throws Exception {
);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(extensionDirectory),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -1302,16 +1302,16 @@ void Check_Manually_Installed_Jars_When_Extension_Installed_With_Index() throws
URI.create("http://test"),
true
);
- Release release = new Release(
+ ReleaseModel release = new ReleaseModel(
"v0.1.2",
URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar"),
null,
List.of(URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar")),
null,
- new VersionRange("v0.0.0", null, null)
+ new VersionRangeModel("v0.0.0", null, null)
);
InstalledExtension installationInformation = new InstalledExtension(release.name(), true);
- Extension extension = new Extension(
+ ExtensionModel extension = new ExtensionModel(
"name",
"description",
"author",
@@ -1326,7 +1326,7 @@ void Check_Manually_Installed_Jars_When_Extension_Installed_With_Index() throws
);
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
new SimpleObjectProperty<>(extensionDirectory),
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
@@ -1352,7 +1352,7 @@ void Check_Manually_Installed_Jars_When_Extension_Directory_Changed() throws Exc
List expectedJars = List.of();
try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager(
extensionDirectory,
- TestExtensionCatalogManager.class.getClassLoader(),
+ TestExtensionModelCatalogModelManager.class.getClassLoader(),
"v1.2.3",
createSampleRegistry()
)) {
diff --git a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestCatalog.java b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestCatalogModel.java
similarity index 80%
rename from extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestCatalog.java
rename to extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestCatalogModel.java
index 1fef9cb..875f127 100644
--- a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestCatalog.java
+++ b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestCatalogModel.java
@@ -1,4 +1,4 @@
-package qupath.ext.extensionmanager.core.catalog;
+package qupath.ext.extensionmanager.core.model;
import com.google.gson.Gson;
import org.junit.jupiter.api.Assertions;
@@ -8,17 +8,17 @@
import java.net.URI;
import java.util.List;
-public class TestCatalog {
+public class TestCatalogModel {
@Nested
public class ConstructorTests {
@Test
void Check_Valid_Catalog() {
- Assertions.assertDoesNotThrow(() -> new Catalog(
+ Assertions.assertDoesNotThrow(() -> new CatalogModel(
"",
"",
- List.of(new Extension("", "", "", URI.create("https://github.com/qupath/qupath"), false, List.of()))
+ List.of(new ExtensionModel("", "", "", URI.create("https://github.com/qupath/qupath"), false, List.of()))
));
}
@@ -26,7 +26,7 @@ void Check_Valid_Catalog() {
void Check_Undefined_Name() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new Catalog(null, "", List.of())
+ () -> new CatalogModel(null, "", List.of())
);
}
@@ -34,7 +34,7 @@ void Check_Undefined_Name() {
void Check_Undefined_Description() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new Catalog("", null, List.of())
+ () -> new CatalogModel("", null, List.of())
);
}
@@ -42,21 +42,21 @@ void Check_Undefined_Description() {
void Check_Undefined_Extensions() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new Catalog("", "", null)
+ () -> new CatalogModel("", "", null)
);
}
@Test
void Check_Extensions_With_Same_Name() {
- List extensions = List.of(
- new Extension("name", "", "", URI.create("https://github.com/qupath/qupath"), false, List.of()),
- new Extension("name", "", "", URI.create("https://github.com/qupath/qupath"), false, List.of()),
- new Extension("other_name", "", "", URI.create("https://github.com/qupath/qupath"), false, List.of())
+ List extensions = List.of(
+ new ExtensionModel("name", "", "", URI.create("https://github.com/qupath/qupath"), false, List.of()),
+ new ExtensionModel("name", "", "", URI.create("https://github.com/qupath/qupath"), false, List.of()),
+ new ExtensionModel("other_name", "", "", URI.create("https://github.com/qupath/qupath"), false, List.of())
);
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new Catalog("", "", extensions)
+ () -> new CatalogModel("", "", extensions)
);
}
}
@@ -66,10 +66,10 @@ public class JsonTests {
@Test
void Check_Valid_Catalog() {
- Catalog expectedCatalog = new Catalog(
+ CatalogModel expectedCatalog = new CatalogModel(
"",
"",
- List.of(new Extension(
+ List.of(new ExtensionModel(
"",
"",
"",
@@ -79,7 +79,7 @@ void Check_Valid_Catalog() {
))
);
- Catalog catalog = new Gson().fromJson("""
+ CatalogModel catalog = new Gson().fromJson("""
{
"name": "",
"description": "",
@@ -94,7 +94,7 @@ void Check_Valid_Catalog() {
]
}
""",
- Catalog.class
+ CatalogModel.class
);
Assertions.assertEquals(expectedCatalog, catalog);
@@ -110,7 +110,7 @@ void Check_Undefined_Name() {
"extensions": []
}
""",
- Catalog.class
+ CatalogModel.class
)
);
}
@@ -125,7 +125,7 @@ void Check_Undefined_Description() {
"extensions": []
}
""",
- Catalog.class
+ CatalogModel.class
)
);
}
@@ -140,7 +140,7 @@ void Check_Undefined_Extensions() {
"description": ""
}
""",
- Catalog.class
+ CatalogModel.class
)
);
}
@@ -156,7 +156,7 @@ void Check_Invalid_Extensions() {
"extensions": [{}]
}
""",
- Catalog.class
+ CatalogModel.class
)
);
}
@@ -194,7 +194,7 @@ void Check_Extensions_With_Same_Name() {
]
}
""",
- Catalog.class
+ CatalogModel.class
)
);
}
diff --git a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestCatalogFetcher.java b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestCatalogModelFetcher.java
similarity index 73%
rename from extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestCatalogFetcher.java
rename to extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestCatalogModelFetcher.java
index ebb3108..9870729 100644
--- a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestCatalogFetcher.java
+++ b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestCatalogModelFetcher.java
@@ -1,4 +1,4 @@
-package qupath.ext.extensionmanager.core.catalog;
+package qupath.ext.extensionmanager.core.model;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
@@ -13,33 +13,33 @@
import java.util.Objects;
import java.util.concurrent.ExecutionException;
-public class TestCatalogFetcher {
+public class TestCatalogModelFetcher {
- private static final Catalog VALID_CATALOG = new Catalog(
+ private static final CatalogModel VALID_CATALOG = new CatalogModel(
"Some catalog",
"Some description",
- List.of(new Extension(
+ List.of(new ExtensionModel(
"Some extension",
"Some extension description",
"Some author",
URI.create("https://github.com/qupath/qupath"),
false,
List.of(
- new Release(
+ new ReleaseModel(
"v0.1.0",
URI.create("https://github.com/qupath/qupath"),
null,
null,
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
),
- new Release(
+ new ReleaseModel(
"v1.0.0",
URI.create("https://github.com/qupath/qupath"),
null,
null,
null,
- new VersionRange("v2.0.0", null, null)
+ new VersionRangeModel("v2.0.0", null, null)
)
)
))
@@ -65,7 +65,7 @@ static void setupServer() throws IOException {
server = new SimpleServer(Arrays.stream(JsonCatalog.values())
.map(jsonCatalog -> new SimpleServer.FileToServe(
jsonCatalog.getFileName(),
- Objects.requireNonNull(TestCatalogFetcher.class.getResourceAsStream(jsonCatalog.getFileName()))
+ Objects.requireNonNull(TestCatalogModelFetcher.class.getResourceAsStream(jsonCatalog.getFileName()))
))
.toList()
);
@@ -82,7 +82,7 @@ static void stopServer() {
void Check_Null_URI() {
Assertions.assertThrows(
ExecutionException.class,
- () -> CatalogFetcher.getCatalog(null).get()
+ () -> CatalogModelFetcher.getCatalog(null).get()
);
}
@@ -90,20 +90,20 @@ void Check_Null_URI() {
void Check_Invalid_URI() {
Assertions.assertThrows(
ExecutionException.class,
- () -> CatalogFetcher.getCatalog(URI.create("")).get()
+ () -> CatalogModelFetcher.getCatalog(URI.create("")).get()
);
}
@Test
void Check_Valid_Catalog_Retrievable() {
- Assertions.assertDoesNotThrow(() -> CatalogFetcher.getCatalog(
+ Assertions.assertDoesNotThrow(() -> CatalogModelFetcher.getCatalog(
server.getURI(JsonCatalog.VALID_CATALOG.getFileName())).get()
);
}
@Test
void Check_Valid_Catalog_Values() throws ExecutionException, InterruptedException {
- Catalog catalog = CatalogFetcher.getCatalog(server.getURI(JsonCatalog.VALID_CATALOG.getFileName())).get();
+ CatalogModel catalog = CatalogModelFetcher.getCatalog(server.getURI(JsonCatalog.VALID_CATALOG.getFileName())).get();
Assertions.assertEquals(VALID_CATALOG, catalog);
}
@@ -112,7 +112,7 @@ void Check_Valid_Catalog_Values() throws ExecutionException, InterruptedExceptio
void Check_Invalid_Catalog() {
Assertions.assertThrows(
ExecutionException.class,
- () -> CatalogFetcher.getCatalog(server.getURI(JsonCatalog.INVALID_CATALOG.getFileName())).get()
+ () -> CatalogModelFetcher.getCatalog(server.getURI(JsonCatalog.INVALID_CATALOG.getFileName())).get()
);
}
}
diff --git a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestExtension.java b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestExtensionModel.java
similarity index 81%
rename from extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestExtension.java
rename to extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestExtensionModel.java
index 57a296f..4beafbd 100644
--- a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestExtension.java
+++ b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestExtensionModel.java
@@ -1,4 +1,4 @@
-package qupath.ext.extensionmanager.core.catalog;
+package qupath.ext.extensionmanager.core.model;
import com.google.gson.Gson;
import org.junit.jupiter.api.Assertions;
@@ -9,26 +9,26 @@
import java.util.List;
import java.util.Optional;
-public class TestExtension {
+public class TestExtensionModel {
@Nested
public class ConstructorTests {
@Test
void Check_Valid_Extension() {
- Assertions.assertDoesNotThrow(() -> new Extension(
+ Assertions.assertDoesNotThrow(() -> new ExtensionModel(
"",
"",
"",
URI.create("https://github.com/qupath/qupath"),
false,
- List.of(new Release(
+ List.of(new ReleaseModel(
"v1.0.0",
URI.create("https://github.com/qupath/qupath"),
List.of(URI.create("https://github.com/qupath/qupath")),
null,
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
))
));
}
@@ -37,19 +37,19 @@ void Check_Valid_Extension() {
void Check_Undefined_Name() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new Extension(
+ () -> new ExtensionModel(
null,
"",
"",
URI.create("https://github.com/qupath/qupath"),
false,
- List.of(new Release(
+ List.of(new ReleaseModel(
"v1.0.0",
URI.create("https://github.com/qupath/qupath"),
List.of(URI.create("https://github.com/qupath/qupath")),
null,
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
))
)
);
@@ -59,19 +59,19 @@ void Check_Undefined_Name() {
void Check_Undefined_Description() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new Extension(
+ () -> new ExtensionModel(
"",
null,
"",
URI.create("https://github.com/qupath/qupath"),
false,
- List.of(new Release(
+ List.of(new ReleaseModel(
"v1.0.0",
URI.create("https://github.com/qupath/qupath"),
List.of(URI.create("https://github.com/qupath/qupath")),
null,
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
))
)
);
@@ -81,19 +81,19 @@ void Check_Undefined_Description() {
void Check_Undefined_Author() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new Extension(
+ () -> new ExtensionModel(
"",
"",
null,
URI.create("https://github.com/qupath/qupath"),
false,
- List.of(new Release(
+ List.of(new ReleaseModel(
"v1.0.0",
URI.create("https://github.com/qupath/qupath"),
List.of(URI.create("https://github.com/qupath/qupath")),
null,
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
))
)
);
@@ -103,19 +103,19 @@ void Check_Undefined_Author() {
void Check_Undefined_Homepage() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new Extension(
+ () -> new ExtensionModel(
"",
"",
"",
null,
false,
- List.of(new Release(
+ List.of(new ReleaseModel(
"v1.0.0",
URI.create("https://github.com/qupath/qupath"),
List.of(URI.create("https://github.com/qupath/qupath")),
null,
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
))
)
);
@@ -125,7 +125,7 @@ void Check_Undefined_Homepage() {
void Check_Undefined_Releases() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new Extension(
+ () -> new ExtensionModel(
"",
"",
"",
@@ -140,19 +140,19 @@ void Check_Undefined_Releases() {
void Check_Homepage_Not_GitHub() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new Extension(
+ () -> new ExtensionModel(
"",
"",
"",
URI.create("https://qupath.readthedocs.io/"),
false,
- List.of(new Release(
+ List.of(new ReleaseModel(
"v1.0.0",
URI.create("https://github.com/qupath/qupath"),
List.of(URI.create("https://github.com/qupath/qupath")),
null,
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
))
)
);
@@ -164,23 +164,23 @@ public class JsonTests {
@Test
void Check_Valid_Extension() {
- Extension expectedExtension = new Extension(
+ ExtensionModel expectedExtension = new ExtensionModel(
"",
"",
"",
URI.create("https://github.com/qupath/qupath"),
true,
- List.of(new Release(
+ List.of(new ReleaseModel(
"v1.0.0",
URI.create("https://github.com/qupath/qupath"),
null,
null,
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
))
);
- Extension extension = new Gson().fromJson("""
+ ExtensionModel extension = new Gson().fromJson("""
{
"name": "",
"description": "",
@@ -198,7 +198,7 @@ void Check_Valid_Extension() {
]
}
""",
- Extension.class
+ ExtensionModel.class
);
Assertions.assertEquals(expectedExtension, extension);
@@ -224,7 +224,7 @@ void Check_Undefined_Name() {
]
}
""",
- Extension.class
+ ExtensionModel.class
)
);
}
@@ -249,7 +249,7 @@ void Check_Undefined_Description() {
]
}
""",
- Extension.class
+ ExtensionModel.class
)
);
}
@@ -274,7 +274,7 @@ void Check_Undefined_Author() {
]
}
""",
- Extension.class
+ ExtensionModel.class
)
);
}
@@ -299,7 +299,7 @@ void Check_Undefined_Homepage() {
]
}
""",
- Extension.class
+ ExtensionModel.class
)
);
}
@@ -316,7 +316,7 @@ void Check_Undefined_Releases() {
"homepage": "https://github.com/qupath/qupath"
}
""",
- Extension.class
+ ExtensionModel.class
)
);
}
@@ -334,7 +334,7 @@ void Check_Invalid_Release() {
"releases": [{}]
}
""",
- Extension.class
+ ExtensionModel.class
)
);
}
@@ -360,171 +360,171 @@ void Check_Homepage_Not_GitHub() {
]
}
""",
- Extension.class
+ ExtensionModel.class
)
);
}
}
@Test
void Check_Max_Compatible_Release_When_Two_Compatibles() {
- Release expectedRelease = new Release(
+ ReleaseModel expectedRelease = new ReleaseModel(
"v2.0.0",
URI.create("https://github.com/qupath/qupath"),
List.of(URI.create("https://github.com/qupath/qupath")),
null,
null,
- new VersionRange("v1.1.0", null, null)
+ new VersionRangeModel("v1.1.0", null, null)
);
- Extension extension = new Extension(
+ ExtensionModel extension = new ExtensionModel(
"",
"",
"",
URI.create("https://github.com/qupath/qupath"),
false,
List.of(
- new Release(
+ new ReleaseModel(
"v1.0.0",
URI.create("https://github.com/qupath/qupath"),
List.of(URI.create("https://github.com/qupath/qupath")),
null,
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
),
expectedRelease,
- new Release(
+ new ReleaseModel(
"v3.0.0",
URI.create("https://github.com/qupath/qupath"),
List.of(URI.create("https://github.com/qupath/qupath")),
null,
null,
- new VersionRange("v1.2.0", null, null)
+ new VersionRangeModel("v1.2.0", null, null)
)
)
);
String version = "v1.1.0";
- Release release = extension.getMaxCompatibleRelease(version).orElse(null);
+ ReleaseModel release = extension.getMaxCompatibleRelease(version).orElse(null);
Assertions.assertEquals(expectedRelease, release);
}
@Test
void Check_Max_Compatible_Release_When_Three_Compatible() {
- Release expectedRelease = new Release(
+ ReleaseModel expectedRelease = new ReleaseModel(
"v3.0.0",
URI.create("https://github.com/qupath/qupath"),
List.of(URI.create("https://github.com/qupath/qupath")),
null,
null,
- new VersionRange("v1.2.0", null, null)
+ new VersionRangeModel("v1.2.0", null, null)
);
- Extension extension = new Extension(
+ ExtensionModel extension = new ExtensionModel(
"",
"",
"",
URI.create("https://github.com/qupath/qupath"),
false,
List.of(
- new Release(
+ new ReleaseModel(
"v1.0.0",
URI.create("https://github.com/qupath/qupath"),
List.of(URI.create("https://github.com/qupath/qupath")),
null,
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
),
- new Release(
+ new ReleaseModel(
"v2.0.0",
URI.create("https://github.com/qupath/qupath"),
List.of(URI.create("https://github.com/qupath/qupath")),
null,
null,
- new VersionRange("v1.1.0", null, null)
+ new VersionRangeModel("v1.1.0", null, null)
),
expectedRelease
)
);
String version = "v2.0.0";
- Release release = extension.getMaxCompatibleRelease(version).orElse(null);
+ ReleaseModel release = extension.getMaxCompatibleRelease(version).orElse(null);
Assertions.assertEquals(expectedRelease, release);
}
@Test
void Check_Max_Compatible_Release_When_Zero_Compatible() {
- Extension extension = new Extension(
+ ExtensionModel extension = new ExtensionModel(
"",
"",
"",
URI.create("https://github.com/qupath/qupath"),
false,
List.of(
- new Release(
+ new ReleaseModel(
"v1.0.0",
URI.create("https://github.com/qupath/qupath"),
List.of(URI.create("https://github.com/qupath/qupath")),
null,
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
),
- new Release(
+ new ReleaseModel(
"v2.0.0",
URI.create("https://github.com/qupath/qupath"),
List.of(URI.create("https://github.com/qupath/qupath")),
null,
null,
- new VersionRange("v1.1.0", null, null)
+ new VersionRangeModel("v1.1.0", null, null)
),
- new Release(
+ new ReleaseModel(
"v3.0.0",
URI.create("https://github.com/qupath/qupath"),
List.of(URI.create("https://github.com/qupath/qupath")),
null,
null,
- new VersionRange("v1.2.0", null, null)
+ new VersionRangeModel("v1.2.0", null, null)
)
)
);
String version = "v0.0.1";
- Optional release = extension.getMaxCompatibleRelease(version);
+ Optional release = extension.getMaxCompatibleRelease(version);
Assertions.assertTrue(release.isEmpty());
}
@Test
void Check_Max_Compatible_Release_When_Invalid_Version() {
- Extension extension = new Extension(
+ ExtensionModel extension = new ExtensionModel(
"",
"",
"",
URI.create("https://github.com/qupath/qupath"),
false,
List.of(
- new Release(
+ new ReleaseModel(
"v1.0.0",
URI.create("https://github.com/qupath/qupath"),
List.of(URI.create("https://github.com/qupath/qupath")),
null,
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
),
- new Release(
+ new ReleaseModel(
"v2.0.0",
URI.create("https://github.com/qupath/qupath"),
List.of(URI.create("https://github.com/qupath/qupath")),
null,
null,
- new VersionRange("v1.1.0", null, null)
+ new VersionRangeModel("v1.1.0", null, null)
),
- new Release(
+ new ReleaseModel(
"v3.0.0",
URI.create("https://github.com/qupath/qupath"),
List.of(URI.create("https://github.com/qupath/qupath")),
null,
null,
- new VersionRange("v1.2.0", null, null)
+ new VersionRangeModel("v1.2.0", null, null)
)
)
);
@@ -538,36 +538,36 @@ void Check_Max_Compatible_Release_When_Invalid_Version() {
@Test
void Check_Max_Compatible_Release_When_Null_Version() {
- Extension extension = new Extension(
+ ExtensionModel extension = new ExtensionModel(
"",
"",
"",
URI.create("https://github.com/qupath/qupath"),
false,
List.of(
- new Release(
+ new ReleaseModel(
"v1.0.0",
URI.create("https://github.com/qupath/qupath"),
List.of(URI.create("https://github.com/qupath/qupath")),
null,
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
),
- new Release(
+ new ReleaseModel(
"v2.0.0",
URI.create("https://github.com/qupath/qupath"),
List.of(URI.create("https://github.com/qupath/qupath")),
null,
null,
- new VersionRange("v1.1.0", null, null)
+ new VersionRangeModel("v1.1.0", null, null)
),
- new Release(
+ new ReleaseModel(
"v3.0.0",
URI.create("https://github.com/qupath/qupath"),
List.of(URI.create("https://github.com/qupath/qupath")),
null,
null,
- new VersionRange("v1.2.0", null, null)
+ new VersionRangeModel("v1.2.0", null, null)
)
)
);
diff --git a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestRelease.java b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestReleaseModel.java
similarity index 84%
rename from extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestRelease.java
rename to extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestReleaseModel.java
index df984ca..287c62b 100644
--- a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestRelease.java
+++ b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestReleaseModel.java
@@ -1,4 +1,4 @@
-package qupath.ext.extensionmanager.core.catalog;
+package qupath.ext.extensionmanager.core.model;
import com.google.gson.Gson;
import org.junit.jupiter.api.Assertions;
@@ -8,20 +8,20 @@
import java.net.URI;
import java.util.List;
-public class TestRelease {
+public class TestReleaseModel {
@Nested
public class ConstructorTests {
@Test
void Check_Valid_Release() {
- Assertions.assertDoesNotThrow(() -> new Release(
+ Assertions.assertDoesNotThrow(() -> new ReleaseModel(
"v0.1.0",
URI.create("https://github.com/qupath/qupath"),
null,
null,
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
));
}
@@ -29,13 +29,13 @@ void Check_Valid_Release() {
void Check_Undefined_Name() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new Release(
+ () -> new ReleaseModel(
null,
URI.create("https://github.com/qupath/qupath"),
null,
null,
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
)
);
}
@@ -44,13 +44,13 @@ void Check_Undefined_Name() {
void Check_Undefined_Main_Url() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new Release(
+ () -> new ReleaseModel(
"v0.1.0",
null,
null,
null,
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
)
);
}
@@ -59,7 +59,7 @@ void Check_Undefined_Main_Url() {
void Check_Undefined_Version_Range() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new Release(
+ () -> new ReleaseModel(
"v0.1.0",
URI.create("https://github.com/qupath/qupath"),
null,
@@ -74,13 +74,13 @@ void Check_Undefined_Version_Range() {
void Check_Invalid_Name() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new Release(
+ () -> new ReleaseModel(
"invalid_version",
URI.create("https://github.com/qupath/qupath"),
null,
null,
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
)
);
}
@@ -89,13 +89,13 @@ void Check_Invalid_Name() {
void Check_Invalid_Main_Url() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new Release(
+ () -> new ReleaseModel(
"v0.1.0",
URI.create("https://qupath.readthedocs.io/"),
null,
null,
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
)
);
}
@@ -103,13 +103,13 @@ void Check_Invalid_Main_Url() {
@Test
void Check_Valid_Required_Dependency_Url() {
Assertions.assertDoesNotThrow(
- () -> new Release(
+ () -> new ReleaseModel(
"v0.1.0",
URI.create("https://github.com/qupath/qupath/"),
List.of(URI.create("https://maven.scijava.org/content")),
null,
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
)
);
}
@@ -118,13 +118,13 @@ void Check_Valid_Required_Dependency_Url() {
void Check_Invalid_Required_Dependency_Url() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new Release(
+ () -> new ReleaseModel(
"v0.1.0",
URI.create("https://github.com/qupath/qupath/"),
List.of(URI.create("https://qupath.readthedocs.io/")),
null,
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
)
);
}
@@ -132,13 +132,13 @@ void Check_Invalid_Required_Dependency_Url() {
@Test
void Check_Valid_Optional_Dependency_Url() {
Assertions.assertDoesNotThrow(
- () -> new Release(
+ () -> new ReleaseModel(
"v0.1.0",
URI.create("https://github.com/qupath/qupath/"),
null,
List.of(URI.create("https://maven.scijava.org/content")),
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
)
);
}
@@ -147,13 +147,13 @@ void Check_Valid_Optional_Dependency_Url() {
void Check_Invalid_Optional_Dependency_Url() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new Release(
+ () -> new ReleaseModel(
"v0.1.0",
URI.create("https://github.com/qupath/qupath/"),
null,
List.of(URI.create("https://qupath.readthedocs.io/")),
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
)
);
}
@@ -161,13 +161,13 @@ void Check_Invalid_Optional_Dependency_Url() {
@Test
void Check_Valid_Javadoc_Url() {
Assertions.assertDoesNotThrow(
- () -> new Release(
+ () -> new ReleaseModel(
"v0.1.0",
URI.create("https://github.com/qupath/qupath/"),
null,
null,
List.of(URI.create("https://maven.scijava.org/content")),
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
)
);
}
@@ -176,13 +176,13 @@ void Check_Valid_Javadoc_Url() {
void Check_Invalid_Javadoc_Url() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new Release(
+ () -> new ReleaseModel(
"v0.1.0",
URI.create("https://github.com/qupath/qupath/"),
null,
null,
List.of(URI.create("https://qupath.readthedocs.io/")),
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
)
);
}
@@ -193,16 +193,16 @@ public class JsonTests {
@Test
void Check_Valid_Release() {
- Release expectedRelease = new Release(
+ ReleaseModel expectedRelease = new ReleaseModel(
"v0.1.0",
URI.create("https://github.com/qupath/qupath"),
null,
null,
null,
- new VersionRange("v1.0.0", null, null)
+ new VersionRangeModel("v1.0.0", null, null)
);
- Release release = new Gson().fromJson("""
+ ReleaseModel release = new Gson().fromJson("""
{
"name": "v0.1.0",
"mainUrl": "https://github.com/qupath/qupath",
@@ -212,7 +212,7 @@ void Check_Valid_Release() {
}
"""
,
- Release.class
+ ReleaseModel.class
);
Assertions.assertEquals(expectedRelease, release);
@@ -230,7 +230,7 @@ void Check_Undefined_Name() {
}
}
""",
- Release.class
+ ReleaseModel.class
)
);
}
@@ -247,7 +247,7 @@ void Check_Undefined_Main_Url() {
}
}
""",
- Release.class
+ ReleaseModel.class
)
);
}
@@ -262,7 +262,7 @@ void Check_Undefined_Version_Range() {
"mainUrl": "https://github.com/qupath/qupath"
}
""",
- Release.class
+ ReleaseModel.class
)
);
}
@@ -280,7 +280,7 @@ void Check_Invalid_Name() {
}
}
""",
- Release.class
+ ReleaseModel.class
)
);
}
@@ -296,7 +296,7 @@ void Check_Invalid_Version_Range() {
"versionRange": {}
}
""",
- Release.class
+ ReleaseModel.class
)
);
}
@@ -314,7 +314,7 @@ void Check_Invalid_Main_Url() {
}
}
""",
- Release.class
+ ReleaseModel.class
)
);
}
@@ -323,7 +323,7 @@ void Check_Invalid_Main_Url() {
void Check_Valid_Required_Dependency_Url() {
List expectedRequiredDependencyUrls = List.of(URI.create("https://maven.scijava.org/content"));
- Release release = new Gson().fromJson("""
+ ReleaseModel release = new Gson().fromJson("""
{
"name": "v0.1.0",
"mainUrl": "https://github.com/qupath/qupath",
@@ -334,7 +334,7 @@ void Check_Valid_Required_Dependency_Url() {
}
"""
,
- Release.class
+ ReleaseModel.class
);
Assertions.assertEquals(expectedRequiredDependencyUrls, release.requiredDependencyUrls());
@@ -354,7 +354,7 @@ void Check_Invalid_Required_Dependency_Url() {
}
}
""",
- Release.class
+ ReleaseModel.class
)
);
}
@@ -363,7 +363,7 @@ void Check_Invalid_Required_Dependency_Url() {
void Check_Valid_Optional_Dependency_Url() {
List expectedOptionalDependencyUrls = List.of(URI.create("https://maven.scijava.org/content"));
- Release release = new Gson().fromJson("""
+ ReleaseModel release = new Gson().fromJson("""
{
"name": "v0.1.0",
"mainUrl": "https://github.com/qupath/qupath",
@@ -374,7 +374,7 @@ void Check_Valid_Optional_Dependency_Url() {
}
"""
,
- Release.class
+ ReleaseModel.class
);
Assertions.assertEquals(expectedOptionalDependencyUrls, release.optionalDependencyUrls());
@@ -394,7 +394,7 @@ void Check_Invalid_Optional_Dependency_Url() {
}
}
""",
- Release.class
+ ReleaseModel.class
)
);
}
@@ -403,7 +403,7 @@ void Check_Invalid_Optional_Dependency_Url() {
void Check_Valid_Javadoc_Url() {
List expectedJavadocUrls = List.of(URI.create("https://maven.scijava.org/content"));
- Release release = new Gson().fromJson("""
+ ReleaseModel release = new Gson().fromJson("""
{
"name": "v0.1.0",
"mainUrl": "https://github.com/qupath/qupath",
@@ -414,7 +414,7 @@ void Check_Valid_Javadoc_Url() {
}
"""
,
- Release.class
+ ReleaseModel.class
);
Assertions.assertEquals(expectedJavadocUrls, release.javadocUrls());
@@ -434,7 +434,7 @@ void Check_Invalid_Javadoc_Url() {
}
}
""",
- Release.class
+ ReleaseModel.class
)
);
}
diff --git a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestVersionRange.java b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestVersionRangeModel.java
similarity index 84%
rename from extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestVersionRange.java
rename to extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestVersionRangeModel.java
index c3a297d..9942073 100644
--- a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestVersionRange.java
+++ b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestVersionRangeModel.java
@@ -1,4 +1,4 @@
-package qupath.ext.extensionmanager.core.catalog;
+package qupath.ext.extensionmanager.core.model;
import com.google.gson.Gson;
import org.junit.jupiter.api.Assertions;
@@ -7,14 +7,14 @@
import java.util.List;
-public class TestVersionRange {
+public class TestVersionRangeModel {
@Nested
public class ConstructorTests {
@Test
void Check_Valid_Version_Range() {
- Assertions.assertDoesNotThrow(() -> new VersionRange(
+ Assertions.assertDoesNotThrow(() -> new VersionRangeModel(
"v1.1.0",
null,
null
@@ -25,7 +25,7 @@ void Check_Valid_Version_Range() {
void Check_Undefined_Name() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new VersionRange(
+ () -> new VersionRangeModel(
null,
null,
null
@@ -35,7 +35,7 @@ void Check_Undefined_Name() {
@Test
void Check_Valid_With_Max() {
- Assertions.assertDoesNotThrow(() -> new VersionRange(
+ Assertions.assertDoesNotThrow(() -> new VersionRangeModel(
"v1.1.0",
"v2.0.0",
null
@@ -46,7 +46,7 @@ void Check_Valid_With_Max() {
void Check_Invalid_When_Max_Lower_Than_Min() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new VersionRange(
+ () -> new VersionRangeModel(
"v2.0.0",
"v1.1.0",
null
@@ -56,7 +56,7 @@ void Check_Invalid_When_Max_Lower_Than_Min() {
@Test
void Check_Valid_With_Excluded() {
- Assertions.assertDoesNotThrow(() -> new VersionRange(
+ Assertions.assertDoesNotThrow(() -> new VersionRangeModel(
"v1.1.0",
null,
List.of("v1.3.0", "v2.0.0")
@@ -67,7 +67,7 @@ void Check_Valid_With_Excluded() {
void Check_Invalid_When_Excluded_Lower_Than_Min() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new VersionRange(
+ () -> new VersionRangeModel(
"v2.0.0",
null,
List.of("v1.3.0", "v2.0.0")
@@ -79,7 +79,7 @@ void Check_Invalid_When_Excluded_Lower_Than_Min() {
void Check_Invalid_When_Excluded_Higher_Than_Max() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new VersionRange(
+ () -> new VersionRangeModel(
"v1.0.0",
"v1.1.0",
List.of("v1.3.0", "v2.0.0")
@@ -91,7 +91,7 @@ void Check_Invalid_When_Excluded_Higher_Than_Max() {
void Check_Invalid_Min_Version() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new VersionRange(
+ () -> new VersionRangeModel(
"invalid_version",
null,
null
@@ -103,7 +103,7 @@ void Check_Invalid_Min_Version() {
void Check_Invalid_Max_Version() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new VersionRange(
+ () -> new VersionRangeModel(
"v1.0.0",
"invalid_version",
null
@@ -115,7 +115,7 @@ void Check_Invalid_Max_Version() {
void Check_Invalid_Excluded_Version() {
Assertions.assertThrows(
IllegalArgumentException.class,
- () -> new VersionRange(
+ () -> new VersionRangeModel(
"v1.0.0",
null,
List.of("v1.3.0", "invalid_version")
@@ -129,14 +129,14 @@ public class JsonTests {
@Test
void Check_Valid_Version_Range() {
- VersionRange expectedVersionRange = new VersionRange("v1.1.0", null, null);
+ VersionRangeModel expectedVersionRange = new VersionRangeModel("v1.1.0", null, null);
- VersionRange versionRange = new Gson().fromJson("""
+ VersionRangeModel versionRange = new Gson().fromJson("""
{
"min": "v1.1.0"
}
""",
- VersionRange.class
+ VersionRangeModel.class
);
Assertions.assertEquals(expectedVersionRange, versionRange);
@@ -149,7 +149,7 @@ void Check_Undefined_Name() {
() -> new Gson().fromJson("""
{}
""",
- VersionRange.class
+ VersionRangeModel.class
)
);
}
@@ -158,13 +158,13 @@ void Check_Undefined_Name() {
void Check_Max() {
String expectedMax = "v2.0.0";
- VersionRange versionRange = new Gson().fromJson("""
+ VersionRangeModel versionRange = new Gson().fromJson("""
{
"min": "v1.1.0",
"max": "v2.0.0"
}
""",
- VersionRange.class
+ VersionRangeModel.class
);
Assertions.assertEquals(expectedMax, versionRange.max());
@@ -180,7 +180,7 @@ void Check_Invalid_When_Max_Lower_Than_Min() {
"max": "v1.1.0"
}
""",
- VersionRange.class
+ VersionRangeModel.class
)
);
}
@@ -189,13 +189,13 @@ void Check_Invalid_When_Max_Lower_Than_Min() {
void Check_Excluded() {
List expectedExcluded = List.of("v1.3.0", "v2.0.0");
- VersionRange versionRange = new Gson().fromJson("""
+ VersionRangeModel versionRange = new Gson().fromJson("""
{
"min": "v1.1.0",
"excludes": ["v1.3.0", "v2.0.0"]
}
""",
- VersionRange.class
+ VersionRangeModel.class
);
Assertions.assertEquals(expectedExcluded, versionRange.excludes());
@@ -211,7 +211,7 @@ void Check_Invalid_When_Excluded_Lower_Than_Min() {
"excludes": ["v1.3.0", "v2.0.0"]
}
""",
- VersionRange.class
+ VersionRangeModel.class
)
);
}
@@ -227,7 +227,7 @@ void Check_Invalid_When_Excluded_Higher_Than_Max() {
"excludes": ["v1.3.0", "v2.0.0"]
}
""",
- VersionRange.class
+ VersionRangeModel.class
)
);
}
@@ -241,7 +241,7 @@ void Check_Invalid_Min_Version() {
"min": "invalid_version"
}
""",
- VersionRange.class
+ VersionRangeModel.class
)
);
}
@@ -256,7 +256,7 @@ void Check_Invalid_Max_Version() {
"max": "invalid_version"
}
""",
- VersionRange.class
+ VersionRangeModel.class
)
);
}
@@ -271,7 +271,7 @@ void Check_Invalid_Excluded_Version() {
"excludes": ["v1.3.0", "invalid_version"]
}
""",
- VersionRange.class
+ VersionRangeModel.class
)
);
}
@@ -279,7 +279,7 @@ void Check_Invalid_Excluded_Version() {
@Test
void Check_Version_Compatibility_When_Version_Null() {
- VersionRange versionRange = new VersionRange(
+ VersionRangeModel versionRange = new VersionRangeModel(
"v0.1.0",
"v1.0.0",
List.of("v0.1.1", "v0.2.0", "v1.0.0")
@@ -293,7 +293,7 @@ void Check_Version_Compatibility_When_Version_Null() {
@Test
void Check_Version_Compatibility_When_Invalid_Version() {
- VersionRange versionRange = new VersionRange(
+ VersionRangeModel versionRange = new VersionRangeModel(
"v0.1.0",
"v1.0.0",
List.of("v0.1.1", "v0.2.0", "v1.0.0")
@@ -308,7 +308,7 @@ void Check_Version_Compatibility_When_Invalid_Version() {
@Test
void Check_Compatible_Version() {
- VersionRange versionRange = new VersionRange(
+ VersionRangeModel versionRange = new VersionRangeModel(
"v0.1.0",
"v1.0.0",
List.of("v0.1.1", "v0.2.0", "v1.0.0")
@@ -322,7 +322,7 @@ void Check_Compatible_Version() {
@Test
void Check_Compatible_Version_When_Minor_Not_Specified_For_Min() {
- VersionRange versionRange = new VersionRange(
+ VersionRangeModel versionRange = new VersionRangeModel(
"v0.1",
"v1.0.0",
List.of("v0.1.1", "v0.2.0", "v1.0.0")
@@ -336,7 +336,7 @@ void Check_Compatible_Version_When_Minor_Not_Specified_For_Min() {
@Test
void Check_Compatible_Version_When_Minor_And_Patch_Not_Specified_For_Min() {
- VersionRange versionRange = new VersionRange(
+ VersionRangeModel versionRange = new VersionRangeModel(
"v0",
"v1.0.0",
List.of("v0.1.1", "v0.2.0", "v1.0.0")
@@ -350,7 +350,7 @@ void Check_Compatible_Version_When_Minor_And_Patch_Not_Specified_For_Min() {
@Test
void Check_Compatible_Version_When_Minor_Not_Specified_For_Max() {
- VersionRange versionRange = new VersionRange(
+ VersionRangeModel versionRange = new VersionRangeModel(
"v0.1.0",
"v1.0",
List.of("v0.1.1", "v0.2.0", "v1.0.0")
@@ -364,7 +364,7 @@ void Check_Compatible_Version_When_Minor_Not_Specified_For_Max() {
@Test
void Check_Compatible_Version_When_Minor_And_Patch_Not_Specified_For_Max() {
- VersionRange versionRange = new VersionRange(
+ VersionRangeModel versionRange = new VersionRangeModel(
"v0.1.0",
"v1",
List.of("v0.1.1", "v0.2.0", "v1.0.0")
@@ -378,7 +378,7 @@ void Check_Compatible_Version_When_Minor_And_Patch_Not_Specified_For_Max() {
@Test
void Check_Incompatible_Version_Because_Of_Min() {
- VersionRange versionRange = new VersionRange(
+ VersionRangeModel versionRange = new VersionRangeModel(
"v0.1.0",
"v1.0.0",
List.of("v0.1.1", "v0.2.0", "v1.0.0")
@@ -392,7 +392,7 @@ void Check_Incompatible_Version_Because_Of_Min() {
@Test
void Check_Incompatible_Version_Because_Of_Max() {
- VersionRange versionRange = new VersionRange(
+ VersionRangeModel versionRange = new VersionRangeModel(
"v0.1.0",
"v1.0.0",
List.of("v0.1.1", "v0.2.0", "v1.0.0")
@@ -406,7 +406,7 @@ void Check_Incompatible_Version_Because_Of_Max() {
@Test
void Check_Incompatible_Version_Because_Of_Excluded() {
- VersionRange versionRange = new VersionRange(
+ VersionRangeModel versionRange = new VersionRangeModel(
"v0.1.0",
"v1.0.0",
List.of("v0.1.1", "v0.2.0", "v1.0.0")
diff --git a/extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/catalog/invalid_catalog.json b/extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/model/invalid_catalog.json
similarity index 100%
rename from extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/catalog/invalid_catalog.json
rename to extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/model/invalid_catalog.json
diff --git a/extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/catalog/valid_catalog.json b/extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/model/valid_catalog.json
similarity index 100%
rename from extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/catalog/valid_catalog.json
rename to extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/model/valid_catalog.json
From b34e4f8d69f58c014d88bdea74e0a3e3f88fad3d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?L=C3=A9o=20Leplat?=
<60394504+Rylern@users.noreply.github.com>
Date: Mon, 5 Jan 2026 15:35:57 +0000
Subject: [PATCH 2/8] Adapt tests
---
.../core/ExtensionCatalogManager.java | 102 +-
.../core/ExtensionFolderManager.java | 33 +-
.../core/catalog/Catalog.java | 42 +-
.../core/catalog/Extension.java | 20 +-
.../core/catalog/Release.java | 2 +
.../core/catalog/UpdateAvailable.java | 13 +-
.../core/model/VersionRangeModel.java | 2 -
.../core/tools/FilesWatcher.java | 2 +-
.../core/tools/RecursiveDirectoryWatcher.java | 21 +-
.../extensionmanager/gui/CatalogManager.java | 27 +-
.../gui/ExtensionManager.java | 10 +-
.../gui/catalog/CatalogPane.java | 2 +-
.../gui/catalog/ExtensionLine.java | 2 +-
.../catalog/ExtensionModificationWindow.java | 8 +-
.../ext/extensionmanager/strings.properties | 2 +
.../extensionmanager/strings_fr.properties | 2 +
.../ext/extensionmanager/SimpleServer.java | 9 +-
.../ext/extensionmanager/TestUtils.java | 58 +-
.../core/TestExtensionCatalogManager.java | 1524 +++++++++++++++++
...TestExtensionModelCatalogModelManager.java | 1374 ---------------
.../core/catalog/TestCatalog.java | 386 +++++
.../core/catalog/TestExtension.java | 397 +++++
.../core/catalog/TestRelease.java | 102 ++
.../core/catalog/TestUpdateAvailable.java | 44 +
.../core/model/TestExtensionModel.java | 213 ---
.../core/model/TestVersionRangeModel.java | 32 +-
.../core/registry/TestRegistry.java | 74 +
.../core/registry/TestRegistryCatalog.java | 101 ++
.../core/registry/TestRegistryExtension.java | 31 +
.../core/tools/TestFilesWatcher.java | 12 +-
.../ext/extensionmanager/core/catalog.json | 2 +
.../core/catalog/catalog.json | 26 +
.../core/registry/catalog1.json | 26 +
.../core/registry/catalog2.json | 26 +
34 files changed, 2967 insertions(+), 1760 deletions(-)
create mode 100644 extensionmanager/src/test/java/qupath/ext/extensionmanager/core/TestExtensionCatalogManager.java
delete mode 100644 extensionmanager/src/test/java/qupath/ext/extensionmanager/core/TestExtensionModelCatalogModelManager.java
create mode 100644 extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestCatalog.java
create mode 100644 extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestExtension.java
create mode 100644 extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestRelease.java
create mode 100644 extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestUpdateAvailable.java
create mode 100644 extensionmanager/src/test/java/qupath/ext/extensionmanager/core/registry/TestRegistry.java
create mode 100644 extensionmanager/src/test/java/qupath/ext/extensionmanager/core/registry/TestRegistryCatalog.java
create mode 100644 extensionmanager/src/test/java/qupath/ext/extensionmanager/core/registry/TestRegistryExtension.java
create mode 100644 extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/catalog/catalog.json
create mode 100644 extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/registry/catalog1.json
create mode 100644 extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/registry/catalog2.json
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 8d0ae5e..b3761b8 100644
--- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/ExtensionCatalogManager.java
+++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/ExtensionCatalogManager.java
@@ -82,10 +82,10 @@ private enum Operation {
/**
* Create the extension catalog manager.
*
- * @param extensionDirectoryPath an observable value pointing to the path the extension directory should have. The
- * path can be null or invalid (but not the observable). If this observable is changed,
- * catalogs and extensions will be set to the content of the new value of the observable
- * (so will be reset if the new path is empty)
+ * @param extensionsDirectoryPath an observable value pointing to the path the extensions directory should have. The
+ * path can be null or invalid (but not the observable). If this observable is changed,
+ * catalogs and extensions will be set to the content of the new value of the observable
+ * (so will be reset if the new path is empty)
* @param parentClassLoader the class loader that should be the parent of the extension class loader. Can be null to use
* the bootstrap class loader
* @param version a text describing the release of the current software with the form "v[MAJOR].[MINOR].[PATCH]" or
@@ -96,12 +96,12 @@ private enum Operation {
* @throws NullPointerException if one of the parameters (except the class loader) is null
*/
public ExtensionCatalogManager(
- ObservableValue extensionDirectoryPath,
+ ObservableValue extensionsDirectoryPath,
ClassLoader parentClassLoader,
String version,
List defaultCatalogs
) {
- this.extensionFolderManager = new ExtensionFolderManager(extensionDirectoryPath);
+ this.extensionFolderManager = new ExtensionFolderManager(extensionsDirectoryPath);
this.extensionClassLoader = new ExtensionClassLoader(parentClassLoader);
this.version = new Version(version);
@@ -127,20 +127,20 @@ public Version getVersion() {
}
/**
- * @return an observable value containing the path to the extension folder. It may be updated from any thread and the
+ * @return an observable value containing the path to the extensions folder. It may be updated from any thread and the
* path (but not the observable) can be null or invalid
*/
- public ObservableValue getExtensionDirectory() {
- return extensionFolderManager.getExtensionDirectoryPath();
+ public ObservableValue getExtensionsDirectory() {
+ return extensionFolderManager.getExtensionsDirectoryPath();
}
/**
- * Get the path to the directory containing the provided catalog.
+ * Get the path to the directory containing the provided catalog. It may not exist.
*
* @param catalogName the name of the catalog to retrieve
* @return the path of the directory containing the provided catalog
* @throws InvalidPathException if the path cannot be created
- * @throws NullPointerException if the provided catalog is null or if the path contained in {@link #getExtensionDirectory()}
+ * @throws NullPointerException if the provided catalog is null or if the path contained in {@link #getExtensionsDirectory()}
* is null
*/
public Path getCatalogDirectory(String catalogName) {
@@ -155,7 +155,7 @@ public Path getCatalogDirectory(String catalogName) {
* @param catalog the catalog to add. It must have a different name from the ones returned by {@link #getCatalogs()}
* @throws IllegalArgumentException if a catalog with the same name already exists
* @throws IOException if an I/O error occurs while saving the catalogs to disk
- * @throws NullPointerException if the path contained in {@link #getExtensionDirectory()} is null or if the provided
+ * @throws NullPointerException if the path contained in {@link #getExtensionsDirectory()} is null or if the provided
* catalog is null
* @throws InvalidPathException if the path to the registry containing the list of catalogs cannot be created
* @throws ExecutionException if an error occurred while saving the registry
@@ -199,18 +199,25 @@ public ObservableList getCatalogs() {
* by this platform or recursively delete it if extension are asked to be removed. If this operation fails, no exception
* is thrown.
*
+ * If the provided catalog does not belong to {@link #getCatalogs()}, nothing happens.
+ *
* This operation may take some time, but can be interrupted.
*
* @param catalog the catalog to remove
- * @throws IllegalArgumentException if the provided catalog is not {@link RegistryCatalog#deletable()}
+ * @throws IllegalArgumentException if the provided catalog is not {@link RegistryCatalog#deletable() deletable}
* @throws IOException if an I/O error occurs while removing the catalog from disk
- * @throws NullPointerException if the path contained in {@link #getExtensionDirectory()} is null or if the provided
+ * @throws NullPointerException if the path contained in {@link #getExtensionsDirectory()} is null or if the provided
* catalog is null
* @throws InvalidPathException if the path to the registry containing the list of catalogs cannot be created
* @throws ExecutionException if an error occurred while saving the registry
* @throws InterruptedException if the calling thread is interrupted
*/
public synchronized void removeCatalog(Catalog catalog) throws IOException, ExecutionException, InterruptedException {
+ if (catalogs.stream().noneMatch(catalog::equals)) {
+ logger.debug("{} was asked to be removed, but does not belong to {}. Doing nothing", catalog, catalogs);
+ return;
+ }
+
if (!catalog.isDeletable()) {
throw new IllegalArgumentException(String.format("Cannot delete %s: this catalog is not deletable", catalog));
}
@@ -244,7 +251,7 @@ public synchronized void removeCatalog(Catalog catalog) throws IOException, Exec
* @param extensionName the name of the extension to retrieve
* @return the path to the folder containing the provided extension
* @throws InvalidPathException if the path cannot be created
- * @throws NullPointerException if one of the parameters is null or if the path contained in {@link #getExtensionDirectory()}
+ * @throws NullPointerException if one of the parameters is null or if the path contained in {@link #getExtensionsDirectory()}
* is null
*/
public Path getExtensionDirectory(String catalogName, String extensionName) {
@@ -253,23 +260,25 @@ public Path getExtensionDirectory(String catalogName, String extensionName) {
/**
* Get the list of links the {@link #installOrUpdateExtension(Catalog, Extension, Release, boolean, Consumer, BiConsumer)}
- * function will download to install the provided extension.
+ * function will download to install the provided release.
*
- * @param catalogName the name of the catalog owning the extension to install
- * @param extensionName the name of the extension to install
* @param release the release of the extension to install
* @param installOptionalDependencies whether to install optional dependencies
* @return the list URIs that will be downloaded to install the extension with the provided parameters
* @throws InvalidPathException if a path cannot be created, for example because the extensions folder path contain
* invalid characters
- * @throws NullPointerException if one of the parameters is null or if the path contained in {@link #getExtensionDirectory()}
+ * @throws NullPointerException if one of the parameters is null or if the path contained in {@link #getExtensionsDirectory()}
* is null
*/
- public List getDownloadLinks(String catalogName, String extensionName, Release release, boolean installOptionalDependencies) {
+ public List getDownloadLinks(Release release, boolean installOptionalDependencies) {
try {
- return getDownloadUrlsToFilePaths(catalogName, extensionName, release, installOptionalDependencies, false).stream()
- .map(UriFileName::uri)
- .toList();
+ return getDownloadUrlsToFilePaths(
+ "catalog", // The catalog and extension names parameters are only used to create the file
+ "extension", // paths, which are not considered here
+ release,
+ installOptionalDependencies,
+ false
+ ).stream().map(UriFileName::uri).toList();
} catch (IOException e) {
// IOException only occurs if directories are created, which is not the case here, so this should never be called
throw new RuntimeException(e);
@@ -285,6 +294,8 @@ public List getDownloadLinks(String catalogName, String extensionName, Rele
* Warning: If the extension already exists, this function will attempt to move to trash the directory returned by
* {@link #getExtensionDirectory(String, String)} or recursively delete it if moving files to trash is not supported.
* If this operation fails, no exception is thrown.
+ *
+ * Note that this function attempts to install the provided release even if it is not compatible with {@link #getVersion()}.
*
* @param catalog the catalog owning the extension to install/update. It must be one of {@link #getCatalogs()}
* @param extension the extension to install/update. It must belong to the provided catalog
@@ -299,7 +310,7 @@ public List getDownloadLinks(String catalogName, String extensionName, Rele
* will be called from the calling thread
* @throws IllegalArgumentException if the provided catalog does not belong to {@link #getCatalogs()}, if the provided
* extension does not belong to the provided catalog, or if the provided release does not belong to the provided extension
- * @throws NullPointerException if one of the parameters is null or if the path contained in {@link #getExtensionDirectory()}
+ * @throws NullPointerException if one of the parameters is null or if the path contained in {@link #getExtensionsDirectory()}
* 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
@@ -355,9 +366,8 @@ public synchronized void installOrUpdateExtension(
}
/**
- * @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 function of this class. This
+ * list can be updated from any thread
*/
public ObservableList getCatalogManagedInstalledJars() {
return catalogManagedInstalledJarsImmutable;
@@ -413,7 +423,7 @@ public synchronized CompletableFuture> getAvailableUpdates
* @param extension the extension to uninstall. It must belong to the provided catalog
* @throws IllegalArgumentException if the provided catalog does not belong to {@link #getCatalogs()}, or if the provided
* extension does not belong to the provided catalog
- * @throws NullPointerException if one of the parameters is null or if the path contained in {@link #getExtensionDirectory()}
+ * @throws NullPointerException if one of the parameters is null or if the path contained in {@link #getExtensionsDirectory()}
* is null
* @throws IOException if an I/O error occurred while deleting the extension
* @throws InvalidPathException if a path cannot be created, for example because the extensions folder path contain
@@ -477,31 +487,33 @@ public ObservableList getManuallyInstalledJars() {
}
private void resetCatalogsAndJars(List defaultCatalogs) {
+ List