diff --git a/pom.xml b/pom.xml index 5a322cb..430c25d 100644 --- a/pom.xml +++ b/pom.xml @@ -208,6 +208,10 @@ com.miglayout miglayout-swing + + org.apache.commons + commons-compress + org.jfree jfreechart diff --git a/src/main/java/net/imagej/ui/swing/updater/ImageJUpdater.java b/src/main/java/net/imagej/ui/swing/updater/ImageJUpdater.java index 2c7e572..2e3bd58 100644 --- a/src/main/java/net/imagej/ui/swing/updater/ImageJUpdater.java +++ b/src/main/java/net/imagej/ui/swing/updater/ImageJUpdater.java @@ -29,38 +29,44 @@ package net.imagej.ui.swing.updater; -import java.io.DataInputStream; -import java.io.File; -import java.io.IOException; +import java.io.*; import java.lang.reflect.InvocationTargetException; -import java.net.Authenticator; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; -import java.net.URLConnection; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.List; +import java.net.*; +import java.util.*; +import java.util.concurrent.ExecutionException; import net.imagej.ui.swing.updater.ViewOptions.Option; import net.imagej.updater.*; import net.imagej.updater.Conflicts.Conflict; import net.imagej.updater.util.*; +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.ArchiveInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; import org.scijava.app.StatusService; import org.scijava.command.CommandService; +import org.scijava.download.Download; +import org.scijava.download.DownloadService; import org.scijava.event.ContextDisposingEvent; import org.scijava.event.EventHandler; +import org.scijava.io.location.LocationService; import org.scijava.log.LogService; import org.scijava.log.Logger; import org.scijava.plugin.Menu; import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; +import org.scijava.ui.DialogPrompt; +import org.scijava.ui.UIService; import org.scijava.util.AppUtils; +import org.scijava.util.PropertiesHelper; import javax.swing.*; +import static org.scijava.ui.DialogPrompt.MessageType.QUESTION_MESSAGE; +import static org.scijava.ui.DialogPrompt.OptionType.YES_NO_OPTION; + /** * The Updater. As a command. *

@@ -78,9 +84,18 @@ public class ImageJUpdater implements UpdaterUI { @Parameter private StatusService statusService; + @Parameter + private DownloadService downloadService; + + @Parameter + private LocationService locationService; + @Parameter private LogService log; + @Parameter + private UIService uiService; + @Parameter private UploaderService uploaderService; @@ -105,6 +120,19 @@ public void run() { final File imagejRoot = imagejDirProperty != null ? new File( imagejDirProperty) : AppUtils.getBaseDirectory("ij.dir", FilesCollection.class, "updater"); + + // -- Check for HTTPs support in Java -- + HTTPSUtil.checkHTTPSSupport(log); + if (!HTTPSUtil.supportsHTTPS()) { + main.warn( + "Your Java might be too old to handle updates via HTTPS. This is a security risk!\n" + + "Please download a recent version of this software.\n"); + } + + // check if there is a new Java update available + updateJavaIfNecessary(imagejRoot); + + // -- Determine which files are governed by the updater -- final FilesCollection files = new FilesCollection(log, imagejRoot); UpdaterUserInterface.set(new SwingUserInterface(log, statusService)); @@ -136,12 +164,6 @@ public void run() { try { files.tryLoadingCollection(); - HTTPSUtil.checkHTTPSSupport(log); - if (!HTTPSUtil.supportsHTTPS()) { - main.warn( - "Your Java might be too old to handle updates via HTTPS. This is a security risk!\n" + - "Please download a recent version of this software.\n"); - } refreshUpdateSites(files); String warnings = files.reloadCollectionAndChecksum(progress); main.checkWritable(); @@ -264,6 +286,195 @@ protected void updateConflictList() { main.updateFilesTable(); } + /** + * Helper method to download and extract the appropriate JDK for this platform + * to the corresponding ImageJ java subdirectory. + */ + private boolean updateJava(final Map jdkVersions, + final File imagejRoot) + { + // Download and unzip the new JDK + final String platform = UpdaterUtil.getPlatform(); + final String jdkUrl = jdkVersions.get(platform); + final String jdkName = jdkUrl.substring(jdkUrl.lastIndexOf("/") + 1); + final File jdkDir = new File(imagejRoot + File.separator + "java" + + File.separator + platform); + + if (!jdkDir.exists() && !jdkDir.mkdirs()) { + log.error("Unable to create platform Java directory: " + jdkDir); + return false; + } + + // Download the JDK + final File jdkDlLoc = new File(jdkDir.getAbsolutePath() + File.separator + + jdkName); + jdkDlLoc.deleteOnExit(); + try { + log.debug("Downloading " + locationService.resolve(jdkUrl) + " to " + + locationService.resolve(jdkDlLoc.toURI())); + Download download = downloadService.download(locationService.resolve( + jdkUrl), locationService.resolve(jdkDlLoc.toURI())); + download.task().waitFor(); + } + catch (URISyntaxException | ExecutionException | InterruptedException e) { + log.error(e); + return false; + } + + String javaLoc = jdkDlLoc.getAbsolutePath(); + int extensionLength = 0; + + // Extract the JDK + if (jdkDlLoc.toString().endsWith("tar.gz")) { + try (FileInputStream fis = new FileInputStream(jdkDlLoc); + GzipCompressorInputStream gzIn = new GzipCompressorInputStream(fis); + TarArchiveInputStream tarIn = new TarArchiveInputStream(gzIn)) + { + doExtraction(jdkDir, tarIn); + extensionLength = 7; + } + catch (IOException e) { + log.error(e); + return false; + } + } + else if (jdkDlLoc.toString().endsWith("zip")) { + try (FileInputStream fis = new FileInputStream(jdkDlLoc); + ZipArchiveInputStream zis = new ZipArchiveInputStream(fis)) + { + doExtraction(jdkDir, zis); + extensionLength = 4; + } + catch (IOException e) { + log.error(e); + return false; + } + } + + // Notify user of success + uiService.showDialog("Java version updated!" + + " Please restart to take advantage of the new Java.", + DialogPrompt.MessageType.INFORMATION_MESSAGE); + + // Update the app configuration file to use the newly downloaded JDK + javaLoc = javaLoc.substring(0, javaLoc.length() - extensionLength); + String exeName = System.getProperty("ij.executable"); + if (exeName != null && !exeName.trim().isEmpty()) { + exeName = exeName.substring(exeName.lastIndexOf(File.separator)); + exeName = exeName.substring(0, exeName.indexOf("-")); + final File appCfg = new File(imagejRoot + File.separator + exeName + + ".cfg"); + Map appProps = appCfg.exists() ? PropertiesHelper.get( + appCfg) : new HashMap<>(); + appProps.put("app-configured", javaLoc); + PropertiesHelper.put(appProps, appCfg); + } + return true; + } + + /** + * Helper method to extract an archive + */ + private void doExtraction(final File jdkDir, final ArchiveInputStream tarIn) + throws IOException + { + ArchiveEntry entry; + while ((entry = tarIn.getNextEntry()) != null) { + if (entry.isDirectory()) { + new File(jdkDir, entry.getName()).mkdirs(); + } + else { + byte[] buffer = new byte[1024]; + File outputFile = new File(jdkDir, entry.getName()); + OutputStream fos = new FileOutputStream(outputFile); + int len; + while ((len = tarIn.read(buffer)) != -1) { + fos.write(buffer, 0, len); + } + fos.close(); + } + } + } + + /** + * Helper method that checks the remote JDK list and compares to a locally + * cached version. If the remote list is newer an available Java update is + * indicated. If the user agrees, the new JDK is downloaded and extracted to + * the appropriate directory. + */ + private void updateJavaIfNecessary(final File imagejRoot) { + final File jdkUrls = new File(imagejRoot.getAbsolutePath() + + File.separator + "jdk-urls.txt"); + final String modifiedKey = "LAST_MODIFIED"; + final String jdkUrl = "https://downloads.imagej.net/java/jdk-urls.txt"; + long lastModifiedRemote; + + // Get the last modified time on the remote JDK list + try { + HttpURLConnection connection = (HttpURLConnection) new URL(jdkUrl) + .openConnection(); + connection.setRequestMethod("HEAD"); + lastModifiedRemote = connection.getLastModified(); + } + catch (IOException e) { + log.error("Unable to read remote JDK list", e); + return; + } + + // Check if we've already cached a local version of the JDK list + if (jdkUrls.exists()) { + // check when the remote was last modified + Map jdkVersionProps = PropertiesHelper.get(jdkUrls); + if (lastModifiedRemote == 0) { // 0 means "not provided" + log.error("No modification date found in jdk-urls.txt"); + return; + } + long lastModifiedLocal = Long.parseLong(jdkVersionProps.getOrDefault( + modifiedKey, "0")); + + // return if up to date + if (lastModifiedLocal == lastModifiedRemote) return; + + // Otherwise delete the conf file and re-download + jdkUrls.delete(); + } + + // Download the new properties file + try { + Download dl = downloadService.download(locationService.resolve(jdkUrl), + locationService.resolve(jdkUrls.toURI())); + dl.task().waitFor(); + } + catch (URISyntaxException e) { + log.error("Failed to download the remote JDK url list: bad URI"); + return; + } + catch (ExecutionException | InterruptedException e) { + log.error( + "Failed to download the remote JDK url list: download task failed"); + return; + } + + // Inject the last modification date to the JDK list + Map jdkUrlMap = PropertiesHelper.get(jdkUrls); + jdkUrlMap.put(modifiedKey, Long.toString(lastModifiedRemote)); + + // Ask the user if they would like to proceed with a Java update + DialogPrompt.Result result = uiService.showDialog( + "A newer version of Java is recommended.\n" + + "Downloading this may take longer than normal updates, but will " + + "eventually be required for continued updates.\n" + + "Would you like to update now?", QUESTION_MESSAGE, YES_NO_OPTION); + + // Do the update, if desired + if (result == DialogPrompt.Result.YES_OPTION && updateJava(jdkUrlMap, + imagejRoot)) + { + // Store the current url list if we updated Java + PropertiesHelper.put(jdkUrlMap, jdkUrls); + } + } + private void refreshUpdateSites(FilesCollection files) throws InterruptedException, InvocationTargetException {