diff --git a/build.gradle.kts b/build.gradle.kts index 4098653..b2b60a5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1 +1 @@ -version = "1.0.2" +version = "1.1.0" 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..8080b41 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.DefaultCatalog; 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 DefaultCatalog( "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..5126331 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,21 @@ 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.DefaultCatalog; +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 +27,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 +54,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,360 +83,293 @@ 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 - * (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 "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 extensionsDirectoryPath, ClassLoader parentClassLoader, String version, - Registry defaultRegistry + List defaultCatalogs ) { - Version.isValid(version, true); - - this.extensionFolderManager = new ExtensionFolderManager(extensionDirectoryPath); + this.extensionFolderManager = new ExtensionFolderManager(extensionsDirectoryPath); 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); - }); + List copyOfDefaultCatalogs = List.copyOf(defaultCatalogs); // make sure the list is not modified later + resetCatalogsAndJars(copyOfDefaultCatalogs); + extensionFolderManager.getCatalogsDirectoryPath().addListener((p, o, n) -> + resetCatalogsAndJars(copyOfDefaultCatalogs) + ); 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 extensions 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 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 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 #getExtensionsDirectory()} + * 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 #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 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. + * Remove the provided catalog from the list of saved catalogs. *

- * Catalogs that are not deletable (see {@link SavedCatalog#deletable()}) won't be deleted. + * 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. *

- * If an exception occurs (see below), the provided catalogs are not added. + * If the provided catalog does not belong to {@link #getCatalogs()}, nothing happens. *

- * 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() deletable} + * @throws IOException if an I/O error occurs while removing the catalog from disk + * @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 void removeCatalogs(List savedCatalogs, boolean removeExtensions) throws IOException { - if (getExtensionDirectoryPath().get() == null) { - throw new NullPointerException("The extension directory path is null"); - } - - 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"); + 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; } - this.savedCatalogs.removeAll(catalogsToRemove); + if (!catalog.isDeletable()) { + throw new IllegalArgumentException(String.format("Cannot delete %s: this catalog is not deletable", catalog)); + } + 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 #getExtensionsDirectory()} + * 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)} - * function will download to install the provided extension. + * Get the list of links the {@link #installOrUpdateExtension(Catalog, Extension, Release, boolean, Consumer, BiConsumer)} + * function will download to install the provided release. * - * @param savedCatalog the catalog owning the extension to install - * @param extension the extension to install - * @param installationInformation what to install on the extension + * @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 #getExtensionsDirectory()} + * 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(Release release, boolean installOptionalDependencies) { + try { + 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); + } } /** * 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. + *

+ * Note that this function attempts to install the provided release even if it is not compatible with {@link #getVersion()}. * - * @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 #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 * 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()); + logger.info("{} of {} installed", extension, catalog); } /** - * 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)) - ); - } - - /** - * @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; @@ -476,49 +402,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 #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 + * 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,27 +488,56 @@ public ObservableList getManuallyInstalledJars() { return extensionFolderManager.getManuallyInstalledJars(); } - private synchronized void setCatalogsFromRegistry() { - this.savedCatalogs.clear(); - + private synchronized void resetCatalogsAndJars(List defaultCatalogs) { + List catalogs; try { - this.savedCatalogs.addAll(extensionFolderManager.getSavedRegistry().catalogs()); - logger.debug("Catalogs set from saved registry"); + catalogs = extensionFolderManager.getSavedRegistry().catalogs(); + this.catalogs.setAll(catalogs.stream() + .map(Catalog::new) + .toList() + ); } catch (Exception e) { - logger.debug("Error while retrieving saved registry. Using default one", e); + logger.debug("Cannot retrieve saved registry. Using default catalogs {}", defaultCatalogs, e); - if (defaultRegistry != null) { - this.savedCatalogs.addAll(defaultRegistry.catalogs()); - logger.debug( - "Catalogs {} set from default registry", - defaultRegistry.catalogs().stream().map(SavedCatalog::name).toList() - ); - } + catalogs = List.of(); + this.catalogs.setAll(defaultCatalogs.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); } } + private void loadJars() { + addJars(extensionFolderManager.getManuallyInstalledJars()); + extensionFolderManager.getManuallyInstalledJars().addListener((ListChangeListener) change -> { + while (change.next()) { + addJars(change.getAddedSubList()); + } + change.reset(); + }); + + addJars(catalogManagedInstalledJarsImmutable); + catalogManagedInstalledJarsImmutable.addListener((ListChangeListener) change -> { + while (change.next()) { + addJars(change.getAddedSubList()); + } + change.reset(); + }); + } + private void updateCatalogManagedInstalledJarsOfDirectory(Path directory, Operation operation) { - if (directory != null) { + if (directory != null && directory.toFile().exists()) { try (Stream files = Files.walk(directory)) { List jars = files.filter(path -> path.toString().endsWith(".jar")).toList(); @@ -572,115 +559,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) change -> { - while (change.next()) { - addJars(change.getAddedSubList()); - removeJars(change.getRemoved()); - } - change.reset(); - }); - - addJars(catalogManagedInstalledJarsImmutable); - catalogManagedInstalledJarsImmutable.addListener((ListChangeListener) 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,54 +675,13 @@ 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 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); } } } - - private void removeJars(List jarPaths) { - for (Path path: jarPaths) { - extensionClassLoader.removeJar(path); - } - } } 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..7a20443 100644 --- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/ExtensionClassLoader.java +++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/ExtensionClassLoader.java @@ -2,6 +2,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import qupath.ext.extensionmanager.core.tools.FileTools; import java.net.MalformedURLException; import java.net.URL; @@ -21,15 +22,13 @@ class ExtensionClassLoader extends URLClassLoader { private static final Logger logger = LoggerFactory.getLogger(ExtensionClassLoader.class); - private final Set filenamesAdded = new HashSet<>(); + private final Set addedJars = new HashSet<>(); private final List runnables = new ArrayList<>(); /** * 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 +38,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 */ @@ -50,15 +47,16 @@ public void addJar(Path jarPath) throws MalformedURLException { addURL(jarPath.toUri().toURL()); logger.debug("File {} loaded by extension class loader", jarPath); - String filename = jarPath.getFileName().toString(); - if (filenamesAdded.contains(filename)) { + String jarName = jarPath.getFileName().toString(); + String nameWithoutVersion = FileTools.stripVersionFromFileName(jarName); + if (addedJars.contains(nameWithoutVersion)) { logger.warn( - "A JAR file with the same file name ({}) was already added to this class loader. {} will probably not be loaded", - filename, - jarPath + "A JAR file with the same name ({}) was already added to this class loader. {} will probably not be loaded", + nameWithoutVersion, + jarName ); } - filenamesAdded.add(filename); + addedJars.add(nameWithoutVersion); } List runnables; @@ -76,21 +74,7 @@ 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 - * - * @param jarPath the path of the JAR file to unload - * @throws NullPointerException if the provided path is null - */ - public synchronized void removeJar(Path jarPath) { - filenamesAdded.remove(jarPath.getFileName().toString()); - } - - /** - * 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..9775acc 100644 --- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/ExtensionFolderManager.java +++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/ExtensionFolderManager.java @@ -1,18 +1,12 @@ package qupath.ext.extensionmanager.core; import com.google.gson.Gson; -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 +18,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 +69,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 extensionsDirectoryPath; private final ObservableValue catalogsDirectoryPath; private final FilesWatcher manuallyInstalledExtensionsWatcher; /** @@ -114,15 +105,15 @@ 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 extensionsDirectoryPath 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) { - this.extensionDirectoryPath = extensionDirectoryPath; - this.catalogsDirectoryPath = extensionDirectoryPath.map(path -> { + public ExtensionFolderManager(ObservableValue extensionsDirectoryPath) { + this.extensionsDirectoryPath = extensionsDirectoryPath; + this.catalogsDirectoryPath = extensionsDirectoryPath.map(path -> { if (path == null) { return null; } @@ -149,11 +140,11 @@ public ExtensionFolderManager(ReadOnlyObjectProperty extensionDirectoryPat }); this.manuallyInstalledExtensionsWatcher = new FilesWatcher( - extensionDirectoryPath, + extensionsDirectoryPath, isJar, path -> { try { - return path.equals(extensionDirectoryPath.get().resolve(CATALOGS_FOLDER)); + return path.equals(extensionsDirectoryPath.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 +159,22 @@ 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 InvalidPathException if the path to the registry cannot be created + * @throws RuntimeException if the registry file exists but contain a malformed JSON element */ - 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,198 +182,140 @@ 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 NullPointerException if the path contained in {@link #getCatalogsDirectoryPath()} is null or if the provided + * registry is null * @throws InvalidPathException if the path to the registry cannot be created */ public synchronized void saveRegistry(Registry registry) throws IOException { try ( - 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 getExtensionsDirectoryPath() { + return extensionsDirectoryPath; + } + + /** + * @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 #getExtensionsDirectoryPath()} + * 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. + * Get the path to the directory containing the provided catalog. It may not exist. * - * @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 * @throws IOException if an I/O error occurs while creating the folder - * @throws java.nio.file.InvalidPathException if the Path object cannot be created, for example because the extensions + * @throws InvalidPathException if the Path object cannot be created, for example because the extensions * folder path contain invalid characters * @throws NullPointerException if the path contained in {@link #getCatalogsDirectoryPath()} is null or if one of the * provided parameters is null */ public synchronized Path getExtensionPath( - SavedCatalog savedCatalog, - 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 +335,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..ce76c7d 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,182 @@ 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. + * 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 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) + * @throws NullPointerException if one of the provided parameters is null + */ + public Catalog(String name, String description, URI uri, URI rawUri) { + this.name = Objects.requireNonNull(name); + this.description = Objects.requireNonNull(description); + this.uri = Objects.requireNonNull(uri); + this.rawUri = Objects.requireNonNull(rawUri); + this.deletable = false; + this.registryExtensions = List.of(); + } + + /** + * Create a non-deletable catalog from a default catalog. + * + * @param defaultCatalog the list of attributes the catalog should have + */ + public Catalog(DefaultCatalog defaultCatalog) { + this(defaultCatalog.name(), defaultCatalog.description(), defaultCatalog.uri(), defaultCatalog.rawUri()); + } + + /** + * 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 + * @throws NullPointerException if one of the provided parameters is null + */ + 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 + * @throws NullPointerException if the provided parameter is null + */ + 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; + } + + /** + * @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. *

- * It must respect the following requirements: + * Depending on which constructor was used to create this catalog, this function may act differently: *

+ * 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). * - * @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) + * @return a CompletableFuture with the list of extensions this catalog owns (that completes exceptionally if the + * operation failed) */ - public Catalog(String name, String description, List extensions) { - this.name = name; - this.description = description; - this.extensions = extensions == null ? null : Collections.unmodifiableList(extensions); - - checkValidity(); + public synchronized CompletableFuture> getExtensions() { + if (extensions == null) { + extensions = CatalogModelFetcher.getCatalog(rawUri).thenApply(this::createExtensionsFromCatalog); + } + return extensions; } - private void checkValidity() { - Utils.checkField(name, "name", "Catalog"); - Utils.checkField(description, "description", "Catalog"); - Utils.checkField(extensions, "extensions", "Catalog"); + private List createExtensionsFromCatalog(CatalogModel catalog) { + return catalog.extensions().stream() + .map(extension -> { + Optional registryExtension = registryExtensions.stream() + .filter(e -> e.name().equals(extension.name())) + .findAny(); - 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 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/DefaultCatalog.java b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/DefaultCatalog.java new file mode 100644 index 0000000..f8d09ee --- /dev/null +++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/DefaultCatalog.java @@ -0,0 +1,28 @@ +package qupath.ext.extensionmanager.core.catalog; + +import java.net.URI; +import java.util.Objects; + +/** + * Represent a non-deletable catalog. + *

+ * 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}) + */ +public record DefaultCatalog( + String name, + String description, + URI uri, + URI rawUri +) { + public DefaultCatalog { + Objects.requireNonNull(name); + Objects.requireNonNull(description); + Objects.requireNonNull(uri); + Objects.requireNonNull(rawUri); + } +} 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..735cba9 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,179 @@ 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 + * @throws NullPointerException if the provided extension model is null + */ + 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; + } + + /** + * @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; + } /** - * Create an Extension. + * Get an observable value showing the currently installed release of this extension. An empty Optional indicates + * that the extension is not installed. *

- * It must respect the following requirements: - *

+ * 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 an observable value showing the currently installed release of this extension */ - 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 ObservableValue> getInstalledRelease() { + return installedRelease; } /** - * Provide the most up-to-date release compatible with the provided version. + * Get an observable boolean value showing whether optional dependencies are currently installed. + *

+ * This observable may be updated from any thread. + * + * @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 synchronized void uninstallRelease() { + installedRelease.set(Optional.empty()); + optionalDependenciesInstalled.set(false); + } + + /** + * 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 +181,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..7aaba7e 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,109 @@ 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: - *

+ * 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 + * @throws NullPointerException if the provided parameter is null */ - 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 + * @throws NullPointerException if the provided version is null + */ + 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..e28cbc3 --- /dev/null +++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/core/catalog/UpdateAvailable.java @@ -0,0 +1,23 @@ +package qupath.ext.extensionmanager.core.catalog; + +import qupath.ext.extensionmanager.core.Version; + +import java.util.Objects; + +/** + * An object indicating an update available. + *

+ * A {@link RuntimeException} is thrown if one parameter is null. + * + * @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) { + + public UpdateAvailable { + Objects.requireNonNull(extensionName); + Objects.requireNonNull(currentVersion); + Objects.requireNonNull(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 80% 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..9dff6ca 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,12 @@ 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 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 +66,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 +75,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..04742f4 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 @@ -11,6 +11,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Stream; /** @@ -19,6 +22,7 @@ public class FileTools { private static final Logger logger = LoggerFactory.getLogger(FileTools.class); + private static final Pattern VERSION_PATTERN = Pattern.compile("(-\\d+\\.\\d+\\.\\d+)"); private FileTools() { throw new AssertionError("This class is not instantiable."); @@ -30,7 +34,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 +54,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 +83,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 +95,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 @@ -128,6 +126,24 @@ public static boolean isFileParentOfAnotherFile(File possibleParent, File possib return false; } + /** + * Strip the version from a file name. A version is defined as "-x.y.z" where x, y, and z are positive numbers. + * If such version is not found, the input name is returned. + * + * @param fileName the name to strip the version from + * @return the input file name without the potential version + * @throws NullPointerException if the provided file name is null + */ + public static String stripVersionFromFileName(String fileName) { + Matcher versionMatcher = VERSION_PATTERN.matcher(Objects.requireNonNull(fileName)); + + if (versionMatcher.find() && versionMatcher.groupCount() > 0) { + return fileName.replace(versionMatcher.group(1), ""); + } else { + return fileName; + } + } + private static boolean moveToTrash(Desktop desktop, File fileToDelete) { if (SwingUtilities.isEventDispatchThread() || !isWindows()) { // It seems safe to call move to trash from any thread on macOS and Linux 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..0b1846a 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; @@ -116,7 +114,7 @@ private synchronized void setDirectoryWatcher() { files.remove(removedFile); } ); - } catch (IOException | UnsupportedOperationException | SecurityException e) { + } catch (IOException | UnsupportedOperationException e) { logger.debug( "Error when creating files watcher for {}. Files added to this directory won't be detected.", directory, 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..abd86c9 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,23 @@ 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 - * @throws IOException if an I/O error occurs + * @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 or if there is no directory on the provided path * @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( @@ -105,10 +100,22 @@ public RecursiveDirectoryWatcher( Path filename = (Path) event.context(); Path filePath = parentFolder.resolve(filename); + boolean removeFiles = false; if (Files.isDirectory(filePath) && kind == StandardWatchEventKinds.ENTRY_CREATE) { - registerDirectory(filePath); + try { + registerDirectory(filePath); + } catch (IOException e) { + logger.debug( + "Error while registering {} directory. Removing all files belonging to it", + filename + ); + removeFiles = true; + } } if (kind == StandardWatchEventKinds.ENTRY_DELETE) { + removeFiles = true; + } + if (removeFiles) { List filesToRemove = addedFiles.stream() .filter(path -> FileTools.isFileParentOfAnotherFile( filePath.toFile(), @@ -145,7 +152,7 @@ public RecursiveDirectoryWatcher( Thread.currentThread().interrupt(); } catch (ClosedWatchServiceException e) { logger.debug("Service watching {} closed", directoryToWatch, e); - } catch (IOException | NullPointerException | SecurityException e) { + } catch (NullPointerException e) { logger.error("Error when watching directory {}", directoryToWatch, e); } }); 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..90cc636 100644 --- a/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/CatalogManager.java +++ b/extensionmanager/src/main/java/qupath/ext/extensionmanager/gui/CatalogManager.java @@ -8,6 +8,8 @@ import javafx.geometry.Pos; import javafx.scene.control.Alert; import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; @@ -27,17 +29,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 +56,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 +74,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#getExtensionsDirectory()}) 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 +100,7 @@ public CatalogManager( @FXML private void onAddClicked(ActionEvent ignored) { - UiUtils.promptExtensionDirectory(extensionCatalogManager.getExtensionDirectoryPath(), onInvalidExtensionDirectory); + UiUtils.promptExtensionDirectory(extensionCatalogManager.getExtensionsDirectory(), onInvalidExtensionDirectory); String catalogUrl = this.catalogUrl.getText(); if (catalogUrl == null || catalogUrl.isBlank()) { @@ -147,10 +148,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 +162,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 +177,38 @@ 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.setOnAction(event -> { - UiUtils.promptExtensionDirectory(extensionCatalogManager.getExtensionDirectoryPath(), onInvalidExtensionDirectory); - deleteCatalogs(List.of(cellData.getValue())); + button.setDisable(!cellData.getValue().isDeletable()); + button.setOnAction(event -> { + UiUtils.promptExtensionDirectory(extensionCatalogManager.getExtensionsDirectory(), onInvalidExtensionDirectory); + + boolean confirmation = new Dialogs.Builder() + .alertType(Alert.AlertType.CONFIRMATION) + .title(resources.getString("CatalogManager.deletionConfirmation")) + .content(new Label(MessageFormat.format( + resources.getString("CatalogManager.confirmation"), + extensionCatalogManager.getCatalogDirectory(cellData.getValue().getName()) + ))) + .resizable() + .showAndWait() + .orElse(ButtonType.CANCEL).getButtonData() == ButtonBar.ButtonData.OK_DONE; + + if (confirmation) { + 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 +223,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 +242,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 +273,7 @@ private void setDoubleClickHandler() { }); } }); + return row; }); } @@ -271,18 +287,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.getExtensionsDirectory(), 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 +332,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 +378,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 catalogsListener; + private final ListChangeListener 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..c9d4857 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#getExtensionsDirectory()}) 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.getExtensionsDirectory(), onInvalidExtensionDirectory); setCatalogs(); - model.getCatalogs().addListener((ListChangeListener) change -> + model.getCatalogs().addListener((ListChangeListener) 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.getExtensionsDirectory(), onInvalidExtensionDirectory); - Path extensionDirectory = extensionCatalogManager.getExtensionDirectoryPath().get(); + Path extensionDirectory = extensionCatalogManager.getExtensionsDirectory().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.getExtensionsDirectory(), 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..feb3576 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,22 +160,25 @@ 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 bindListInUIThread(List listToUpdate, ObservableList listToListen) { listToUpdate.addAll(listToListen); - listToListen.addListener((ListChangeListener) change -> Platform.runLater(() -> { + ListChangeListener listener = change -> Platform.runLater(() -> { if (Platform.isFxApplicationThread()) { while (change.next()) { if (change.wasAdded()) { listToUpdate.addAll(change.getAddedSubList()); - } else { + } + + if (change.wasRemoved()) { listToUpdate.removeAll(change.getRemoved()); } } @@ -196,33 +190,43 @@ public static void bindListInUIThread(ObservableList listToUpdate, Observ while (change.next()) { if (change.wasAdded()) { listToUpdate.addAll(change.getAddedSubList()); - } else { + } + + if (change.wasRemoved()) { listToUpdate.removeAll(change.getRemoved()); } } 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 bindPropertyInUIThread(WritableValue propertyToUpdate, ObservableValue propertyToListen) { propertyToUpdate.setValue(propertyToListen.getValue()); - propertyToListen.addListener((p, o, n) -> { + ChangeListener 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..4a2b2c8 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#getExtensionsDirectory()}) 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..189f605 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#getExtensionsDirectory()}) 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,17 +258,17 @@ 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) -> { if (error == null) { - Dialogs.showInfoNotification( + Dialogs.showMessageDialog( 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> 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..63c2230 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#getExtensionsDirectory()}) 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.getExtensionsDirectory(), 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,22 @@ 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() - ) + 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 +168,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 +177,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 +203,11 @@ private void onSubmitClicked(ActionEvent ignored) { } Release selectedRelease = release.getSelectionModel().getSelectedItem(); - UiUtils.promptExtensionDirectory(extensionCatalogManager.getExtensionDirectoryPath(), onInvalidExtensionDirectory); + UiUtils.promptExtensionDirectory(extensionCatalogManager.getExtensionsDirectory(), 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 +223,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 +237,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 ); @@ -266,13 +256,13 @@ private void onSubmitClicked(ActionEvent ignored) { executor.execute(() -> { try { + Optional previousRelease = extension.getInstalledRelease().getValue(); + 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) { @@ -285,30 +275,44 @@ private void onSubmitClicked(ActionEvent ignored) { Platform.runLater(() -> { progressWindow.close(); - Dialogs.showInfoNotification( - resources.getString("Catalog.ExtensionModificationWindow.extensionManager"), - MessageFormat.format( - resources.getString("Catalog.ExtensionModificationWindow.installed"), - extension.name(), - release.getSelectionModel().getSelectedItem().name() - ) - ); + + if (previousRelease.isPresent()) { + Dialogs.showMessageDialog( + resources.getString("Catalog.ExtensionModificationWindow.extensionManager"), + MessageFormat.format( + resources.getString("Catalog.ExtensionModificationWindow.removedAndInstalled"), + extension.getName(), + previousRelease.get().getVersion().toString(), + release.getSelectionModel().getSelectedItem().getVersion().toString() + ) + ); + } else { + Dialogs.showInfoNotification( + resources.getString("Catalog.ExtensionModificationWindow.extensionManager"), + MessageFormat.format( + resources.getString("Catalog.ExtensionModificationWindow.installed"), + extension.getName(), + release.getSelectionModel().getSelectedItem().getVersion().toString() + ) + ); + } + close(); }); } catch (Exception e) { 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 +323,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 +349,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..3d1522e 100644 --- a/extensionmanager/src/main/resources/qupath/ext/extensionmanager/strings.properties +++ b/extensionmanager/src/main/resources/qupath/ext/extensionmanager/strings.properties @@ -30,9 +30,10 @@ CatalogManager.browserError = Browser error CatalogManager.cannotOpen = Cannot open "{0}":\n{1} CatalogManager.copyUrl = Copy URL CatalogManager.remove = Remove +CatalogManager.deletionConfirmation = Deletion confirmation +CatalogManager.confirmation = This will attempt to delete the following directory:\n{0}\nContinue? 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 +44,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 @@ -57,7 +60,7 @@ Catalog.ExtensionLine.updateExtension = Update extension Catalog.ExtensionLine.removeExtension = Remove extension Catalog.ExtensionLine.remove = Remove {0}?\nThis will delete the "{1}" directory and all its content. Catalog.ExtensionLine.extensionManager = Extension manager -Catalog.ExtensionLine.removed = {0} removed. +Catalog.ExtensionLine.removed = {0} removed.\n\nYou might need to restart the application to see the changes. Catalog.ExtensionLine.error = Error Catalog.ExtensionLine.cannotDeleteExtension = Cannot delete extension:\n{0} @@ -83,5 +86,6 @@ Catalog.ExtensionModificationWindow.downloading = Downloading {0}... Catalog.ExtensionModificationWindow.extracting = Extracting {0}... Catalog.ExtensionModificationWindow.extensionManager = Extension manager Catalog.ExtensionModificationWindow.installed = {0} {1} installed. +Catalog.ExtensionModificationWindow.removedAndInstalled = {0} {1} removed and {0} {2} installed.\n\nYou might need to restart the application to see the changes. Catalog.ExtensionModificationWindow.installationError = Installation error Catalog.ExtensionModificationWindow.notInstalled = {0} {1} not installed: {2}. \ No newline at end of file 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..2d50c83 100644 --- a/extensionmanager/src/main/resources/qupath/ext/extensionmanager/strings_fr.properties +++ b/extensionmanager/src/main/resources/qupath/ext/extensionmanager/strings_fr.properties @@ -30,9 +30,10 @@ CatalogManager.browserError = Erreur de navigateur CatalogManager.cannotOpen = Impossible d'ouvrir "{0}":\n{1} CatalogManager.copyUrl = Copier l'URL CatalogManager.remove = Supprimer +CatalogManager.deletionConfirmation = Confirmation de suppression +CatalogManager.confirmation = Cette opération tentera de supprimer le dossier suivant :\n{0}\nContinuer ? 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 +44,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 @@ -57,7 +60,7 @@ Catalog.ExtensionLine.updateExtension = Mettre Catalog.ExtensionLine.removeExtension = Supprimer l'extension Catalog.ExtensionLine.remove = Supprimer {0}?\nCela va supprimer le dossier "{1}" et tout son contenu. Catalog.ExtensionLine.extensionManager = Gestionnaire d'extensions -Catalog.ExtensionLine.removed = {0} supprimée. +Catalog.ExtensionLine.removed = {0} supprimée.\n\nVous devrez peut-être redémarrer l'application pour voir les modifications. Catalog.ExtensionLine.error = Erreur Catalog.ExtensionLine.cannotDeleteExtension = Impossible de supprimer l'extension:\n{0} @@ -83,5 +86,6 @@ Catalog.ExtensionModificationWindow.downloading = T Catalog.ExtensionModificationWindow.extracting = Extraction de {0}... Catalog.ExtensionModificationWindow.extensionManager = Gestionnaire d'extensions Catalog.ExtensionModificationWindow.installed = {0} {1} installée. +Catalog.ExtensionModificationWindow.removedAndInstalled = {0} {1} supprimée et {0} {2} installée.\n\nVous devrez peut-être redémarrer l'application pour voir les modifications. Catalog.ExtensionModificationWindow.installationError = Erreur d'installation Catalog.ExtensionModificationWindow.notInstalled = {0} {1} pas installée: {2}. \ No newline at end of file diff --git a/extensionmanager/src/test/java/qupath/ext/extensionmanager/SimpleServer.java b/extensionmanager/src/test/java/qupath/ext/extensionmanager/SimpleServer.java index 92ce407..57a1e7e 100644 --- a/extensionmanager/src/test/java/qupath/ext/extensionmanager/SimpleServer.java +++ b/extensionmanager/src/test/java/qupath/ext/extensionmanager/SimpleServer.java @@ -3,6 +3,7 @@ import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.SimpleFileServer; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.InetSocketAddress; @@ -13,8 +14,7 @@ import java.util.concurrent.atomic.AtomicInteger; /** - * A web server that serves static files. - * It must be {@link #close() closed} once no longer used. + * A web server that serves static files. It must be {@link #close() closed} once no longer used. */ public class SimpleServer implements AutoCloseable { @@ -35,7 +35,6 @@ public record FileToServe(String name, InputStream content) {} * * @param files the files to serve * @throws IOException if the files cannot be copied - * @throws SecurityException if the user doesn't have enough rights to create a temporary directory * @throws java.nio.file.InvalidPathException if a path to a file cannot be created * @throws java.io.UncheckedIOException if an I/O error occurs */ @@ -62,8 +61,8 @@ public void close() { /** * Get the link from where this server serves the file with the provided name. - * The link is not guaranteed to work if the provided file is not served by this - * server. + *

    + * The link is not guaranteed to work if the provided file is not served by this server. * * @param fileName the name of the file that should be served from the given URI * @return the URI pointing to the provided file diff --git a/extensionmanager/src/test/java/qupath/ext/extensionmanager/TestUtils.java b/extensionmanager/src/test/java/qupath/ext/extensionmanager/TestUtils.java index f1f2ab5..67ad1d2 100644 --- a/extensionmanager/src/test/java/qupath/ext/extensionmanager/TestUtils.java +++ b/extensionmanager/src/test/java/qupath/ext/extensionmanager/TestUtils.java @@ -1,6 +1,8 @@ package qupath.ext.extensionmanager; import org.junit.jupiter.api.Assertions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Collection; @@ -9,23 +11,69 @@ */ public class TestUtils { + private static final Logger logger = LoggerFactory.getLogger(TestUtils.class); + private static final long TIME_BETWEEN_ATTEMPTS_MS = 200; private TestUtils() { throw new AssertionError("This class is not instantiable."); } /** - * Assert that two lists are equal without taking the order - * of elements into account. - * This function doesn't work if some duplicates are present in one - * of the list. + * Assert that two collections are equal without taking the order of elements into account. + *

    + * Warning: this function doesn't work if some duplicates are present in one of the collection. * * @param expectedCollection the expected values * @param actualCollection the actual values * @param the type of the elements of the collection */ - public static void assertCollectionsEqualsWithoutOrder(Collection expectedCollection, Collection actualCollection) { + public static void assertCollectionsEqualsWithoutOrder( + Collection expectedCollection, + Collection actualCollection + ) { Assertions.assertEquals(expectedCollection.size(), actualCollection.size()); + Assertions.assertTrue(expectedCollection.containsAll(actualCollection)); Assertions.assertTrue(actualCollection.containsAll(expectedCollection)); } + + /** + * Assert that two collections are equal without taking the order of elements into account. This method will retry the + * assertion for a specified waiting period if the collections are not immediately equal. + *

    + * Warning: this function doesn't work if some duplicates are present in one of the collection. + * + * @param expectedCollection the expected values + * @param actualCollection the actual values + * @param waitingTimeMs the maximum time (in milliseconds) to wait for the collections to become equal + * @param the type of the elements of the collection + * @throws InterruptedException if the calling thread is interrupted + */ + public static void assertCollectionsEqualsWithoutOrder( + Collection expectedCollection, + Collection actualCollection, + int waitingTimeMs + ) throws InterruptedException { + long startTime = System.currentTimeMillis(); + long time = System.currentTimeMillis(); + + while (time < startTime + waitingTimeMs) { + try { + assertCollectionsEqualsWithoutOrder(expectedCollection, actualCollection); + return; + } catch (Throwable e) { + logger.debug( + "Expected: {} and actual: {} are not equal. Waiting {} and attempting to compare again", + expectedCollection, + actualCollection, + TIME_BETWEEN_ATTEMPTS_MS + ); + + Thread.sleep(TIME_BETWEEN_ATTEMPTS_MS); + + time = System.currentTimeMillis(); + } + } + + assertCollectionsEqualsWithoutOrder(expectedCollection, actualCollection); + } } diff --git a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/TestExtensionCatalogManager.java b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/TestExtensionCatalogManager.java index d55be27..61208dc 100644 --- a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/TestExtensionCatalogManager.java +++ b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/TestExtensionCatalogManager.java @@ -2,29 +2,33 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; 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.Catalog; +import qupath.ext.extensionmanager.core.catalog.DefaultCatalog; 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.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.model.CatalogModel; +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.catalog.UpdateAvailable; import java.io.IOException; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Consumer; import java.util.stream.Stream; /** @@ -36,8 +40,7 @@ public class TestExtensionCatalogManager { private static final String CATALOG_NAME = "catalog.json"; - private static final int CHANGE_WAITING_TIME_CATALOG_MS = 100; - private static final int CHANGE_WAITING_TIME_MANUAL_MS = 10000; + private static final int JAR_LOADED_TIMEOUT_MS = 10000; private static SimpleServer server; @BeforeAll @@ -56,897 +59,1045 @@ static void stopServer() { } @Test - void Check_Creation_When_Extension_Directory_Null() { + void Check_Creation_When_Extensions_Directory_Observable_Null() { + ObservableValue extensionsDirectory = null; + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = "v1.2.3"; + List defaultCatalogs = createDefaultCatalogs(); + + Assertions.assertThrows( + NullPointerException.class, + () -> new ExtensionCatalogManager(extensionsDirectory, classLoader, version, defaultCatalogs) + ); + } + + @Test + void Check_Creation_When_Extensions_Directory_Null() { + ObservableValue extensionsDirectory = new SimpleObjectProperty<>(null); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = "v1.2.3"; + List defaultCatalogs = createDefaultCatalogs(); + Assertions.assertDoesNotThrow(() -> { ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - new SimpleObjectProperty<>(null), - TestExtensionCatalogManager.class.getClassLoader(), - "v1.2.3", - createSampleRegistry() + extensionsDirectory, + classLoader, + version, + defaultCatalogs ); extensionCatalogManager.close(); }); } @Test - void Check_Creation_When_Extension_Directory_Property_Null() { - Assertions.assertThrows( - NullPointerException.class, - () -> { - ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - null, - TestExtensionCatalogManager.class.getClassLoader(), - "v1.2.3", - createSampleRegistry() - ); - extensionCatalogManager.close(); - } - ); - } + void Check_Creation_When_Parent_Class_Loader_Null() throws IOException { + ObservableValue extensionsDirectory = new SimpleObjectProperty<>(Files.createTempDirectory(null)); + ClassLoader classLoader = null; + String version = "v1.2.3"; + List defaultCatalogs = createDefaultCatalogs(); - @Test - void Check_Creation_When_Parent_Class_Loader_Null() { Assertions.assertDoesNotThrow(() -> { ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - new SimpleObjectProperty<>(Files.createTempDirectory(null)), - null, - "v1.2.3", - createSampleRegistry() + extensionsDirectory, + classLoader, + version, + defaultCatalogs ); extensionCatalogManager.close(); }); } @Test - void Check_Creation_When_Version_Null() { + void Check_Creation_When_Version_Null() throws IOException { + ObservableValue extensionsDirectory = new SimpleObjectProperty<>(Files.createTempDirectory(null)); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = null; + List defaultCatalogs = createDefaultCatalogs(); + Assertions.assertThrows( NullPointerException.class, - () -> { - ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - new SimpleObjectProperty<>(Files.createTempDirectory(null)), - TestExtensionCatalogManager.class.getClassLoader(), - null, - createSampleRegistry() - ); - extensionCatalogManager.close(); - } + () -> new ExtensionCatalogManager(extensionsDirectory, classLoader, version, defaultCatalogs) ); } @Test - void Check_Creation_When_Version_Not_Valid() { + void Check_Creation_When_Version_Not_Valid() throws IOException { + ObservableValue extensionsDirectory = new SimpleObjectProperty<>(Files.createTempDirectory(null)); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = "invalid_version"; + List defaultCatalogs = createDefaultCatalogs(); + Assertions.assertThrows( IllegalArgumentException.class, - () -> { - ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - new SimpleObjectProperty<>(Files.createTempDirectory(null)), - TestExtensionCatalogManager.class.getClassLoader(), - "invalid_version", - createSampleRegistry() - ); - extensionCatalogManager.close(); - } + () -> new ExtensionCatalogManager(extensionsDirectory, classLoader, version, defaultCatalogs) ); } @Test - void Check_Creation_When_Default_Registry_Null() { - Assertions.assertDoesNotThrow(() -> { - ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - new SimpleObjectProperty<>(Files.createTempDirectory(null)), - TestExtensionCatalogManager.class.getClassLoader(), - "v1.2.3", - null - ); - extensionCatalogManager.close(); - }); + void Check_Creation_When_Default_Catalogs_Null() throws IOException { + ObservableValue extensionsDirectory = new SimpleObjectProperty<>(Files.createTempDirectory(null)); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = "v1.2.3"; + List defaultCatalogs = null; + + Assertions.assertThrows( + NullPointerException.class, + () -> new ExtensionCatalogManager(extensionsDirectory, classLoader, version, defaultCatalogs) + ); } @Test - void Check_Extension_Directory_Path() throws Exception { - Path expectedExtensionDirectory = Files.createTempDirectory(null); + void Check_Version() throws Exception { + ObservableValue extensionsDirectory = new SimpleObjectProperty<>(Files.createTempDirectory(null)); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + Version expectedVersion = new Version("v1.2.3"); + List defaultCatalogs = createDefaultCatalogs(); try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - new SimpleObjectProperty<>(expectedExtensionDirectory), - TestExtensionCatalogManager.class.getClassLoader(), - "v1.2.3", - createSampleRegistry() + extensionsDirectory, + classLoader, + expectedVersion.toString(), + defaultCatalogs )) { - Path extensionDirectory = extensionCatalogManager.getExtensionDirectoryPath().get(); + Version version = extensionCatalogManager.getVersion(); - Assertions.assertEquals(expectedExtensionDirectory, extensionDirectory); + Assertions.assertEquals(expectedVersion, version); } } @Test - void Check_Extension_Directory_Path_After_Changed() throws Exception { - Path firstExtensionDirectory = Files.createTempDirectory(null); - Path expectedExtensionDirectory = Files.createTempDirectory(null); - ObjectProperty extensionDirectoryProperty = new SimpleObjectProperty<>(firstExtensionDirectory); + void Check_Extensions_Directory_Path() throws Exception { + Path expectedExtensionsDirectory = Files.createTempDirectory(null); + ObservableValue extensionsDirectory = new SimpleObjectProperty<>(expectedExtensionsDirectory); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = "v1.2.3"; + List defaultCatalogs = createDefaultCatalogs(); try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - extensionDirectoryProperty, - TestExtensionCatalogManager.class.getClassLoader(), - "v1.2.3", - createSampleRegistry() + extensionsDirectory, + classLoader, + version, + defaultCatalogs )) { - extensionDirectoryProperty.set(expectedExtensionDirectory); - Path extensionDirectory = extensionCatalogManager.getExtensionDirectoryPath().get(); + Path extensionsDirectoryValue = extensionCatalogManager.getExtensionsDirectory().getValue(); - Assertions.assertEquals(expectedExtensionDirectory, extensionDirectory); + Assertions.assertEquals(expectedExtensionsDirectory, extensionsDirectoryValue); } } @Test - void Check_Version() throws Exception { - String expectedVersion = "v1.2.4"; + void Check_Extensions_Directory_Path_After_Changed() throws Exception { + ObjectProperty extensionsDirectory = new SimpleObjectProperty<>(Files.createTempDirectory(null)); + Path expectedExtensionsDirectory = Files.createTempDirectory(null); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = "v1.2.3"; + List defaultCatalogs = createDefaultCatalogs(); try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - new SimpleObjectProperty<>(Files.createTempDirectory(null)), - TestExtensionCatalogManager.class.getClassLoader(), - expectedVersion, - createSampleRegistry() + extensionsDirectory, + classLoader, + version, + defaultCatalogs )) { - String version = extensionCatalogManager.getVersion(); + extensionsDirectory.set(expectedExtensionsDirectory); + Path extensionsDirectoryValue = extensionCatalogManager.getExtensionsDirectory().getValue(); - Assertions.assertEquals(expectedVersion, version); + Assertions.assertEquals(expectedExtensionsDirectory, extensionsDirectoryValue); } } @Test - void Check_Catalog_Directory_When_Catalog_Is_Null() throws Exception { + void Check_Catalog_Directory_With_Null_Catalog_Name() throws Exception { + ObservableValue extensionsDirectory = new SimpleObjectProperty<>(Files.createTempDirectory(null)); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = "v1.2.3"; + List defaultCatalogs = createDefaultCatalogs(); + String catalogName = null; try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - new SimpleObjectProperty<>(Files.createTempDirectory(null)), - TestExtensionCatalogManager.class.getClassLoader(), - "v1.2.3", - createSampleRegistry() + extensionsDirectory, + classLoader, + version, + defaultCatalogs )) { Assertions.assertThrows( NullPointerException.class, - () -> extensionCatalogManager.getCatalogDirectory(null) + () -> extensionCatalogManager.getCatalogDirectory(catalogName) + ); + } + } + + @Test + void Check_Catalog_Directory_With_Null_Extensions_Directory() throws Exception { + ObservableValue extensionsDirectory = new SimpleObjectProperty<>(null); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = "v1.2.3"; + List defaultCatalogs = createDefaultCatalogs(); + String catalogName = "catalog"; + try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( + extensionsDirectory, + classLoader, + version, + defaultCatalogs + )) { + Assertions.assertThrows( + NullPointerException.class, + () -> extensionCatalogManager.getCatalogDirectory(catalogName) ); } } @Test void Check_Catalog_Directory_Not_Null() throws Exception { + ObservableValue extensionsDirectory = new SimpleObjectProperty<>(Files.createTempDirectory(null)); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = "v1.2.3"; + List defaultCatalogs = createDefaultCatalogs(); + String catalogName = "catalog"; try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - new SimpleObjectProperty<>(Files.createTempDirectory(null)), - TestExtensionCatalogManager.class.getClassLoader(), - "v1.2.3", - createSampleRegistry() + extensionsDirectory, + classLoader, + version, + defaultCatalogs )) { - Path catalogDirectory = extensionCatalogManager.getCatalogDirectory(new SavedCatalog( - "some name", - "some description", - URI.create("http://test"), - URI.create("http://test"), - true - )); + Path catalogDirectory = extensionCatalogManager.getCatalogDirectory(catalogName); Assertions.assertNotNull(catalogDirectory); } } @Test - void Check_Catalog_Addition_When_Extension_Directory_Null() throws Exception { + void Check_Catalog_With_Existing_Name_Not_Added() throws Exception { + ObservableValue extensionsDirectory = new SimpleObjectProperty<>(Files.createTempDirectory(null)); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = "v1.2.3"; + List defaultCatalogs = createDefaultCatalogs(); + Catalog firstCatalog = new Catalog( + "same name", + "some description", + server.getURI(CATALOG_NAME), + server.getURI(CATALOG_NAME) + ); + Catalog secondCatalog = new Catalog( + "same name", + "some other description", + server.getURI(CATALOG_NAME), + server.getURI(CATALOG_NAME) + ); try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - new SimpleObjectProperty<>(null), - TestExtensionCatalogManager.class.getClassLoader(), - "v1.2.3", - createSampleRegistry() + extensionsDirectory, + classLoader, + version, + defaultCatalogs )) { + extensionCatalogManager.addCatalog(firstCatalog); + Assertions.assertThrows( - NullPointerException.class, - () -> extensionCatalogManager.addCatalog(List.of()) + IllegalArgumentException.class, + () -> extensionCatalogManager.addCatalog(secondCatalog) ); } } @Test - void Check_Catalog_Addition_When_List_Null() throws Exception { + void Check_Catalog_Addition_When_Extensions_Directory_Null() throws Exception { + ObservableValue extensionsDirectory = new SimpleObjectProperty<>(null); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = "v1.2.3"; + List defaultCatalogs = createDefaultCatalogs(); + Catalog catalog = new Catalog( + "name", + "description", + server.getURI(CATALOG_NAME), + server.getURI(CATALOG_NAME) + ); try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - new SimpleObjectProperty<>(Files.createTempDirectory(null)), - TestExtensionCatalogManager.class.getClassLoader(), - "v1.2.3", - createSampleRegistry() + extensionsDirectory, + classLoader, + version, + defaultCatalogs )) { Assertions.assertThrows( NullPointerException.class, - () -> extensionCatalogManager.addCatalog(null) + () -> extensionCatalogManager.addCatalog(catalog) ); } } @Test - void Check_Catalog_Addition_When_One_Null() throws Exception { - List catalogsToAdd = new ArrayList<>(); - catalogsToAdd.add(new SavedCatalog( - "some other name", - "some other description", - URI.create("http://test"), - URI.create("http://test"), - true - )); - catalogsToAdd.add(null); + void Check_Catalog_Addition_When_Null() throws Exception { + ObservableValue extensionsDirectory = new SimpleObjectProperty<>(Files.createTempDirectory(null)); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = "v1.2.3"; + List defaultCatalogs = createDefaultCatalogs(); + Catalog catalog = null; try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - new SimpleObjectProperty<>(Files.createTempDirectory(null)), - TestExtensionCatalogManager.class.getClassLoader(), - "v1.2.3", - createSampleRegistry() + extensionsDirectory, + classLoader, + version, + defaultCatalogs )) { Assertions.assertThrows( NullPointerException.class, - () -> extensionCatalogManager.addCatalog(catalogsToAdd) + () -> extensionCatalogManager.addCatalog(catalog) ); } } @Test void Check_Catalog_Added() throws Exception { - Registry defaultRegistry = createSampleRegistry(); - SavedCatalog catalogToAdd = new SavedCatalog( - "some other name", - "some other description", - URI.create("http://test"), - URI.create("http://test"), - true + ObservableValue extensionsDirectory = new SimpleObjectProperty<>(Files.createTempDirectory(null)); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = "v1.2.3"; + List defaultCatalogs = createDefaultCatalogs(); + Catalog catalog = new Catalog( + "name", + "description", + server.getURI(CATALOG_NAME), + server.getURI(CATALOG_NAME) ); - List expectedCatalogs = Stream.concat( - defaultRegistry.catalogs().stream(), - Stream.of(catalogToAdd) - ).toList(); try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - new SimpleObjectProperty<>(Files.createTempDirectory(null)), - TestExtensionCatalogManager.class.getClassLoader(), - "v1.2.3", - defaultRegistry + extensionsDirectory, + classLoader, + version, + defaultCatalogs )) { - extensionCatalogManager.addCatalog(List.of(catalogToAdd)); + List expectedCatalogs = Stream.concat( + extensionCatalogManager.getCatalogs().stream(), + Stream.of(catalog) + ).toList(); + + extensionCatalogManager.addCatalog(catalog); TestUtils.assertCollectionsEqualsWithoutOrder(expectedCatalogs, extensionCatalogManager.getCatalogs()); } } @Test - void Check_Catalog_Addition_When_Same_Name() throws Exception { - SavedCatalog firstCatalog = new SavedCatalog( - "same name", + void Check_Only_Catalogs_From_Default_Registry_When_Extensions_Directory_Changed() throws Exception { + SimpleObjectProperty extensionsDirectory = new SimpleObjectProperty<>(Files.createTempDirectory(null)); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = "v1.2.3"; + List defaultCatalogs = createDefaultCatalogs(); + int expectedNumberOfCatalogs = defaultCatalogs.size(); + Catalog catalog = new Catalog( + "some name", "some description", - URI.create("http://test"), - URI.create("http://test"), - true + server.getURI(CATALOG_NAME), + server.getURI(CATALOG_NAME) ); - SavedCatalog secondCatalog = new SavedCatalog( - "same name", - "some other description", - URI.create("http://test/other"), - URI.create("http://test/other"), - true + try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( + extensionsDirectory, + classLoader, + version, + defaultCatalogs + )) { + extensionCatalogManager.addCatalog(catalog); + extensionsDirectory.set(Files.createTempDirectory(null)); + + List catalogs = extensionCatalogManager.getCatalogs(); + + Assertions.assertEquals(expectedNumberOfCatalogs, catalogs.size()); + } + } + + @Test + void Check_Undeletable_Catalog_Not_Removed() throws Exception { + SimpleObjectProperty extensionsDirectory = new SimpleObjectProperty<>(Files.createTempDirectory(null)); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = "v1.2.3"; + List defaultCatalogs = createDefaultCatalogs(); + Catalog catalog = new Catalog( + "some name", + "some description", + server.getURI(CATALOG_NAME), + server.getURI(CATALOG_NAME) ); try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - new SimpleObjectProperty<>(Files.createTempDirectory(null)), - TestExtensionCatalogManager.class.getClassLoader(), - "v1.2.3", - null + extensionsDirectory, + classLoader, + version, + defaultCatalogs )) { + extensionCatalogManager.addCatalog(catalog); + Assertions.assertThrows( IllegalArgumentException.class, - () -> extensionCatalogManager.addCatalog(List.of(firstCatalog, secondCatalog)) + () -> extensionCatalogManager.removeCatalog(catalog) ); } } @Test - void Check_Catalog_With_Existing_Name_Not_Added() throws Exception { - SavedCatalog firstCatalog = new SavedCatalog( - "same name", - "some description", - URI.create("http://test"), - URI.create("http://test"), - true - ); - SavedCatalog secondCatalog = new SavedCatalog( - "same name", - "some other description", - URI.create("http://test/other"), - URI.create("http://test/other"), + void Check_Catalog_Removal_When_Catalog_Null() throws Exception { + SimpleObjectProperty extensionsDirectory = new SimpleObjectProperty<>(Files.createTempDirectory(null)); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = "v1.2.3"; + List defaultCatalogs = createDefaultCatalogs(); + Catalog catalog = null; + try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( + extensionsDirectory, + classLoader, + version, + defaultCatalogs + )) { + Assertions.assertThrows( + NullPointerException.class, + () -> extensionCatalogManager.removeCatalog(catalog) + ); + } + } + + @Test + void Check_Catalog_Removed() throws Exception { + SimpleObjectProperty extensionsDirectory = new SimpleObjectProperty<>(Files.createTempDirectory(null)); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = "v1.2.3"; + List defaultCatalogs = createDefaultCatalogs(); + Catalog catalog = new Catalog( + new CatalogModel("some name", "some description", List.of()), + server.getURI(CATALOG_NAME), + server.getURI(CATALOG_NAME), true ); - List expectedCatalogs = List.of(firstCatalog); try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - new SimpleObjectProperty<>(Files.createTempDirectory(null)), - TestExtensionCatalogManager.class.getClassLoader(), - "v1.2.3", - null + extensionsDirectory, + classLoader, + version, + defaultCatalogs )) { - extensionCatalogManager.addCatalog(List.of(firstCatalog)); - extensionCatalogManager.addCatalog(List.of(secondCatalog)); + List expectedCatalogs = List.copyOf(extensionCatalogManager.getCatalogs()); // defensive copy because + extensionCatalogManager.addCatalog(catalog); // extensionCatalogManager.getCatalogs() + // will be modified + extensionCatalogManager.removeCatalog(catalog); TestUtils.assertCollectionsEqualsWithoutOrder(expectedCatalogs, extensionCatalogManager.getCatalogs()); } } @Test - void Check_Only_Catalogs_From_Default_Registry_When_Extension_Directory_Changed() throws Exception { - SimpleObjectProperty extensionDirectory = new SimpleObjectProperty<>(Files.createTempDirectory(null)); - SavedCatalog catalogToAdd = new SavedCatalog( - "some other name", - "some other description", - URI.create("http://test"), - URI.create("http://test"), - true - ); - Registry defaultRegistry = createSampleRegistry(); - List expectedCatalogs = defaultRegistry.catalogs(); + void Check_Extensions_Directory_When_Catalog_Name_Is_Null() throws Exception { + SimpleObjectProperty extensionsDirectory = new SimpleObjectProperty<>(Files.createTempDirectory(null)); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = "v1.2.3"; + List defaultCatalogs = createDefaultCatalogs(); + String catalogName = null; + String extensionName = "extension"; try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - extensionDirectory, - TestExtensionCatalogManager.class.getClassLoader(), - "v1.2.3", - defaultRegistry + extensionsDirectory, + classLoader, + version, + defaultCatalogs )) { - extensionCatalogManager.addCatalog(List.of(catalogToAdd)); + Assertions.assertThrows( + NullPointerException.class, + () -> extensionCatalogManager.getExtensionDirectory(catalogName, extensionName) + ); + } + } - extensionDirectory.set(Files.createTempDirectory(null)); + @Test + void Check_Extensions_Directory_When_Extension_Name_Is_Null() throws Exception { + SimpleObjectProperty extensionsDirectory = new SimpleObjectProperty<>(Files.createTempDirectory(null)); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = "v1.2.3"; + List defaultCatalogs = createDefaultCatalogs(); + String catalogName = "catalog"; + String extensionName = null; + try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( + extensionsDirectory, + classLoader, + version, + defaultCatalogs + )) { + Assertions.assertThrows( + NullPointerException.class, + () -> extensionCatalogManager.getExtensionDirectory(catalogName, extensionName) + ); + } + } - TestUtils.assertCollectionsEqualsWithoutOrder(expectedCatalogs, extensionCatalogManager.getCatalogs()); + @Test + void Check_Extensions_Directory_Not_Null() throws Exception { + SimpleObjectProperty extensionsDirectory = new SimpleObjectProperty<>(Files.createTempDirectory(null)); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = "v1.2.3"; + List defaultCatalogs = createDefaultCatalogs(); + String catalogName = "catalog"; + String extensionName = "extension"; + try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( + extensionsDirectory, + classLoader, + version, + defaultCatalogs + )) { + Assertions.assertNotNull(extensionCatalogManager.getExtensionDirectory(catalogName, extensionName)); } } @Test - void Check_Catalog_Deletion_When_Extension_Directory_Null() throws Exception { + void Check_Download_Links_With_Null_Release() throws Exception { + SimpleObjectProperty extensionsDirectory = new SimpleObjectProperty<>(Files.createTempDirectory(null)); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = "v1.2.3"; + List defaultCatalogs = createDefaultCatalogs(); + Release release = null; + boolean installOptionalDependencies = true; try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - new SimpleObjectProperty<>(null), - TestExtensionCatalogManager.class.getClassLoader(), - "v1.2.3", - createSampleRegistry() + extensionsDirectory, + classLoader, + version, + defaultCatalogs )) { Assertions.assertThrows( NullPointerException.class, - () -> extensionCatalogManager.removeCatalogs(List.of(), true) + () -> extensionCatalogManager.getDownloadLinks(release, installOptionalDependencies) ); } } @Test - void Check_Catalog_Deletion_When_List_Null() throws Exception { + void Check_Download_Links_With_Null_Extensions_Directory() throws Exception { + SimpleObjectProperty extensionsDirectory = new SimpleObjectProperty<>(null); + ClassLoader classLoader = TestExtensionCatalogManager.class.getClassLoader(); + String version = "v1.2.3"; + List defaultCatalogs = createDefaultCatalogs(); + Release release = new Release(new ReleaseModel( + "v0.1.2", + URI.create("https://github.com/main"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v0.0.0", null, null) + )); + boolean installOptionalDependencies = true; try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - new SimpleObjectProperty<>(Files.createTempDirectory(null)), - TestExtensionCatalogManager.class.getClassLoader(), - "v1.2.3", - createSampleRegistry() + extensionsDirectory, + classLoader, + version, + defaultCatalogs )) { Assertions.assertThrows( NullPointerException.class, - () -> extensionCatalogManager.removeCatalogs(null, true) + () -> extensionCatalogManager.getDownloadLinks(release, installOptionalDependencies) ); } } @Test - void Check_Catalog_Deletion_When_One_Null() throws Exception { - List catalogsToRemove = new ArrayList<>(); - catalogsToRemove.add(new SavedCatalog( - "some other name", - "some other description", - URI.create("http://test"), - URI.create("http://test"), - true + void Check_Download_Links_With_Optional_Dependencies() throws Exception { + Release release = new Release(new ReleaseModel( + "v0.1.2", + URI.create("https://github.com/main"), + List.of(URI.create("https://github.com/required1"), URI.create("https://github.com/required2")), + List.of(URI.create("https://github.com/optional1"), URI.create("https://github.com/optional2")), + null, + new VersionRangeModel("v0.0.0", null, null) )); - catalogsToRemove.add(null); + List expectedDownloadLinks = List.of( + URI.create("https://github.com/main"), + URI.create("https://github.com/required1"), + URI.create("https://github.com/required2"), + URI.create("https://github.com/optional1"), + URI.create("https://github.com/optional2") + ); + boolean optionalDependencies = true; try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( new SimpleObjectProperty<>(Files.createTempDirectory(null)), TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - createSampleRegistry() + createDefaultCatalogs() )) { - Assertions.assertThrows( - NullPointerException.class, - () -> extensionCatalogManager.removeCatalogs(catalogsToRemove, true) + List downloadLinks = extensionCatalogManager.getDownloadLinks(release, optionalDependencies); + + TestUtils.assertCollectionsEqualsWithoutOrder( + expectedDownloadLinks, + downloadLinks ); } } @Test - void Check_Catalog_Removed() throws Exception { - Registry defaultRegistry = createSampleRegistry(); - SavedCatalog catalogToAdd = new SavedCatalog( - "some other name", - "some other description", - URI.create("http://test"), - URI.create("http://test"), - true + void Check_Download_Links_Without_Optional_Dependencies() throws Exception { + Release release = new Release(new ReleaseModel( + "v0.1.2", + URI.create("https://github.com/main"), + List.of(URI.create("https://github.com/required1"), URI.create("https://github.com/required2")), + List.of(URI.create("https://github.com/optional1"), URI.create("https://github.com/optional2")), + null, + new VersionRangeModel("v0.0.0", null, null) + )); + List expectedDownloadLinks = List.of( + URI.create("https://github.com/main"), + URI.create("https://github.com/required1"), + URI.create("https://github.com/required2") ); - List expectedCatalogs = defaultRegistry.catalogs(); + boolean optionalDependencies = false; try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( new SimpleObjectProperty<>(Files.createTempDirectory(null)), TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - defaultRegistry + createDefaultCatalogs() )) { - extensionCatalogManager.addCatalog(List.of(catalogToAdd)); - extensionCatalogManager.removeCatalogs(List.of(catalogToAdd), true); + List downloadLinks = extensionCatalogManager.getDownloadLinks(release, optionalDependencies); - TestUtils.assertCollectionsEqualsWithoutOrder(expectedCatalogs, extensionCatalogManager.getCatalogs()); + TestUtils.assertCollectionsEqualsWithoutOrder( + expectedDownloadLinks, + downloadLinks + ); } } @Test - void Check_Undeletable_Catalog_Not_Removed() throws Exception { - Registry defaultRegistry = createSampleRegistry(); - SavedCatalog catalogToAdd = new SavedCatalog( - "some other name", - "some other description", - URI.create("http://test"), - URI.create("http://test"), - false - ); - List expectedCatalogs = Stream.concat( - defaultRegistry.catalogs().stream(), - Stream.of(catalogToAdd) - ).toList(); + void Check_Extension_Installation_When_Unknown_Catalog() throws Exception { try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( new SimpleObjectProperty<>(Files.createTempDirectory(null)), TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - defaultRegistry + createDefaultCatalogs() )) { - extensionCatalogManager.addCatalog(List.of(catalogToAdd)); - extensionCatalogManager.removeCatalogs(List.of(catalogToAdd), true); + Catalog catalog = new Catalog( + "Unknown catalog", + "Description", + server.getURI(CATALOG_NAME), + server.getURI(CATALOG_NAME) + ); + Extension extension = catalog.getExtensions().get().getFirst(); + Release release = extension.getReleases().getFirst(); + boolean optionalDependencies = false; + Consumer onProgress = progress -> {}; + BiConsumer onStatusChanged = (step, res) -> {}; - TestUtils.assertCollectionsEqualsWithoutOrder(expectedCatalogs, extensionCatalogManager.getCatalogs()); + Assertions.assertThrows( + IllegalArgumentException.class, + () -> extensionCatalogManager.installOrUpdateExtension( + catalog, + extension, + release, + optionalDependencies, + onProgress, + onStatusChanged + ) + ); } } @Test - void Check_Extension_Directory_When_Catalog_Is_Null() throws Exception { + void Check_Extension_Installation_When_Extension_Does_Not_Belong_To_Catalog() throws Exception { try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( new SimpleObjectProperty<>(Files.createTempDirectory(null)), TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - createSampleRegistry() + createDefaultCatalogs() )) { + Catalog catalog = extensionCatalogManager.getCatalogs().getFirst(); + Extension extension = new Extension( + new ExtensionModel( + "Unknown extension", + "Description", + "Some author", + URI.create("https://github.com/qupath/qupath-macOS-extension"), + false, + List.of(new ReleaseModel( + "v0.1.0", + URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar"), + null, + null, + null, + new VersionRangeModel("v1.0.0", null, null) + )) + ), + null, + false + ); + Release release = extension.getReleases().getFirst(); + boolean optionalDependencies = false; + Consumer onProgress = progress -> {}; + BiConsumer onStatusChanged = (step, res) -> {}; + Assertions.assertThrows( - NullPointerException.class, - () -> extensionCatalogManager.getExtensionDirectory( - null, - new Extension("", "", "", URI.create("http://github.com"), false, List.of()) + IllegalArgumentException.class, + () -> extensionCatalogManager.installOrUpdateExtension( + catalog, + extension, + release, + optionalDependencies, + onProgress, + onStatusChanged ) ); } } @Test - void Check_Extension_Directory_When_Extension_Is_Null() throws Exception { + void Check_Extension_Installation_When_Release_Does_Not_Belong_To_Extension() throws Exception { try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( new SimpleObjectProperty<>(Files.createTempDirectory(null)), TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - createSampleRegistry() + createDefaultCatalogs() )) { + Catalog catalog = extensionCatalogManager.getCatalogs().getFirst(); + Extension extension = catalog.getExtensions().get().getFirst(); + Release release = new Release(new ReleaseModel( + "v0.4.6", + URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar"), + null, + null, + null, + new VersionRangeModel("v5.0.0", null, null) + )); + boolean optionalDependencies = false; + Consumer onProgress = progress -> {}; + BiConsumer onStatusChanged = (step, res) -> {}; + Assertions.assertThrows( - NullPointerException.class, - () -> extensionCatalogManager.getExtensionDirectory( - new SavedCatalog( - "some name", - "some description", - URI.create("http://test"), - URI.create("http://test"), - true - ), - null + IllegalArgumentException.class, + () -> extensionCatalogManager.installOrUpdateExtension( + catalog, + extension, + release, + optionalDependencies, + onProgress, + onStatusChanged ) ); } } @Test - void Check_Extension_Directory_Not_Null() throws Exception { + void Check_Extension_Installation_With_Null_Catalog() throws Exception { try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( new SimpleObjectProperty<>(Files.createTempDirectory(null)), TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - createSampleRegistry() + createDefaultCatalogs() )) { - Path extensionDirectory = extensionCatalogManager.getExtensionDirectory( - new SavedCatalog( - "some name", - "some description", - URI.create("http://test"), - URI.create("http://test"), - true - ), - new Extension("", "", "", URI.create("http://github.com"), false, List.of()) - ); + Catalog catalog = null; + Extension extension = extensionCatalogManager.getCatalogs().getFirst().getExtensions().get().getFirst(); + Release release = extension.getReleases().getFirst(); + boolean optionalDependencies = false; + Consumer onProgress = progress -> {}; + BiConsumer onStatusChanged = (step, res) -> {}; - Assertions.assertNotNull(extensionDirectory); + Assertions.assertThrows( + NullPointerException.class, + () -> extensionCatalogManager.installOrUpdateExtension( + catalog, + extension, + release, + optionalDependencies, + onProgress, + onStatusChanged + ) + ); } } @Test - void Check_Download_Links() throws Exception { - List expectedDownloadLinks = List.of( - URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar"), - URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar") - ); - SavedCatalog catalog = new SavedCatalog( - "name", - "description", - URI.create("http://test"), - URI.create("http://test"), - true - ); - Release release = new Release( - "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) - ); - InstalledExtension installationInformation = new InstalledExtension(release.name(), true); - Extension extension = new Extension( - "name", - "description", - "author", - URI.create("https://github.com"), - false, - List.of(release) - ); + void Check_Extension_Installation_With_Null_Extension() throws Exception { try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( new SimpleObjectProperty<>(Files.createTempDirectory(null)), TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - createSampleRegistry() + createDefaultCatalogs() )) { - List downloadLinks = extensionCatalogManager.getDownloadLinks(catalog, extension, installationInformation); + Catalog catalog = extensionCatalogManager.getCatalogs().getFirst(); + Extension extension = null; + Release release = catalog.getExtensions().get().getFirst().getReleases().getFirst(); + boolean optionalDependencies = false; + Consumer onProgress = progress -> {}; + BiConsumer onStatusChanged = (step, res) -> {}; - TestUtils.assertCollectionsEqualsWithoutOrder( - expectedDownloadLinks, - downloadLinks + Assertions.assertThrows( + NullPointerException.class, + () -> extensionCatalogManager.installOrUpdateExtension( + catalog, + extension, + release, + optionalDependencies, + onProgress, + onStatusChanged + ) ); } } @Test - void Check_Download_Links_Cannot_Be_Retrieved_When_Desired_Release_Does_Not_Exist() throws Exception { - SavedCatalog catalog = new SavedCatalog( - "name", - "description", - URI.create("http://test"), - URI.create("http://test"), - true - ); - InstalledExtension installationInformation = new InstalledExtension("invalid_release", false); - Extension extension = new Extension( - "name", - "description", - "author", - URI.create("https://github.com"), - false, - List.of() - ); + void Check_Extension_Installation_With_Null_Release() throws Exception { try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( new SimpleObjectProperty<>(Files.createTempDirectory(null)), TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - createSampleRegistry() + createDefaultCatalogs() )) { + Catalog catalog = extensionCatalogManager.getCatalogs().getFirst(); + Extension extension = catalog.getExtensions().get().getFirst(); + Release release = null; + boolean optionalDependencies = false; + Consumer onProgress = progress -> {}; + BiConsumer onStatusChanged = (step, res) -> {}; + Assertions.assertThrows( - IllegalArgumentException.class, - () -> extensionCatalogManager.getDownloadLinks(catalog, extension, installationInformation) + NullPointerException.class, + () -> extensionCatalogManager.installOrUpdateExtension( + catalog, + extension, + release, + optionalDependencies, + onProgress, + onStatusChanged + ) ); } } @Test - void Check_Download_Links_Cannot_Be_Retrieved_When_Extension_Directory_Null() throws Exception { - SavedCatalog catalog = new SavedCatalog( - "name", - "description", - URI.create("http://test"), - URI.create("http://test"), - true - ); - Release release = new Release( - "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) - ); - InstalledExtension installationInformation = new InstalledExtension(release.name(), true); - Extension extension = new Extension( - "name", - "description", - "author", - URI.create("https://github.com"), - false, - List.of(release) - ); + void Check_Extension_Installation_With_Null_Extensions_Directory() throws Exception { try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( new SimpleObjectProperty<>(null), TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - createSampleRegistry() + createDefaultCatalogs() + )) { + Catalog catalog = extensionCatalogManager.getCatalogs().getFirst(); + Extension extension = catalog.getExtensions().get().getFirst(); + Release release = extension.getReleases().getFirst(); + boolean optionalDependencies = false; + Consumer onProgress = progress -> {}; + BiConsumer onStatusChanged = (step, res) -> {}; + + Assertions.assertThrows( + NullPointerException.class, + () -> extensionCatalogManager.installOrUpdateExtension( + catalog, + extension, + release, + optionalDependencies, + onProgress, + onStatusChanged + ) + ); + } + } + + @Test + void Check_Extension_Installation_With_Null_On_Progress() throws Exception { + try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( + new SimpleObjectProperty<>(Files.createTempDirectory(null)), + TestExtensionCatalogManager.class.getClassLoader(), + "v1.2.3", + createDefaultCatalogs() )) { + Catalog catalog = extensionCatalogManager.getCatalogs().getFirst(); + Extension extension = catalog.getExtensions().get().getFirst(); + Release release = extension.getReleases().getFirst(); + boolean optionalDependencies = false; + Consumer onProgress = null; + BiConsumer onStatusChanged = (step, res) -> {}; + Assertions.assertThrows( NullPointerException.class, - () -> extensionCatalogManager.getDownloadLinks(catalog, extension, installationInformation) + () -> extensionCatalogManager.installOrUpdateExtension( + catalog, + extension, + release, + optionalDependencies, + onProgress, + onStatusChanged + ) ); } } @Test - void Check_Extension_Installed() throws Exception { - SavedCatalog catalog = new SavedCatalog( - "name", - "description", - URI.create("http://test"), - URI.create("http://test"), - true - ); - Release release = new Release( - "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) - ); - InstalledExtension installationInformation = new InstalledExtension(release.name(), true); - Extension extension = new Extension( - "name", - "description", - "author", - URI.create("https://github.com"), - false, - List.of(release) - ); + void Check_Extension_Installation_With_Null_On_Status_Changed() throws Exception { + try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( + new SimpleObjectProperty<>(Files.createTempDirectory(null)), + TestExtensionCatalogManager.class.getClassLoader(), + "v1.2.3", + createDefaultCatalogs() + )) { + Catalog catalog = extensionCatalogManager.getCatalogs().getFirst(); + Extension extension = catalog.getExtensions().get().getFirst(); + Release release = extension.getReleases().getFirst(); + boolean optionalDependencies = false; + Consumer onProgress = progress -> {}; + BiConsumer onStatusChanged = null; + + Assertions.assertThrows( + NullPointerException.class, + () -> extensionCatalogManager.installOrUpdateExtension( + catalog, + extension, + release, + optionalDependencies, + onProgress, + onStatusChanged + ) + ); + } + } + + @Test + void Check_Extension_Release_Installed() throws Exception { try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( new SimpleObjectProperty<>(Files.createTempDirectory(null)), TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - createSampleRegistry() + createDefaultCatalogs() )) { + Catalog catalog = extensionCatalogManager.getCatalogs().getFirst(); + Extension extension = catalog.getExtensions().get().getFirst(); + Release release = extension.getReleases().getFirst(); + boolean optionalDependencies = false; + Consumer onProgress = progress -> {}; + BiConsumer onStatusChanged = (step, res) -> {}; + extensionCatalogManager.installOrUpdateExtension( catalog, extension, - installationInformation, - progress -> {}, - (step, resource) -> {} + release, + optionalDependencies, + onProgress, + onStatusChanged ); Assertions.assertEquals( - installationInformation, - extensionCatalogManager.getInstalledExtension(catalog, extension).get().orElse(null) + release, + extension.getInstalledRelease().getValue().orElse(null) ); } } @Test - void Check_Extension_Reinstalled_When_Already_Installed() throws Exception { - SavedCatalog catalog = new SavedCatalog( - "name", - "description", - URI.create("http://test"), - URI.create("http://test"), - true - ); - Release release = new Release( - "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) - ); - InstalledExtension firstInstallationInformation = new InstalledExtension(release.name(), true); - InstalledExtension secondInstallationInformation = new InstalledExtension(release.name(), false); - Extension extension = new Extension( - "name", - "description", - "author", - URI.create("https://github.com"), - false, - List.of(release) - ); + void Check_Extension_Optional_Dependencies_Installed() throws Exception { try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( new SimpleObjectProperty<>(Files.createTempDirectory(null)), TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - createSampleRegistry() + createDefaultCatalogs() )) { + Catalog catalog = extensionCatalogManager.getCatalogs().getFirst(); + Extension extension = catalog.getExtensions().get().getFirst(); + Release release = extension.getReleases().getFirst(); + boolean optionalDependencies = true; + Consumer onProgress = progress -> {}; + BiConsumer onStatusChanged = (step, res) -> {}; + extensionCatalogManager.installOrUpdateExtension( catalog, extension, - firstInstallationInformation, - progress -> {}, - (step, resource) -> {} - ); - extensionCatalogManager.installOrUpdateExtension( - catalog, - extension, - secondInstallationInformation, - progress -> {}, - (step, resource) -> {} + release, + optionalDependencies, + onProgress, + onStatusChanged ); Assertions.assertEquals( - secondInstallationInformation, - extensionCatalogManager.getInstalledExtension(catalog, extension).get().orElse(null) + optionalDependencies, + extension.areOptionalDependenciesInstalled().getValue() ); } } @Test - void Check_Extension_Installation_Fails_When_Desired_Release_Does_Not_Exist() throws Exception { - SavedCatalog catalog = new SavedCatalog( - "name", - "description", - URI.create("http://test"), - URI.create("http://test"), - true - ); - InstalledExtension installationInformation = new InstalledExtension("invalid_release", false); - Extension extension = new Extension( - "name", - "description", - "author", - URI.create("https://github.com"), - false, - List.of() - ); + void Check_Extension_Reinstalled_When_Already_Installed() throws Exception { try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( new SimpleObjectProperty<>(Files.createTempDirectory(null)), TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - createSampleRegistry() + createDefaultCatalogs() )) { - Assertions.assertThrows( - IllegalArgumentException.class, - () -> extensionCatalogManager.installOrUpdateExtension( - catalog, - extension, - installationInformation, - progress -> {}, - (step, resource) -> {} - ) + Catalog catalog = extensionCatalogManager.getCatalogs().getFirst(); + Extension extension = catalog.getExtensions().get().getFirst(); + Release firstRelease = extension.getReleases().get(0); + Release secondRelease = extension.getReleases().get(1); + boolean optionalDependencies = false; + Consumer onProgress = progress -> {}; + BiConsumer onStatusChanged = (step, res) -> {}; + extensionCatalogManager.installOrUpdateExtension( + catalog, + extension, + firstRelease, + optionalDependencies, + onProgress, + onStatusChanged ); - } - } - @Test - void Check_Extension_Installation_Fails_When_Extension_Directory_Null() throws Exception { - SavedCatalog catalog = new SavedCatalog( - "name", - "description", - URI.create("http://test"), - URI.create("http://test"), - true - ); - Release release = new Release( - "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) - ); - InstalledExtension installationInformation = new InstalledExtension(release.name(), true); - Extension extension = new Extension( - "name", - "description", - "author", - URI.create("https://github.com"), - false, - List.of(release) - ); - try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - new SimpleObjectProperty<>(null), - TestExtensionCatalogManager.class.getClassLoader(), - "v1.2.3", - createSampleRegistry() - )) { - Assertions.assertThrows( - NullPointerException.class, - () -> extensionCatalogManager.installOrUpdateExtension( - catalog, - extension, - installationInformation, - progress -> {}, - (step, resource) -> {} - ) + extensionCatalogManager.installOrUpdateExtension( + catalog, + extension, + secondRelease, + optionalDependencies, + onProgress, + onStatusChanged + ); + + Assertions.assertEquals( + secondRelease, + extension.getInstalledRelease().getValue().orElse(null) ); } } @Test - void Check_Extension_Removed_After_Changing_Extension_Directory() throws Exception { - SimpleObjectProperty extensionDirectory = new SimpleObjectProperty<>(Files.createTempDirectory(null)); - SavedCatalog catalog = new SavedCatalog( - "name", - "description", - URI.create("http://test"), - URI.create("http://test"), - true - ); - Release release = new Release( - "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) - ); - Extension extension = new Extension( - "name", - "description", - "author", - URI.create("https://github.com"), - false, - List.of(release) - ); + void Check_Extension_Not_Installed_After_Extensions_Directory_Moved() throws Exception { + SimpleObjectProperty extensionsDirectoryPath = new SimpleObjectProperty<>(Files.createTempDirectory(null)); try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - extensionDirectory, + extensionsDirectoryPath, TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - createSampleRegistry() + createDefaultCatalogs() )) { + Catalog catalog = extensionCatalogManager.getCatalogs().getFirst(); + Extension extension = catalog.getExtensions().get().getFirst(); + Release release = extension.getReleases().getFirst(); + boolean optionalDependencies = false; + Consumer onProgress = progress -> {}; + BiConsumer onStatusChanged = (step, res) -> {}; extensionCatalogManager.installOrUpdateExtension( catalog, extension, - new InstalledExtension(release.name(), true), - progress -> {}, - (step, resource) -> {} + release, + optionalDependencies, + onProgress, + onStatusChanged ); - extensionDirectory.set(Files.createTempDirectory(null)); + extensionsDirectoryPath.set(Files.createTempDirectory(null)); - Assertions.assertNull(extensionCatalogManager.getInstalledExtension(catalog, extension).get().orElse(null)); + // Changing extensionsDirectoryPath reset catalogs and extensions, so the extension needs to be retrieved + Extension newExtension = extensionCatalogManager.getCatalogs().getFirst().getExtensions().get().getFirst(); + Assertions.assertFalse(newExtension.getInstalledRelease().getValue().isPresent()); } } @Test void Check_Installed_Jars_After_Extension_Installation() throws Exception { - SavedCatalog catalog = new SavedCatalog( - "name", - "description", - URI.create("http://test"), - URI.create("http://test"), - true - ); - Release release = new Release( - "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) - ); - List expectedJarNames = List.of("qupath-extension-macos.jar", "qupath-extension-macos.jar"); - InstalledExtension installationInformation = new InstalledExtension(release.name(), true); - Extension extension = new Extension( - "name", - "description", - "author", - URI.create("https://github.com"), - false, - List.of(release) + List expectedJarNames = List.of( + "qupath-extension-macos.jar", // for main URL + "qupath-extension-macos.jar", // for required dependency URL + "qupath-extension-macos.jar" // for optional dependency URL ); try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( new SimpleObjectProperty<>(Files.createTempDirectory(null)), TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - createSampleRegistry() + createDefaultCatalogs() )) { + Catalog catalog = extensionCatalogManager.getCatalogs().getFirst(); + Extension extension = catalog.getExtensions().get().getFirst(); + Release release = extension.getReleases().getFirst(); + boolean optionalDependencies = true; + Consumer onProgress = progress -> {}; + BiConsumer onStatusChanged = (step, res) -> {}; + extensionCatalogManager.installOrUpdateExtension( catalog, extension, - installationInformation, - progress -> {}, - (step, resource) -> {} + release, + optionalDependencies, + onProgress, + onStatusChanged ); - Thread.sleep(CHANGE_WAITING_TIME_CATALOG_MS); // wait for list to update TestUtils.assertCollectionsEqualsWithoutOrder( expectedJarNames, extensionCatalogManager.getCatalogManagedInstalledJars().stream() @@ -957,69 +1108,34 @@ void Check_Installed_Jars_After_Extension_Installation() throws Exception { } } - @Test - void Check_Installed_Jars_After_Extension_Manually_Installed() throws Exception { - Path extensionDirectory = Files.createTempDirectory(null); - Files.createFile(extensionDirectory.resolve("lib.jar")); - List expectedJars = List.of(); - try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( - new SimpleObjectProperty<>(extensionDirectory), - TestExtensionCatalogManager.class.getClassLoader(), - "v1.2.3", - createSampleRegistry() - )) { - TestUtils.assertCollectionsEqualsWithoutOrder( - expectedJars, - extensionCatalogManager.getCatalogManagedInstalledJars() - ); - } - } - @Test void Check_Jar_Loaded_Runnable_Run_After_Extension_Installation() throws Exception { - SavedCatalog catalog = new SavedCatalog( - "name", - "description", - URI.create("http://test"), - URI.create("http://test"), - true - ); - Release release = new Release( - "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) - ); - int expectedNumberOfCalls = 2; - InstalledExtension installationInformation = new InstalledExtension(release.name(), true); - Extension extension = new Extension( - "name", - "description", - "author", - URI.create("https://github.com"), - false, - List.of(release) - ); + int expectedNumberOfCalls = 3; // one jar for main URL, one required dependency, and one optional dependency try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( new SimpleObjectProperty<>(Files.createTempDirectory(null)), TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - createSampleRegistry() + createDefaultCatalogs() )) { - AtomicInteger numberOfJarLoaded = new AtomicInteger(); - extensionCatalogManager.addOnJarLoadedRunnable(numberOfJarLoaded::getAndIncrement); + Catalog catalog = extensionCatalogManager.getCatalogs().getFirst(); + Extension extension = catalog.getExtensions().get().getFirst(); + Release release = extension.getReleases().getFirst(); + boolean optionalDependencies = true; + Consumer onProgress = progress -> {}; + BiConsumer onStatusChanged = (step, res) -> {}; + CountDownLatch countDownLatch = new CountDownLatch(expectedNumberOfCalls); + + extensionCatalogManager.addOnJarLoadedRunnable(countDownLatch::countDown); extensionCatalogManager.installOrUpdateExtension( catalog, extension, - installationInformation, - progress -> {}, - (step, resource) -> {} + release, + optionalDependencies, + onProgress, + onStatusChanged ); - Thread.sleep(CHANGE_WAITING_TIME_CATALOG_MS); // wait for list to update - Assertions.assertEquals(expectedNumberOfCalls, numberOfJarLoaded.get()); + Assertions.assertTrue(countDownLatch.await(JAR_LOADED_TIMEOUT_MS, TimeUnit.MILLISECONDS)); } } @@ -1030,7 +1146,7 @@ void Check_No_Available_Updates_When_No_Extension_Installed() throws Exception { new SimpleObjectProperty<>(Files.createTempDirectory(null)), TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - createSampleRegistry() + createDefaultCatalogs() )) { List updates = extensionCatalogManager.getAvailableUpdates().get(); @@ -1040,23 +1156,27 @@ void Check_No_Available_Updates_When_No_Extension_Installed() throws Exception { @Test 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(); + String version = "v1.2.3"; List expectedUpdates = List.of(); try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( new SimpleObjectProperty<>(Files.createTempDirectory(null)), TestExtensionCatalogManager.class.getClassLoader(), - "v1.2.3", - defaultRegistry + version, + createDefaultCatalogs() )) { + Catalog catalog = extensionCatalogManager.getCatalogs().getFirst(); + Extension extension = catalog.getExtensions().get().getFirst(); + Release release = extension.getReleases().getFirst(); + boolean optionalDependencies = false; + Consumer onProgress = progress -> {}; + BiConsumer onStatusChanged = (step, res) -> {}; extensionCatalogManager.installOrUpdateExtension( catalog, extension, - new InstalledExtension(release.name(), true), - progress -> {}, - (step, resource) -> {} + release, + optionalDependencies, + onProgress, + onStatusChanged ); List updates = extensionCatalogManager.getAvailableUpdates().get(); @@ -1067,27 +1187,31 @@ void Check_No_Available_Updates_When_Incompatible_Version() throws Exception { @Test 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(); + String version = "v2.2.3"; List expectedUpdates = List.of(new UpdateAvailable( "Some extension", - "v0.1.0", - "v1.0.0" + new Version("v0.1.0"), + new Version("v1.0.0") )); // see catalog.json in resources try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( new SimpleObjectProperty<>(Files.createTempDirectory(null)), TestExtensionCatalogManager.class.getClassLoader(), - "v2.2.3", - defaultRegistry + version, + createDefaultCatalogs() )) { + Catalog catalog = extensionCatalogManager.getCatalogs().getFirst(); + Extension extension = catalog.getExtensions().get().getFirst(); + Release release = extension.getReleases().getFirst(); + boolean optionalDependencies = false; + Consumer onProgress = progress -> {}; + BiConsumer onStatusChanged = (step, res) -> {}; extensionCatalogManager.installOrUpdateExtension( catalog, extension, - new InstalledExtension(release.name(), true), - progress -> {}, - (step, resource) -> {} + release, + optionalDependencies, + onProgress, + onStatusChanged ); List updates = extensionCatalogManager.getAvailableUpdates().get(); @@ -1097,132 +1221,170 @@ void Check_Update_Available() throws Exception { } @Test - void Check_Extension_Removed() throws Exception { - SavedCatalog catalog = new SavedCatalog( - "name", - "description", - URI.create("http://test"), - URI.create("http://test"), - true - ); - Release release = new Release( - "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) + void Check_Extension_Removal_When_Unknown_Catalog() throws Exception { + Catalog catalog = new Catalog( + "Unknown catalog", + "Description", + server.getURI(CATALOG_NAME), + server.getURI(CATALOG_NAME) ); - InstalledExtension installationInformation = new InstalledExtension(release.name(), true); + Extension extension = catalog.getExtensions().get().getFirst(); + try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( + new SimpleObjectProperty<>(Files.createTempDirectory(null)), + TestExtensionCatalogManager.class.getClassLoader(), + "v1.2.3", + createDefaultCatalogs() + )) { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> extensionCatalogManager.removeExtension(catalog, extension) + ); + } + } + + @Test + void Check_Extension_Removal_When_Extension_Does_Not_Belong_To_Catalog() throws Exception { + try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( + new SimpleObjectProperty<>(Files.createTempDirectory(null)), + TestExtensionCatalogManager.class.getClassLoader(), + "v1.2.3", + createDefaultCatalogs() + )) { + Catalog catalog = extensionCatalogManager.getCatalogs().getFirst(); + Extension extension = new Extension( + new ExtensionModel( + "Unknown extension", + "Description", + "Some author", + URI.create("https://github.com/qupath/qupath-macOS-extension"), + false, + List.of(new ReleaseModel( + "v0.1.0", + URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar"), + null, + null, + null, + new VersionRangeModel("v1.0.0", null, null) + )) + ), + null, + false + ); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> extensionCatalogManager.removeExtension(catalog, extension) + ); + } + } + + @Test + void Check_Extension_Removal_With_Null_Catalog() throws Exception { + Catalog catalog = null; Extension extension = new Extension( - "name", - "description", - "author", - URI.create("https://github.com"), - false, - List.of(release) + getResourceCatalog().extensions().getFirst(), + null, + false ); try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( new SimpleObjectProperty<>(Files.createTempDirectory(null)), TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - createSampleRegistry() + createDefaultCatalogs() + )) { + Assertions.assertThrows( + NullPointerException.class, + () -> extensionCatalogManager.removeExtension(catalog, extension) + ); + } + } + + @Test + void Check_Extension_Removal_With_Null_Extension() throws Exception { + try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( + new SimpleObjectProperty<>(Files.createTempDirectory(null)), + TestExtensionCatalogManager.class.getClassLoader(), + "v1.2.3", + createDefaultCatalogs() + )) { + Catalog catalog = extensionCatalogManager.getCatalogs().getFirst(); + Extension extension = null; + + Assertions.assertThrows( + NullPointerException.class, + () -> extensionCatalogManager.removeExtension(catalog, extension) + ); + } + } + + @Test + void Check_Extension_Removed() throws Exception { + try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( + new SimpleObjectProperty<>(Files.createTempDirectory(null)), + TestExtensionCatalogManager.class.getClassLoader(), + "v1.2.3", + createDefaultCatalogs() )) { + Catalog catalog = extensionCatalogManager.getCatalogs().getFirst(); + Extension extension = catalog.getExtensions().get().getFirst(); extensionCatalogManager.installOrUpdateExtension( catalog, extension, - installationInformation, + extension.getReleases().getFirst(), + false, progress -> {}, - (step, resource) -> {} + (step, res) -> {} ); extensionCatalogManager.removeExtension(catalog, extension); - Assertions.assertNull(extensionCatalogManager.getInstalledExtension(catalog, extension).get().orElse(null)); + Assertions.assertTrue(extension.getInstalledRelease().getValue().isEmpty()); } } @Test void Check_Extension_Removal_When_Not_Installed() throws Exception { - SavedCatalog catalog = new SavedCatalog( - "name", - "description", - URI.create("http://test"), - URI.create("http://test"), - true - ); - Release release = new Release( - "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) - ); - Extension extension = new Extension( - "name", - "description", - "author", - URI.create("https://github.com"), - false, - List.of(release) - ); try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( new SimpleObjectProperty<>(Files.createTempDirectory(null)), TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - createSampleRegistry() + createDefaultCatalogs() )) { + Catalog catalog = extensionCatalogManager.getCatalogs().getFirst(); + Extension extension = catalog.getExtensions().get().getFirst(); + extensionCatalogManager.removeExtension(catalog, extension); - Assertions.assertNull(extensionCatalogManager.getInstalledExtension(catalog, extension).get().orElse(null)); + Assertions.assertTrue(extension.getInstalledRelease().getValue().isEmpty()); } } @Test void Check_Installed_Jars_After_Extension_Removal() throws Exception { - SavedCatalog catalog = new SavedCatalog( - "name", - "description", - URI.create("http://test"), - URI.create("http://test"), - true - ); - Release release = new Release( - "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) - ); List expectedJarNames = List.of(); - InstalledExtension installationInformation = new InstalledExtension(release.name(), true); - Extension extension = new Extension( - "name", - "description", - "author", - URI.create("https://github.com"), - false, - List.of(release) - ); try (ExtensionCatalogManager extensionCatalogManager = new ExtensionCatalogManager( new SimpleObjectProperty<>(Files.createTempDirectory(null)), TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - createSampleRegistry() + createDefaultCatalogs() )) { + Catalog catalog = extensionCatalogManager.getCatalogs().getFirst(); + Extension extension = catalog.getExtensions().get().getFirst(); + Release release = extension.getReleases().getFirst(); + boolean optionalDependencies = false; + Consumer onProgress = progress -> {}; + BiConsumer onStatusChanged = (step, res) -> {}; extensionCatalogManager.installOrUpdateExtension( catalog, extension, - installationInformation, - progress -> {}, - (step, resource) -> {} + release, + optionalDependencies, + onProgress, + onStatusChanged ); extensionCatalogManager.removeExtension(catalog, extension); - Thread.sleep(CHANGE_WAITING_TIME_CATALOG_MS); // wait for list to update TestUtils.assertCollectionsEqualsWithoutOrder( expectedJarNames, extensionCatalogManager.getCatalogManagedInstalledJars().stream() @@ -1244,7 +1406,7 @@ void Check_Manually_Installed_Jars_When_Two_Jars_Added_Before_Manager_Creation() new SimpleObjectProperty<>(extensionDirectory), TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - createSampleRegistry() + createDefaultCatalogs() )) { List jars = extensionCatalogManager.getManuallyInstalledJars(); @@ -1259,7 +1421,7 @@ void Check_Manually_Installed_Jars_When_Two_Jars_Added_After_Manager_Creation() new SimpleObjectProperty<>(extensionDirectory), TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - createSampleRegistry() + createDefaultCatalogs() )) { List expectedJars = List.of( Files.createFile(extensionDirectory.resolve("lib1.jar")), @@ -1268,8 +1430,11 @@ void Check_Manually_Installed_Jars_When_Two_Jars_Added_After_Manager_Creation() List jars = extensionCatalogManager.getManuallyInstalledJars(); - Thread.sleep(CHANGE_WAITING_TIME_MANUAL_MS); // wait for list to update - TestUtils.assertCollectionsEqualsWithoutOrder(expectedJars, jars); + TestUtils.assertCollectionsEqualsWithoutOrder( + expectedJars, + jars, + JAR_LOADED_TIMEOUT_MS // wait for list to update + ); } } @@ -1285,7 +1450,7 @@ void Check_Manually_Installed_Jars_When_Non_Jar_File_Added() throws Exception { new SimpleObjectProperty<>(extensionDirectory), TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - createSampleRegistry() + createDefaultCatalogs() )) { List jars = extensionCatalogManager.getManuallyInstalledJars(); @@ -1294,31 +1459,7 @@ void Check_Manually_Installed_Jars_When_Non_Jar_File_Added() throws Exception { } @Test - void Check_Manually_Installed_Jars_When_Extension_Installed_With_Index() throws Exception { - SavedCatalog catalog = new SavedCatalog( - "name", - "description", - URI.create("http://test"), - URI.create("http://test"), - true - ); - Release release = new Release( - "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) - ); - InstalledExtension installationInformation = new InstalledExtension(release.name(), true); - Extension extension = new Extension( - "name", - "description", - "author", - URI.create("https://github.com"), - false, - List.of(release) - ); + void Check_Manually_Installed_Jars_When_Extension_Installed_With_Catalog() throws Exception { Path extensionDirectory = Files.createTempDirectory(null); List expectedJars = List.of( Files.createFile(extensionDirectory.resolve("lib1.jar")), @@ -1328,14 +1469,18 @@ void Check_Manually_Installed_Jars_When_Extension_Installed_With_Index() throws new SimpleObjectProperty<>(extensionDirectory), TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - createSampleRegistry() + createDefaultCatalogs() )) { + Catalog catalog = extensionCatalogManager.getCatalogs().getFirst(); + Extension extension = catalog.getExtensions().get().getFirst(); + Release release = extension.getReleases().getFirst(); extensionCatalogManager.installOrUpdateExtension( catalog, extension, - installationInformation, + release, + false, progress -> {}, - (step, resource) -> {} + (step, res) -> {} ); List jars = extensionCatalogManager.getManuallyInstalledJars(); @@ -1354,7 +1499,7 @@ void Check_Manually_Installed_Jars_When_Extension_Directory_Changed() throws Exc extensionDirectory, TestExtensionCatalogManager.class.getClassLoader(), "v1.2.3", - createSampleRegistry() + createDefaultCatalogs() )) { extensionDirectory.set(Files.createTempDirectory(null)); @@ -1362,13 +1507,44 @@ void Check_Manually_Installed_Jars_When_Extension_Directory_Changed() throws Exc } } - private static Registry createSampleRegistry() { - return new Registry(List.of(new SavedCatalog( + private static List createDefaultCatalogs() { + return List.of(new DefaultCatalog( + getResourceCatalog().name(), + getResourceCatalog().description(), + server.getURI(CATALOG_NAME), + server.getURI(CATALOG_NAME) + )); + } + + private static CatalogModel getResourceCatalog() { + return new CatalogModel( "Some catalog", "Some description", - server.getURI(CATALOG_NAME), - server.getURI(CATALOG_NAME), - true - ))); + List.of(new ExtensionModel( + "Some extension", + "Some extension description", + "Some author", + URI.create("https://github.com/qupath/qupath-macOS-extension"), + false, + List.of( + new ReleaseModel( + "v0.1.0", + URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar"), + List.of(URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar")), + List.of(URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar")), + null, + new VersionRangeModel("v1.0.0", null, null) + ), + new ReleaseModel( + "v1.0.0", + URI.create("https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar"), + null, + null, + null, + new VersionRangeModel("v2.0.0", null, null) + ) + ) + )) + ); } } diff --git a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestCatalog.java b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestCatalog.java index 1fef9cb..940ef36 100644 --- a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestCatalog.java +++ b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestCatalog.java @@ -1,202 +1,386 @@ package qupath.ext.extensionmanager.core.catalog; -import com.google.gson.Gson; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import qupath.ext.extensionmanager.SimpleServer; +import qupath.ext.extensionmanager.TestUtils; +import qupath.ext.extensionmanager.core.model.CatalogModel; +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.registry.RegistryCatalog; +import qupath.ext.extensionmanager.core.registry.RegistryExtension; +import java.io.IOException; import java.net.URI; import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutionException; public class TestCatalog { - @Nested - public class ConstructorTests { + abstract static class GenericCreation { @Test - void Check_Valid_Catalog() { - Assertions.assertDoesNotThrow(() -> new Catalog( - "", - "", - List.of(new Extension("", "", "", URI.create("https://github.com/qupath/qupath"), false, List.of())) - )); + void Check_Name() { + String expectedName = "name"; + + Catalog catalog = createCatalog(); + + Assertions.assertEquals(expectedName, catalog.getName()); } @Test - void Check_Undefined_Name() { - Assertions.assertThrows( - IllegalArgumentException.class, - () -> new Catalog(null, "", List.of()) - ); + void Check_Description() { + String expectedDescription = "description"; + + Catalog catalog = createCatalog(); + + Assertions.assertEquals(expectedDescription, catalog.getDescription()); } @Test - void Check_Undefined_Description() { - Assertions.assertThrows( - IllegalArgumentException.class, - () -> new Catalog("", null, List.of()) - ); + void Check_Uri() { + URI expectedUri = URI.create("http://uri.com"); + + Catalog catalog = createCatalog(); + + Assertions.assertEquals(expectedUri, catalog.getUri()); } @Test - void Check_Undefined_Extensions() { - Assertions.assertThrows( - IllegalArgumentException.class, - () -> new Catalog("", "", null) - ); + void Check_Raw_Uri() { + URI expectedRawUri = URI.create("http://raw.com"); + + Catalog catalog = createCatalog(); + + Assertions.assertEquals(expectedRawUri, catalog.getRawUri()); } @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()) - ); + void Check_Deletable() { + boolean expectedDeletable = false; - Assertions.assertThrows( - IllegalArgumentException.class, - () -> new Catalog("", "", extensions) - ); + Catalog catalog = createCatalog(); + + Assertions.assertEquals(expectedDeletable, catalog.isDeletable()); } + + @Test + abstract void Check_Extensions() throws IOException, ExecutionException, InterruptedException; + + protected abstract Catalog createCatalog(); } @Nested - public class JsonTests { + class AttributeCreation extends GenericCreation { @Test - void Check_Valid_Catalog() { - Catalog expectedCatalog = new Catalog( - "", - "", - List.of(new Extension( - "", - "", - "", - URI.create("https://github.com/qupath/qupath"), + void Check_Null_Name() { + String name = null; + String description = "description"; + URI uri = URI.create("http://uri.com"); + URI rawUri = URI.create("http://raw.com"); + + Assertions.assertThrows(NullPointerException.class, () -> new Catalog(name, description, uri, rawUri)); + } + + @Test + void Check_Null_Description() { + String name = null; + String description = "description"; + URI uri = URI.create("http://uri.com"); + URI rawUri = URI.create("http://raw.com"); + + Assertions.assertThrows(NullPointerException.class, () -> new Catalog(name, description, uri, rawUri)); + } + + @Test + void Check_Null_Uri() { + String name = "name"; + String description = "description"; + URI uri = null; + URI rawUri = URI.create("http://raw.com"); + + Assertions.assertThrows(NullPointerException.class, () -> new Catalog(name, description, uri, rawUri)); + } + + @Test + void Check_Null_Raw_Uri() { + String name = "name"; + String description = "description"; + URI uri = URI.create("http://uri.com"); + URI rawUri = null; + + Assertions.assertThrows(NullPointerException.class, () -> new Catalog(name, description, uri, rawUri)); + } + + @Test + @Override + void Check_Extensions() throws IOException, ExecutionException, InterruptedException { + SimpleServer server = new SimpleServer(List.of(new SimpleServer.FileToServe( + "catalog.json", + Objects.requireNonNull(TestCatalog.class.getResourceAsStream("catalog.json")) + ))); + Catalog catalog = new Catalog( + "name", + "description", + server.getURI("catalog.json"), + server.getURI("catalog.json") + ); + List expectedExtensions = List.of(new Extension( + new ExtensionModel( + "Some extension", + "Some extension description", + "Some author", + URI.create("http://github.com/qupath/qupath"), false, - List.of() - )) + List.of( + new ReleaseModel( + "v0.1.0", + URI.create("https://github.com/qupath/qupath"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v1.0.0", null, null) + ), + new ReleaseModel( + "v1.0.0", + URI.create("https://github.com/qupath/qupath"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v2.0.0", null, null) + ) + ) + ), + null, + false + )); + + List extensions = catalog.getExtensions().get(); + + TestUtils.assertCollectionsEqualsWithoutOrder( + expectedExtensions.stream().map(Extension::getName).toList(), // Extension does not override equals, + extensions.stream().map(Extension::getName).toList() // so only the name is checked ); - Catalog catalog = new Gson().fromJson(""" - { - "name": "", - "description": "", - "extensions": [ - { - "name": "", - "description": "", - "author": "", - "homepage": "https://github.com/qupath/qupath", - "releases": [] - } - ] - } - """, - Catalog.class + server.close(); + } + + @Override + protected Catalog createCatalog() { + return new Catalog( + "name", + "description", + URI.create("http://uri.com"), + URI.create("http://raw.com") ); + } + } + + @Nested + class CatalogModelCreation extends GenericCreation { + + @Test + void Check_Null_Catalog_Model() { + CatalogModel catalogModel = null; + URI uri = URI.create("http://uri.com"); + URI rawUri = URI.create("http://raw.com"); + boolean deletable = true; - Assertions.assertEquals(expectedCatalog, catalog); + Assertions.assertThrows(NullPointerException.class, () -> new Catalog(catalogModel, uri, rawUri, deletable)); } @Test - void Check_Undefined_Name() { - Assertions.assertThrows( - RuntimeException.class, - () -> new Gson().fromJson(""" - { - "description": "", - "extensions": [] - } - """, - Catalog.class - ) - ); + void Check_Null_Uri() { + CatalogModel catalogModel = new CatalogModel("name", "description", List.of()); + URI uri = null; + URI rawUri = URI.create("http://raw.com"); + boolean deletable = true; + + Assertions.assertThrows(NullPointerException.class, () -> new Catalog(catalogModel, uri, rawUri, deletable)); } @Test - void Check_Undefined_Description() { - Assertions.assertThrows( - RuntimeException.class, - () -> new Gson().fromJson(""" - { - "name": "", - "extensions": [] - } - """, - Catalog.class - ) - ); + void Check_Null_Raw_Uri() { + CatalogModel catalogModel = new CatalogModel("name", "description", List.of()); + URI uri = URI.create("http://uri.com"); + URI rawUri = null; + boolean deletable = true; + + Assertions.assertThrows(NullPointerException.class, () -> new Catalog(catalogModel, uri, rawUri, deletable)); } @Test - void Check_Undefined_Extensions() { - Assertions.assertThrows( - RuntimeException.class, - () -> new Gson().fromJson(""" - { - "name": "", - "description": "" - } - """, - Catalog.class - ) + @Override + void Check_Extensions() throws ExecutionException, InterruptedException { + Catalog catalog = new Catalog( + new CatalogModel( + "name", + "description", + List.of(new ExtensionModel( + "name", + "description", + "author", + URI.create("https://github.com/qupath/qupath"), + true, + List.of( + new ReleaseModel( + "v0.1.0", + URI.create("https://github.com/qupath/qupath"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v1.0.0", null, null) + ), + new ReleaseModel( + "v1.0.0", + URI.create("https://github.com/qupath/qupath"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v2.0.0", null, null) + ) + ) + )) + ), + URI.create("http://uri.com"), + URI.create("http://raw.com"), + false + ); + List expectedExtensions = List.of(new Extension( + new ExtensionModel( + "name", + "description", + "author", + URI.create("https://github.com/qupath/qupath"), + true, + List.of( + new ReleaseModel( + "v0.1.0", + URI.create("https://github.com/qupath/qupath"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v1.0.0", null, null) + ), + new ReleaseModel( + "v1.0.0", + URI.create("https://github.com/qupath/qupath"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v2.0.0", null, null) + ) + ) + ), + null, + false + )); + + List extensions = catalog.getExtensions().get(); + + TestUtils.assertCollectionsEqualsWithoutOrder( + expectedExtensions.stream().map(Extension::getName).toList(), // Extension does not override equals, + extensions.stream().map(Extension::getName).toList() // so only the name is checked ); } - @Test - void Check_Invalid_Extensions() { - Assertions.assertThrows( - RuntimeException.class, - () -> new Gson().fromJson(""" - { - "name": "", - "description": "", - "extensions": [{}] - } - """, - Catalog.class - ) + @Override + protected Catalog createCatalog() { + return new Catalog( + new CatalogModel("name", "description", List.of()), + URI.create("http://uri.com"), + URI.create("http://raw.com"), + false ); } + } + + @Nested + class RegistryCatalogCreation extends GenericCreation { @Test - void Check_Extensions_With_Same_Name() { - Assertions.assertThrows( - RuntimeException.class, - () -> new Gson().fromJson(""" - { - "name": "", - "description": "", - "extensions": [ - { - "name": "name", - "description": "", - "author": "", - "homepage": "https://github.com/qupath/qupath", - "releases": [] - }, - { - "name": "name", - "description": "", - "author": "", - "homepage": "https://github.com/qupath/qupath", - "releases": [] - }, - { - "name": "other_name", - "description": "", - "author": "", - "homepage": "https://github.com/qupath/qupath", - "releases": [] - } - ] - } - """, - Catalog.class - ) + void Check_Null_Registry_Catalog() { + RegistryCatalog registryCatalog = null; + + Assertions.assertThrows(NullPointerException.class, () -> new Catalog(registryCatalog)); + } + + @Test + @Override + void Check_Extensions() throws IOException, ExecutionException, InterruptedException { + SimpleServer server = new SimpleServer(List.of(new SimpleServer.FileToServe( + "catalog.json", + Objects.requireNonNull(TestCatalog.class.getResourceAsStream("catalog.json")) + ))); + Catalog catalog = new Catalog(new RegistryCatalog( + "name", + "description", + server.getURI("catalog.json"), + server.getURI("catalog.json"), + false, + List.of(new RegistryExtension("Some extension", "v1.0.0", true)) + )); + List expectedExtensions = List.of(new Extension( + new ExtensionModel( + "Some extension", + "Some extension description", + "Some author", + URI.create("http://github.com/qupath/qupath"), + false, + List.of( + new ReleaseModel( + "v0.1.0", + URI.create("https://github.com/qupath/qupath"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v1.0.0", null, null) + ), + new ReleaseModel( + "v1.0.0", + URI.create("https://github.com/qupath/qupath"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v2.0.0", null, null) + ) + ) + ), + new Release(new ReleaseModel( + "v1.0.0", + URI.create("https://github.com/qupath/qupath"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v2.0.0", null, null) + )), + true + )); + + List extensions = catalog.getExtensions().get(); + + TestUtils.assertCollectionsEqualsWithoutOrder( + expectedExtensions.stream().map(Extension::getName).toList(), // Extension does not override equals, + extensions.stream().map(Extension::getName).toList() // so only the name is checked ); + + server.close(); + } + + @Override + protected Catalog createCatalog() { + return new Catalog(new RegistryCatalog( + "name", + "description", + URI.create("http://uri.com"), + URI.create("http://raw.com"), + false, + List.of() + )); } } } diff --git a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestExtension.java b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestExtension.java index 57a296f..f041318 100644 --- a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestExtension.java +++ b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestExtension.java @@ -1,9 +1,12 @@ package qupath.ext.extensionmanager.core.catalog; -import com.google.gson.Gson; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import qupath.ext.extensionmanager.TestUtils; +import qupath.ext.extensionmanager.core.Version; +import qupath.ext.extensionmanager.core.model.ExtensionModel; +import qupath.ext.extensionmanager.core.model.ReleaseModel; +import qupath.ext.extensionmanager.core.model.VersionRangeModel; import java.net.URI; import java.util.List; @@ -11,570 +14,384 @@ public class TestExtension { - @Nested - public class ConstructorTests { - - @Test - void Check_Valid_Extension() { - Assertions.assertDoesNotThrow(() -> new Extension( - "", - "", - "", - URI.create("https://github.com/qupath/qupath"), - false, - List.of(new Release( - "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) - )) - )); - } - - @Test - void Check_Undefined_Name() { - Assertions.assertThrows( - IllegalArgumentException.class, - () -> new Extension( - null, - "", - "", - URI.create("https://github.com/qupath/qupath"), - false, - List.of(new Release( - "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) - )) - ) - ); - } - - @Test - void Check_Undefined_Description() { - Assertions.assertThrows( - IllegalArgumentException.class, - () -> new Extension( - "", - null, - "", - URI.create("https://github.com/qupath/qupath"), - false, - List.of(new Release( - "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) - )) - ) - ); - } - - @Test - void Check_Undefined_Author() { - Assertions.assertThrows( - IllegalArgumentException.class, - () -> new Extension( - "", - "", - null, - URI.create("https://github.com/qupath/qupath"), - false, - List.of(new Release( - "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) - )) - ) - ); - } - - @Test - void Check_Undefined_Homepage() { - Assertions.assertThrows( - IllegalArgumentException.class, - () -> new Extension( - "", - "", - "", - null, - false, - List.of(new Release( - "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) - )) - ) - ); - } - - @Test - void Check_Undefined_Releases() { - Assertions.assertThrows( - IllegalArgumentException.class, - () -> new Extension( - "", - "", - "", - URI.create("https://github.com/qupath/qupath"), - false, - null - ) - ); - } - - @Test - void Check_Homepage_Not_GitHub() { - Assertions.assertThrows( - IllegalArgumentException.class, - () -> new Extension( - "", - "", - "", - URI.create("https://qupath.readthedocs.io/"), - false, - List.of(new Release( - "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) - )) - ) - ); - } + @Test + void Check_Null_Extension_Model() { + ExtensionModel extensionModel = null; + Release installedRelease = new Release(new ReleaseModel( + "v1.0.0", + URI.create("http://github.com/1.0.0"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v1.0.0", null, null) + )); + boolean optionalDependenciesInstalled = true; + + Assertions.assertThrows( + NullPointerException.class, + () -> new Extension(extensionModel, installedRelease, optionalDependenciesInstalled) + ); } - @Nested - public class JsonTests { - - @Test - void Check_Valid_Extension() { - Extension expectedExtension = new Extension( - "", - "", - "", - URI.create("https://github.com/qupath/qupath"), - true, - List.of(new Release( - "v1.0.0", - URI.create("https://github.com/qupath/qupath"), - null, - null, - null, - new VersionRange("v1.0.0", null, null) - )) - ); - - Extension extension = new Gson().fromJson(""" - { - "name": "", - "description": "", - "author": "", - "homepage": "https://github.com/qupath/qupath", - "starred": true, - "releases": [ - { - "name": "v1.0.0", - "mainUrl": "https://github.com/qupath/qupath", - "versionRange": { - "min": "v1.0.0" - } - } - ] - } - """, - Extension.class - ); - - Assertions.assertEquals(expectedExtension, extension); - } - - @Test - void Check_Undefined_Name() { - Assertions.assertThrows( - RuntimeException.class, - () -> new Gson().fromJson(""" - { - "description": "", - "author": "", - "homepage": "https://github.com/qupath/qupath", - "releases": [ - { - "name": "v1.0.0", - "mainUrl": "https://github.com/qupath/qupath", - "versionRange": { - "min": "v1.0.0" - } - } - ] - } - """, - Extension.class - ) - ); - } - - @Test - void Check_Undefined_Description() { - Assertions.assertThrows( - RuntimeException.class, - () -> new Gson().fromJson(""" - { - "name": "", - "author": "", - "homepage": "https://github.com/qupath/qupath", - "releases": [ - { - "name": "v1.0.0", - "mainUrl": "https://github.com/qupath/qupath", - "versionRange": { - "min": "v1.0.0" - } - } - ] - } - """, - Extension.class - ) - ); - } - - @Test - void Check_Undefined_Author() { - Assertions.assertThrows( - RuntimeException.class, - () -> new Gson().fromJson(""" - { - "name": "", - "description": "", - "homepage": "https://github.com/qupath/qupath", - "releases": [ - { - "name": "v1.0.0", - "mainUrl": "https://github.com/qupath/qupath", - "versionRange": { - "min": "v1.0.0" - } - } - ] - } - """, - Extension.class - ) - ); - } - - @Test - void Check_Undefined_Homepage() { - Assertions.assertThrows( - RuntimeException.class, - () -> new Gson().fromJson(""" - { - "name": "", - "description": "", - "author": "", - "releases": [ - { - "name": "v1.0.0", - "mainUrl": "https://github.com/qupath/qupath", - "versionRange": { - "min": "v1.0.0" - } - } - ] - } - """, - Extension.class - ) - ); - } - - @Test - void Check_Undefined_Releases() { - Assertions.assertThrows( - RuntimeException.class, - () -> new Gson().fromJson(""" - { - "name": "", - "description": "", - "author": "", - "homepage": "https://github.com/qupath/qupath" - } - """, - Extension.class - ) - ); - } - - @Test - void Check_Invalid_Release() { - Assertions.assertThrows( - RuntimeException.class, - () -> new Gson().fromJson(""" - { - "name": "", - "description": "", - "author": "", - "homepage": "https://github.com/qupath/qupath", - "releases": [{}] - } - """, - Extension.class - ) - ); - } - - @Test - void Check_Homepage_Not_GitHub() { - Assertions.assertThrows( - RuntimeException.class, - () -> new Gson().fromJson(""" - { - "name": "", - "description": "", - "author": "", - "homepage": "https://qupath.readthedocs.io/", - "releases": [ - { - "name": "v1.0.0", - "mainUrl": "https://github.com/qupath/qupath", - "versionRange": { - "min": "v1.0.0" - } - } - ] - } - """, - Extension.class - ) - ); - } + @Test + void Check_Name() { + Extension extension = new Extension( + createExtensionModel(), + null, + false + ); + String expectedName = "name"; + + String name = extension.getName(); + + Assertions.assertEquals(expectedName, name); } + @Test - void Check_Max_Compatible_Release_When_Two_Compatibles() { - Release expectedRelease = new Release( - "v2.0.0", - URI.create("https://github.com/qupath/qupath"), - List.of(URI.create("https://github.com/qupath/qupath")), + void Check_Description() { + Extension extension = new Extension( + createExtensionModel(), null, + false + ); + String expectedDescription = "description"; + + String description = extension.getDescription(); + + Assertions.assertEquals(expectedDescription, description); + } + + @Test + void Check_Releases() { + Extension extension = new Extension( + createExtensionModel(), null, - new VersionRange("v1.1.0", null, null) + false ); + List expectedReleases = List.of( + new Release(new ReleaseModel( + "v0.1.0", + URI.create("http://github.com/0.1.0"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v0.1.0", "v0.2.0", null) + )), + new Release(new ReleaseModel( + "v0.2.0", + URI.create("http://github.com/0.2.0"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v0.1.0", "v0.2.0", null) + )) + ); + + List releases = extension.getReleases(); + + TestUtils.assertCollectionsEqualsWithoutOrder(expectedReleases, releases); + } + + @Test + void Check_Homepage() { Extension extension = new Extension( - "", - "", - "", - URI.create("https://github.com/qupath/qupath"), - false, - List.of( - new Release( - "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) - ), - expectedRelease, - new Release( - "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) - ) - ) + createExtensionModel(), + null, + false ); - String version = "v1.1.0"; + URI expectedHomepage = URI.create("http://github.com/extension"); - Release release = extension.getMaxCompatibleRelease(version).orElse(null); + URI homepage = extension.getHomepage(); - Assertions.assertEquals(expectedRelease, release); + Assertions.assertEquals(expectedHomepage, homepage); } @Test - void Check_Max_Compatible_Release_When_Three_Compatible() { - Release expectedRelease = new Release( - "v3.0.0", - URI.create("https://github.com/qupath/qupath"), - List.of(URI.create("https://github.com/qupath/qupath")), + void Check_Starred() { + Extension extension = new Extension( + createExtensionModel(), null, + false + ); + boolean expectedStarred = true; + + boolean starred = extension.isStarred(); + + Assertions.assertEquals(expectedStarred, starred); + } + + @Test + void Check_Installed_Release_When_Not_Installed() { + Extension extension = new Extension( + createExtensionModel(), null, - new VersionRange("v1.2.0", null, null) + false ); + + Optional installedRelease = extension.getInstalledRelease().getValue(); + + Assertions.assertTrue(installedRelease.isEmpty()); + } + + @Test + void Check_Installed_Release_When_Installed() { + Release expectedInstalledRelease = new Release(new ReleaseModel( + "v0.2.0", + URI.create("http://github.com/0.2.0"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v0.1.0", "v0.2.0", null) + )); Extension extension = new Extension( - "", - "", - "", - URI.create("https://github.com/qupath/qupath"), - false, - List.of( - new Release( - "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 Release( - "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) - ), - expectedRelease - ) + createExtensionModel(), + expectedInstalledRelease, + false ); - String version = "v2.0.0"; - Release release = extension.getMaxCompatibleRelease(version).orElse(null); + Release installedRelease = extension.getInstalledRelease().getValue().orElse(null); - Assertions.assertEquals(expectedRelease, release); + Assertions.assertEquals(expectedInstalledRelease, installedRelease); } @Test - void Check_Max_Compatible_Release_When_Zero_Compatible() { + void Check_Installed_Release_When_Installed_After_Initialization() { Extension extension = new Extension( - "", - "", - "", - URI.create("https://github.com/qupath/qupath"), - false, - List.of( - new Release( - "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 Release( - "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 Release( - "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) - ) - ) + createExtensionModel(), + null, + false + ); + Release expectedInstalledRelease = new Release(new ReleaseModel( + "v0.2.0", + URI.create("http://github.com/0.2.0"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v0.1.0", "v0.2.0", null) + )); + extension.installRelease(expectedInstalledRelease, false); + + Release installedRelease = extension.getInstalledRelease().getValue().orElse(null); + + Assertions.assertEquals(expectedInstalledRelease, installedRelease); + } + + @Test + void Check_Installed_Release_When_Uninstalled() { + Extension extension = new Extension( + createExtensionModel(), + new Release(new ReleaseModel( + "v0.2.0", + URI.create("http://github.com/0.2.0"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v0.1.0", "v0.2.0", null) + )), + false ); - String version = "v0.0.1"; + extension.uninstallRelease(); - Optional release = extension.getMaxCompatibleRelease(version); + Optional installedRelease = extension.getInstalledRelease().getValue(); - Assertions.assertTrue(release.isEmpty()); + Assertions.assertTrue(installedRelease.isEmpty()); } @Test - void Check_Max_Compatible_Release_When_Invalid_Version() { + void Check_Optional_Dependencies_Installed_When_Not_Installed() { Extension extension = new Extension( - "", - "", - "", - URI.create("https://github.com/qupath/qupath"), - false, - List.of( - new Release( - "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 Release( - "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 Release( - "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) - ) - ) + createExtensionModel(), + null, + false ); - String version = "invalid_version"; - Assertions.assertThrows( - IllegalArgumentException.class, - () -> extension.getMaxCompatibleRelease(version) + boolean optionalDependenciesInstalled = extension.areOptionalDependenciesInstalled().getValue(); + + Assertions.assertFalse(optionalDependenciesInstalled); + } + + @Test + void Check_Optional_Dependencies_Installed_When_Installed() { + Extension extension = new Extension( + createExtensionModel(), + null, + true + ); + + boolean optionalDependenciesInstalled = extension.areOptionalDependenciesInstalled().getValue(); + + Assertions.assertTrue(optionalDependenciesInstalled); + } + + @Test + void Check_Optional_Dependencies_Installed_When_Installed_After_Initialization() { + Extension extension = new Extension( + createExtensionModel(), + null, + false + ); + extension.installRelease( + new Release(new ReleaseModel( + "v0.2.0", + URI.create("http://github.com/0.2.0"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v0.1.0", "v0.2.0", null) + )), + true + ); + + boolean optionalDependenciesInstalled = extension.areOptionalDependenciesInstalled().getValue(); + + Assertions.assertTrue(optionalDependenciesInstalled); + } + + @Test + void Check_Optional_Dependencies_Installed_When_Not_Installed_After_Initialization() { + Extension extension = new Extension( + createExtensionModel(), + null, + false + ); + extension.installRelease( + new Release(new ReleaseModel( + "v0.2.0", + URI.create("http://github.com/0.2.0"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v0.1.0", "v0.2.0", null) + )), + false + ); + + boolean optionalDependenciesInstalled = extension.areOptionalDependenciesInstalled().getValue(); + + Assertions.assertFalse(optionalDependenciesInstalled); + } + + @Test + void Check_Optional_Dependencies_Installed_When_Uninstalled() { + Extension extension = new Extension( + createExtensionModel(), + null, + true + ); + extension.uninstallRelease(); + + boolean optionalDependenciesInstalled = extension.areOptionalDependenciesInstalled().getValue(); + + Assertions.assertFalse(optionalDependenciesInstalled); + } + + @Test + void Check_No_Available_Updates_When_Extension_Not_Installed() { + Extension extension = new Extension( + createExtensionModel(), + null, + false ); + Version version = new Version("v0.1.0"); + + Optional updateAvailable = extension.getUpdateAvailable(version); + + Assertions.assertTrue(updateAvailable.isEmpty()); } @Test - void Check_Max_Compatible_Release_When_Null_Version() { + void Check_No_Available_Updates_When_No_Compatible_Release() { Extension extension = new Extension( - "", - "", - "", - URI.create("https://github.com/qupath/qupath"), - false, + createExtensionModel(), + new Release(new ReleaseModel( + "v0.1.0", + URI.create("http://github.com/0.1.0"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v0.1.0", "v0.2.0", null) + )), + false + ); + Version version = new Version("v0.30.0"); + + Optional updateAvailable = extension.getUpdateAvailable(version); + + Assertions.assertTrue(updateAvailable.isEmpty()); + } + + @Test + void Check_No_Available_Updates_When_Already_Latest_Release() { + Extension extension = new Extension( + createExtensionModel(), + new Release(new ReleaseModel( + "v0.2.0", + URI.create("http://github.com/0.2.0"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v0.1.0", "v0.2.0", null) + )), + false + ); + Version version = new Version("v0.1.0"); + + Optional updateAvailable = extension.getUpdateAvailable(version); + + Assertions.assertTrue(updateAvailable.isEmpty()); + } + + @Test + void Check_Available_Update() { + Extension extension = new Extension( + createExtensionModel(), + new Release(new ReleaseModel( + "v0.1.0", + URI.create("http://github.com/0.1.0"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v0.1.0", "v0.2.0", null) + )), + false + ); + Version version = new Version("v0.1.0"); + UpdateAvailable expectedUpdateAvailable = new UpdateAvailable( + "name", + new Version("v0.1.0"), + new Version("v0.2.0") + ); + + UpdateAvailable updateAvailable = extension.getUpdateAvailable(version).orElse(null); + + Assertions.assertEquals(expectedUpdateAvailable, updateAvailable); + } + + private static ExtensionModel createExtensionModel() { + return new ExtensionModel( + "name", + "description", + "author", + URI.create("http://github.com/extension"), + true, List.of( - new Release( - "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 Release( - "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 ReleaseModel( + "v0.1.0", + URI.create("http://github.com/0.1.0"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v0.1.0", "v0.2.0", null) ), - new Release( - "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 ReleaseModel( + "v0.2.0", + URI.create("http://github.com/0.2.0"), + List.of(), + List.of(), + List.of(), + new VersionRangeModel("v0.1.0", "v0.2.0", null) ) ) ); - - Assertions.assertThrows( - NullPointerException.class, - () -> extension.getMaxCompatibleRelease(null) - ); } } diff --git a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestRelease.java b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestRelease.java index df984ca..007a423 100644 --- a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestRelease.java +++ b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestRelease.java @@ -1,442 +1,102 @@ package qupath.ext.extensionmanager.core.catalog; -import com.google.gson.Gson; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import qupath.ext.extensionmanager.TestUtils; +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.List; public class TestRelease { - @Nested - public class ConstructorTests { + @Test + void Check_Null_Release_Model() { + ReleaseModel releaseModel = null; - @Test - void Check_Valid_Release() { - Assertions.assertDoesNotThrow(() -> new Release( - "v0.1.0", - URI.create("https://github.com/qupath/qupath"), - null, - null, - null, - new VersionRange("v1.0.0", null, null) - )); - } - - @Test - void Check_Undefined_Name() { - Assertions.assertThrows( - IllegalArgumentException.class, - () -> new Release( - null, - URI.create("https://github.com/qupath/qupath"), - null, - null, - null, - new VersionRange("v1.0.0", null, null) - ) - ); - } - - @Test - void Check_Undefined_Main_Url() { - Assertions.assertThrows( - IllegalArgumentException.class, - () -> new Release( - "v0.1.0", - null, - null, - null, - null, - new VersionRange("v1.0.0", null, null) - ) - ); - } - - @Test - void Check_Undefined_Version_Range() { - Assertions.assertThrows( - IllegalArgumentException.class, - () -> new Release( - "v0.1.0", - URI.create("https://github.com/qupath/qupath"), - null, - null, - null, - null - ) - ); - } - - @Test - void Check_Invalid_Name() { - Assertions.assertThrows( - IllegalArgumentException.class, - () -> new Release( - "invalid_version", - URI.create("https://github.com/qupath/qupath"), - null, - null, - null, - new VersionRange("v1.0.0", null, null) - ) - ); - } - - @Test - void Check_Invalid_Main_Url() { - Assertions.assertThrows( - IllegalArgumentException.class, - () -> new Release( - "v0.1.0", - URI.create("https://qupath.readthedocs.io/"), - null, - null, - null, - new VersionRange("v1.0.0", null, null) - ) - ); - } - - @Test - void Check_Valid_Required_Dependency_Url() { - Assertions.assertDoesNotThrow( - () -> new Release( - "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) - ) - ); - } - - @Test - void Check_Invalid_Required_Dependency_Url() { - Assertions.assertThrows( - IllegalArgumentException.class, - () -> new Release( - "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) - ) - ); - } - - @Test - void Check_Valid_Optional_Dependency_Url() { - Assertions.assertDoesNotThrow( - () -> new Release( - "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) - ) - ); - } - - @Test - void Check_Invalid_Optional_Dependency_Url() { - Assertions.assertThrows( - IllegalArgumentException.class, - () -> new Release( - "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) - ) - ); - } - - @Test - void Check_Valid_Javadoc_Url() { - Assertions.assertDoesNotThrow( - () -> new Release( - "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) - ) - ); - } - - @Test - void Check_Invalid_Javadoc_Url() { - Assertions.assertThrows( - IllegalArgumentException.class, - () -> new Release( - "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) - ) - ); - } + Assertions.assertThrows(NullPointerException.class, () -> new Release(releaseModel)); } - @Nested - public class JsonTests { + @Test + void Check_Version() { + Release release = createRelease(); + Version expectedVersion = new Version("v1.0.0"); - @Test - void Check_Valid_Release() { - Release expectedRelease = new Release( - "v0.1.0", - URI.create("https://github.com/qupath/qupath"), - null, - null, - null, - new VersionRange("v1.0.0", null, null) - ); + Version version = release.getVersion(); - Release release = new Gson().fromJson(""" - { - "name": "v0.1.0", - "mainUrl": "https://github.com/qupath/qupath", - "versionRange": { - "min": "v1.0.0" - } - } - """ - , - Release.class - ); + Assertions.assertEquals(expectedVersion, version); + } - Assertions.assertEquals(expectedRelease, release); - } + @Test + void Check_Compatible_Version() { + Release release = createRelease(); + Version compatibleVersion = new Version("v0.1.0"); - @Test - void Check_Undefined_Name() { - Assertions.assertThrows( - RuntimeException.class, - () -> new Gson().fromJson(""" - { - "mainUrl": "https://github.com/qupath/qupath", - "versionRange": { - "min": "v1.0.0" - } - } - """, - Release.class - ) - ); - } + boolean isCompatible = release.isCompatible(compatibleVersion); - @Test - void Check_Undefined_Main_Url() { - Assertions.assertThrows( - RuntimeException.class, - () -> new Gson().fromJson(""" - { - "name": "v0.1.0", - "versionRange": { - "min": "v1.0.0" - } - } - """, - Release.class - ) - ); - } + Assertions.assertTrue(isCompatible); + } - @Test - void Check_Undefined_Version_Range() { - Assertions.assertThrows( - RuntimeException.class, - () -> new Gson().fromJson(""" - { - "name": "v0.1.0", - "mainUrl": "https://github.com/qupath/qupath" - } - """, - Release.class - ) - ); - } + @Test + void Check_Incompatible_Version() { + Release release = createRelease(); + Version compatibleVersion = new Version("v0.30.0"); - @Test - void Check_Invalid_Name() { - Assertions.assertThrows( - RuntimeException.class, - () -> new Gson().fromJson(""" - { - "name": "invalid_version", - "mainUrl": "https://github.com/qupath/qupath", - "versionRange": { - "min": "v1.0.0" - } - } - """, - Release.class - ) - ); - } + boolean isCompatible = release.isCompatible(compatibleVersion); - @Test - void Check_Invalid_Version_Range() { - Assertions.assertThrows( - RuntimeException.class, - () -> new Gson().fromJson(""" - { - "name": "v0.1.0", - "mainUrl": "https://github.com/qupath/qupath", - "versionRange": {} - } - """, - Release.class - ) - ); - } + Assertions.assertFalse(isCompatible); + } - @Test - void Check_Invalid_Main_Url() { - Assertions.assertThrows( - RuntimeException.class, - () -> new Gson().fromJson(""" - { - "name": "v0.1.0", - "mainUrl": "https://qupath.readthedocs.io/", - "versionRange": { - "min": "v1.0.0" - } - } - """, - Release.class - ) - ); - } + @Test + void Check_Main_Url() { + Release release = createRelease(); + URI expectedMainUrl = URI.create("https://github.com/main"); - @Test - void Check_Valid_Required_Dependency_Url() { - List expectedRequiredDependencyUrls = List.of(URI.create("https://maven.scijava.org/content")); + URI mainUrl = release.getMainUrl(); - Release release = new Gson().fromJson(""" - { - "name": "v0.1.0", - "mainUrl": "https://github.com/qupath/qupath", - "requiredDependencyUrls": ["https://maven.scijava.org/content"], - "versionRange": { - "min": "v1.0.0" - } - } - """ - , - Release.class - ); + Assertions.assertEquals(expectedMainUrl, mainUrl); + } - Assertions.assertEquals(expectedRequiredDependencyUrls, release.requiredDependencyUrls()); - } + @Test + void Check_Javadocs_Urls() { + Release release = createRelease(); + List expectedJavadocsUrls = List.of(URI.create("https://github.com/javadocs")); - @Test - void Check_Invalid_Required_Dependency_Url() { - Assertions.assertThrows( - RuntimeException.class, - () -> new Gson().fromJson(""" - { - "name": "v0.1.0", - "mainUrl": "https://github.com/qupath/qupath/", - "requiredDependencyUrls": ["https://qupath.readthedocs.io/"], - "versionRange": { - "min": "v1.0.0" - } - } - """, - Release.class - ) - ); - } + List javadocUrls = release.getJavadocUrls(); - @Test - void Check_Valid_Optional_Dependency_Url() { - List expectedOptionalDependencyUrls = List.of(URI.create("https://maven.scijava.org/content")); + TestUtils.assertCollectionsEqualsWithoutOrder(expectedJavadocsUrls, javadocUrls); + } - Release release = new Gson().fromJson(""" - { - "name": "v0.1.0", - "mainUrl": "https://github.com/qupath/qupath", - "optionalDependencyUrls": ["https://maven.scijava.org/content"], - "versionRange": { - "min": "v1.0.0" - } - } - """ - , - Release.class - ); + @Test + void Check_Required_Dependencies_Urls() { + Release release = createRelease(); + List expectedRequiredDependenciesUrls = List.of(URI.create("https://github.com/required")); - Assertions.assertEquals(expectedOptionalDependencyUrls, release.optionalDependencyUrls()); - } + List requiredDependencyUrls = release.getRequiredDependencyUrls(); - @Test - void Check_Invalid_Optional_Dependency_Url() { - Assertions.assertThrows( - RuntimeException.class, - () -> new Gson().fromJson(""" - { - "name": "v0.1.0", - "mainUrl": "https://github.com/qupath/qupath/", - "optionalDependencyUrls": ["https://qupath.readthedocs.io/"], - "versionRange": { - "min": "v1.0.0" - } - } - """, - Release.class - ) - ); - } + TestUtils.assertCollectionsEqualsWithoutOrder(expectedRequiredDependenciesUrls, requiredDependencyUrls); + } - @Test - void Check_Valid_Javadoc_Url() { - List expectedJavadocUrls = List.of(URI.create("https://maven.scijava.org/content")); + @Test + void Check_Optional_Dependencies_Urls() { + Release release = createRelease(); + List expectedOptionalDependenciesUrls = List.of(URI.create("https://github.com/optional")); - Release release = new Gson().fromJson(""" - { - "name": "v0.1.0", - "mainUrl": "https://github.com/qupath/qupath", - "javadocUrls": ["https://maven.scijava.org/content"], - "versionRange": { - "min": "v1.0.0" - } - } - """ - , - Release.class - ); + List optionalDependencyUrls = release.getOptionalDependencyUrls(); - Assertions.assertEquals(expectedJavadocUrls, release.javadocUrls()); - } + TestUtils.assertCollectionsEqualsWithoutOrder(expectedOptionalDependenciesUrls, optionalDependencyUrls); + } - @Test - void Check_Invalid_Javadoc_Url() { - Assertions.assertThrows( - RuntimeException.class, - () -> new Gson().fromJson(""" - { - "name": "v0.1.0", - "mainUrl": "https://github.com/qupath/qupath/", - "javadocUrls": ["https://qupath.readthedocs.io/"], - "versionRange": { - "min": "v1.0.0" - } - } - """, - Release.class - ) - ); - } + private static Release createRelease() { + return new Release(new ReleaseModel( + "v1.0.0", + URI.create("https://github.com/main"), + List.of(URI.create("https://github.com/required")), + List.of(URI.create("https://github.com/optional")), + List.of(URI.create("https://github.com/javadocs")), + new VersionRangeModel("v0.1.0", "v0.2.0", null) + )); } } diff --git a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestUpdateAvailable.java b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestUpdateAvailable.java new file mode 100644 index 0000000..844bf08 --- /dev/null +++ b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/catalog/TestUpdateAvailable.java @@ -0,0 +1,44 @@ +package qupath.ext.extensionmanager.core.catalog; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import qupath.ext.extensionmanager.core.Version; + +public class TestUpdateAvailable { + + @Test + void Check_Null_Extension_Name() { + String extensionName = null; + Version currentVersion = new Version("v0.1.2"); + Version newVersion = new Version("v1.2.3"); + + Assertions.assertThrows( + RuntimeException.class, + () -> new UpdateAvailable(extensionName, currentVersion, newVersion) + ); + } + + @Test + void Check_Null_Current_Version() { + String extensionName = "extension name"; + Version currentVersion = null; + Version newVersion = new Version("v1.2.3"); + + Assertions.assertThrows( + RuntimeException.class, + () -> new UpdateAvailable(extensionName, currentVersion, newVersion) + ); + } + + @Test + void Check_Null_New_Version() { + String extensionName = "extension name"; + Version currentVersion = new Version("v0.1.2"); + Version newVersion = null; + + Assertions.assertThrows( + RuntimeException.class, + () -> new UpdateAvailable(extensionName, currentVersion, newVersion) + ); + } +} diff --git a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestCatalogModel.java b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestCatalogModel.java new file mode 100644 index 0000000..875f127 --- /dev/null +++ b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestCatalogModel.java @@ -0,0 +1,202 @@ +package qupath.ext.extensionmanager.core.model; + +import com.google.gson.Gson; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.util.List; + +public class TestCatalogModel { + + @Nested + public class ConstructorTests { + + @Test + void Check_Valid_Catalog() { + Assertions.assertDoesNotThrow(() -> new CatalogModel( + "", + "", + List.of(new ExtensionModel("", "", "", URI.create("https://github.com/qupath/qupath"), false, List.of())) + )); + } + + @Test + void Check_Undefined_Name() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new CatalogModel(null, "", List.of()) + ); + } + + @Test + void Check_Undefined_Description() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new CatalogModel("", null, List.of()) + ); + } + + @Test + void Check_Undefined_Extensions() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new CatalogModel("", "", null) + ); + } + + @Test + void Check_Extensions_With_Same_Name() { + 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 CatalogModel("", "", extensions) + ); + } + } + + @Nested + public class JsonTests { + + @Test + void Check_Valid_Catalog() { + CatalogModel expectedCatalog = new CatalogModel( + "", + "", + List.of(new ExtensionModel( + "", + "", + "", + URI.create("https://github.com/qupath/qupath"), + false, + List.of() + )) + ); + + CatalogModel catalog = new Gson().fromJson(""" + { + "name": "", + "description": "", + "extensions": [ + { + "name": "", + "description": "", + "author": "", + "homepage": "https://github.com/qupath/qupath", + "releases": [] + } + ] + } + """, + CatalogModel.class + ); + + Assertions.assertEquals(expectedCatalog, catalog); + } + + @Test + void Check_Undefined_Name() { + Assertions.assertThrows( + RuntimeException.class, + () -> new Gson().fromJson(""" + { + "description": "", + "extensions": [] + } + """, + CatalogModel.class + ) + ); + } + + @Test + void Check_Undefined_Description() { + Assertions.assertThrows( + RuntimeException.class, + () -> new Gson().fromJson(""" + { + "name": "", + "extensions": [] + } + """, + CatalogModel.class + ) + ); + } + + @Test + void Check_Undefined_Extensions() { + Assertions.assertThrows( + RuntimeException.class, + () -> new Gson().fromJson(""" + { + "name": "", + "description": "" + } + """, + CatalogModel.class + ) + ); + } + + @Test + void Check_Invalid_Extensions() { + Assertions.assertThrows( + RuntimeException.class, + () -> new Gson().fromJson(""" + { + "name": "", + "description": "", + "extensions": [{}] + } + """, + CatalogModel.class + ) + ); + } + + @Test + void Check_Extensions_With_Same_Name() { + Assertions.assertThrows( + RuntimeException.class, + () -> new Gson().fromJson(""" + { + "name": "", + "description": "", + "extensions": [ + { + "name": "name", + "description": "", + "author": "", + "homepage": "https://github.com/qupath/qupath", + "releases": [] + }, + { + "name": "name", + "description": "", + "author": "", + "homepage": "https://github.com/qupath/qupath", + "releases": [] + }, + { + "name": "other_name", + "description": "", + "author": "", + "homepage": "https://github.com/qupath/qupath", + "releases": [] + } + ] + } + """, + 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/model/TestExtensionModel.java b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestExtensionModel.java new file mode 100644 index 0000000..b10297f --- /dev/null +++ b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestExtensionModel.java @@ -0,0 +1,367 @@ +package qupath.ext.extensionmanager.core.model; + +import com.google.gson.Gson; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.util.List; + +public class TestExtensionModel { + + @Nested + public class ConstructorTests { + + @Test + void Check_Valid_Extension() { + Assertions.assertDoesNotThrow(() -> new ExtensionModel( + "", + "", + "", + URI.create("https://github.com/qupath/qupath"), + false, + 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 VersionRangeModel("v1.0.0", null, null) + )) + )); + } + + @Test + void Check_Undefined_Name() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new ExtensionModel( + null, + "", + "", + URI.create("https://github.com/qupath/qupath"), + false, + 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 VersionRangeModel("v1.0.0", null, null) + )) + ) + ); + } + + @Test + void Check_Undefined_Description() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new ExtensionModel( + "", + null, + "", + URI.create("https://github.com/qupath/qupath"), + false, + 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 VersionRangeModel("v1.0.0", null, null) + )) + ) + ); + } + + @Test + void Check_Undefined_Author() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new ExtensionModel( + "", + "", + null, + URI.create("https://github.com/qupath/qupath"), + false, + 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 VersionRangeModel("v1.0.0", null, null) + )) + ) + ); + } + + @Test + void Check_Undefined_Homepage() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new ExtensionModel( + "", + "", + "", + null, + false, + 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 VersionRangeModel("v1.0.0", null, null) + )) + ) + ); + } + + @Test + void Check_Undefined_Releases() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new ExtensionModel( + "", + "", + "", + URI.create("https://github.com/qupath/qupath"), + false, + null + ) + ); + } + + @Test + void Check_Homepage_Not_GitHub() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new ExtensionModel( + "", + "", + "", + URI.create("https://qupath.readthedocs.io/"), + false, + 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 VersionRangeModel("v1.0.0", null, null) + )) + ) + ); + } + } + + @Nested + public class JsonTests { + + @Test + void Check_Valid_Extension() { + ExtensionModel expectedExtension = new ExtensionModel( + "", + "", + "", + URI.create("https://github.com/qupath/qupath"), + true, + List.of(new ReleaseModel( + "v1.0.0", + URI.create("https://github.com/qupath/qupath"), + null, + null, + null, + new VersionRangeModel("v1.0.0", null, null) + )) + ); + + ExtensionModel extension = new Gson().fromJson(""" + { + "name": "", + "description": "", + "author": "", + "homepage": "https://github.com/qupath/qupath", + "starred": true, + "releases": [ + { + "name": "v1.0.0", + "mainUrl": "https://github.com/qupath/qupath", + "versionRange": { + "min": "v1.0.0" + } + } + ] + } + """, + ExtensionModel.class + ); + + Assertions.assertEquals(expectedExtension, extension); + } + + @Test + void Check_Undefined_Name() { + Assertions.assertThrows( + RuntimeException.class, + () -> new Gson().fromJson(""" + { + "description": "", + "author": "", + "homepage": "https://github.com/qupath/qupath", + "releases": [ + { + "name": "v1.0.0", + "mainUrl": "https://github.com/qupath/qupath", + "versionRange": { + "min": "v1.0.0" + } + } + ] + } + """, + ExtensionModel.class + ) + ); + } + + @Test + void Check_Undefined_Description() { + Assertions.assertThrows( + RuntimeException.class, + () -> new Gson().fromJson(""" + { + "name": "", + "author": "", + "homepage": "https://github.com/qupath/qupath", + "releases": [ + { + "name": "v1.0.0", + "mainUrl": "https://github.com/qupath/qupath", + "versionRange": { + "min": "v1.0.0" + } + } + ] + } + """, + ExtensionModel.class + ) + ); + } + + @Test + void Check_Undefined_Author() { + Assertions.assertThrows( + RuntimeException.class, + () -> new Gson().fromJson(""" + { + "name": "", + "description": "", + "homepage": "https://github.com/qupath/qupath", + "releases": [ + { + "name": "v1.0.0", + "mainUrl": "https://github.com/qupath/qupath", + "versionRange": { + "min": "v1.0.0" + } + } + ] + } + """, + ExtensionModel.class + ) + ); + } + + @Test + void Check_Undefined_Homepage() { + Assertions.assertThrows( + RuntimeException.class, + () -> new Gson().fromJson(""" + { + "name": "", + "description": "", + "author": "", + "releases": [ + { + "name": "v1.0.0", + "mainUrl": "https://github.com/qupath/qupath", + "versionRange": { + "min": "v1.0.0" + } + } + ] + } + """, + ExtensionModel.class + ) + ); + } + + @Test + void Check_Undefined_Releases() { + Assertions.assertThrows( + RuntimeException.class, + () -> new Gson().fromJson(""" + { + "name": "", + "description": "", + "author": "", + "homepage": "https://github.com/qupath/qupath" + } + """, + ExtensionModel.class + ) + ); + } + + @Test + void Check_Invalid_Release() { + Assertions.assertThrows( + RuntimeException.class, + () -> new Gson().fromJson(""" + { + "name": "", + "description": "", + "author": "", + "homepage": "https://github.com/qupath/qupath", + "releases": [{}] + } + """, + ExtensionModel.class + ) + ); + } + + @Test + void Check_Homepage_Not_GitHub() { + Assertions.assertThrows( + RuntimeException.class, + () -> new Gson().fromJson(""" + { + "name": "", + "description": "", + "author": "", + "homepage": "https://qupath.readthedocs.io/", + "releases": [ + { + "name": "v1.0.0", + "mainUrl": "https://github.com/qupath/qupath", + "versionRange": { + "min": "v1.0.0" + } + } + ] + } + """, + ExtensionModel.class + ) + ); + } + } +} diff --git a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestReleaseModel.java b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestReleaseModel.java new file mode 100644 index 0000000..287c62b --- /dev/null +++ b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/model/TestReleaseModel.java @@ -0,0 +1,442 @@ +package qupath.ext.extensionmanager.core.model; + +import com.google.gson.Gson; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.util.List; + +public class TestReleaseModel { + + @Nested + public class ConstructorTests { + + @Test + void Check_Valid_Release() { + Assertions.assertDoesNotThrow(() -> new ReleaseModel( + "v0.1.0", + URI.create("https://github.com/qupath/qupath"), + null, + null, + null, + new VersionRangeModel("v1.0.0", null, null) + )); + } + + @Test + void Check_Undefined_Name() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new ReleaseModel( + null, + URI.create("https://github.com/qupath/qupath"), + null, + null, + null, + new VersionRangeModel("v1.0.0", null, null) + ) + ); + } + + @Test + void Check_Undefined_Main_Url() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new ReleaseModel( + "v0.1.0", + null, + null, + null, + null, + new VersionRangeModel("v1.0.0", null, null) + ) + ); + } + + @Test + void Check_Undefined_Version_Range() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new ReleaseModel( + "v0.1.0", + URI.create("https://github.com/qupath/qupath"), + null, + null, + null, + null + ) + ); + } + + @Test + void Check_Invalid_Name() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new ReleaseModel( + "invalid_version", + URI.create("https://github.com/qupath/qupath"), + null, + null, + null, + new VersionRangeModel("v1.0.0", null, null) + ) + ); + } + + @Test + void Check_Invalid_Main_Url() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new ReleaseModel( + "v0.1.0", + URI.create("https://qupath.readthedocs.io/"), + null, + null, + null, + new VersionRangeModel("v1.0.0", null, null) + ) + ); + } + + @Test + void Check_Valid_Required_Dependency_Url() { + Assertions.assertDoesNotThrow( + () -> new ReleaseModel( + "v0.1.0", + URI.create("https://github.com/qupath/qupath/"), + List.of(URI.create("https://maven.scijava.org/content")), + null, + null, + new VersionRangeModel("v1.0.0", null, null) + ) + ); + } + + @Test + void Check_Invalid_Required_Dependency_Url() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new ReleaseModel( + "v0.1.0", + URI.create("https://github.com/qupath/qupath/"), + List.of(URI.create("https://qupath.readthedocs.io/")), + null, + null, + new VersionRangeModel("v1.0.0", null, null) + ) + ); + } + + @Test + void Check_Valid_Optional_Dependency_Url() { + Assertions.assertDoesNotThrow( + () -> new ReleaseModel( + "v0.1.0", + URI.create("https://github.com/qupath/qupath/"), + null, + List.of(URI.create("https://maven.scijava.org/content")), + null, + new VersionRangeModel("v1.0.0", null, null) + ) + ); + } + + @Test + void Check_Invalid_Optional_Dependency_Url() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new ReleaseModel( + "v0.1.0", + URI.create("https://github.com/qupath/qupath/"), + null, + List.of(URI.create("https://qupath.readthedocs.io/")), + null, + new VersionRangeModel("v1.0.0", null, null) + ) + ); + } + + @Test + void Check_Valid_Javadoc_Url() { + Assertions.assertDoesNotThrow( + () -> new ReleaseModel( + "v0.1.0", + URI.create("https://github.com/qupath/qupath/"), + null, + null, + List.of(URI.create("https://maven.scijava.org/content")), + new VersionRangeModel("v1.0.0", null, null) + ) + ); + } + + @Test + void Check_Invalid_Javadoc_Url() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new ReleaseModel( + "v0.1.0", + URI.create("https://github.com/qupath/qupath/"), + null, + null, + List.of(URI.create("https://qupath.readthedocs.io/")), + new VersionRangeModel("v1.0.0", null, null) + ) + ); + } + } + + @Nested + public class JsonTests { + + @Test + void Check_Valid_Release() { + ReleaseModel expectedRelease = new ReleaseModel( + "v0.1.0", + URI.create("https://github.com/qupath/qupath"), + null, + null, + null, + new VersionRangeModel("v1.0.0", null, null) + ); + + ReleaseModel release = new Gson().fromJson(""" + { + "name": "v0.1.0", + "mainUrl": "https://github.com/qupath/qupath", + "versionRange": { + "min": "v1.0.0" + } + } + """ + , + ReleaseModel.class + ); + + Assertions.assertEquals(expectedRelease, release); + } + + @Test + void Check_Undefined_Name() { + Assertions.assertThrows( + RuntimeException.class, + () -> new Gson().fromJson(""" + { + "mainUrl": "https://github.com/qupath/qupath", + "versionRange": { + "min": "v1.0.0" + } + } + """, + ReleaseModel.class + ) + ); + } + + @Test + void Check_Undefined_Main_Url() { + Assertions.assertThrows( + RuntimeException.class, + () -> new Gson().fromJson(""" + { + "name": "v0.1.0", + "versionRange": { + "min": "v1.0.0" + } + } + """, + ReleaseModel.class + ) + ); + } + + @Test + void Check_Undefined_Version_Range() { + Assertions.assertThrows( + RuntimeException.class, + () -> new Gson().fromJson(""" + { + "name": "v0.1.0", + "mainUrl": "https://github.com/qupath/qupath" + } + """, + ReleaseModel.class + ) + ); + } + + @Test + void Check_Invalid_Name() { + Assertions.assertThrows( + RuntimeException.class, + () -> new Gson().fromJson(""" + { + "name": "invalid_version", + "mainUrl": "https://github.com/qupath/qupath", + "versionRange": { + "min": "v1.0.0" + } + } + """, + ReleaseModel.class + ) + ); + } + + @Test + void Check_Invalid_Version_Range() { + Assertions.assertThrows( + RuntimeException.class, + () -> new Gson().fromJson(""" + { + "name": "v0.1.0", + "mainUrl": "https://github.com/qupath/qupath", + "versionRange": {} + } + """, + ReleaseModel.class + ) + ); + } + + @Test + void Check_Invalid_Main_Url() { + Assertions.assertThrows( + RuntimeException.class, + () -> new Gson().fromJson(""" + { + "name": "v0.1.0", + "mainUrl": "https://qupath.readthedocs.io/", + "versionRange": { + "min": "v1.0.0" + } + } + """, + ReleaseModel.class + ) + ); + } + + @Test + void Check_Valid_Required_Dependency_Url() { + List expectedRequiredDependencyUrls = List.of(URI.create("https://maven.scijava.org/content")); + + ReleaseModel release = new Gson().fromJson(""" + { + "name": "v0.1.0", + "mainUrl": "https://github.com/qupath/qupath", + "requiredDependencyUrls": ["https://maven.scijava.org/content"], + "versionRange": { + "min": "v1.0.0" + } + } + """ + , + ReleaseModel.class + ); + + Assertions.assertEquals(expectedRequiredDependencyUrls, release.requiredDependencyUrls()); + } + + @Test + void Check_Invalid_Required_Dependency_Url() { + Assertions.assertThrows( + RuntimeException.class, + () -> new Gson().fromJson(""" + { + "name": "v0.1.0", + "mainUrl": "https://github.com/qupath/qupath/", + "requiredDependencyUrls": ["https://qupath.readthedocs.io/"], + "versionRange": { + "min": "v1.0.0" + } + } + """, + ReleaseModel.class + ) + ); + } + + @Test + void Check_Valid_Optional_Dependency_Url() { + List expectedOptionalDependencyUrls = List.of(URI.create("https://maven.scijava.org/content")); + + ReleaseModel release = new Gson().fromJson(""" + { + "name": "v0.1.0", + "mainUrl": "https://github.com/qupath/qupath", + "optionalDependencyUrls": ["https://maven.scijava.org/content"], + "versionRange": { + "min": "v1.0.0" + } + } + """ + , + ReleaseModel.class + ); + + Assertions.assertEquals(expectedOptionalDependencyUrls, release.optionalDependencyUrls()); + } + + @Test + void Check_Invalid_Optional_Dependency_Url() { + Assertions.assertThrows( + RuntimeException.class, + () -> new Gson().fromJson(""" + { + "name": "v0.1.0", + "mainUrl": "https://github.com/qupath/qupath/", + "optionalDependencyUrls": ["https://qupath.readthedocs.io/"], + "versionRange": { + "min": "v1.0.0" + } + } + """, + ReleaseModel.class + ) + ); + } + + @Test + void Check_Valid_Javadoc_Url() { + List expectedJavadocUrls = List.of(URI.create("https://maven.scijava.org/content")); + + ReleaseModel release = new Gson().fromJson(""" + { + "name": "v0.1.0", + "mainUrl": "https://github.com/qupath/qupath", + "javadocUrls": ["https://maven.scijava.org/content"], + "versionRange": { + "min": "v1.0.0" + } + } + """ + , + ReleaseModel.class + ); + + Assertions.assertEquals(expectedJavadocUrls, release.javadocUrls()); + } + + @Test + void Check_Invalid_Javadoc_Url() { + Assertions.assertThrows( + RuntimeException.class, + () -> new Gson().fromJson(""" + { + "name": "v0.1.0", + "mainUrl": "https://github.com/qupath/qupath/", + "javadocUrls": ["https://qupath.readthedocs.io/"], + "versionRange": { + "min": "v1.0.0" + } + } + """, + 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 81% 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..6f09da2 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,20 +1,21 @@ -package qupath.ext.extensionmanager.core.catalog; +package qupath.ext.extensionmanager.core.model; import com.google.gson.Gson; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import qupath.ext.extensionmanager.core.Version; 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 +26,7 @@ void Check_Valid_Version_Range() { void Check_Undefined_Name() { Assertions.assertThrows( IllegalArgumentException.class, - () -> new VersionRange( + () -> new VersionRangeModel( null, null, null @@ -35,7 +36,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 +47,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 +57,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 +68,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 +80,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 +92,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 +104,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 +116,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 +130,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 +150,7 @@ void Check_Undefined_Name() { () -> new Gson().fromJson(""" {} """, - VersionRange.class + VersionRangeModel.class ) ); } @@ -158,13 +159,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 +181,7 @@ void Check_Invalid_When_Max_Lower_Than_Min() { "max": "v1.1.0" } """, - VersionRange.class + VersionRangeModel.class ) ); } @@ -189,13 +190,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 +212,7 @@ void Check_Invalid_When_Excluded_Lower_Than_Min() { "excludes": ["v1.3.0", "v2.0.0"] } """, - VersionRange.class + VersionRangeModel.class ) ); } @@ -227,7 +228,7 @@ void Check_Invalid_When_Excluded_Higher_Than_Max() { "excludes": ["v1.3.0", "v2.0.0"] } """, - VersionRange.class + VersionRangeModel.class ) ); } @@ -241,7 +242,7 @@ void Check_Invalid_Min_Version() { "min": "invalid_version" } """, - VersionRange.class + VersionRangeModel.class ) ); } @@ -256,7 +257,7 @@ void Check_Invalid_Max_Version() { "max": "invalid_version" } """, - VersionRange.class + VersionRangeModel.class ) ); } @@ -271,7 +272,7 @@ void Check_Invalid_Excluded_Version() { "excludes": ["v1.3.0", "invalid_version"] } """, - VersionRange.class + VersionRangeModel.class ) ); } @@ -279,7 +280,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") @@ -291,29 +292,14 @@ void Check_Version_Compatibility_When_Version_Null() { ); } - @Test - void Check_Version_Compatibility_When_Invalid_Version() { - VersionRange versionRange = new VersionRange( - "v0.1.0", - "v1.0.0", - List.of("v0.1.1", "v0.2.0", "v1.0.0") - ); - String version = "invalid_version"; - - Assertions.assertThrows( - IllegalArgumentException.class, - () -> versionRange.isCompatible(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") ); - String version = "v0.1.4"; + Version version = new Version("v0.1.4"); boolean isCompatible = versionRange.isCompatible(version); @@ -322,12 +308,12 @@ 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") ); - String version = "v0.1.4"; + Version version = new Version("v0.1.4"); boolean isCompatible = versionRange.isCompatible(version); @@ -336,12 +322,12 @@ 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") ); - String version = "v0.1.4"; + Version version = new Version("v0.1.4"); boolean isCompatible = versionRange.isCompatible(version); @@ -350,12 +336,12 @@ 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") ); - String version = "v1.0.4"; + Version version = new Version("v1.0.4"); boolean isCompatible = versionRange.isCompatible(version); @@ -364,12 +350,12 @@ 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") ); - String version = "v1.1.4"; + Version version = new Version("v1.1.4"); boolean isCompatible = versionRange.isCompatible(version); @@ -378,12 +364,12 @@ 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") ); - String version = "v0.0.4"; + Version version = new Version("v0.0.4"); boolean isCompatible = versionRange.isCompatible(version); @@ -392,12 +378,12 @@ 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") ); - String version = "v1.0.4"; + Version version = new Version("v1.0.4"); boolean isCompatible = versionRange.isCompatible(version); @@ -406,12 +392,12 @@ 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") ); - String version = "v0.2.0"; + Version version = new Version("v0.2.0"); boolean isCompatible = versionRange.isCompatible(version); diff --git a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/registry/TestRegistry.java b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/registry/TestRegistry.java new file mode 100644 index 0000000..4043e39 --- /dev/null +++ b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/registry/TestRegistry.java @@ -0,0 +1,74 @@ +package qupath.ext.extensionmanager.core.registry; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import qupath.ext.extensionmanager.SimpleServer; +import qupath.ext.extensionmanager.core.catalog.Catalog; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutionException; + +public class TestRegistry { + + @Test + void Check_Null_Catalogs() { + List catalogs = null; + + Assertions.assertThrows( + RuntimeException.class, + () -> new Registry(catalogs) + ); + } + + @Test + void Check_Creation_From_Catalogs() throws ExecutionException, InterruptedException, IOException { + List catalogNames = List.of("catalog1.json", "catalog2.json"); + SimpleServer server = new SimpleServer(catalogNames.stream() + .map(catalogName -> new SimpleServer.FileToServe( + catalogName, + Objects.requireNonNull(TestRegistry.class.getResourceAsStream(catalogName)) + )) + .toList() + ); + List catalogs = List.of( + new Catalog( + "Some catalog 1", + "Some description 1", + server.getURI("catalog1.json"), + server.getURI("catalog1.json") + ), + new Catalog( + "Some catalog 2", + "Some description 2", + server.getURI("catalog2.json"), + server.getURI("catalog2.json") + ) + ); + Registry expectedRegistry = new Registry(List.of( + new RegistryCatalog( + "Some catalog 1", + "Some description 1", + server.getURI("catalog1.json"), + server.getURI("catalog1.json"), + false, + List.of() + ), + new RegistryCatalog( + "Some catalog 2", + "Some description 2", + server.getURI("catalog2.json"), + server.getURI("catalog2.json"), + false, + List.of() + ) + )); + + Registry registry = Registry.createFromCatalogs(catalogs).get(); + + Assertions.assertEquals(expectedRegistry, registry); + + server.close(); + } +} diff --git a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/registry/TestRegistryCatalog.java b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/registry/TestRegistryCatalog.java new file mode 100644 index 0000000..317f06a --- /dev/null +++ b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/registry/TestRegistryCatalog.java @@ -0,0 +1,101 @@ +package qupath.ext.extensionmanager.core.registry; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.util.List; + +public class TestRegistryCatalog { + + @Test + void Check_Null_Name() { + String name = null; + String description = "description"; + URI uri = URI.create("http://uri.com"); + URI rawUri = URI.create("http://raw.com"); + boolean deletable = true; + List extensions = List.of(new RegistryExtension( + "name", + "installed version", + false + )); + + Assertions.assertThrows( + RuntimeException.class, + () -> new RegistryCatalog(name, description, uri, rawUri, deletable, extensions) + ); + } + + @Test + void Check_Null_Description() { + String name = "name"; + String description = null; + URI uri = URI.create("http://uri.com"); + URI rawUri = URI.create("http://raw.com"); + boolean deletable = true; + List extensions = List.of(new RegistryExtension( + "name", + "installed version", + false + )); + + Assertions.assertThrows( + RuntimeException.class, + () -> new RegistryCatalog(name, description, uri, rawUri, deletable, extensions) + ); + } + + @Test + void Check_Null_Uri() { + String name = "name"; + String description = "description"; + URI uri = null; + URI rawUri = URI.create("http://raw.com"); + boolean deletable = true; + List extensions = List.of(new RegistryExtension( + "name", + "installed version", + false + )); + + Assertions.assertThrows( + RuntimeException.class, + () -> new RegistryCatalog(name, description, uri, rawUri, deletable, extensions) + ); + } + + @Test + void Check_Null_Raw_Uri() { + String name = "name"; + String description = "description"; + URI uri = URI.create("http://uri.com"); + URI rawUri = null; + boolean deletable = true; + List extensions = List.of(new RegistryExtension( + "name", + "installed version", + false + )); + + Assertions.assertThrows( + RuntimeException.class, + () -> new RegistryCatalog(name, description, uri, rawUri, deletable, extensions) + ); + } + + @Test + void Check_Null_Extensions() { + String name = "name"; + String description = "description"; + URI uri = URI.create("http://uri.com"); + URI rawUri = URI.create("http://raw.com"); + boolean deletable = true; + List extensions = null; + + Assertions.assertThrows( + RuntimeException.class, + () -> new RegistryCatalog(name, description, uri, rawUri, deletable, extensions) + ); + } +} diff --git a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/registry/TestRegistryExtension.java b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/registry/TestRegistryExtension.java new file mode 100644 index 0000000..3401ab8 --- /dev/null +++ b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/registry/TestRegistryExtension.java @@ -0,0 +1,31 @@ +package qupath.ext.extensionmanager.core.registry; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class TestRegistryExtension { + + @Test + void Check_Null_Name() { + String name = null; + String installedVersion = "installed version"; + boolean optionalDependenciesInstalled = true; + + Assertions.assertThrows( + RuntimeException.class, + () -> new RegistryExtension(name, installedVersion, optionalDependenciesInstalled) + ); + } + + @Test + void Check_Null_Installed_Version() { + String name = "name"; + String installedVersion = null; + boolean optionalDependenciesInstalled = true; + + Assertions.assertThrows( + RuntimeException.class, + () -> new RegistryExtension(name, installedVersion, optionalDependenciesInstalled) + ); + } +} diff --git a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/tools/TestFileTools.java b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/tools/TestFileTools.java index 3c1b867..0d461f2 100644 --- a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/tools/TestFileTools.java +++ b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/tools/TestFileTools.java @@ -166,4 +166,31 @@ void Check_File_Parent_Of_Another_File_When_Descendant() { Assertions.assertTrue(isParent); } + + @Test + void Check_Stripped_Version_With_Null_File_Name() { + String fileName = null; + + Assertions.assertThrows(NullPointerException.class, () -> FileTools.stripVersionFromFileName(fileName)); + } + + @Test + void Check_Stripped_Version_When_Version_Present() { + String fileName = "qupath-extension-04.2.145-javadoc.jar"; + String expectedStrippedName = "qupath-extension-javadoc.jar"; + + String strippedName = FileTools.stripVersionFromFileName(fileName); + + Assertions.assertEquals(expectedStrippedName, strippedName); + } + + @Test + void Check_Stripped_Version_When_Version_Not_Present() { + String fileName = "qupath-extension.jar"; + String expectedStrippedName = "qupath-extension.jar"; + + String strippedName = FileTools.stripVersionFromFileName(fileName); + + Assertions.assertEquals(expectedStrippedName, strippedName); + } } diff --git a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/tools/TestFilesWatcher.java b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/tools/TestFilesWatcher.java index dd5c901..a39d921 100644 --- a/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/tools/TestFilesWatcher.java +++ b/extensionmanager/src/test/java/qupath/ext/extensionmanager/core/tools/TestFilesWatcher.java @@ -111,10 +111,10 @@ void Check_Files_Added_After_Watcher_Creation_Detected() throws Exception { Files.createFile(directoryToWatch.resolve("file2")) ); - Thread.sleep(CHANGE_WAITING_TIME_MS); // wait for files to be detected TestUtils.assertCollectionsEqualsWithoutOrder( expectedFiles, - filesWatcher.getFiles() + filesWatcher.getFiles(), + CHANGE_WAITING_TIME_MS // wait for files to be detected ); filesWatcher.close(); @@ -162,10 +162,10 @@ void Check_Files_Detected_After_Some_Are_Deleted() throws Exception { ); Files.delete(directoryToWatch.resolve("file1")); - Thread.sleep(CHANGE_WAITING_TIME_MS); // wait for files to be detected TestUtils.assertCollectionsEqualsWithoutOrder( expectedFiles, - filesWatcher.getFiles() + filesWatcher.getFiles(), + CHANGE_WAITING_TIME_MS // wait for files to be detected ); filesWatcher.close(); @@ -211,10 +211,10 @@ void Check_Files_Detected_After_Sub_Folder_Moved() throws Exception { ); Files.move(subFolder, Files.createTempDirectory(null).resolve("folder")); - Thread.sleep(CHANGE_WAITING_TIME_MS); // wait for files to be detected TestUtils.assertCollectionsEqualsWithoutOrder( expectedFiles, - filesWatcher.getFiles() + filesWatcher.getFiles(), + CHANGE_WAITING_TIME_MS // wait for files to be detected ); filesWatcher.close(); diff --git a/extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/catalog.json b/extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/catalog.json index 9cea989..9b882b2 100644 --- a/extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/catalog.json +++ b/extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/catalog.json @@ -10,6 +10,8 @@ { "name": "v0.1.0", "main_url": "https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar", + "required_dependency_urls": ["https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar"], + "optional_dependency_urls": ["https://github.com/qupath/qupath-macOS-extension/releases/download/v0.0.1/qupath-extension-macos.jar"], "version_range": { "min": "v1.0.0" } diff --git a/extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/catalog/catalog.json b/extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/catalog/catalog.json new file mode 100644 index 0000000..fa0bd42 --- /dev/null +++ b/extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/catalog/catalog.json @@ -0,0 +1,26 @@ +{ + "name": "Some catalog", + "description": "Some description", + "extensions": [{ + "name": "Some extension", + "description": "Some extension description", + "author": "Some author", + "homepage": "http://github.com/qupath/qupath", + "releases": [ + { + "name": "v0.1.0", + "main_url": "https://github.com/qupath/qupath", + "version_range": { + "min": "v1.0.0" + } + }, + { + "name": "v1.0.0", + "main_url": "https://github.com/qupath/qupath", + "version_range": { + "min": "v2.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 diff --git a/extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/registry/catalog1.json b/extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/registry/catalog1.json new file mode 100644 index 0000000..edbeb8f --- /dev/null +++ b/extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/registry/catalog1.json @@ -0,0 +1,26 @@ +{ + "name": "Some catalog 1", + "description": "Some description 1", + "extensions": [{ + "name": "Some extension 1", + "description": "Some extension description 1", + "author": "Some author 1", + "homepage": "http://github.com/qupath/qupath", + "releases": [ + { + "name": "v0.1.0", + "main_url": "https://github.com/qupath/qupath", + "version_range": { + "min": "v1.0.0" + } + }, + { + "name": "v1.0.0", + "main_url": "https://github.com/qupath/qupath", + "version_range": { + "min": "v2.0.0" + } + } + ] + }] +} diff --git a/extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/registry/catalog2.json b/extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/registry/catalog2.json new file mode 100644 index 0000000..332d975 --- /dev/null +++ b/extensionmanager/src/test/resources/qupath/ext/extensionmanager/core/registry/catalog2.json @@ -0,0 +1,26 @@ +{ + "name": "Some catalog 2", + "description": "Some description 2", + "extensions": [{ + "name": "Some extension 2", + "description": "Some extension description 2", + "author": "Some author 2", + "homepage": "http://github.com/qupath/qupath", + "releases": [ + { + "name": "v0.2.0", + "main_url": "https://github.com/qupath/qupath", + "version_range": { + "min": "v2.0.0" + } + }, + { + "name": "v2.0.0", + "main_url": "https://github.com/qupath/qupath", + "version_range": { + "min": "v2.0.0" + } + } + ] + }] +}