/*
* Copyright 2016 MovingBlocks
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.terasology.launcher.updater;
import javafx.application.Platform;
import javafx.scene.Parent;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.TextArea;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.launcher.util.BundleUtils;
import org.terasology.launcher.util.DirectoryUtils;
import org.terasology.launcher.util.DownloadException;
import org.terasology.launcher.util.DownloadUtils;
import org.terasology.launcher.util.DummyProgressListener;
import org.terasology.launcher.util.FileUtils;
import org.terasology.launcher.util.GuiUtils;
import org.terasology.launcher.version.TerasologyLauncherVersionInfo;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public final class LauncherUpdater {
private static final Logger logger = LoggerFactory.getLogger(LauncherUpdater.class);
private final TerasologyLauncherVersionInfo currentVersionInfo;
private final String currentVersion;
private final String jobName;
private Integer upstreamVersion;
private TerasologyLauncherVersionInfo versionInfo;
private String changeLog;
private File launcherInstallationDirectory;
public LauncherUpdater(TerasologyLauncherVersionInfo currentVersionInfo) {
this.currentVersionInfo = currentVersionInfo;
if ((currentVersionInfo.getBuildNumber() == null) || (currentVersionInfo.getBuildNumber().trim().length() == 0)) {
this.currentVersion = "0";
} else {
this.currentVersion = currentVersionInfo.getBuildNumber();
}
if ((currentVersionInfo.getJobName() == null) || (currentVersionInfo.getJobName().trim().length() == 0)) {
this.jobName = DownloadUtils.TERASOLOGY_LAUNCHER_DEVELOP_JOB_NAME;
} else {
this.jobName = currentVersionInfo.getJobName();
}
}
/**
* This method indicates if a new launcher version is available.
* <br>
* Compares the current launcher version number to the upstream version number if an internet connection is available.
*
* @return whether an update is available
*/
public boolean updateAvailable() {
boolean updateAvailable = false;
upstreamVersion = null;
versionInfo = null;
changeLog = null;
try {
upstreamVersion = DownloadUtils.loadLastStableBuildNumberJenkins(jobName);
logger.trace("Launcher upstream version: {}", upstreamVersion);
updateAvailable = Integer.parseInt(currentVersion) < upstreamVersion;
} catch (DownloadException e) {
logger.warn("The latest stable version of the launcher could not be determined!", e);
} catch (NumberFormatException e) {
logger.error("The current version '{}' could not be parsed!", currentVersion, e);
}
if (updateAvailable) {
URL urlVersionInfo = null;
try {
urlVersionInfo = DownloadUtils.createFileDownloadUrlJenkins(jobName, upstreamVersion, DownloadUtils.FILE_TERASOLOGY_LAUNCHER_VERSION_INFO);
versionInfo = TerasologyLauncherVersionInfo.loadFromInputStream(urlVersionInfo.openStream());
} catch (IOException e) {
logger.warn("The launcher version info could not be loaded! '{}' '{}'", upstreamVersion, urlVersionInfo, e);
}
try {
changeLog = DownloadUtils.loadLauncherChangeLogJenkins(jobName, upstreamVersion);
} catch (DownloadException e) {
logger.warn("The launcher change log could not be loaded! '{}'", upstreamVersion, e);
}
logger.info("An update is available to the TerasologyLauncher. '{}' '{}'", upstreamVersion, versionInfo);
}
return updateAvailable;
}
public void detectAndCheckLauncherInstallationDirectory() throws URISyntaxException, IOException {
final File launcherLocation = new File(LauncherUpdater.class.getProtectionDomain().getCodeSource().getLocation().toURI());
logger.trace("Launcher location: {}", launcherLocation);
launcherInstallationDirectory = launcherLocation.getParentFile().getParentFile();
DirectoryUtils.checkDirectory(launcherInstallationDirectory);
logger.trace("Launcher installation directory: {}", launcherInstallationDirectory);
}
public boolean showUpdateDialog(Stage parentStage) {
final String infoText = getUpdateInfo();
FutureTask<Boolean> dialog = new FutureTask<Boolean>(() -> {
Parent root = BundleUtils.getFXMLLoader("update_dialog").load();
((TextArea) root.lookup("#infoTextArea")).setText(infoText);
((TextArea) root.lookup("#changelogTextArea")).setText(changeLog);
final Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle(BundleUtils.getLabel("message_update_launcher_title"));
alert.setHeaderText(BundleUtils.getLabel("message_update_launcher"));
alert.getDialogPane().setContent(root);
alert.initOwner(parentStage);
alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO);
alert.initModality(Modality.APPLICATION_MODAL);
alert.setResizable(true);
return alert.showAndWait()
.filter(response -> response == ButtonType.YES)
.isPresent();
});
Platform.runLater(dialog);
boolean result = false;
try {
result = dialog.get();
} catch (InterruptedException | ExecutionException e) {
logger.error("Uh oh, something went wrong with the update dialog!", e);
}
return result;
}
/**
* Assemble an information message about currently installed launcher version and possible update.
*
* @return a multi-line information message
*/
private String getUpdateInfo() {
final StringBuilder builder = new StringBuilder();
builder.append(" ")
.append(BundleUtils.getLabel("message_update_current"))
.append(" ")
.append(currentVersionInfo.getDisplayVersion())
.append(" \n")
.append(" ")
.append(BundleUtils.getLabel("message_update_latest"))
.append(" ");
if (versionInfo != null) {
builder.append(versionInfo.getDisplayVersion());
} else if (upstreamVersion != null) {
builder.append(upstreamVersion);
}
builder.append(" \n")
.append(" ")
.append(BundleUtils.getLabel("message_update_installationDirectory"))
.append(" ")
.append(launcherInstallationDirectory.getPath())
.append(" ");
return builder.toString();
}
public boolean update(File downloadDirectory, File tempDirectory) {
try {
logger.trace("Downloading launcher...");
//TODO: splash.getInfoLabel().setText(BundleUtils.getLabel("splash_updatingLauncher_download"));
// Download launcher ZIP file
final URL updateURL = DownloadUtils.createFileDownloadUrlJenkins(jobName, upstreamVersion, DownloadUtils.FILE_TERASOLOGY_LAUNCHER_ZIP);
logger.trace("Update URL: {}", updateURL);
final File downloadedZipFile = new File(downloadDirectory, jobName + "_" + upstreamVersion + "_" + System.currentTimeMillis() + ".zip");
logger.trace("Download ZIP file: {}", downloadedZipFile);
DownloadUtils.downloadToFile(updateURL, downloadedZipFile, new DummyProgressListener());
//TODO: splash.getInfoLabel().setText(BundleUtils.getLabel("splash_updatingLauncher_updating"));
// Extract launcher ZIP file
final boolean extracted = FileUtils.extractZipTo(downloadedZipFile, tempDirectory);
if (!extracted) {
throw new IOException("Could not extract ZIP file! " + downloadedZipFile);
}
logger.trace("ZIP file extracted");
final File tempLauncherDirectory = new File(tempDirectory, "TerasologyLauncher");
DirectoryUtils.checkDirectory(tempLauncherDirectory);
logger.info("Current launcher path: {}", launcherInstallationDirectory.getPath());
logger.info("New files temporarily located in: {}", tempLauncherDirectory.getPath());
// Start SelfUpdater
SelfUpdater.runUpdate(tempLauncherDirectory, launcherInstallationDirectory);
} catch (DownloadException | IOException | RuntimeException e) {
logger.error("Launcher update failed! Aborting update process!", e);
GuiUtils.showErrorMessageDialog(null, BundleUtils.getLabel("update_launcher_updateFailed"));
return false;
}
return true;
}
}