diff --git a/src/main/java/qupath/ext/omero/core/WebClient.java b/src/main/java/qupath/ext/omero/core/WebClient.java index ea4f345..16619d5 100644 --- a/src/main/java/qupath/ext/omero/core/WebClient.java +++ b/src/main/java/qupath/ext/omero/core/WebClient.java @@ -1,8 +1,12 @@ package qupath.ext.omero.core; -import com.drew.lang.annotations.Nullable; -import javafx.beans.property.*; -import javafx.collections.*; +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 javafx.collections.ObservableSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import qupath.ext.omero.core.apis.ApisHandler; @@ -16,8 +20,11 @@ import qupath.ext.omero.core.pixelapis.mspixelbuffer.MsPixelBufferAPI; import qupath.lib.gui.viewer.QuPathViewer; -import java.net.*; -import java.util.*; +import java.net.URI; +import java.util.List; +import java.util.Optional; +import java.util.Timer; +import java.util.TimerTask; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -53,15 +60,15 @@ public class WebClient implements AutoCloseable { private final ObjectProperty selectedPixelAPI = new SimpleObjectProperty<>(); private final ObservableSet openedImagesURIs = FXCollections.observableSet(); private final ObservableSet openedImagesURIsImmutable = FXCollections.unmodifiableObservableSet(openedImagesURIs); - private Server server; - private ApisHandler apisHandler; - private String username = null; - private boolean authenticated; - private List allPixelAPIs; - private Timer timeoutTimer; - private String sessionUuid; - private Status status; - private FailReason failReason; + private final Server server; + private final ApisHandler apisHandler; + private final String username; + private final boolean authenticated; + private final List allPixelAPIs; + private final Timer timeoutTimer; + private final String sessionUuid; + private final Status status; + private final FailReason failReason; /** * Status of a client creation @@ -114,7 +121,53 @@ public enum Authentication { SKIP } - private WebClient() {} + private WebClient( + Server server, + ApisHandler apisHandler, + String username, + boolean authenticated, + List allPixelAPIs, + Timer timeoutTimer, + String sessionUuid, + Status status, + FailReason failReason + ) { + this.server = server; + this.apisHandler = apisHandler; + this.username = username; + this.authenticated = authenticated; + this.allPixelAPIs = allPixelAPIs; + this.timeoutTimer = timeoutTimer; + this.sessionUuid = sessionUuid; + this.status = status; + this.failReason = failReason; + + if (timeoutTimer != null) { + timeoutTimer.schedule(new TimerTask() { + @Override + public void run() { + apisHandler.ping().thenAccept(success -> { + if (!success) { + logger.warn("Ping failed. Removing client"); + WebClients.removeClient(WebClient.this); + } + }); + } + }, PING_DELAY_MILLISECONDS, PING_DELAY_MILLISECONDS); + } + + if (allPixelAPIs != null) { + setUpPixelAPIs(); + } + } + + private WebClient(Status status, FailReason failReason) { + this(null, null, null, false, null, null, null, status, failReason); + } + + private WebClient(Status status) { + this(status, null); + } /** *

@@ -144,7 +197,13 @@ private WebClient() {} * @return a CompletableFuture with the client */ static CompletableFuture create(URI uri, Authentication authentication, String... args) { - return new WebClient().initialize(uri, authentication, args); + return ApisHandler.create(uri).thenApplyAsync(apisHandler -> { + if (apisHandler.isPresent()) { + return authenticateAndCreateClient(apisHandler.get(), uri, authentication, args); + } else { + return new WebClient(Status.FAILED); + } + }); } /** @@ -152,9 +211,18 @@ static CompletableFuture create(URI uri, Authentication authenticatio *

This function may block the calling thread for around a second.

*/ static WebClient createSync(URI uri, Authentication authentication, String... args) { - WebClient webClient = new WebClient(); - webClient.initializeSync(uri, authentication, args); - return webClient; + Optional apisHandler = Optional.empty(); + try { + apisHandler = ApisHandler.create(uri).get(); + } catch (InterruptedException | ExecutionException e) { + logger.error("Error initializing client", e); + } + + if (apisHandler.isPresent()) { + return authenticateAndCreateClient(apisHandler.get(), uri, authentication, args); + } else { + return new WebClient(Status.FAILED); + } } /** @@ -168,10 +236,7 @@ static WebClient createSync(URI uri, Authentication authentication, String... ar * @return an invalid client */ static WebClient createInvalidClient(FailReason failReason) { - WebClient webClient = new WebClient(); - webClient.status = Status.FAILED; - webClient.failReason = failReason; - return webClient; + return new WebClient(Status.FAILED, failReason); } @Override @@ -342,112 +407,91 @@ public boolean canBeClosed() { .anyMatch(server -> server instanceof OmeroImageServer omeroImageServer && omeroImageServer.getClient().equals(this))); } - private CompletableFuture initialize(URI uri, Authentication authentication, String... args) { - return ApisHandler.create(this, uri).thenApplyAsync(apisHandler -> { - if (apisHandler.isPresent()) { - this.apisHandler = apisHandler.get(); - return authenticate(uri, authentication, args); - } else { - return LoginResponse.createNonSuccessfulLoginResponse(LoginResponse.Status.FAILED); - } - }).thenApplyAsync(loginResponse -> { - LoginResponse.Status status = loginResponse.getStatus(); - if (status.equals(LoginResponse.Status.SUCCESS) || status.equals(LoginResponse.Status.UNAUTHENTICATED)) { - try { - var server = status.equals(LoginResponse.Status.SUCCESS) ? - Server.create(apisHandler, loginResponse.getGroup(), loginResponse.getUserId()).get() : - Server.create(apisHandler).get(); - - if (server.isPresent()) { - this.server = server.get(); - } else { - status = LoginResponse.Status.FAILED; - } - } catch (InterruptedException | ExecutionException e) { - logger.error("Error initializing client", e); - status = LoginResponse.Status.FAILED; + private void setUpPixelAPIs() { + availablePixelAPIs.setAll(allPixelAPIs.stream() + .filter(pixelAPI -> pixelAPI.isAvailable().get()) + .toList() + ); + for (PixelAPI pixelAPI: allPixelAPIs) { + pixelAPI.isAvailable().addListener((p, o, n) -> { + if (n && !availablePixelAPIs.contains(pixelAPI)) { + availablePixelAPIs.add(pixelAPI); + } else { + availablePixelAPIs.remove(pixelAPI); } - } - this.status = switch (status) { - case SUCCESS, UNAUTHENTICATED -> Status.SUCCESS; - case FAILED -> Status.FAILED; - case CANCELED -> Status.CANCELED; - }; - - if (this.status.equals(Status.SUCCESS)) { - setUpPixelAPIs(); - } + }); + } - return this; - }); + selectedPixelAPI.set(availablePixelAPIs.stream() + .filter(PixelAPI::canAccessRawPixels) + .findAny() + .orElse(availablePixelAPIs.get(0)) + ); + availablePixelAPIs.addListener((ListChangeListener) change -> + selectedPixelAPI.set(availablePixelAPIs.stream() + .filter(PixelAPI::canAccessRawPixels) + .findAny() + .orElse(availablePixelAPIs.get(0)) + )); } - private void initializeSync(URI uri, Authentication authentication, String... args) { - try { - var apisHandler = ApisHandler.create(this, uri).get(); + private static WebClient authenticateAndCreateClient(ApisHandler apisHandler, URI uri, Authentication authentication, String... args) { + LoginResponse loginResponse = authenticate(apisHandler, uri, authentication, args); - if (apisHandler.isPresent()) { - this.apisHandler = apisHandler.get(); - - LoginResponse loginResponse = authenticate(uri, authentication, args); - - LoginResponse.Status status = loginResponse.getStatus(); - if (status.equals(LoginResponse.Status.SUCCESS) || status.equals(LoginResponse.Status.UNAUTHENTICATED)) { - try { - var server = status.equals(LoginResponse.Status.SUCCESS) ? - Server.create(this.apisHandler, loginResponse.getGroup(), loginResponse.getUserId()).get() : - Server.create(this.apisHandler).get(); + return switch (loginResponse.getStatus()) { + case CANCELED -> new WebClient(Status.CANCELED); + case FAILED -> new WebClient(Status.FAILED); + case UNAUTHENTICATED, SUCCESS -> { + try { + Optional server = loginResponse.getStatus().equals(LoginResponse.Status.SUCCESS) ? + Server.create(apisHandler, loginResponse.getGroup(), loginResponse.getUserId()).get() : + Server.create(apisHandler).get(); - if (server.isPresent()) { - this.server = server.get(); - } else { - status = LoginResponse.Status.FAILED; - } - } catch (InterruptedException | ExecutionException e) { - logger.error("Error initializing client", e); - status = LoginResponse.Status.FAILED; - } - } - this.status = switch (status) { - case SUCCESS, UNAUTHENTICATED -> Status.SUCCESS; - case FAILED -> Status.FAILED; - case CANCELED -> Status.CANCELED; - }; - - if (this.status.equals(Status.SUCCESS)) { - setUpPixelAPIs(); + yield server.map(value -> new WebClient( + value, + apisHandler, + loginResponse.getUsername(), + loginResponse.getStatus().equals(LoginResponse.Status.SUCCESS), + List.of( + new WebAPI(apisHandler), + new IceAPI(apisHandler, loginResponse.getStatus().equals(LoginResponse.Status.SUCCESS), loginResponse.getSessionUuid()), + new MsPixelBufferAPI(apisHandler) + ), + loginResponse.getStatus().equals(LoginResponse.Status.SUCCESS) ? new Timer("omero-keep-alive", true) : null, + loginResponse.getSessionUuid(), + Status.SUCCESS, + null + )).orElseGet(() -> new WebClient(Status.FAILED)); + } catch (InterruptedException | ExecutionException e) { + logger.error("Error initializing client", e); + yield new WebClient(Status.FAILED); } - } else { - status = Status.FAILED; } - } catch (InterruptedException | ExecutionException e) { - logger.error("Error initializing client", e); - status = Status.FAILED; - } + }; } - private LoginResponse authenticate(URI uri, Authentication authentication, String... args) { + private static LoginResponse authenticate(ApisHandler apisHandler, URI uri, Authentication authentication, String... args) { try { Optional usernameFromArgs = getCredentialFromArgs("--username", "-u", args); Optional passwordFromArgs = getCredentialFromArgs("--password", "-p", args); return switch (authentication) { - case ENFORCE -> login( + case ENFORCE -> apisHandler.login( usernameFromArgs.orElse(null), passwordFromArgs.orElse(null) ).get(); case TRY_TO_SKIP -> { - if (this.apisHandler.canSkipAuthentication()) { + if (apisHandler.canSkipAuthentication()) { yield LoginResponse.createNonSuccessfulLoginResponse(LoginResponse.Status.UNAUTHENTICATED); } else { - yield login( + yield apisHandler.login( usernameFromArgs.orElse(null), passwordFromArgs.orElse(null) ).get(); } } case SKIP -> { - if (this.apisHandler.canSkipAuthentication()) { + if (apisHandler.canSkipAuthentication()) { yield LoginResponse.createNonSuccessfulLoginResponse(LoginResponse.Status.UNAUTHENTICATED); } else { logger.warn(String.format("The server %s doesn't allow browsing without being authenticated", uri)); @@ -461,51 +505,6 @@ yield login( } } - private void setUpPixelAPIs() { - allPixelAPIs = List.of( - new WebAPI(this), - new IceAPI(this), - new MsPixelBufferAPI(this) - ); - - availablePixelAPIs.setAll(allPixelAPIs.stream() - .filter(pixelAPI -> pixelAPI.isAvailable().get()) - .toList() - ); - for (PixelAPI pixelAPI: allPixelAPIs) { - pixelAPI.isAvailable().addListener((p, o, n) -> { - if (n && !availablePixelAPIs.contains(pixelAPI)) { - availablePixelAPIs.add(pixelAPI); - } else { - availablePixelAPIs.remove(pixelAPI); - } - }); - } - - selectedPixelAPI.set(availablePixelAPIs.stream() - .filter(PixelAPI::canAccessRawPixels) - .findAny() - .orElse(availablePixelAPIs.get(0)) - ); - availablePixelAPIs.addListener((ListChangeListener) change -> - selectedPixelAPI.set(availablePixelAPIs.stream() - .filter(PixelAPI::canAccessRawPixels) - .findAny() - .orElse(availablePixelAPIs.get(0)) - )); - } - - private CompletableFuture login(@Nullable String username, @Nullable String password) { - return apisHandler.login(username, password).thenApply(loginResponse -> { - if (loginResponse.getStatus().equals(LoginResponse.Status.SUCCESS)) { - setAuthenticationInformation(loginResponse); - startTimer(); - } - - return loginResponse; - }); - } - private static Optional getCredentialFromArgs( String credentialLabel, String credentialLabelAlternative, @@ -522,27 +521,4 @@ private static Optional getCredentialFromArgs( return Optional.ofNullable(credential); } - - private void setAuthenticationInformation(LoginResponse loginResponse) { - authenticated = true; - username = loginResponse.getUsername(); - sessionUuid = loginResponse.getSessionUuid(); - } - - private void startTimer() { - if (timeoutTimer == null) { - timeoutTimer = new Timer("omero-keep-alive", true); - timeoutTimer.schedule(new TimerTask() { - @Override - public void run() { - apisHandler.ping().thenAccept(success -> { - if (!success) { - logger.warn("Ping failed. Removing client"); - WebClients.removeClient(WebClient.this); - } - }); - } - }, PING_DELAY_MILLISECONDS, PING_DELAY_MILLISECONDS); - } - } } diff --git a/src/main/java/qupath/ext/omero/core/WebClients.java b/src/main/java/qupath/ext/omero/core/WebClients.java index 7f9ac0a..36db1a5 100644 --- a/src/main/java/qupath/ext/omero/core/WebClients.java +++ b/src/main/java/qupath/ext/omero/core/WebClients.java @@ -123,6 +123,16 @@ public static WebClient createClientSync(String url, WebClient.Authentication au } } + /** + * Retrieve the client corresponding to the provided uri. + * + * @param uri the web server URI of the client to retrieve + * @return the client corresponding to the URI, or an empty Optional if not found + */ + public static Optional getClientFromURI(URI uri) { + return clients.stream().filter(client -> client.getApisHandler().getWebServerURI().equals(uri)).findAny(); + } + /** * Close the given client connection. The function may return * before the connection is actually closed. diff --git a/src/main/java/qupath/ext/omero/core/apis/ApisHandler.java b/src/main/java/qupath/ext/omero/core/apis/ApisHandler.java index a111d27..14fb50b 100644 --- a/src/main/java/qupath/ext/omero/core/apis/ApisHandler.java +++ b/src/main/java/qupath/ext/omero/core/apis/ApisHandler.java @@ -1,7 +1,10 @@ package qupath.ext.omero.core.apis; import com.drew.lang.annotations.Nullable; -import javafx.beans.property.*; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyIntegerProperty; +import javafx.beans.property.SimpleBooleanProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import qupath.ext.omero.core.entities.annotations.AnnotationGroup; @@ -14,17 +17,21 @@ import qupath.ext.omero.core.entities.search.SearchQuery; import qupath.ext.omero.core.entities.search.SearchResult; import qupath.ext.omero.core.entities.shapes.Shape; -import qupath.lib.images.servers.*; -import qupath.ext.omero.core.WebClient; import qupath.ext.omero.core.WebUtilities; import qupath.ext.omero.core.entities.imagemetadata.ImageMetadataResponse; import qupath.ext.omero.core.entities.permissions.Group; import qupath.ext.omero.core.entities.permissions.Owner; import qupath.ext.omero.core.entities.repositoryentities.serverentities.image.Image; +import qupath.lib.images.servers.PixelType; +import qupath.lib.images.servers.TileRequest; import java.awt.image.BufferedImage; import java.net.URI; -import java.util.*; +import java.util.Collection; +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.ExecutionException; @@ -56,6 +63,7 @@ public class ApisHandler implements AutoCloseable { private final WebclientApi webclientApi; private final WebGatewayApi webGatewayApi; private final IViewerApi iViewerApi; + private final BooleanProperty areOrphanedImagesLoading = new SimpleBooleanProperty(false); private final Map thumbnails = new ConcurrentHashMap<>(); private final Map, BufferedImage> omeroIcons = new ConcurrentHashMap<>(); private final boolean canSkipAuthentication; @@ -75,12 +83,11 @@ private ApisHandler(URI host, JsonApi jsonApi, boolean canSkipAuthentication) { *

Attempt to create a request handler.

*

This function is asynchronous.

* - * @param client the corresponding web client * @param host the base server URI (e.g. https://idr.openmicroscopy.org) * @return a CompletableFuture with the request handler, an empty Optional if an error occurred */ - public static CompletableFuture> create(WebClient client, URI host) { - return JsonApi.create(client, host).thenApplyAsync(jsonApi -> { + public static CompletableFuture> create(URI host) { + return JsonApi.create(host).thenApplyAsync(jsonApi -> { if (jsonApi.isPresent()) { try { return Optional.of(new ApisHandler(host, jsonApi.get(), jsonApi.get().canSkipAuthentication().get())); @@ -280,24 +287,39 @@ public CompletableFuture> getImage(long imageID) { } /** - * See {@link JsonApi#getNumberOfOrphanedImages()}. + *

Attempt to get the number of orphaned images of this server.

+ * + * @return a CompletableFuture with the number of orphaned images, or 0 if it couldn't be retrieved */ public CompletableFuture getNumberOfOrphanedImages() { - return jsonApi.getNumberOfOrphanedImages(); + return getOrphanedImagesIds().thenApply(jsonApi::getNumberOfOrphanedImages); } /** - * See {@link JsonApi#populateOrphanedImagesIntoList(List)}. + *

+ * Populate all orphaned images of this server to the list specified in parameter. + * This function populates and doesn't return a list because the number of images can + * be large, so this operation can take tens of seconds. + *

+ *

The list can be updated from any thread.

+ * + * @param children the list which should be populated by the orphaned images. It should + * be possible to add elements to this list */ public void populateOrphanedImagesIntoList(List children) { - jsonApi.populateOrphanedImagesIntoList(children); + setOrphanedImagesLoading(true); + + getOrphanedImagesIds() + .thenAcceptAsync(orphanedImageIds -> jsonApi.populateOrphanedImagesIntoList(children, orphanedImageIds)) + .thenRun(() -> setOrphanedImagesLoading(false)); } /** - * See {@link JsonApi#areOrphanedImagesLoading()}. + * @return whether orphaned images are currently being loaded. + * This property may be updated from any thread */ public ReadOnlyBooleanProperty areOrphanedImagesLoading() { - return jsonApi.areOrphanedImagesLoading(); + return areOrphanedImagesLoading; } /** @@ -554,4 +576,8 @@ public CompletableFuture writeROIs(long id, Collection shapes, b public CompletableFuture> getImageSettings(long imageId) { return iViewerApi.getImageSettings(imageId); } + + private synchronized void setOrphanedImagesLoading(boolean orphanedImagesLoading) { + areOrphanedImagesLoading.set(orphanedImagesLoading); + } } diff --git a/src/main/java/qupath/ext/omero/core/apis/JsonApi.java b/src/main/java/qupath/ext/omero/core/apis/JsonApi.java index 0095ec9..824086b 100644 --- a/src/main/java/qupath/ext/omero/core/apis/JsonApi.java +++ b/src/main/java/qupath/ext/omero/core/apis/JsonApi.java @@ -2,12 +2,17 @@ import com.drew.lang.annotations.Nullable; import com.google.common.collect.Lists; -import com.google.gson.*; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; -import javafx.beans.property.*; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ReadOnlyIntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import qupath.ext.omero.core.WebClient; import qupath.ext.omero.core.apis.authenticators.Authenticator; import qupath.ext.omero.core.entities.login.LoginResponse; import qupath.ext.omero.core.entities.repositoryentities.serverentities.*; @@ -22,7 +27,11 @@ import java.net.PasswordAuthentication; import java.net.URI; -import java.util.*; +import java.util.Arrays; +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.ExecutionException; @@ -59,44 +68,83 @@ class JsonApi { private static final String WELLS_URL = "%s/api/v0/m/plateacquisitions/%d/wellsampleindex/%d/wells/"; private static final String ROIS_URL = "%s/api/v0/m/rois/?image=%s"; private final IntegerProperty numberOfEntitiesLoading = new SimpleIntegerProperty(0); - private final BooleanProperty areOrphanedImagesLoading = new SimpleBooleanProperty(false); private final IntegerProperty numberOfOrphanedImagesLoaded = new SimpleIntegerProperty(0); - private final URI webHost; - private final WebClient client; - private Map urls; - private int serverID; - private String serverHost; - private int port; - private String token; - - private JsonApi(WebClient client, URI host) { - this.client = client; - this.webHost = host; + private final URI webURI; + private final Map urls; + private final int serverID; + private final String serverURI; + private final int port; + private final String token; + + private JsonApi(URI webURI, Map urls, int serverID, String serverURI, int port, String token) { + this.webURI = webURI; + this.urls = urls; + this.serverID = serverID; + this.serverURI = serverURI; + this.port = port; + this.token = token; } @Override public String toString() { - return String.format("JSON API of %s", webHost); + return String.format("JSON API of %s", webURI); } /** *

Attempt to create a JSON API client.

*

This function is asynchronous.

* - * @param client the corresponding web client - * @param host the base server URI (e.g. https://idr.openmicroscopy.org) + * @param uri the web server URI (e.g. https://idr.openmicroscopy.org) * @return a CompletableFuture with the JSON API client, an empty Optional if an error occurred */ - public static CompletableFuture> create(WebClient client, URI host) { - JsonApi jsonApi = new JsonApi(client, host); + public static CompletableFuture> create(URI uri) { + var apiURI = WebUtilities.createURI(String.format(API_URL, uri)); - return jsonApi.initialize().thenApply(initialized -> { - if (initialized) { - return Optional.of(jsonApi); - } else { - return Optional.empty(); - } - }); + if (apiURI.isPresent()) { + return RequestSender.getAndConvert(apiURI.get(), OmeroAPI.class) + .thenCompose(omeroAPI -> { + if (omeroAPI.isPresent() && omeroAPI.get().getLatestVersionURL().isPresent()) { + return getURLs(omeroAPI.get().getLatestVersionURL().get()); + } else { + return CompletableFuture.completedFuture(Map.of()); + } + }) + .thenApplyAsync(urls -> { + if (urls.isEmpty()) { + logger.error("Could not find API URL in " + apiURI.get()); + return Optional.empty(); + } else { + try { + Optional serverInformation = getServerInformation(urls).get(); + Optional token = getToken(urls).get(); + + if (serverInformation.isPresent() && + serverInformation.get().getServerHost().isPresent() && + serverInformation.get().getServerId().isPresent() && + serverInformation.get().getServerPort().isPresent() && + token.isPresent() + ) { + return Optional.of(new JsonApi( + uri, + urls, + serverInformation.get().getServerId().getAsInt(), + serverInformation.get().getServerHost().get(), + serverInformation.get().getServerPort().getAsInt(), + token.get() + )); + } else { + logger.error("A server information (server uri, server ID, or server port) is missing"); + return Optional.empty(); + } + } catch (InterruptedException | ExecutionException e) { + logger.error("Error when retrieving information about the server", e); + return Optional.empty(); + } + } + }); + } else { + return CompletableFuture.completedFuture(Optional.empty()); + } } /** @@ -123,7 +171,7 @@ public String getToken() { * @return the server host */ public String getServerURI() { - return serverHost; + return serverURI; } /** @@ -162,7 +210,7 @@ public CompletableFuture login(@Nullable String username, @Nullab var uri = WebUtilities.createURI(urls.get(LOGIN_URL_KEY)); PasswordAuthentication authentication = username == null || password == null ? - Authenticator.getPasswordAuthentication(webHost.toString()) : + Authenticator.getPasswordAuthentication(webURI.toString()) : new PasswordAuthentication(username, password.toCharArray()); if (uri.isEmpty() || authentication == null) { @@ -317,7 +365,7 @@ public CompletableFuture> getImage(long imageID) { changeNumberOfEntitiesLoading(false); if (jsonElement.isPresent()) { - var serverEntity = ServerEntity.createFromJsonElement(jsonElement.get().getAsJsonObject().get("data"), client); + var serverEntity = ServerEntity.createFromJsonElement(jsonElement.get().getAsJsonObject().get("data"), webURI); if (serverEntity.isPresent() && serverEntity.get() instanceof Image image) { return Optional.of(image); } @@ -330,61 +378,50 @@ public CompletableFuture> getImage(long imageID) { } /** - *

Attempt to get the number of orphaned images of this server.

+ * Get the number of orphaned images of this server. * - * @return a CompletableFuture with the number of orphaned images, or 0 if it couldn't be retrieved + * @param orphanedImagesIds the IDs of the orphaned images of this server + * @return the number of orphaned images */ - public CompletableFuture getNumberOfOrphanedImages() { - return client.getApisHandler().getOrphanedImagesIds().thenApply(ids -> ids.stream() + public int getNumberOfOrphanedImages(List orphanedImagesIds) { + return (int) orphanedImagesIds.stream() .map(id -> WebUtilities.createURI(urls.get(IMAGES_URL_KEY) + id)) .flatMap(Optional::stream) - .toList() - ).thenApply(List::size); + .count(); } /** *

* Populate all orphaned images of this server to the list specified in parameter. * This function populates and doesn't return a list because the number of images can - * be large, so this operation can take tens of seconds. + * be large, so this operation can take tens of seconds and should be run in a background thread. *

*

The list can be updated from any thread.

* * @param children the list which should be populated by the orphaned images. It should * be possible to add elements to this list + * @param orphanedImagesIds the Ids of all orphaned images of the server */ - public void populateOrphanedImagesIntoList(List children) { - setOrphanedImagesLoading(true); + public void populateOrphanedImagesIntoList(List children, List orphanedImagesIds) { resetNumberOfOrphanedImagesLoaded(); - getOrphanedImagesURIs().thenAcceptAsync(uris -> { - // The number of parallel requests is limited to 16 - // to avoid too many concurrent streams - List> batches = Lists.partition(uris, 16); - for (List batch: batches) { - children.addAll(batch.stream() - .map(this::requestImageInfo) - .map(CompletableFuture::join) - .flatMap(Optional::stream) - .map(jsonObject -> ServerEntity.createFromJsonElement(jsonObject, client)) - .flatMap(Optional::stream) - .map(serverEntity -> (Image) serverEntity) - .toList() - ); - - addToNumberOfOrphanedImagesLoaded(batch.size()); - } - - setOrphanedImagesLoading(false); - }); - } + List orphanedImagesURIs = getOrphanedImagesURIs(orphanedImagesIds); + // The number of parallel requests is limited to 16 + // to avoid too many concurrent streams + List> batches = Lists.partition(orphanedImagesURIs, 16); + for (List batch: batches) { + children.addAll(batch.stream() + .map(this::requestImageInfo) + .map(CompletableFuture::join) + .flatMap(Optional::stream) + .map(jsonObject -> ServerEntity.createFromJsonElement(jsonObject, webURI)) + .flatMap(Optional::stream) + .map(serverEntity -> (Image) serverEntity) + .toList() + ); - /** - * @return whether orphaned images are currently being loaded. - * This property may be updated from any thread - */ - public ReadOnlyBooleanProperty areOrphanedImagesLoading() { - return areOrphanedImagesLoading; + addToNumberOfOrphanedImagesLoaded(batch.size()); + } } /** @@ -440,7 +477,7 @@ public CompletableFuture> getPlates(long screenID) { * @return a CompletableFuture with the list containing all plate acquisitions of the plate */ public CompletableFuture> getPlateAcquisitions(long plateID) { - return getChildren(String.format(PLATE_ACQUISITIONS_URL, webHost, plateID)).thenApply( + return getChildren(String.format(PLATE_ACQUISITIONS_URL, webURI, plateID)).thenApply( children -> children.stream().map(child -> (PlateAcquisition) child).toList() ); } @@ -453,7 +490,7 @@ public CompletableFuture> getPlateAcquisitions(long plate * @return a CompletableFuture with the list containing all wells of the plate */ public CompletableFuture> getWellsFromPlate(long plateID) { - return getChildren(String.format(PLATE_WELLS_URL, webHost, plateID)).thenApply( + return getChildren(String.format(PLATE_WELLS_URL, webURI, plateID)).thenApply( children -> children.stream().map(child -> (Well) child).toList() ); } @@ -467,7 +504,7 @@ public CompletableFuture> getWellsFromPlate(long plateID) { * @return a CompletableFuture with the list containing all wells of the plate acquisition */ public CompletableFuture> getWellsFromPlateAcquisition(long plateAcquisitionID, int wellSampleIndex) { - return getChildren(String.format(WELLS_URL, webHost, plateAcquisitionID, wellSampleIndex)).thenApply( + return getChildren(String.format(WELLS_URL, webURI, plateAcquisitionID, wellSampleIndex)).thenApply( children -> children.stream().map(child -> (Well) child).toList() ); } @@ -497,7 +534,7 @@ public CompletableFuture canSkipAuthentication() { * @return a CompletableFuture with the list of ROIs. If an error occurs, the list is empty */ public CompletableFuture> getROIs(long id) { - var uri = WebUtilities.createURI(String.format(ROIS_URL, webHost, id)); + var uri = WebUtilities.createURI(String.format(ROIS_URL, webURI, id)); if (uri.isPresent()) { var gson = new GsonBuilder().registerTypeAdapter(Shape.class, new Shape.GsonShapeDeserializer()).setLenient().create(); @@ -526,7 +563,7 @@ public CompletableFuture> getROIs(long id) { } } - private CompletableFuture> getURLs(String url) { + private static CompletableFuture> getURLs(String url) { var uri = WebUtilities.createURI(url); if (uri.isPresent()) { @@ -536,58 +573,7 @@ private CompletableFuture> getURLs(String url) { } } - private CompletableFuture initialize() { - var uri = WebUtilities.createURI(String.format(API_URL, webHost)); - - if (uri.isPresent()) { - return RequestSender.getAndConvert(uri.get(), OmeroAPI.class) - .thenCompose(omeroAPI -> { - if (omeroAPI.isPresent() && omeroAPI.get().getLatestVersionURL().isPresent()) { - return getURLs(omeroAPI.get().getLatestVersionURL().get()); - } else { - return CompletableFuture.completedFuture(Map.of()); - } - }) - .thenApplyAsync(urls -> { - if (urls.isEmpty()) { - logger.error("Could not find API URL in " + uri.get()); - return false; - } else { - if (setServerInformation(urls).join() && setToken(urls).join()) { - this.urls = urls; - - return true; - } else { - return false; - } - } - }); - } else { - return CompletableFuture.completedFuture(false); - } - } - - private synchronized void setOrphanedImagesLoading(boolean orphanedImagesLoading) { - areOrphanedImagesLoading.set(orphanedImagesLoading); - } - - private CompletableFuture> getOrphanedImagesURIs() { - return client.getApisHandler().getOrphanedImagesIds().thenApply(ids -> ids.stream() - .map(id -> WebUtilities.createURI(urls.get(IMAGES_URL_KEY) + id)) - .flatMap(Optional::stream) - .toList() - ); - } - - private synchronized void resetNumberOfOrphanedImagesLoaded() { - numberOfOrphanedImagesLoaded.set(0); - } - - private synchronized void addToNumberOfOrphanedImagesLoaded(int addition) { - numberOfOrphanedImagesLoaded.set(numberOfOrphanedImagesLoaded.get() + addition); - } - - private CompletableFuture setServerInformation(Map urls) { + private static CompletableFuture> getServerInformation(Map urls) { String url = SERVERS_URL_KEY; if (urls.containsKey(url)) { @@ -597,31 +583,17 @@ private CompletableFuture setServerInformation(Map urls return RequestSender.getAndConvert( uri.get(), OmeroServerList.class - ).thenApply(serverList -> { - if (serverList.isPresent() && - serverList.get().getServerId().isPresent() && - serverList.get().getServerHost().isPresent() && - serverList.get().getServerPort().isPresent() - ) { - serverID = serverList.get().getServerId().getAsInt(); - serverHost = serverList.get().getServerHost().get(); - port = serverList.get().getServerPort().getAsInt(); - return true; - } else { - logger.error("Couldn't get id. The server response doesn't contain the required information."); - return false; - } - }); + ); } else { - return CompletableFuture.completedFuture(false); + return CompletableFuture.completedFuture(Optional.empty()); } } else { logger.error("Couldn't find the URL corresponding to " + url); - return CompletableFuture.completedFuture(false); + return CompletableFuture.completedFuture(Optional.empty()); } } - private CompletableFuture setToken(Map urls) { + private static CompletableFuture> getToken(Map urls) { String url = TOKEN_URL_KEY; if (urls.containsKey(url)) { @@ -632,25 +604,37 @@ private CompletableFuture setToken(Map urls) { uri.get(), new TypeToken>() {} ).thenApply(response -> { - boolean canGetToken = response.isPresent() && response.get().containsKey("data"); - - if (canGetToken) { - token = response.get().get("data"); + if (response.isPresent() && response.get().containsKey("data")) { + return Optional.of(response.get().get("data")); } else { logger.error("Couldn't get token. The server response doesn't contain the required information."); + return Optional.empty(); } - - return canGetToken; }); } else { - return CompletableFuture.completedFuture(false); + return CompletableFuture.completedFuture(Optional.empty()); } } else { logger.error("Couldn't find the URL corresponding to " + url); - return CompletableFuture.completedFuture(false); + return CompletableFuture.completedFuture(Optional.empty()); } } + private List getOrphanedImagesURIs(List orphanedImagesIds) { + return orphanedImagesIds.stream() + .map(id -> WebUtilities.createURI(urls.get(IMAGES_URL_KEY) + id)) + .flatMap(Optional::stream) + .toList(); + } + + private synchronized void resetNumberOfOrphanedImagesLoaded() { + numberOfOrphanedImagesLoaded.set(0); + } + + private synchronized void addToNumberOfOrphanedImagesLoaded(int addition) { + numberOfOrphanedImagesLoaded.set(numberOfOrphanedImagesLoaded.get() + addition); + } + private CompletableFuture> getChildren(String url) { var uri = WebUtilities.createURI(url); @@ -660,7 +644,7 @@ private CompletableFuture> getChildren(String url) { return RequestSender.getPaginated(uri.get()).thenApply(jsonElements -> { changeNumberOfEntitiesLoading(false); - return ServerEntity.createFromJsonElements(jsonElements, client).toList(); + return ServerEntity.createFromJsonElements(jsonElements, webURI).toList(); }); } else { return CompletableFuture.completedFuture(List.of()); diff --git a/src/main/java/qupath/ext/omero/core/apis/WebclientApi.java b/src/main/java/qupath/ext/omero/core/apis/WebclientApi.java index 896ced9..846b642 100644 --- a/src/main/java/qupath/ext/omero/core/apis/WebclientApi.java +++ b/src/main/java/qupath/ext/omero/core/apis/WebclientApi.java @@ -11,7 +11,12 @@ import qupath.ext.omero.core.entities.annotations.FileAnnotation; import qupath.ext.omero.core.entities.annotations.MapAnnotation; import qupath.ext.omero.core.entities.repositoryentities.RepositoryEntity; -import qupath.ext.omero.core.entities.repositoryentities.serverentities.*; +import qupath.ext.omero.core.entities.repositoryentities.serverentities.Dataset; +import qupath.ext.omero.core.entities.repositoryentities.serverentities.Plate; +import qupath.ext.omero.core.entities.repositoryentities.serverentities.PlateAcquisition; +import qupath.ext.omero.core.entities.repositoryentities.serverentities.Project; +import qupath.ext.omero.core.entities.repositoryentities.serverentities.Screen; +import qupath.ext.omero.core.entities.repositoryentities.serverentities.ServerEntity; import qupath.ext.omero.core.entities.repositoryentities.serverentities.image.Image; import qupath.ext.omero.core.entities.search.SearchQuery; import qupath.ext.omero.core.entities.search.SearchResult; @@ -46,14 +51,14 @@ class WebclientApi implements AutoCloseable { private static final String LOGOUT_URL = "%s/webclient/logout/"; private static final String ORPHANED_IMAGES_URL = "%s/webclient/api/images/?orphaned=true"; private static final String READ_ANNOTATION_URL = "%s/webclient/api/annotations/?%s=%d"; - private final static String SEARCH_URL = "%s/webclient/load_searching/form/" + + private static final String SEARCH_URL = "%s/webclient/load_searching/form/" + "?query=%s&%s&%s&searchGroup=%s&ownedBy=%s" + "&useAcquisitionDate=false&startdateinput=&enddateinput=&_=%d"; - private final static String WRITE_KEY_VALUES_URL = "%s/webclient/annotate_map/"; - private final static String WRITE_NAME_URL = "%s/webclient/action/savename/image/%d/"; - private final static String WRITE_CHANNEL_NAMES_URL = "%s/webclient/edit_channel_names/%d/"; - private final static String SEND_ATTACHMENT_URL = "%s/webclient/annotate_file/"; - private final static String DELETE_ATTACHMENT_URL = "%s/webclient/action/delete/file/%d/"; + private static final String WRITE_KEY_VALUES_URL = "%s/webclient/annotate_map/"; + private static final String WRITE_NAME_URL = "%s/webclient/action/savename/image/%d/"; + private static final String WRITE_CHANNEL_NAMES_URL = "%s/webclient/edit_channel_names/%d/"; + private static final String SEND_ATTACHMENT_URL = "%s/webclient/annotate_file/"; + private static final String DELETE_ATTACHMENT_URL = "%s/webclient/action/delete/file/%d/"; private static final String IMAGE_ICON_URL = "%s/static/webclient/image/image16.png"; private static final String SCREEN_ICON_URL = "%s/static/webclient/image/folder_screen16.png"; private static final String PLATE_ICON_URL = "%s/static/webclient/image/folder_plate16.png"; diff --git a/src/main/java/qupath/ext/omero/core/entities/login/LoginResponse.java b/src/main/java/qupath/ext/omero/core/entities/login/LoginResponse.java index 5e2f767..c17de98 100644 --- a/src/main/java/qupath/ext/omero/core/entities/login/LoginResponse.java +++ b/src/main/java/qupath/ext/omero/core/entities/login/LoginResponse.java @@ -19,29 +19,45 @@ public class LoginResponse { private static final Logger logger = LoggerFactory.getLogger(LoginResponse.class); private final Status status; - private Group group; - private int userId; - private String username; - private String sessionUuid; + private final Group group; + private final int userId; + private final String username; + private final String sessionUuid; + + /** + * The login status + */ public enum Status { + /** + * The login was cancelled by the user + */ CANCELED, + /** + * The login failed + */ FAILED, + /** + * The guest account (without authentication) is used + */ UNAUTHENTICATED, + /** + * The login succeeded + */ SUCCESS } - private LoginResponse(Status reason) { - this.status = reason; - } - - private LoginResponse(Group group, int userId, String username, String sessionUuid) { - this(Status.SUCCESS); + private LoginResponse(Status status, Group group, int userId, String username, String sessionUuid) { + this.status = status; this.group = group; this.userId = userId; this.username = username; this.sessionUuid = sessionUuid; } + private LoginResponse(Status status) { + this(status, null, -1, null, null); + } + @Override public String toString() { return String.format("LoginResponse of status %s for %s of ID %d", status, username, userId); @@ -74,6 +90,7 @@ public static LoginResponse createSuccessfulLoginResponse(String serverResponse) JsonElement element = JsonParser.parseString(serverResponse).getAsJsonObject().get("eventContext"); return new LoginResponse( + Status.SUCCESS, new Gson().fromJson(element, Group.class), element.getAsJsonObject().get("userId").getAsInt(), element.getAsJsonObject().get("userName").getAsString(), @@ -93,7 +110,7 @@ public Status getStatus() { } /** - * @return the user ID of the authenticated user, or 0 if the login + * @return the user ID of the authenticated user, or -1 if the login * attempt failed */ public int getUserId() { diff --git a/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/Dataset.java b/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/Dataset.java index 739186d..3f5bcb2 100644 --- a/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/Dataset.java +++ b/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/Dataset.java @@ -5,12 +5,15 @@ import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import qupath.ext.omero.core.apis.ApisHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import qupath.ext.omero.core.WebClients; import qupath.ext.omero.core.entities.repositoryentities.serverentities.image.Image; import qupath.ext.omero.gui.UiUtilities; import qupath.ext.omero.core.entities.repositoryentities.OrphanedFolder; import qupath.ext.omero.core.entities.repositoryentities.RepositoryEntity; +import java.net.URI; import java.util.ResourceBundle; /** @@ -20,6 +23,7 @@ */ public class Dataset extends ServerEntity { + private static final Logger logger = LoggerFactory.getLogger(Dataset.class); private static final ResourceBundle resources = UiUtilities.getResources(); private static final String[] ATTRIBUTES = new String[] { resources.getString("Web.Entities.Dataset.name"), @@ -32,7 +36,7 @@ public class Dataset extends ServerEntity { private final transient ObservableList children = FXCollections.observableArrayList(); private final transient ObservableList childrenImmutable = FXCollections.unmodifiableObservableList(children); private transient boolean childrenPopulated = false; - private transient ApisHandler apisHandler; + private transient URI webServerURI; private transient boolean isPopulating = false; @SerializedName(value = "Description") private String description; @SerializedName(value = "omero:childCount") private int childCount; @@ -58,7 +62,7 @@ public boolean hasChildren() { } /** - * @throws IllegalStateException when the APIs handler has not been set (see {@link #setApisHandler(ApisHandler)}) + * @throws IllegalStateException when the web server URI has not been set (see {@link #setWebServerURI(URI)}) */ @Override public ObservableList getChildren() { @@ -123,25 +127,31 @@ public static boolean isDataset(String type) { } /** - * Set the APIs handler for this dataset. This is needed to populate its children. + * Set the web server URI of the server owning this dataset. This is needed to populate its children. * - * @param apisHandler the request handler of this browser + * @param webServerURI the web server URI of this server */ - public void setApisHandler(ApisHandler apisHandler) { - this.apisHandler = apisHandler; + public void setWebServerURI(URI webServerURI) { + this.webServerURI = webServerURI; } private void populateChildren() { - if (apisHandler == null) { + if (webServerURI == null) { throw new IllegalStateException( - "The APIs handler has not been set on this dataset. See the setApisHandler(ApisHandler) function." + "The web server URI has not been set on this dataset. See the setWebServerURI(URI) function." ); } else { - isPopulating = true; - apisHandler.getImages(getId()).thenAccept(images -> { - children.addAll(images); - isPopulating = false; - }); + WebClients.getClientFromURI(webServerURI).ifPresentOrElse(client -> { + isPopulating = true; + client.getApisHandler().getImages(getId()).thenAccept(images -> { + children.addAll(images); + isPopulating = false; + }); + }, () -> logger.warn(String.format( + "Could not find the web client corresponding to %s. Impossible to get the children of this dataset (%s).", + webServerURI, + this + ))); } } } diff --git a/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/Plate.java b/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/Plate.java index c346e94..236ae92 100644 --- a/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/Plate.java +++ b/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/Plate.java @@ -6,10 +6,13 @@ import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import qupath.ext.omero.core.apis.ApisHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import qupath.ext.omero.core.WebClients; import qupath.ext.omero.core.entities.repositoryentities.RepositoryEntity; import qupath.ext.omero.gui.UiUtilities; +import java.net.URI; import java.util.List; import java.util.Optional; import java.util.ResourceBundle; @@ -22,6 +25,7 @@ */ public class Plate extends ServerEntity { + private static final Logger logger = LoggerFactory.getLogger(Plate.class); private static final ResourceBundle resources = UiUtilities.getResources(); private static final String[] ATTRIBUTES = new String[] { resources.getString("Web.Entities.Plate.name"), @@ -34,7 +38,7 @@ public class Plate extends ServerEntity { private final transient ObservableList children = FXCollections.observableArrayList(); private final transient ObservableList childrenImmutable = FXCollections.unmodifiableObservableList(children); private transient boolean childrenPopulated = false; - private transient ApisHandler apisHandler; + private transient URI webServerURI; private transient boolean isPopulating = false; @SerializedName(value = "Columns") private int columns; @SerializedName(value = "Rows") private int rows; @@ -60,12 +64,12 @@ public boolean hasChildren() { if (!childrenPopulated) { return true; } else { - return children.size() > 0; + return !children.isEmpty(); } } /** - * @throws IllegalStateException when the APIs handler has not been set (see {@link #setApisHandler(ApisHandler)}) + * @throws IllegalStateException when the web server URI has not been set (see {@link #setWebServerURI(URI)}) */ @Override public ObservableList getChildren() { @@ -129,45 +133,51 @@ public static boolean isPlate(String type) { } /** - * Set the APIs handler for this plate. This is needed to populate its children. + * Set the web server URI of the server owning this plate. This is needed to populate its children. * - * @param apisHandler the APIs handler of this browser + * @param webServerURI the web server URI of this server */ - public void setApisHandler(ApisHandler apisHandler) { - this.apisHandler = apisHandler; + public void setWebServerURI(URI webServerURI) { + this.webServerURI = webServerURI; } private void populateChildren() { - if (apisHandler == null) { + if (webServerURI == null) { throw new IllegalStateException( - "The APIs handler has not been set on this plate. See the setApisHandler(ApisHandler) function." + "The web server URI has not been set on this plate. See the setWebServerURI(URI) function." ); } else { - isPopulating = true; - apisHandler.getPlateAcquisitions(getId()).thenCompose(plateAcquisitions -> { - for (PlateAcquisition plateAcquisition: plateAcquisitions) { - plateAcquisition.setNumberOfWells(columns * rows); - } - - children.addAll(plateAcquisitions); - return apisHandler.getWellsFromPlate(getId()); - }).thenAcceptAsync(wells -> { - List ids = wells.stream() - .map(well -> well.getImagesIds(false)) - .flatMap(List::stream) - .toList(); - List> batches = Lists.partition(ids, 16); - - for (List batch: batches) { - children.addAll(batch.stream() - .map(id -> apisHandler.getImage(id)) - .map(CompletableFuture::join) - .flatMap(Optional::stream) - .toList()); - } - - isPopulating = false; - }); + WebClients.getClientFromURI(webServerURI).ifPresentOrElse(client -> { + isPopulating = true; + client.getApisHandler().getPlateAcquisitions(getId()).thenCompose(plateAcquisitions -> { + for (PlateAcquisition plateAcquisition: plateAcquisitions) { + plateAcquisition.setNumberOfWells(columns * rows); + } + + children.addAll(plateAcquisitions); + return client.getApisHandler().getWellsFromPlate(getId()); + }).thenAcceptAsync(wells -> { + List ids = wells.stream() + .map(well -> well.getImagesIds(false)) + .flatMap(List::stream) + .toList(); + List> batches = Lists.partition(ids, 16); + + for (List batch: batches) { + children.addAll(batch.stream() + .map(id -> client.getApisHandler().getImage(id)) + .map(CompletableFuture::join) + .flatMap(Optional::stream) + .toList()); + } + + isPopulating = false; + }); + }, () -> logger.warn(String.format( + "Could not find the web client corresponding to %s. Impossible to get the children of this plate (%s).", + webServerURI, + this + ))); } } } diff --git a/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/PlateAcquisition.java b/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/PlateAcquisition.java index f98c3a0..c7d4c83 100644 --- a/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/PlateAcquisition.java +++ b/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/PlateAcquisition.java @@ -2,14 +2,19 @@ import com.google.common.collect.Lists; import com.google.gson.annotations.SerializedName; -import javafx.beans.property.*; +import javafx.beans.property.ReadOnlyStringProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import qupath.ext.omero.core.apis.ApisHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import qupath.ext.omero.core.WebClients; import qupath.ext.omero.core.entities.repositoryentities.RepositoryEntity; import qupath.ext.omero.core.entities.repositoryentities.serverentities.image.Image; import qupath.ext.omero.gui.UiUtilities; +import java.net.URI; import java.util.Date; import java.util.List; import java.util.Optional; @@ -22,6 +27,7 @@ */ public class PlateAcquisition extends ServerEntity { + private static final Logger logger = LoggerFactory.getLogger(PlateAcquisition.class); private static final ResourceBundle resources = UiUtilities.getResources(); private static final String[] ATTRIBUTES = new String[] { resources.getString("Web.Entities.PlateAcquisition.name"), @@ -34,7 +40,7 @@ public class PlateAcquisition extends ServerEntity { private final transient ObservableList childrenImmutable = FXCollections.unmodifiableObservableList(children); private final transient StringProperty label = new SimpleStringProperty((this.name == null ? "" : this.name) + " (0)"); private transient boolean childrenPopulated = false; - private transient ApisHandler apisHandler; + private transient URI webServerURI; private transient boolean isPopulating = false; private transient int numberOfWells = 0; @SerializedName(value = "omero:wellsampleIndex") private List wellSampleIndices; @@ -61,7 +67,7 @@ public boolean hasChildren() { } /** - * @throws IllegalStateException when the APIs handler has not been set (see {@link #setApisHandler(ApisHandler)}) + * @throws IllegalStateException when the web server URI has not been set (see {@link #setWebServerURI(URI)}) */ @Override public ObservableList getChildren() { @@ -124,12 +130,12 @@ public static boolean isPlateAcquisition(String type) { } /** - * Set the APIs handler for this plate acquisition. This is needed to populate its children. + * Set the web server URI of the server owning this plate acquisition. This is needed to populate its children. * - * @param apisHandler the APIs handler of this browser + * @param webServerURI the web server URI of this server */ - public void setApisHandler(ApisHandler apisHandler) { - this.apisHandler = apisHandler; + public void setWebServerURI(URI webServerURI) { + this.webServerURI = webServerURI; } /** @@ -143,35 +149,41 @@ public void setNumberOfWells(int numberOfWells) { } private void populateChildren() { - if (apisHandler == null) { + if (webServerURI == null) { throw new IllegalStateException( - "The APIs handler has not been set on this plate acquisition. See the setApisHandler(ApisHandler) function." + "The web server URI has not been set on this plate acquisition. See the setWebServerURI(URI) function." ); } else { - isPopulating = true; - - int wellSampleIndex = 0; - if (wellSampleIndices != null && wellSampleIndices.size() > 1) { - wellSampleIndex = wellSampleIndices.get(0); - } - - apisHandler.getWellsFromPlateAcquisition(getId(), wellSampleIndex).thenAcceptAsync(wells -> { - List ids = wells.stream() - .map(well -> well.getImagesIds(true)) - .flatMap(List::stream) - .toList(); - List> batches = Lists.partition(ids, 16); - - for (List batch: batches) { - children.addAll(batch.stream() - .map(id -> apisHandler.getImage(id)) - .map(CompletableFuture::join) - .flatMap(Optional::stream) - .toList()); + WebClients.getClientFromURI(webServerURI).ifPresentOrElse(client -> { + isPopulating = true; + + int wellSampleIndex = 0; + if (wellSampleIndices != null && wellSampleIndices.size() > 1) { + wellSampleIndex = wellSampleIndices.get(0); } - isPopulating = false; - }); + client.getApisHandler().getWellsFromPlateAcquisition(getId(), wellSampleIndex).thenAcceptAsync(wells -> { + List ids = wells.stream() + .map(well -> well.getImagesIds(true)) + .flatMap(List::stream) + .toList(); + List> batches = Lists.partition(ids, 16); + + for (List batch: batches) { + children.addAll(batch.stream() + .map(id -> client.getApisHandler().getImage(id)) + .map(CompletableFuture::join) + .flatMap(Optional::stream) + .toList()); + } + + isPopulating = false; + }); + }, () -> logger.warn(String.format( + "Could not find the web client corresponding to %s. Impossible to get the children of this plate acquisition (%s).", + webServerURI, + this + ))); } } } diff --git a/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/Project.java b/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/Project.java index c118ecf..3d10a83 100644 --- a/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/Project.java +++ b/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/Project.java @@ -5,10 +5,13 @@ import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import qupath.ext.omero.core.apis.ApisHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import qupath.ext.omero.core.WebClients; import qupath.ext.omero.gui.UiUtilities; import qupath.ext.omero.core.entities.repositoryentities.RepositoryEntity; +import java.net.URI; import java.util.ResourceBundle; /** @@ -17,6 +20,7 @@ */ public class Project extends ServerEntity { + private static final Logger logger = LoggerFactory.getLogger(Project.class); private static final ResourceBundle resources = UiUtilities.getResources(); private static final String[] ATTRIBUTES = new String[] { resources.getString("Web.Entities.Project.name"), @@ -29,7 +33,7 @@ public class Project extends ServerEntity { private final transient ObservableList children = FXCollections.observableArrayList(); private final transient ObservableList childrenImmutable = FXCollections.unmodifiableObservableList(children); private transient boolean childrenPopulated = false; - private transient ApisHandler apisHandler; + private transient URI webServerURI; private transient boolean isPopulating = false; @SerializedName(value = "Description") private String description; @SerializedName(value = "omero:childCount") private int childCount; @@ -55,7 +59,7 @@ public boolean hasChildren() { } /** - * @throws IllegalStateException when the APIs handler has not been set (see {@link #setApisHandler(ApisHandler)}) + * @throws IllegalStateException when the web server URI has not been set (see {@link #setWebServerURI(URI)}) */ @Override public ObservableList getChildren() { @@ -120,25 +124,31 @@ public static boolean isProject(String type) { } /** - * Set the APIs handler for this project. This is needed to populate its children. + * Set the web server URI of the server owning this project. This is needed to populate its children. * - * @param apisHandler the APIs handler of this browser + * @param webServerURI the web server URI of this server */ - public void setApisHandler(ApisHandler apisHandler) { - this.apisHandler = apisHandler; + public void setWebServerURI(URI webServerURI) { + this.webServerURI = webServerURI; } private void populateChildren() { - if (apisHandler == null) { + if (webServerURI == null) { throw new IllegalStateException( - "The APIs handler has not been set on this project. See the setApisHandler(ApisHandler) function." + "The web server URI has not been set on this project. See the setWebServerURI(URI) function." ); } else { - isPopulating = true; - apisHandler.getDatasets(getId()).thenAccept(datasets -> { - children.addAll(datasets); - isPopulating = false; - }); + WebClients.getClientFromURI(webServerURI).ifPresentOrElse(client -> { + isPopulating = true; + client.getApisHandler().getDatasets(getId()).thenAccept(datasets -> { + children.addAll(datasets); + isPopulating = false; + }); + }, () -> logger.warn(String.format( + "Could not find the web client corresponding to %s. Impossible to get the children of this project (%s).", + webServerURI, + this + ))); } } } diff --git a/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/Screen.java b/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/Screen.java index edc41d0..98aa9c5 100644 --- a/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/Screen.java +++ b/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/Screen.java @@ -5,10 +5,13 @@ import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import qupath.ext.omero.core.apis.ApisHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import qupath.ext.omero.core.WebClients; import qupath.ext.omero.core.entities.repositoryentities.RepositoryEntity; import qupath.ext.omero.gui.UiUtilities; +import java.net.URI; import java.util.ResourceBundle; /** @@ -17,6 +20,7 @@ */ public class Screen extends ServerEntity { + private static final Logger logger = LoggerFactory.getLogger(Screen.class); private static final ResourceBundle resources = UiUtilities.getResources(); private static final String[] ATTRIBUTES = new String[] { resources.getString("Web.Entities.Screen.name"), @@ -29,7 +33,7 @@ public class Screen extends ServerEntity { private final transient ObservableList children = FXCollections.observableArrayList(); private final transient ObservableList childrenImmutable = FXCollections.unmodifiableObservableList(children); private transient boolean childrenPopulated = false; - private transient ApisHandler apisHandler; + private transient URI webServerURI; private transient boolean isPopulating = false; @SerializedName(value = "Description") private String description; @SerializedName(value = "omero:childCount") private int childCount; @@ -55,7 +59,7 @@ public boolean hasChildren() { } /** - * @throws IllegalStateException when the APIs handler has not been set (see {@link #setApisHandler(ApisHandler)}) + * @throws IllegalStateException when the web server URI has not been set (see {@link #setWebServerURI(URI)}) */ @Override public ObservableList getChildren() { @@ -120,25 +124,31 @@ public static boolean isScreen(String type) { } /** - * Set the APIs handler for this screen. This is needed to populate its children. + * Set the web server URI of the server owning this screen. This is needed to populate its children. * - * @param apisHandler the APIs handler of this browser + * @param webServerURI the web server URI of this server */ - public void setApisHandler(ApisHandler apisHandler) { - this.apisHandler = apisHandler; + public void setWebServerURI(URI webServerURI) { + this.webServerURI = webServerURI; } private void populateChildren() { - if (apisHandler == null) { + if (webServerURI == null) { throw new IllegalStateException( - "The APIs handler has not been set on this screen. See the setApisHandler(ApisHandler) function." + "The web server URI has not been set on this screen. See the setWebServerURI(URI) function." ); } else { - isPopulating = true; - apisHandler.getPlates(getId()).thenAccept(plates -> { - children.addAll(plates); - isPopulating = false; - }); + WebClients.getClientFromURI(webServerURI).ifPresentOrElse(client -> { + isPopulating = true; + client.getApisHandler().getPlates(getId()).thenAccept(plates -> { + children.addAll(plates); + isPopulating = false; + }); + }, () -> logger.warn(String.format( + "Could not find the web client corresponding to %s. Impossible to get the children of this screen (%s).", + webServerURI, + this + ))); } } } diff --git a/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/ServerEntity.java b/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/ServerEntity.java index a05ebf4..565b343 100644 --- a/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/ServerEntity.java +++ b/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/ServerEntity.java @@ -1,16 +1,22 @@ package qupath.ext.omero.core.entities.repositoryentities.serverentities; -import com.google.gson.*; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; import com.google.gson.annotations.SerializedName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import qupath.ext.omero.core.WebClient; import qupath.ext.omero.core.entities.repositoryentities.serverentities.image.Image; import qupath.ext.omero.core.entities.permissions.Group; import qupath.ext.omero.core.entities.permissions.Owner; import qupath.ext.omero.core.entities.repositoryentities.RepositoryEntity; import java.lang.reflect.Type; +import java.net.URI; import java.util.List; import java.util.Optional; import java.util.stream.Stream; @@ -80,12 +86,12 @@ public boolean isFilteredByGroupOwner(Group groupFilter, Owner ownerFilter) { * If an entity cannot be created from a JSON element, it is discarded. * * @param jsonElements the JSON elements supposed to represent server entities - * @param client the corresponding web client + * @param uri the URI of the corresponding web server * @return a stream of server entities */ - public static Stream createFromJsonElements(List jsonElements, WebClient client) { + public static Stream createFromJsonElements(List jsonElements, URI uri) { return jsonElements.stream() - .map(jsonElement -> createFromJsonElement(jsonElement, client)) + .map(jsonElement -> createFromJsonElement(jsonElement, uri)) .flatMap(Optional::stream); } @@ -93,11 +99,11 @@ public static Stream createFromJsonElements(List json * Creates a server entity from a JSON element. * * @param jsonElement the JSON element supposed to represent a server entity - * @param client the corresponding web client + * @param uri the URI of the corresponding web server * @return a server entity, or an empty Optional if it was impossible to create */ - public static Optional createFromJsonElement(JsonElement jsonElement, WebClient client) { - Gson deserializer = new GsonBuilder().registerTypeAdapter(ServerEntity.class, new ServerEntityDeserializer(client)).setLenient().create(); + public static Optional createFromJsonElement(JsonElement jsonElement, URI uri) { + Gson deserializer = new GsonBuilder().registerTypeAdapter(ServerEntity.class, new ServerEntityDeserializer(uri)).setLenient().create(); try { return Optional.ofNullable(deserializer.fromJson(jsonElement, ServerEntity.class)); @@ -128,7 +134,7 @@ public Group getGroup() { return group; } - private record ServerEntityDeserializer(WebClient client) implements JsonDeserializer { + private record ServerEntityDeserializer(URI uri) implements JsonDeserializer { @Override public ServerEntity deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { try { @@ -137,22 +143,22 @@ public ServerEntity deserialize(JsonElement json, Type typeOfT, JsonDeserializat ServerEntity serverEntity = null; if (Image.isImage(type)) { serverEntity = context.deserialize(json, Image.class); - ((Image) serverEntity).setWebClient(client); + ((Image) serverEntity).setWebServerURI(uri); } else if (Dataset.isDataset(type)) { serverEntity = context.deserialize(json, Dataset.class); - ((Dataset) serverEntity).setApisHandler(client.getApisHandler()); + ((Dataset) serverEntity).setWebServerURI(uri); } else if (Project.isProject(type)) { serverEntity = context.deserialize(json, Project.class); - ((Project) serverEntity).setApisHandler(client.getApisHandler()); + ((Project) serverEntity).setWebServerURI(uri); } else if (Screen.isScreen(type)) { serverEntity = context.deserialize(json, Screen.class); - ((Screen) serverEntity).setApisHandler(client.getApisHandler()); + ((Screen) serverEntity).setWebServerURI(uri); } else if (Plate.isPlate(type)) { serverEntity = context.deserialize(json, Plate.class); - ((Plate) serverEntity).setApisHandler(client.getApisHandler()); + ((Plate) serverEntity).setWebServerURI(uri); } else if (PlateAcquisition.isPlateAcquisition(type)) { serverEntity = context.deserialize(json, PlateAcquisition.class); - ((PlateAcquisition) serverEntity).setApisHandler(client.getApisHandler()); + ((PlateAcquisition) serverEntity).setWebServerURI(uri); } else if (Well.isWell(type)) { serverEntity = context.deserialize(json, Well.class); } else { diff --git a/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/Well.java b/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/Well.java index 58d81f7..3986f98 100644 --- a/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/Well.java +++ b/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/Well.java @@ -41,11 +41,10 @@ public Well(long id) { @Override public boolean hasChildren() { - return wellSamples != null && wellSamples.stream() + return wellSamples != null && !wellSamples.stream() .map(WellSample::getImage) .flatMap(Optional::stream) - .toList() - .size() > 0; + .toList().isEmpty(); } @Override diff --git a/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/image/Image.java b/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/image/Image.java index f70c69f..7f8f16b 100644 --- a/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/image/Image.java +++ b/src/main/java/qupath/ext/omero/core/entities/repositoryentities/serverentities/image/Image.java @@ -1,16 +1,30 @@ package qupath.ext.omero.core.entities.repositoryentities.serverentities.image; import com.google.gson.annotations.SerializedName; -import javafx.beans.property.*; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyStringProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import qupath.ext.omero.core.WebClient; +import qupath.ext.omero.core.WebClients; import qupath.ext.omero.core.apis.ApisHandler; +import qupath.ext.omero.core.pixelapis.PixelAPI; import qupath.ext.omero.gui.UiUtilities; import qupath.ext.omero.core.entities.repositoryentities.RepositoryEntity; import qupath.ext.omero.core.entities.repositoryentities.serverentities.ServerEntity; -import java.util.*; +import java.net.URI; +import java.util.Date; +import java.util.EnumSet; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.Set; /** *

@@ -22,6 +36,7 @@ */ public class Image extends ServerEntity { + private static final Logger logger = LoggerFactory.getLogger(Image.class); private static final ResourceBundle resources = UiUtilities.getResources(); private static final String[] ATTRIBUTES = new String[] { resources.getString("Web.Entities.Image.name"), @@ -44,9 +59,21 @@ public class Image extends ServerEntity { private transient BooleanProperty isSupported; @SerializedName(value = "AcquisitionDate") private long acquisitionDate; @SerializedName(value = "Pixels") private PixelInfo pixels; + /** + * The reason why an image may not be supported by a pixel API + */ public enum UnsupportedReason { + /** + * The selected pixel API does not support images with this number of channels + */ NUMBER_OF_CHANNELS, + /** + * The selected pixel API does not support images with this pixel type + */ PIXEL_TYPE, + /** + * The selected pixel API is not available + */ PIXEL_API_UNAVAILABLE } @@ -156,28 +183,36 @@ public static boolean isImage(String type) { } /** - * Set the web client of this image. This is needed to determine if this image + * Indicate the web server URI corresponding to this image. This is needed to determine if this image * can be opened. * - * @param client the web client of this image + * @param webServerURI the web server URI of this image */ - public void setWebClient(WebClient client) { - isSupported = new SimpleBooleanProperty(); + public void setWebServerURI(URI webServerURI) { + isSupported = new SimpleBooleanProperty(false); unsupportedReasons = EnumSet.noneOf(UnsupportedReason.class); - setSupported(client); - client.getSelectedPixelAPI().addListener(change -> setSupported(client)); + WebClients.getClientFromURI(webServerURI).map(WebClient::getSelectedPixelAPI).ifPresentOrElse(selectedPixelAPI -> { + setSupported(selectedPixelAPI); + selectedPixelAPI.addListener(change -> setSupported(selectedPixelAPI)); + }, () -> + logger.warn(String.format( + "Could not find the web client corresponding to %s. Impossible to determine if this image (%s) is supported.", + webServerURI, + this + )) + ); } /** * @return whether this image can be opened within QuPath. This property may be updated * from any thread - * @throws IllegalStateException when the web client has not been set (see {@link #setWebClient(WebClient)}) + * @throws IllegalStateException when the web server URI has not been set (see {@link #setWebServerURI(URI)}) */ public ReadOnlyBooleanProperty isSupported() { if (isSupported == null) { throw new IllegalStateException( - "The web client has not been set on this image. See the setWebClient(WebClient) function." + "The web server URI has not been set on this image. See the setWebServerURI(URI) function." ); } return isSupported; @@ -185,12 +220,12 @@ public ReadOnlyBooleanProperty isSupported() { /** * @return the reasons why this image is unsupported (empty if this image is supported) - * @throws IllegalStateException when the web client has not been set (see {@link #setWebClient(WebClient)}) + * @throws IllegalStateException when the web server URI has not been set (see {@link #setWebServerURI(URI)}) */ public Set getUnsupportedReasons() { if (unsupportedReasons == null) { throw new IllegalStateException( - "The web client has not been set on this image. See the setWebClient(WebClient) function." + "The web server URI has not been set on this image. See the setWebServerURI(URI) function." ); } return unsupportedReasons; @@ -220,17 +255,17 @@ private Optional getPhysicalSizeZ() { return pixels == null ? Optional.empty() : pixels.getPhysicalSizeZ(); } - private void setSupported(WebClient client) { + private synchronized void setSupported(ReadOnlyObjectProperty selectedPixelAPI) { isSupported.set(true); unsupportedReasons.clear(); - if (client.getSelectedPixelAPI() == null) { + if (selectedPixelAPI == null) { isSupported.set(false); unsupportedReasons.add(UnsupportedReason.PIXEL_API_UNAVAILABLE); } else { if (!getPixelType() .flatMap(ApisHandler::getPixelType) - .map(pixelType -> client.getSelectedPixelAPI().get().canReadImage(pixelType)) + .map(pixelType -> selectedPixelAPI.get().canReadImage(pixelType)) .orElse(false) ) { isSupported.set(false); @@ -238,7 +273,7 @@ private void setSupported(WebClient client) { } if (!getImageDimensions() - .map(imageDimensions -> client.getSelectedPixelAPI().get().canReadImage(imageDimensions[3])) + .map(imageDimensions -> selectedPixelAPI.get().canReadImage(imageDimensions[3])) .orElse(false) ) { isSupported.set(false); diff --git a/src/main/java/qupath/ext/omero/core/pixelapis/ice/IceAPI.java b/src/main/java/qupath/ext/omero/core/pixelapis/ice/IceAPI.java index 63634b1..01d0f2e 100644 --- a/src/main/java/qupath/ext/omero/core/pixelapis/ice/IceAPI.java +++ b/src/main/java/qupath/ext/omero/core/pixelapis/ice/IceAPI.java @@ -1,10 +1,11 @@ package qupath.ext.omero.core.pixelapis.ice; +import com.drew.lang.annotations.Nullable; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ObservableBooleanValue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import qupath.ext.omero.core.WebClient; +import qupath.ext.omero.core.apis.ApisHandler; import qupath.lib.images.servers.ImageServerMetadata; import qupath.ext.omero.core.pixelapis.PixelAPI; import qupath.ext.omero.core.pixelapis.PixelAPIReader; @@ -23,7 +24,9 @@ public class IceAPI implements PixelAPI { private static final Logger logger = LoggerFactory.getLogger(IceAPI.class); static final String NAME = "Ice"; private static boolean gatewayAvailable; - private final WebClient client; + private final ApisHandler apisHandler; + private final boolean isAuthenticated; + private final String sessionUuid; static { try { @@ -41,10 +44,14 @@ public class IceAPI implements PixelAPI { /** * Creates a new IceAPI. * - * @param client the WebClient owning this API + * @param apisHandler the apis handler owning this API + * @param isAuthenticated whether the user is currently authenticated to the OMERO server + * @param sessionUuid the session UUID of the client connection. Can be null if the user is not authenticated */ - public IceAPI(WebClient client) { - this.client = client; + public IceAPI(ApisHandler apisHandler, boolean isAuthenticated, @Nullable String sessionUuid) { + this.apisHandler = apisHandler; + this.isAuthenticated = isAuthenticated; + this.sessionUuid = sessionUuid; } @Override @@ -54,7 +61,7 @@ public String getName() { @Override public ObservableBooleanValue isAvailable() { - return new SimpleBooleanProperty(client.isAuthenticated() && gatewayAvailable); + return new SimpleBooleanProperty(isAuthenticated && gatewayAvailable); } @Override @@ -74,14 +81,14 @@ public boolean canReadImage(int numberOfChannels) { @Override public PixelAPIReader createReader(long id, ImageServerMetadata metadata) throws IOException { - if (!isAvailable().get()) { + if (!isAvailable().get() || sessionUuid == null) { throw new IllegalStateException("This API is not available and cannot be used"); } if (!canReadImage(metadata.getPixelType(), metadata.getSizeC())) { throw new IllegalArgumentException("The provided image cannot be read by this API"); } - return new IceReader(client, id, metadata.getChannels()); + return new IceReader(apisHandler, sessionUuid, id, metadata.getChannels()); } @Override @@ -90,16 +97,16 @@ public boolean equals(Object obj) { return true; if (!(obj instanceof IceAPI iceAPI)) return false; - return iceAPI.client.equals(client); + return iceAPI.apisHandler.equals(apisHandler); } @Override public int hashCode() { - return client.hashCode(); + return apisHandler.hashCode(); } @Override public String toString() { - return String.format("Ice API of %s", client.getApisHandler().getWebServerURI()); + return String.format("Ice API of %s", apisHandler.getWebServerURI()); } } diff --git a/src/main/java/qupath/ext/omero/core/pixelapis/ice/IceReader.java b/src/main/java/qupath/ext/omero/core/pixelapis/ice/IceReader.java index da206fb..d4ef98e 100644 --- a/src/main/java/qupath/ext/omero/core/pixelapis/ice/IceReader.java +++ b/src/main/java/qupath/ext/omero/core/pixelapis/ice/IceReader.java @@ -13,7 +13,6 @@ import omero.model.ExperimenterGroup; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import qupath.ext.omero.core.WebClient; import qupath.ext.omero.core.apis.ApisHandler; import qupath.lib.color.ColorModelFactory; import qupath.lib.images.servers.ImageChannel; @@ -48,14 +47,15 @@ class IceReader implements PixelAPIReader { /** * Creates a new Ice reader. * - * @param client the WebClient owning the image to open + * @param apisHandler the ApisHandler owning the image to open + * @param sessionUuid the session UUID of the client connection * @param imageID the ID of the image to open * @param channels the channels of the image to open * @throws IOException when the reader creation fails */ - public IceReader(WebClient client, long imageID, List channels) throws IOException { + public IceReader(ApisHandler apisHandler, String sessionUuid, long imageID, List channels) throws IOException { try { - ExperimenterData user = connect(client); + ExperimenterData user = connect(apisHandler, sessionUuid); context = new SecurityContext(user.getGroupId()); @@ -147,35 +147,36 @@ public String toString() { * used, and if not successful, the OMERO server host will be used (see * {@link ApisHandler#getServerURI()}). * - * @param client the connection to use + * @param apisHandler the ApisHandler owning the image to open + * @param sessionUuid the session UUID of the client connection * @return a valid connection * @throws DSOutOfServiceException when a connection cannot be established */ - private ExperimenterData connect(WebClient client) throws DSOutOfServiceException { - String firstURI = client.getApisHandler().getWebServerURI().getHost(); - String secondURI = client.getApisHandler().getServerURI(); + private ExperimenterData connect(ApisHandler apisHandler, String sessionUuid) throws DSOutOfServiceException { + String firstURI = apisHandler.getWebServerURI().getHost(); + String secondURI = apisHandler.getServerURI(); try { return gateway.connect(new LoginCredentials( - client.getSessionUuid().orElse(""), - client.getSessionUuid().orElse(""), + sessionUuid, + sessionUuid, firstURI, - client.getApisHandler().getServerPort() + apisHandler.getServerPort() )); } catch (Exception e) { logger.warn(String.format( "Can't connect to %s:%d. Trying %s:%d...", firstURI, - client.getApisHandler().getServerPort(), + apisHandler.getServerPort(), secondURI, - client.getApisHandler().getServerPort() + apisHandler.getServerPort() ), e); return gateway.connect(new LoginCredentials( - client.getSessionUuid().orElse(""), - client.getSessionUuid().orElse(""), + sessionUuid, + sessionUuid, secondURI, - client.getApisHandler().getServerPort() + apisHandler.getServerPort() )); } } diff --git a/src/main/java/qupath/ext/omero/core/pixelapis/mspixelbuffer/MsPixelBufferAPI.java b/src/main/java/qupath/ext/omero/core/pixelapis/mspixelbuffer/MsPixelBufferAPI.java index 821c8ba..108787a 100644 --- a/src/main/java/qupath/ext/omero/core/pixelapis/mspixelbuffer/MsPixelBufferAPI.java +++ b/src/main/java/qupath/ext/omero/core/pixelapis/mspixelbuffer/MsPixelBufferAPI.java @@ -1,13 +1,17 @@ package qupath.ext.omero.core.pixelapis.mspixelbuffer; -import javafx.beans.property.*; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ReadOnlyIntegerProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.value.ObservableBooleanValue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import qupath.ext.omero.core.ClientsPreferencesManager; import qupath.ext.omero.core.RequestSender; -import qupath.ext.omero.core.WebClient; import qupath.ext.omero.core.WebUtilities; +import qupath.ext.omero.core.apis.ApisHandler; import qupath.ext.omero.core.pixelapis.PixelAPI; import qupath.ext.omero.core.pixelapis.PixelAPIReader; import qupath.lib.images.servers.ImageServerMetadata; @@ -30,7 +34,7 @@ public class MsPixelBufferAPI implements PixelAPI { private static final int DEFAULT_PORT = 8082; private static final String PORT_PARAMETER = "--msPixelBufferPort"; private static final Logger logger = LoggerFactory.getLogger(MsPixelBufferAPI.class); - private final WebClient client; + private final ApisHandler apisHandler; private final BooleanProperty isAvailable = new SimpleBooleanProperty(false); private final IntegerProperty port; private String host; @@ -38,12 +42,12 @@ public class MsPixelBufferAPI implements PixelAPI { /** * Creates a new MsPixelBufferAPI. * - * @param client the WebClient owning this API + * @param apisHandler the apis handler owning this API */ - public MsPixelBufferAPI(WebClient client) { - this.client = client; + public MsPixelBufferAPI(ApisHandler apisHandler) { + this.apisHandler = apisHandler; port = new SimpleIntegerProperty( - ClientsPreferencesManager.getMsPixelBufferPort(client.getApisHandler().getWebServerURI()).orElse(DEFAULT_PORT) + ClientsPreferencesManager.getMsPixelBufferPort(apisHandler.getWebServerURI()).orElse(DEFAULT_PORT) ); setHost(); @@ -103,7 +107,6 @@ public PixelAPIReader createReader(long id, ImageServerMetadata metadata) { } return new MsPixelBufferReader( - client, host, id, metadata.getPixelType(), @@ -118,17 +121,17 @@ public boolean equals(Object obj) { return true; if (!(obj instanceof MsPixelBufferAPI msPixelBufferAPI)) return false; - return msPixelBufferAPI.client.equals(client); + return msPixelBufferAPI.apisHandler.equals(apisHandler); } @Override public int hashCode() { - return client.hashCode(); + return apisHandler.hashCode(); } @Override public String toString() { - return String.format("Ms pixel buffer API of %s", client.getApisHandler().getWebServerURI()); + return String.format("Ms pixel buffer API of %s", apisHandler.getWebServerURI()); } /** @@ -153,7 +156,7 @@ public void setPort(int port, boolean checkAvailabilityNow) { this.port.set(port); ClientsPreferencesManager.setMsPixelBufferPort( - client.getApisHandler().getWebServerURI(), + apisHandler.getWebServerURI(), port ); @@ -162,11 +165,11 @@ public void setPort(int port, boolean checkAvailabilityNow) { } private void setHost() { - Optional uri = changePortOfURI(client.getApisHandler().getWebServerURI(), port.get()); + Optional uri = changePortOfURI(apisHandler.getWebServerURI(), port.get()); if (uri.isPresent()) { host = uri.get().toString(); } else { - host = client.getApisHandler().getWebServerURI().toString(); + host = apisHandler.getWebServerURI().toString(); } } diff --git a/src/main/java/qupath/ext/omero/core/pixelapis/mspixelbuffer/MsPixelBufferReader.java b/src/main/java/qupath/ext/omero/core/pixelapis/mspixelbuffer/MsPixelBufferReader.java index 8850f77..6569cd4 100644 --- a/src/main/java/qupath/ext/omero/core/pixelapis/mspixelbuffer/MsPixelBufferReader.java +++ b/src/main/java/qupath/ext/omero/core/pixelapis/mspixelbuffer/MsPixelBufferReader.java @@ -2,7 +2,6 @@ import loci.formats.gui.AWTImageTools; import qupath.ext.omero.core.RequestSender; -import qupath.ext.omero.core.WebClient; import qupath.ext.omero.core.WebUtilities; import qupath.ext.omero.core.pixelapis.PixelAPIReader; import qupath.lib.color.ColorModelFactory; @@ -10,7 +9,17 @@ import qupath.lib.images.servers.PixelType; import qupath.lib.images.servers.TileRequest; -import java.awt.image.*; +import java.awt.image.BandedSampleModel; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.DataBufferDouble; +import java.awt.image.DataBufferFloat; +import java.awt.image.DataBufferInt; +import java.awt.image.DataBufferShort; +import java.awt.image.DataBufferUShort; +import java.awt.image.WritableRaster; import java.io.IOException; import java.util.List; import java.util.Optional; @@ -23,7 +32,6 @@ class MsPixelBufferReader implements PixelAPIReader { private static final String TILE_URI = "%s/tile/%d/%d/%d/%d?x=%d&y=%d&w=%d&h=%d&format=tif&resolution=%d"; - private final WebClient client; private final String host; private final long imageID; private final PixelType pixelType; @@ -34,7 +42,6 @@ class MsPixelBufferReader implements PixelAPIReader { /** * Create a new MsPixelBuffer reader. * - * @param client the WebClient owning the image to open * @param host the URI from which this microservice is available * @param imageID the ID of the image to open * @param pixelType the pixel type of the image to open @@ -42,14 +49,12 @@ class MsPixelBufferReader implements PixelAPIReader { * @param numberOfLevels the number of resolution levels of the image to open */ public MsPixelBufferReader( - WebClient client, String host, long imageID, PixelType pixelType, List channels, int numberOfLevels ) { - this.client = client; this.host = host; this.imageID = imageID; this.pixelType = pixelType; @@ -118,7 +123,7 @@ public String toString() { return String.format( "Ms pixel buffer reader for image %d of %s", imageID, - client.getApisHandler().getWebServerURI() + host ); } diff --git a/src/main/java/qupath/ext/omero/core/pixelapis/web/WebAPI.java b/src/main/java/qupath/ext/omero/core/pixelapis/web/WebAPI.java index 214df7e..75862d3 100644 --- a/src/main/java/qupath/ext/omero/core/pixelapis/web/WebAPI.java +++ b/src/main/java/qupath/ext/omero/core/pixelapis/web/WebAPI.java @@ -1,10 +1,14 @@ package qupath.ext.omero.core.pixelapis.web; -import javafx.beans.property.*; +import javafx.beans.property.FloatProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyFloatProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleFloatProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import qupath.ext.omero.core.ClientsPreferencesManager; -import qupath.ext.omero.core.WebClient; +import qupath.ext.omero.core.apis.ApisHandler; import qupath.lib.images.servers.ImageServerMetadata; import qupath.ext.omero.core.pixelapis.PixelAPI; import qupath.ext.omero.core.pixelapis.PixelAPIReader; @@ -23,18 +27,18 @@ public class WebAPI implements PixelAPI { private static final float DEFAULT_JPEG_QUALITY = 0.9F; private static final String JPEG_QUALITY_PARAMETER = "--jpegQuality"; private static final Logger logger = LoggerFactory.getLogger(WebAPI.class); - private final WebClient client; + private final ApisHandler apisHandler; private final FloatProperty jpegQuality; /** * Creates a new WebAPI. * - * @param client the WebClient owning this API + * @param apisHandler the api handler owning this API */ - public WebAPI(WebClient client) { - this.client = client; + public WebAPI(ApisHandler apisHandler) { + this.apisHandler = apisHandler; jpegQuality = new SimpleFloatProperty( - ClientsPreferencesManager.getWebJpegQuality(client.getApisHandler().getWebServerURI()).orElse(DEFAULT_JPEG_QUALITY) + ClientsPreferencesManager.getWebJpegQuality(apisHandler.getWebServerURI()).orElse(DEFAULT_JPEG_QUALITY) ); } @@ -44,12 +48,12 @@ public boolean equals(Object obj) { return true; if (!(obj instanceof WebAPI webAPI)) return false; - return webAPI.client.equals(client); + return webAPI.apisHandler.equals(apisHandler); } @Override public int hashCode() { - return client.hashCode(); + return apisHandler.hashCode(); } @Override @@ -105,7 +109,7 @@ public PixelAPIReader createReader(long id, ImageServerMetadata metadata) { } return new WebReader( - client.getApisHandler(), + apisHandler, id, metadata.getPreferredTileWidth(), metadata.getPreferredTileHeight(), @@ -115,7 +119,7 @@ public PixelAPIReader createReader(long id, ImageServerMetadata metadata) { @Override public String toString() { - return String.format("Web API of %s", client.getApisHandler().getWebServerURI()); + return String.format("Web API of %s", apisHandler.getWebServerURI()); } /** @@ -136,7 +140,7 @@ public void setJpegQuality(float jpegQuality) { this.jpegQuality.set(jpegQuality); ClientsPreferencesManager.setWebJpegQuality( - client.getApisHandler().getWebServerURI(), + apisHandler.getWebServerURI(), jpegQuality ); } else { diff --git a/src/test/java/qupath/ext/omero/core/TestWebClients.java b/src/test/java/qupath/ext/omero/core/TestWebClients.java index 10b6af8..4945020 100644 --- a/src/test/java/qupath/ext/omero/core/TestWebClients.java +++ b/src/test/java/qupath/ext/omero/core/TestWebClients.java @@ -6,7 +6,9 @@ import qupath.ext.omero.OmeroServer; import qupath.ext.omero.TestUtilities; +import java.net.URI; import java.util.List; +import java.util.Optional; import java.util.concurrent.ExecutionException; public class TestWebClients extends OmeroServer { @@ -108,6 +110,43 @@ void Check_Client_Creation_With_Invalid_URI() throws ExecutionException, Interru WebClients.removeClient(client); } + @Test + void Check_Client_Can_Be_Retrieved_After_Added() throws ExecutionException, InterruptedException { + URI uri = URI.create(OmeroServer.getWebServerURI()); + WebClient expectedClient = createClient( + uri.toString(), + WebClient.Authentication.ENFORCE, + "-u", + OmeroServer.getRootUsername(), + "-p", + OmeroServer.getRootPassword() + ); + + WebClient client = WebClients.getClientFromURI(uri).orElse(null); + + Assertions.assertEquals(expectedClient, client); + + WebClients.removeClient(expectedClient); + } + + @Test + void Check_Client_Cannot_Be_Retrieved_After_Removed() throws ExecutionException, InterruptedException { + URI uri = URI.create(OmeroServer.getWebServerURI()); + WebClient removedClient = createClient( + uri.toString(), + WebClient.Authentication.ENFORCE, + "-u", + OmeroServer.getRootUsername(), + "-p", + OmeroServer.getRootPassword() + ); + + WebClients.removeClient(removedClient); + Optional client = WebClients.getClientFromURI(uri); + + Assertions.assertTrue(client.isEmpty()); + } + @Test void Check_Client_List_After_Added() throws ExecutionException, InterruptedException { WebClient client = createClient(