diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index cb8f9b6bc..8db372c7a 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -52,12 +52,13 @@ open module org.cryptomator.desktop { requires io.github.coffeelibs.tinyoauth2client; requires org.slf4j; requires org.apache.commons.lang3; + requires com.github.benmanes.caffeine; + requires com.fasterxml.jackson.annotation; /* dagger bs */ requires jakarta.inject; requires static javax.inject; requires java.compiler; - requires com.github.benmanes.caffeine; uses org.cryptomator.common.locationpresets.LocationPresetsProvider; uses SSLContextProvider; diff --git a/src/main/java/org/cryptomator/common/CommonsModule.java b/src/main/java/org/cryptomator/common/CommonsModule.java index a1e3c0950..739c358a9 100644 --- a/src/main/java/org/cryptomator/common/CommonsModule.java +++ b/src/main/java/org/cryptomator/common/CommonsModule.java @@ -74,13 +74,6 @@ public abstract class CommonsModule { return new MasterkeyFileAccess(Constants.PEPPER, csprng); } - @Provides - @Singleton - @Named("SemVer") - static Comparator providesSemVerComparator() { - return new SemVerComparator(); - } - @Provides @Singleton static Optional provideRevealPathService() { diff --git a/src/main/java/org/cryptomator/common/SemVerComparator.java b/src/main/java/org/cryptomator/common/SemVerComparator.java deleted file mode 100644 index 0f9148bd5..000000000 --- a/src/main/java/org/cryptomator/common/SemVerComparator.java +++ /dev/null @@ -1,81 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2016, 2017 Sebastian Stenzel and others. - * All rights reserved. - * This program and the accompanying materials are made available under the terms of the accompanying LICENSE file. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - *******************************************************************************/ -package org.cryptomator.common; - -import org.apache.commons.lang3.StringUtils; - -import java.util.Comparator; - -/** - * Compares version strings according to SemVer 2.0.0. - */ -public class SemVerComparator implements Comparator { - - private static final char VERSION_SEP = '.'; // http://semver.org/spec/v2.0.0.html#spec-item-2 - private static final String PRE_RELEASE_SEP = "-"; // http://semver.org/spec/v2.0.0.html#spec-item-9 - private static final String BUILD_SEP = "+"; // http://semver.org/spec/v2.0.0.html#spec-item-10 - - @Override - public int compare(String version1, String version2) { - // "Build metadata SHOULD be ignored when determining version precedence. - // Thus two versions that differ only in the build metadata, have the same precedence." - String v1WithoutBuildMetadata = StringUtils.substringBefore(version1, BUILD_SEP); - String v2WithoutBuildMetadata = StringUtils.substringBefore(version2, BUILD_SEP); - - if (v1WithoutBuildMetadata.equals(v2WithoutBuildMetadata)) { - return 0; - } - - String v1MajorMinorPatch = StringUtils.substringBefore(v1WithoutBuildMetadata, PRE_RELEASE_SEP); - String v2MajorMinorPatch = StringUtils.substringBefore(v2WithoutBuildMetadata, PRE_RELEASE_SEP); - String v1PreReleaseVersion = StringUtils.substringAfter(v1WithoutBuildMetadata, PRE_RELEASE_SEP); - String v2PreReleaseVersion = StringUtils.substringAfter(v2WithoutBuildMetadata, PRE_RELEASE_SEP); - return compare(v1MajorMinorPatch, v1PreReleaseVersion, v2MajorMinorPatch, v2PreReleaseVersion); - } - - private int compare(String v1MajorMinorPatch, String v1PreReleaseVersion, String v2MajorMinorPatch, String v2PreReleaseVersion) { - int comparisonResult = compareNumericallyThenLexicographically(v1MajorMinorPatch, v2MajorMinorPatch); - if (comparisonResult == 0) { - if (v1PreReleaseVersion.isEmpty()) { - return 1; // 1.0.0 > 1.0.0-BETA - } else if (v2PreReleaseVersion.isEmpty()) { - return -1; // 1.0.0-BETA < 1.0.0 - } else { - return compareNumericallyThenLexicographically(v1PreReleaseVersion, v2PreReleaseVersion); - } - } else { - return comparisonResult; - } - } - - private int compareNumericallyThenLexicographically(String version1, String version2) { - final String[] vComps1 = StringUtils.split(version1, VERSION_SEP); - final String[] vComps2 = StringUtils.split(version2, VERSION_SEP); - final int commonCompCount = Math.min(vComps1.length, vComps2.length); - - for (int i = 0; i < commonCompCount; i++) { - int subversionComparisonResult = 0; - try { - final int v1 = Integer.parseInt(vComps1[i]); - final int v2 = Integer.parseInt(vComps2[i]); - subversionComparisonResult = v1 - v2; - } catch (NumberFormatException ex) { - // ok, lets compare this fragment lexicographically - subversionComparisonResult = vComps1[i].compareTo(vComps2[i]); - } - if (subversionComparisonResult != 0) { - return subversionComparisonResult; - } - } - - // all in common so far? longest version string is considered the higher version: - return vComps1.length - vComps2.length; - } - -} \ No newline at end of file diff --git a/src/main/java/org/cryptomator/ui/fxapp/DelegatingHttpClient.java b/src/main/java/org/cryptomator/ui/fxapp/DelegatingHttpClient.java new file mode 100644 index 000000000..d8ac6db3a --- /dev/null +++ b/src/main/java/org/cryptomator/ui/fxapp/DelegatingHttpClient.java @@ -0,0 +1,86 @@ +package org.cryptomator.ui.fxapp; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import java.io.IOException; +import java.net.Authenticator; +import java.net.CookieHandler; +import java.net.ProxySelector; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +abstract class DelegatingHttpClient extends HttpClient { + + private final HttpClient delegate; + + public DelegatingHttpClient(HttpClient delegate) { + this.delegate = Objects.requireNonNull(delegate, "delegate must not be null"); + } + + @Override + public Optional cookieHandler() { + return delegate.cookieHandler(); + } + + @Override + public Optional connectTimeout() { + return delegate.connectTimeout(); + } + + @Override + public Redirect followRedirects() { + return delegate.followRedirects(); + } + + @Override + public Optional proxy() { + return delegate.proxy(); + } + + @Override + public SSLContext sslContext() { + return delegate.sslContext(); + } + + @Override + public SSLParameters sslParameters() { + return delegate.sslParameters(); + } + + @Override + public Optional authenticator() { + return delegate.authenticator(); + } + + @Override + public Version version() { + return delegate.version(); + } + + @Override + public Optional executor() { + return delegate.executor(); + } + + @Override + public HttpResponse send(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler) throws IOException, InterruptedException { + return delegate.send(request, responseBodyHandler); + } + + @Override + public CompletableFuture> sendAsync(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler) { + return delegate.sendAsync(request, responseBodyHandler); + } + + @Override + public CompletableFuture> sendAsync(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler, HttpResponse.PushPromiseHandler pushPromiseHandler) { + return delegate.sendAsync(request, responseBodyHandler, pushPromiseHandler); + } + +} diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java index 74abac546..70319df5b 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java @@ -26,7 +26,7 @@ import javafx.scene.image.Image; import java.io.IOException; import java.io.InputStream; -@Module(includes = {UpdateCheckerModule.class}, subcomponents = {TrayMenuComponent.class, // +@Module(subcomponents = {TrayMenuComponent.class, // DecryptNameComponent.class, // MainWindowComponent.class, // PreferencesComponent.class, // diff --git a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java index e835793c6..59adc1910 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java +++ b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java @@ -1,53 +1,68 @@ package org.cryptomator.ui.fxapp; import org.cryptomator.common.Environment; -import org.cryptomator.common.SemVerComparator; import org.cryptomator.common.settings.Settings; +import org.cryptomator.integrations.update.UpdateFailedException; +import org.cryptomator.integrations.update.UpdateInfo; +import org.cryptomator.integrations.update.UpdateMechanism; +import org.cryptomator.updater.FallbackUpdateMechanism; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; +import javafx.beans.binding.ObjectBinding; +import javafx.beans.binding.StringExpression; import javafx.beans.property.ObjectProperty; -import javafx.beans.property.ReadOnlyStringProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; import javafx.concurrent.ScheduledService; +import javafx.concurrent.Task; import javafx.concurrent.Worker; -import javafx.concurrent.WorkerStateEvent; import javafx.util.Duration; +import java.net.http.HttpClient; import java.time.Instant; -import java.util.Comparator; +import java.util.concurrent.Executors; -@Deprecated(forRemoval = true) // to be replaced by fallback org.cryptomator.integrations.update.UpdateMechanism @FxApplicationScoped -public class UpdateChecker { +public class UpdateChecker extends ScheduledService { private static final Logger LOG = LoggerFactory.getLogger(UpdateChecker.class); private static final Duration AUTO_CHECK_DELAY = Duration.seconds(5); + private static final Duration UPDATE_CHECK_INTERVAL = Duration.hours(3); + private static final Duration DISABLED_UPDATE_CHECK_INTERVAL = Duration.hours(100000); // Duration.INDEFINITE leads to overflows... + + public enum UpdateCheckState { + NOT_CHECKED, + IS_CHECKING, + CHECK_SUCCESSFUL, + CHECK_FAILED + } private final Environment env; private final Settings settings; - private final StringProperty latestVersion = new SimpleStringProperty(); - private final ScheduledService updateCheckerService; - private final ObjectProperty state = new SimpleObjectProperty<>(UpdateCheckState.NOT_CHECKED); private final ObjectProperty lastSuccessfulUpdateCheck; - private final Comparator versionComparator = new SemVerComparator(); - private final BooleanBinding updateAvailable; - private final BooleanBinding checkFailed; + private final StringExpression latestVersion = StringExpression.stringExpression(lastValueProperty().map(UpdateInfo::version)); + private final BooleanBinding updateAvailable = lastValueProperty().isNotNull(); + private final ObjectBinding updateState = Bindings.createObjectBinding(this::getUpdateCheckState, stateProperty()); + private final BooleanBinding checkFailed = Bindings.equal(UpdateCheckState.CHECK_FAILED, updateState); + private final HttpClient httpClient; + private final UpdateMechanism primaryUpdateMechanism; + private final UpdateMechanism fallbackUpdateMechanism; @Inject UpdateChecker(Settings settings, // - Environment env, // - ScheduledService updateCheckerService) { + Environment env, + FallbackUpdateMechanism fallbackUpdateMechanism, + UpdateCheckerHttpClient httpClient) { this.env = env; this.settings = settings; - this.updateCheckerService = updateCheckerService; this.lastSuccessfulUpdateCheck = settings.lastSuccessfulUpdateCheck; - this.updateAvailable = Bindings.createBooleanBinding(this::isUpdateAvailable, latestVersion); - this.checkFailed = Bindings.equal(UpdateCheckState.CHECK_FAILED, state); + this.httpClient = httpClient; + this.primaryUpdateMechanism = UpdateMechanism.get().orElse(fallbackUpdateMechanism); + this.fallbackUpdateMechanism = fallbackUpdateMechanism; + + setExecutor(Executors.newVirtualThreadPerTaskExecutor()); + periodProperty().bind(Bindings.when(settings.checkForUpdates).then(UPDATE_CHECK_INTERVAL).otherwise(DISABLED_UPDATE_CHECK_INTERVAL)); } public void automaticallyCheckForUpdatesIfEnabled() { @@ -61,46 +76,42 @@ public class UpdateChecker { } private void startCheckingForUpdates(Duration initialDelay) { - updateCheckerService.cancel(); - updateCheckerService.reset(); - updateCheckerService.setDelay(initialDelay); - updateCheckerService.setOnRunning(this::checkStarted); - updateCheckerService.setOnSucceeded(this::checkSucceeded); - updateCheckerService.setOnFailed(this::checkFailed); - updateCheckerService.start(); + cancel(); + reset(); + setDelay(initialDelay); + start(); } - private void checkStarted(WorkerStateEvent event) { - LOG.debug("Checking for updates..."); - state.set(UpdateCheckState.IS_CHECKING); + @Override + protected void succeeded() { + var updateInfo = getValue(); + super.succeeded(); // this will nil the value property! + if (updateInfo != null) { + LOG.info("Current version: {}, latest version: {}", getCurrentVersion(), updateInfo.version()); + lastSuccessfulUpdateCheck.set(Instant.now()); + } } - private void checkSucceeded(WorkerStateEvent event) { - var latestVersionString = updateCheckerService.getValue(); - LOG.info("Current version: {}, latest version: {}", getCurrentVersion(), latestVersionString); - lastSuccessfulUpdateCheck.set(Instant.now()); - latestVersion.set(latestVersionString); - state.set(UpdateCheckState.CHECK_SUCCESSFUL); + @Override + protected Task createTask() { + return new UpdateCheckTask(); } - private void checkFailed(WorkerStateEvent event) { - state.set(UpdateCheckState.CHECK_FAILED); + @Override + protected void failed() { + super.failed(); + LOG.error("Update check failed.", getException()); } - public enum UpdateCheckState { - NOT_CHECKED, - IS_CHECKING, - CHECK_SUCCESSFUL, - CHECK_FAILED - } /* Observable Properties */ - public BooleanBinding checkingForUpdatesProperty() { - return updateCheckerService.stateProperty().isEqualTo(Worker.State.RUNNING); + + public StringExpression latestVersionProperty() { + return latestVersion; } - public ReadOnlyStringProperty latestVersionProperty() { - return latestVersion; + public boolean isUpdateAvailable() { + return updateAvailable.get(); } public BooleanBinding updateAvailableProperty() { @@ -111,26 +122,38 @@ public class UpdateChecker { return checkFailed; } - public boolean isUpdateAvailable() { - String currentVersion = getCurrentVersion(); - String latestVersionString = latestVersion.get(); - - if (currentVersion == null || latestVersionString == null) { - return false; - } else { - return versionComparator.compare(currentVersion, latestVersionString) < 0; - } - } - public ObjectProperty lastSuccessfulUpdateCheckProperty() { return lastSuccessfulUpdateCheck; } - public ObjectProperty updateCheckStateProperty() { - return state; + public ObjectBinding updateCheckStateProperty() { + return updateState; + } + + private UpdateCheckState getUpdateCheckState() { + return switch (getState()) { + case READY -> UpdateCheckState.NOT_CHECKED; + case SCHEDULED, RUNNING -> UpdateCheckState.IS_CHECKING; + case SUCCEEDED -> UpdateCheckState.CHECK_SUCCESSFUL; + case FAILED, CANCELLED -> UpdateCheckState.CHECK_FAILED; + }; } public String getCurrentVersion() { return env.getAppVersion(); } + + private class UpdateCheckTask extends Task { + + @Override + protected UpdateInfo call() throws UpdateFailedException, InterruptedException { + var result = primaryUpdateMechanism.checkForUpdate(env.getAppVersion(), httpClient); + if (result == null && primaryUpdateMechanism != fallbackUpdateMechanism) { + LOG.debug("Primary update mechanism did not find an update. Try fallback update mechanism..."); + result = fallbackUpdateMechanism.checkForUpdate(env.getAppVersion(), httpClient); + } + return result; + } + } + } diff --git a/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerHttpClient.java b/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerHttpClient.java new file mode 100644 index 000000000..0a442c32a --- /dev/null +++ b/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerHttpClient.java @@ -0,0 +1,53 @@ +package org.cryptomator.ui.fxapp; + +import jakarta.inject.Inject; +import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.common.Environment; + +import java.io.IOException; +import java.net.ProxySelector; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; + +@FxApplicationScoped +public class UpdateCheckerHttpClient extends DelegatingHttpClient { + + private final String userAgent; + + @Inject + public UpdateCheckerHttpClient(Environment env) { + var delegate = HttpClient.newBuilder() // + .followRedirects(HttpClient.Redirect.NORMAL) // from version 1.6.11 onwards, Cryptomator can follow redirects, in case this URL ever changes + .proxy(ProxySelector.getDefault()).build(); + super(delegate); + this.userAgent = String.format("Cryptomator VersionChecker/%s %s %s (%s)", env.getAppVersion(), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH); + } + + @Override + public HttpResponse send(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler) throws IOException, InterruptedException { + return super.send(decorateRequest(request), responseBodyHandler); + } + + @Override + public CompletableFuture> sendAsync(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler) { + return super.sendAsync(decorateRequest(request), responseBodyHandler); + } + + @Override + public CompletableFuture> sendAsync(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler, HttpResponse.PushPromiseHandler pushPromiseHandler) { + return super.sendAsync(decorateRequest(request), responseBodyHandler, pushPromiseHandler); + } + + private HttpRequest decorateRequest(HttpRequest request) { + return HttpRequest.newBuilder(request, (_, _) -> true) // + .header("User-Agent", this.userAgent) // + .timeout(Duration.ofSeconds(10)) // + .build(); + + } + + +} diff --git a/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java b/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java deleted file mode 100644 index 585180662..000000000 --- a/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java +++ /dev/null @@ -1,93 +0,0 @@ -package org.cryptomator.ui.fxapp; - -import dagger.Module; -import dagger.Provides; -import org.apache.commons.lang3.SystemUtils; -import org.cryptomator.common.Environment; -import org.cryptomator.common.settings.Settings; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.inject.Named; -import javafx.beans.binding.Bindings; -import javafx.beans.binding.ObjectBinding; -import javafx.concurrent.ScheduledService; -import javafx.concurrent.Task; -import javafx.util.Duration; -import java.io.UncheckedIOException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.util.Optional; -import java.util.concurrent.ExecutorService; - -@Module -public abstract class UpdateCheckerModule { - - private static final Logger LOG = LoggerFactory.getLogger(UpdateCheckerModule.class); - - private static final URI LATEST_VERSION_URI = URI.create("https://api.cryptomator.org/desktop/latest-version.json"); - private static final Duration UPDATE_CHECK_INTERVAL = Duration.hours(3); - private static final Duration DISABLED_UPDATE_CHECK_INTERVAL = Duration.hours(100000); // Duration.INDEFINITE leads to overflows... - - @Provides - @FxApplicationScoped - static Optional provideHttpClient() { - try { - return Optional.of(HttpClient.newBuilder() // - .followRedirects(HttpClient.Redirect.NORMAL) // from version 1.6.11 onwards, Cryptomator can follow redirects, in case this URL ever changes - .build()); - } catch (UncheckedIOException e) { - LOG.error("HttpClient for update check cannot be created.", e); - return Optional.empty(); - } - } - - @Provides - @FxApplicationScoped - static HttpRequest provideCheckForUpdatesRequest(Environment env) { - String userAgent = String.format("Cryptomator VersionChecker/%s %s %s (%s)", // - env.getAppVersion(), // - SystemUtils.OS_NAME, // - SystemUtils.OS_VERSION, // - SystemUtils.OS_ARCH); // - return HttpRequest.newBuilder() // - .uri(LATEST_VERSION_URI) // - .header("User-Agent", userAgent) // - .timeout(java.time.Duration.ofSeconds(10)) - .build(); - } - - @Provides - @Named("checkForUpdatesInterval") - @FxApplicationScoped - static ObjectBinding provideCheckForUpdateInterval(Settings settings) { - return Bindings.when(settings.checkForUpdates).then(UPDATE_CHECK_INTERVAL).otherwise(DISABLED_UPDATE_CHECK_INTERVAL); - } - - @Provides - @FxApplicationScoped - static ScheduledService provideCheckForUpdatesService(ExecutorService executor, Optional httpClient, HttpRequest checkForUpdatesRequest, @Named("checkForUpdatesInterval") ObjectBinding period) { - ScheduledService service = new ScheduledService<>() { - @Override - protected Task createTask() { - if (httpClient.isPresent()) { - return new UpdateCheckerTask(httpClient.get(), checkForUpdatesRequest); - } else { - return new Task<>() { - @Override - protected String call() { - throw new NullPointerException("No HttpClient present."); - } - }; - } - } - }; - service.setOnFailed(event -> LOG.error("Failed to execute update service", service.getException())); - service.setExecutor(executor); - service.periodProperty().bind(period); - return service; - } - - -} diff --git a/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerTask.java b/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerTask.java deleted file mode 100644 index 36ad53331..000000000 --- a/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerTask.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.cryptomator.ui.fxapp; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.io.ByteStreams; -import org.apache.commons.lang3.SystemUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javafx.concurrent.Task; -import java.io.IOException; -import java.io.InputStream; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; - -public class UpdateCheckerTask extends Task { - - private static final ObjectMapper JSON = new ObjectMapper(); - private static final Logger LOG = LoggerFactory.getLogger(UpdateCheckerTask.class); - - private static final long MAX_RESPONSE_SIZE = 10L * 1024; // 10kb should be sufficient. protect against flooding - - private final HttpClient httpClient; - private final HttpRequest checkForUpdatesRequest; - - UpdateCheckerTask(HttpClient httpClient, HttpRequest checkForUpdatesRequest) { - this.httpClient = httpClient; - this.checkForUpdatesRequest = checkForUpdatesRequest; - - setOnFailed(event -> LOG.error("Failed to check for updates", getException())); - } - - @Override - protected String call() throws IOException, InterruptedException { - HttpResponse response = httpClient.send(checkForUpdatesRequest, HttpResponse.BodyHandlers.ofInputStream()); - if (response.statusCode() == 200) { - return processBody(response); - } else { - throw new IOException("Unexpected HTTP response code " + response.statusCode()); - } - } - - private String processBody(HttpResponse response) throws IOException { - try (InputStream in = response.body(); // - InputStream limitedIn = ByteStreams.limit(in, MAX_RESPONSE_SIZE)) { - var json = JSON.reader().readTree(limitedIn); - if (SystemUtils.IS_OS_MAC_OSX) { - return json.get("mac").asText(); - } else if (SystemUtils.IS_OS_WINDOWS) { - return json.get("win").asText(); - } else if (SystemUtils.IS_OS_LINUX) { - return json.get("linux").asText(); - } else { - throw new IllegalStateException("Unsupported operating system"); - } - } - } -} diff --git a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java index 70839b92f..9b0282112 100644 --- a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java +++ b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java @@ -2,7 +2,7 @@ package org.cryptomator.ui.preferences; import org.cryptomator.common.Environment; import org.cryptomator.common.settings.Settings; -import org.cryptomator.integrations.update.UpdateMechanism; +import org.cryptomator.integrations.update.UpdateInfo; import org.cryptomator.integrations.update.UpdateStep; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.fxapp.UpdateChecker; @@ -17,21 +17,18 @@ import javafx.application.Application; import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; +import javafx.beans.binding.BooleanExpression; import javafx.beans.binding.ObjectBinding; import javafx.beans.binding.StringBinding; +import javafx.beans.binding.StringExpression; import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ReadOnlyStringProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ObservableValue; -import javafx.concurrent.Service; import javafx.concurrent.Worker; import javafx.concurrent.WorkerStateEvent; -import javafx.event.EventType; import javafx.fxml.FXML; import javafx.scene.control.CheckBox; import javafx.scene.control.ContentDisplay; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; import java.time.LocalDateTime; @@ -46,60 +43,56 @@ import java.util.ResourceBundle; public class UpdatesPreferencesController implements FxController { private static final Logger LOG = LoggerFactory.getLogger(UpdatesPreferencesController.class); - private static final String DOWNLOADS_URI_TEMPLATE = "https://cryptomator.org/downloads/" // - + "?utm_source=cryptomator-desktop" // - + "&utm_medium=update-notification&" // - + "utm_campaign=app-update-%s"; private final Application application; private final Environment environment; private final ResourceBundle resourceBundle; private final Settings settings; - private final Environment env; private final UpdateChecker updateChecker; - private final ObjectBinding checkForUpdatesButtonState; - private final ReadOnlyStringProperty latestVersion; + private final ObjectBinding updateButtonState; + private final StringExpression latestVersion; private final ObservableValue lastSuccessfulUpdateCheck; private final StringBinding lastUpdateCheckMessage; private final ObservableValue timeDifferenceMessage; private final String currentVersion; - private final BooleanBinding updateAvailable; private final BooleanBinding checkFailed; private final BooleanProperty upToDateLabelVisible = new SimpleBooleanProperty(false); private final DateTimeFormatter formatter; private final BooleanBinding upToDate; - private final String downloadsUri; - private final UpdateMechanism updateMechanism; - private final Service updateService; + private final UpdateService updateService; private final StringBinding updateButtonTitle; - private final BooleanBinding updateReady; + + private final ObjectBinding> worker; + private final BooleanExpression running; /* FXML */ public CheckBox checkForUpdatesCheckbox; @Inject - UpdatesPreferencesController(Application application, Environment environment, ResourceBundle resourceBundle, Settings settings, UpdateChecker updateChecker, Environment env, FallbackUpdateMechanism fallbackUpdateMechanism) { + UpdatesPreferencesController(Application application, Environment environment, ResourceBundle resourceBundle, Settings settings, UpdateChecker updateChecker, FallbackUpdateMechanism fallbackUpdateMechanism) { this.application = application; this.environment = environment; this.resourceBundle = resourceBundle; this.settings = settings; - this.env = env; this.updateChecker = updateChecker; - this.checkForUpdatesButtonState = Bindings.when(updateChecker.checkingForUpdatesProperty()).then(ContentDisplay.LEFT).otherwise(ContentDisplay.TEXT_ONLY); + this.latestVersion = updateChecker.latestVersionProperty(); this.lastSuccessfulUpdateCheck = updateChecker.lastSuccessfulUpdateCheckProperty(); this.timeDifferenceMessage = Bindings.createStringBinding(this::getTimeDifferenceMessage, lastSuccessfulUpdateCheck); - this.currentVersion = environment.getAppVersion(); - this.updateAvailable = updateChecker.updateAvailableProperty(); + this.lastUpdateCheckMessage = Bindings.createStringBinding(this::getLastUpdateCheckMessage, lastSuccessfulUpdateCheck); + + this.currentVersion = updateChecker.getCurrentVersion(); this.formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault()); this.upToDate = updateChecker.updateCheckStateProperty().isEqualTo(UpdateChecker.UpdateCheckState.CHECK_SUCCESSFUL).and(latestVersion.isEqualTo(currentVersion)); this.checkFailed = updateChecker.checkFailedProperty(); - this.lastUpdateCheckMessage = Bindings.createStringBinding(this::getLastUpdateCheckMessage, lastSuccessfulUpdateCheck); - this.downloadsUri = DOWNLOADS_URI_TEMPLATE.formatted(URLEncoder.encode(currentVersion, StandardCharsets.US_ASCII)); - this.updateMechanism = UpdateMechanism.get().orElse(fallbackUpdateMechanism); - this.updateService = new UpdateService(updateMechanism); - this.updateButtonTitle = Bindings.createStringBinding(this::getUpdateButtonTitle, updateService.stateProperty(), updateService.messageProperty()); - this.updateReady = updateService.stateProperty().isEqualTo(Worker.State.READY); + + this.updateService = new UpdateService(updateChecker.lastValueProperty().map(UpdateInfo::updateMechanism)); + this.worker = Bindings.when(updateChecker.updateAvailableProperty()).>then(updateService).otherwise(updateChecker); + this.running = Bindings.createBooleanBinding(this::isRunning, updateService.stateProperty(), updateChecker.stateProperty()); + this.updateButtonTitle = Bindings.createStringBinding(this::getUpdateButtonTitle, worker, updateService.stateProperty(), updateService.messageProperty()); + this.updateButtonState = Bindings.createObjectBinding(this::getUpdateButtonState, updateChecker.stateProperty(), updateService.stateProperty()); + + updateChecker.updateAvailableProperty().addListener((_, _, newVal) -> LOG.info("Update available: {}", newVal)); updateService.setOnSucceeded(this::updateSucceeded); updateService.setOnFailed(this::updateFailed); @@ -117,24 +110,18 @@ public class UpdatesPreferencesController implements FxController { }); } - @FXML - public void checkNow() { - updateChecker.checkForUpdatesNow(); - } - - @FXML - public void visitDownloadsPage() { - application.getHostServices().showDocument(downloadsUri); - } - @FXML public void showLogfileDirectory() { environment.getLogDir().ifPresent(logDirPath -> application.getHostServices().showDocument(logDirPath.toUri().toString())); } @FXML - public void doUpdate() { - updateService.start(); + public void startWork() { + if (worker.get().equals(updateChecker)) { + updateChecker.checkForUpdatesNow(); + } else if (worker.get().equals(updateService)) { + updateService.start(); + } } private void updateSucceeded(WorkerStateEvent workerStateEvent) { @@ -157,15 +144,21 @@ public class UpdatesPreferencesController implements FxController { /* Observable Properties */ - public ObjectBinding checkForUpdatesButtonStateProperty() { - return checkForUpdatesButtonState; + public ObjectBinding updateButtonStateProperty() { + return updateButtonState; } - public ContentDisplay getCheckForUpdatesButtonState() { - return checkForUpdatesButtonState.get(); + public ContentDisplay getUpdateButtonState() { + if (updateService.isRunning()) { // isRunning() covers RUNNING and SCHEDULED states + return ContentDisplay.BOTTOM; + } else if (updateChecker.getState() == Worker.State.RUNNING) { + return ContentDisplay.LEFT; + } else { + return ContentDisplay.TEXT_ONLY; + } } - public ReadOnlyStringProperty latestVersionProperty() { + public StringExpression latestVersionProperty() { return latestVersion; } @@ -217,14 +210,6 @@ public class UpdatesPreferencesController implements FxController { return upToDateLabelVisible.get(); } - public BooleanBinding updateAvailableProperty() { - return updateAvailable; - } - - public boolean isUpdateAvailable() { - return updateAvailable.get(); - } - public BooleanBinding checkFailedProperty() { return checkFailed; } @@ -233,8 +218,20 @@ public class UpdatesPreferencesController implements FxController { return checkFailed.getValue(); } - public Service getUpdateService() { - return updateService; + public ObjectBinding> workerProperty() { + return worker; + } + + public Worker getWorker() { + return worker.get(); + } + + public BooleanExpression runningProperty() { + return running; + } + + public boolean isRunning() { + return updateChecker.getState() == Worker.State.RUNNING || updateService.getState() == Worker.State.RUNNING; } public StringBinding updateButtonTitleProperty() { @@ -242,20 +239,20 @@ public class UpdatesPreferencesController implements FxController { } public String getUpdateButtonTitle() { - return switch (updateService.getState()) { - case READY -> updateMechanism.getName(); - case SCHEDULED, RUNNING -> updateService.getMessage(); // "Preparing Update..."; // TODO: resourceBundle.getString("preferences.updates.preparingUpdate")... - case SUCCEEDED -> "Restart to Update"; // TODO: resourceBundle.getString("preferences.updates.readyToRestart")... - case FAILED, CANCELLED -> "failed"; - }; + if (worker.get() == updateChecker) { + return resourceBundle.getString("preferences.updates.checkNowBtn"); + } else { + return switch (updateService.getState()) { + case READY -> updateChecker.getLastValue().updateMechanism().getName(); + case SCHEDULED, RUNNING -> updateService.getMessage(); // "Preparing Update..."; // TODO: resourceBundle.getString("preferences.updates.preparingUpdate")... + case SUCCEEDED -> "Restart to Update"; // TODO: resourceBundle.getString("preferences.updates.readyToRestart")... + case FAILED, CANCELLED -> "failed"; + }; + } } - public boolean isUpdateReady() { - return updateReady.get(); - } - - public BooleanBinding updateReadyProperty() { - return updateReady; + public UpdateChecker getUpdateChecker() { + return updateChecker; } } diff --git a/src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java b/src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java index eac2ba617..8f0246cb4 100644 --- a/src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java +++ b/src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java @@ -3,7 +3,12 @@ package org.cryptomator.updater; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; +import org.cryptomator.integrations.update.UpdateInfo; import org.cryptomator.integrations.update.UpdateMechanism; +import org.jetbrains.annotations.Blocking; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; @@ -15,45 +20,53 @@ import java.util.List; public abstract class DownloadUpdateMechanism implements UpdateMechanism { + private static final Logger LOG = LoggerFactory .getLogger(DownloadUpdateMechanism.class); + private static final String LATEST_VERSION_API_URL = "https://api.cryptomator.org/connect/apps/desktop/latest-version?format=1"; private static final ObjectMapper MAPPER = new ObjectMapper(); - private final String assetSuffix; - - protected DownloadUpdateMechanism(String assetSuffix) { - this.assetSuffix = assetSuffix; - } - @Override - public boolean isUpdateAvailable(String currentVersion) { - try (var client = HttpClient.newHttpClient()) { - // TODO: check different source - HttpRequest request = HttpRequest.newBuilder().uri(URI.create("https://api.github.com/repos/cryptomator/cryptomator/releases/latest")).header("Accept", "application/vnd.github+json").build(); - - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofInputStream()); - + public UpdateInfo checkForUpdate(String currentVersion, HttpClient httpClient) { + try { + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(LATEST_VERSION_API_URL)).build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); if (response.statusCode() != 200) { throw new RuntimeException("Failed to fetch release: " + response.statusCode()); } - - var release = MAPPER.readValue(response.body(), GitHubRelease.class); - - return release.assets.stream().anyMatch(a -> a.name.endsWith(assetSuffix)) - && UpdateMechanism.isUpdateAvailable(release.tagName, currentVersion); - } catch (IOException | InterruptedException e) { - return false; + var release = MAPPER.readValue(response.body(), LatestVersionResponse.class); + return checkForUpdate(currentVersion, release); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.debug("Update check interrupted."); + return null; + } catch (IOException e) { + LOG.warn("Update check failed", e); + return null; } } + @Nullable + @Blocking + abstract UpdateInfo checkForUpdate(String currentVersion, LatestVersionResponse response); + @JsonIgnoreProperties(ignoreUnknown = true) - public record GitHubRelease( - @JsonProperty("tag_name") String tagName, - List assets + public record LatestVersionResponse( + @JsonProperty("latestVersion") LatestVersion latestVersion, + @JsonProperty("assets") List assets + ) {} + + @JsonIgnoreProperties(ignoreUnknown = true) + public record LatestVersion( + @JsonProperty("mac") String macVersion, + @JsonProperty("win") String winVersion, + @JsonProperty("linux") String linuxVersion ) {} @JsonIgnoreProperties(ignoreUnknown = true) public record Asset( - String name, - @JsonProperty("browser_download_url") String downloadUrl + @JsonProperty("name") String name, + @JsonProperty("digest") String digest, + @JsonProperty("size") long size, + @JsonProperty("downloadUrl") String downloadUrl ) {} } diff --git a/src/main/java/org/cryptomator/updater/FallbackUpdateMechanism.java b/src/main/java/org/cryptomator/updater/FallbackUpdateMechanism.java index ec0328bd2..c2448cf68 100644 --- a/src/main/java/org/cryptomator/updater/FallbackUpdateMechanism.java +++ b/src/main/java/org/cryptomator/updater/FallbackUpdateMechanism.java @@ -1,18 +1,31 @@ package org.cryptomator.updater; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.Environment; import org.cryptomator.integrations.common.DisplayName; import org.cryptomator.integrations.common.Priority; +import org.cryptomator.integrations.update.UpdateInfo; import org.cryptomator.integrations.update.UpdateMechanism; import org.cryptomator.integrations.update.UpdateStep; import org.cryptomator.integrations.update.UpdateStepAdapter; import org.cryptomator.ui.fxapp.FxApplicationScoped; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.inject.Inject; import javafx.application.Application; import javafx.application.Platform; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; @@ -21,6 +34,9 @@ import java.util.concurrent.TimeUnit; @DisplayName("Show Download Page") // TODO localize public class FallbackUpdateMechanism implements UpdateMechanism { + private static final Logger LOG = LoggerFactory.getLogger(FallbackUpdateMechanism.class); + private static final String LATEST_VERSION_API_URL = "https://api.cryptomator.org/connect/apps/desktop/latest-version"; + private static final ObjectMapper MAPPER = new ObjectMapper(); private static final String DOWNLOADS_URI_TEMPLATE = "https://cryptomator.org/downloads/" // + "?utm_source=cryptomator-desktop" // + "&utm_medium=update-notification&" // @@ -36,9 +52,24 @@ public class FallbackUpdateMechanism implements UpdateMechanism { } @Override - public boolean isUpdateAvailable(String currentVersion) { - // FIXME: what source shall we use? self-hosted JSON? - return true; + public UpdateInfo checkForUpdate(String currentVersion, HttpClient httpClient) { + try { + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(LATEST_VERSION_API_URL)).build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); + if (response.statusCode() != 200) { + throw new RuntimeException("Failed to fetch release: " + response.statusCode()); + } + var release = MAPPER.readValue(response.body(), LatestVersion.class); + var updateVersion = release.versionForCurrentOS(); + if (UpdateMechanism.isUpdateAvailable(currentVersion, updateVersion)) { + return new UpdateInfo(updateVersion, this); + } else { + return null; + } + } catch (IOException | InterruptedException e) { + LOG.warn("Update check failed", e); + return null; + } } @Override @@ -54,4 +85,23 @@ public class FallbackUpdateMechanism implements UpdateMechanism { return UpdateStep.RETRY; // allow running this "update mechanism" as many times as the user wants } + @JsonIgnoreProperties(ignoreUnknown = true) + public record LatestVersion( + @JsonProperty("mac") String macVersion, + @JsonProperty("win") String winVersion, + @JsonProperty("linux") String linuxVersion + ) { + public String versionForCurrentOS() { + if (SystemUtils.IS_OS_MAC_OSX) { + return macVersion; + } else if (SystemUtils.IS_OS_WINDOWS) { + return winVersion; + } else if (SystemUtils.IS_OS_LINUX) { + return linuxVersion; + } else { + throw new IllegalStateException("Unsupported operating system"); + } + } + } + } diff --git a/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java b/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java index c08a4bf12..a6f30ff3e 100644 --- a/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java +++ b/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java @@ -5,6 +5,8 @@ import org.cryptomator.integrations.common.OperatingSystem; import org.cryptomator.integrations.common.Priority; import org.cryptomator.integrations.update.DownloadUpdateStep; import org.cryptomator.integrations.update.UpdateFailedException; +import org.cryptomator.integrations.update.UpdateInfo; +import org.cryptomator.integrations.update.UpdateMechanism; import org.cryptomator.integrations.update.UpdateStep; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -27,12 +29,18 @@ public class MacOsDmgUpdateMechanism extends DownloadUpdateMechanism { private static final Logger LOG = LoggerFactory.getLogger(MacOsDmgUpdateMechanism.class); - public MacOsDmgUpdateMechanism() { + @Override + UpdateInfo checkForUpdate(String currentVersion, LatestVersionResponse response) { String suffix = switch (System.getProperty("os.arch")) { case "aarch64", "arm64" -> "arm64.dmg"; default -> "x64.dmg"; }; - super(suffix); + if (UpdateMechanism.isUpdateAvailable(response.latestVersion().macVersion(), currentVersion) + && response.assets().stream().map(Asset::name).anyMatch(s -> s.endsWith(suffix))) { + return new UpdateInfo(response.latestVersion().macVersion(), this); + } else { + return null; + } } @Override diff --git a/src/main/java/org/cryptomator/updater/UpdateService.java b/src/main/java/org/cryptomator/updater/UpdateService.java index d1e746dfd..7cc3ecf97 100644 --- a/src/main/java/org/cryptomator/updater/UpdateService.java +++ b/src/main/java/org/cryptomator/updater/UpdateService.java @@ -3,10 +3,12 @@ package org.cryptomator.updater; import org.cryptomator.integrations.update.UpdateMechanism; import org.cryptomator.integrations.update.UpdateStep; +import javafx.beans.value.ObservableValue; import javafx.concurrent.Service; import javafx.concurrent.Task; import java.io.IOException; import java.io.InterruptedIOException; +import java.util.Objects; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -15,18 +17,25 @@ import java.util.concurrent.TimeUnit; */ public class UpdateService extends Service { - private final UpdateMechanism updateMechanism; + private ObservableValue updateMechanism; - public UpdateService(UpdateMechanism updateMechanism) { + public UpdateService(ObservableValue updateMechanism) { + setExecutor(Executors.newVirtualThreadPerTaskExecutor()); this.updateMechanism = updateMechanism; - setExecutor(Executors.newVirtualThreadPerTaskExecutor()); } + } @Override protected Task createTask() { - return new RunAllStepsTask(); + return new RunAllStepsTask(updateMechanism.getValue()); } - private class RunAllStepsTask extends Task { + private static class RunAllStepsTask extends Task { + + private final UpdateMechanism updateMechanism; + + public RunAllStepsTask(UpdateMechanism updateMechanism) { + this.updateMechanism = Objects.requireNonNull(updateMechanism); + } @Override protected UpdateStep call() throws IOException { @@ -52,8 +61,6 @@ public class UpdateService extends Service { updateMessage(step.description()); } while (!step.await(100, TimeUnit.MILLISECONDS)); } - } - } diff --git a/src/main/resources/fxml/preferences_updates.fxml b/src/main/resources/fxml/preferences_updates.fxml index 0a37f0650..52e209f8b 100644 --- a/src/main/resources/fxml/preferences_updates.fxml +++ b/src/main/resources/fxml/preferences_updates.fxml @@ -18,9 +18,6 @@ xmlns="http://javafx.com/javafx" fx:controller="org.cryptomator.ui.preferences.UpdatesPreferencesController" spacing="12"> - - - @@ -29,9 +26,20 @@ - @@ -52,19 +60,5 @@ - - - diff --git a/src/test/java/org/cryptomator/common/SemVerComparatorTest.java b/src/test/java/org/cryptomator/common/SemVerComparatorTest.java deleted file mode 100644 index 52a9366c9..000000000 --- a/src/test/java/org/cryptomator/common/SemVerComparatorTest.java +++ /dev/null @@ -1,58 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2016, 2017 Sebastian Stenzel and others. - * All rights reserved. - * This program and the accompanying materials are made available under the terms of the accompanying LICENSE file. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - *******************************************************************************/ -package org.cryptomator.common; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.util.Comparator; - -public class SemVerComparatorTest { - - private final Comparator semVerComparator = new SemVerComparator(); - - // equal versions - - @Test - public void compareEqualVersions() { - Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4"))); - Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-alpha"))); - Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4+20170101", "1.23.4+20171231"))); - Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4-alpha+20170101", "1.23.4-alpha+20171231"))); - } - - // newer versions in first argument - - @Test - public void compareHigherToLowerVersions() { - Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.5", "1.23.4"))); - Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.24.4", "1.23.4"))); - Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23"))); - Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4-SNAPSHOT"))); - Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4-56.78"))); - Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-beta", "1.23.4-alpha"))); - Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-alpha.1", "1.23.4-alpha"))); - Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-56.79", "1.23.4-56.78"))); - } - - // newer versions in second argument - - @Test - public void compareLowerToHigherVersions() { - Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.5"))); - Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.24.4"))); - Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23", "1.23.4"))); - Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-SNAPSHOT", "1.23.4"))); - Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-56.78", "1.23.4"))); - Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-beta"))); - Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-alpha.1"))); - Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-56.78", "1.23.4-56.79"))); - } - -}