Skip to content

Commit 2ee0eda

Browse files
committed
Add mechanism for updating Java
We can now control the java version per-platform using https://downloads.imagej.net/java/jdk-urls.txt, which contains URLs for JDK downloads. These are cached locally and any time a more-recent version is available, it is downloaded to the appropriate /java sub-directory. If jaunch is being used, an app.cfg file is created to tell the launcher which JDK to use on subsequent launches.
1 parent db370d7 commit 2ee0eda

File tree

2 files changed

+233
-18
lines changed

2 files changed

+233
-18
lines changed

pom.xml

+4
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@
208208
<groupId>com.miglayout</groupId>
209209
<artifactId>miglayout-swing</artifactId>
210210
</dependency>
211+
<dependency>
212+
<groupId>org.apache.commons</groupId>
213+
<artifactId>commons-compress</artifactId>
214+
</dependency>
211215
<dependency>
212216
<groupId>org.jfree</groupId>
213217
<artifactId>jfreechart</artifactId>

src/main/java/net/imagej/ui/swing/updater/ImageJUpdater.java

+229-18
Original file line numberDiff line numberDiff line change
@@ -29,38 +29,44 @@
2929

3030
package net.imagej.ui.swing.updater;
3131

32-
import java.io.DataInputStream;
33-
import java.io.File;
34-
import java.io.IOException;
32+
import java.io.*;
3533
import java.lang.reflect.InvocationTargetException;
36-
import java.net.Authenticator;
37-
import java.net.HttpURLConnection;
38-
import java.net.MalformedURLException;
39-
import java.net.URL;
40-
import java.net.URLClassLoader;
41-
import java.net.URLConnection;
42-
import java.net.UnknownHostException;
43-
import java.util.ArrayList;
44-
import java.util.List;
34+
import java.net.*;
35+
import java.util.*;
36+
import java.util.concurrent.ExecutionException;
4537

4638
import net.imagej.ui.swing.updater.ViewOptions.Option;
4739
import net.imagej.updater.*;
4840
import net.imagej.updater.Conflicts.Conflict;
4941
import net.imagej.updater.util.*;
5042

43+
import org.apache.commons.compress.archivers.ArchiveEntry;
44+
import org.apache.commons.compress.archivers.ArchiveInputStream;
45+
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
46+
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
47+
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
5148
import org.scijava.app.StatusService;
5249
import org.scijava.command.CommandService;
50+
import org.scijava.download.Download;
51+
import org.scijava.download.DownloadService;
5352
import org.scijava.event.ContextDisposingEvent;
5453
import org.scijava.event.EventHandler;
54+
import org.scijava.io.location.LocationService;
5555
import org.scijava.log.LogService;
5656
import org.scijava.log.Logger;
5757
import org.scijava.plugin.Menu;
5858
import org.scijava.plugin.Parameter;
5959
import org.scijava.plugin.Plugin;
60+
import org.scijava.ui.DialogPrompt;
61+
import org.scijava.ui.UIService;
6062
import org.scijava.util.AppUtils;
63+
import org.scijava.util.PropertiesHelper;
6164

6265
import javax.swing.*;
6366

67+
import static org.scijava.ui.DialogPrompt.MessageType.QUESTION_MESSAGE;
68+
import static org.scijava.ui.DialogPrompt.OptionType.YES_NO_OPTION;
69+
6470
/**
6571
* The Updater. As a command.
6672
* <p>
@@ -78,9 +84,18 @@ public class ImageJUpdater implements UpdaterUI {
7884
@Parameter
7985
private StatusService statusService;
8086

87+
@Parameter
88+
private DownloadService downloadService;
89+
90+
@Parameter
91+
private LocationService locationService;
92+
8193
@Parameter
8294
private LogService log;
8395

96+
@Parameter
97+
private UIService uiService;
98+
8499
@Parameter
85100
private UploaderService uploaderService;
86101

@@ -105,6 +120,19 @@ public void run() {
105120
final File imagejRoot = imagejDirProperty != null ? new File(
106121
imagejDirProperty) : AppUtils.getBaseDirectory("ij.dir",
107122
FilesCollection.class, "updater");
123+
124+
// -- Check for HTTPs support in Java --
125+
HTTPSUtil.checkHTTPSSupport(log);
126+
if (!HTTPSUtil.supportsHTTPS()) {
127+
main.warn(
128+
"Your Java might be too old to handle updates via HTTPS. This is a security risk!\n" +
129+
"Please download a recent version of this software.\n");
130+
}
131+
132+
// check if there is a new Java update available
133+
updateJavaIfNecessary(imagejRoot);
134+
135+
// -- Determine which files are governed by the updater --
108136
final FilesCollection files = new FilesCollection(log, imagejRoot);
109137

110138
UpdaterUserInterface.set(new SwingUserInterface(log, statusService));
@@ -136,12 +164,6 @@ public void run() {
136164

137165
try {
138166
files.tryLoadingCollection();
139-
HTTPSUtil.checkHTTPSSupport(log);
140-
if (!HTTPSUtil.supportsHTTPS()) {
141-
main.warn(
142-
"Your Java might be too old to handle updates via HTTPS. This is a security risk!\n" +
143-
"Please download a recent version of this software.\n");
144-
}
145167
refreshUpdateSites(files);
146168
String warnings = files.reloadCollectionAndChecksum(progress);
147169
main.checkWritable();
@@ -264,6 +286,195 @@ protected void updateConflictList() {
264286
main.updateFilesTable();
265287
}
266288

289+
/**
290+
* Helper method to download and extract the appropriate JDK for this platform
291+
* to the corresponding ImageJ java subdirectory.
292+
*/
293+
private boolean updateJava(final Map<String, String> jdkVersions,
294+
final File imagejRoot)
295+
{
296+
// Download and unzip the new JDK
297+
final String platform = UpdaterUtil.getPlatform();
298+
final String jdkUrl = jdkVersions.get(platform);
299+
final String jdkName = jdkUrl.substring(jdkUrl.lastIndexOf("/") + 1);
300+
final File jdkDir = new File(imagejRoot + File.separator + "java" +
301+
File.separator + platform);
302+
303+
if (!jdkDir.exists() && !jdkDir.mkdirs()) {
304+
log.error("Unable to create platform Java directory: " + jdkDir);
305+
return false;
306+
}
307+
308+
// Download the JDK
309+
final File jdkDlLoc = new File(jdkDir.getAbsolutePath() + File.separator +
310+
jdkName);
311+
jdkDlLoc.deleteOnExit();
312+
try {
313+
log.debug("Downloading " + locationService.resolve(jdkUrl) + " to " +
314+
locationService.resolve(jdkDlLoc.toURI()));
315+
Download download = downloadService.download(locationService.resolve(
316+
jdkUrl), locationService.resolve(jdkDlLoc.toURI()));
317+
download.task().waitFor();
318+
}
319+
catch (URISyntaxException | ExecutionException | InterruptedException e) {
320+
log.error(e);
321+
return false;
322+
}
323+
324+
String javaLoc = jdkDlLoc.getAbsolutePath();
325+
int extensionLength = 0;
326+
327+
// Extract the JDK
328+
if (jdkDlLoc.toString().endsWith("tar.gz")) {
329+
try (FileInputStream fis = new FileInputStream(jdkDlLoc);
330+
GzipCompressorInputStream gzIn = new GzipCompressorInputStream(fis);
331+
TarArchiveInputStream tarIn = new TarArchiveInputStream(gzIn))
332+
{
333+
doExtraction(jdkDir, tarIn);
334+
extensionLength = 7;
335+
}
336+
catch (IOException e) {
337+
log.error(e);
338+
return false;
339+
}
340+
}
341+
else if (jdkDlLoc.toString().endsWith("zip")) {
342+
try (FileInputStream fis = new FileInputStream(jdkDlLoc);
343+
ZipArchiveInputStream zis = new ZipArchiveInputStream(fis))
344+
{
345+
doExtraction(jdkDir, zis);
346+
extensionLength = 4;
347+
}
348+
catch (IOException e) {
349+
log.error(e);
350+
return false;
351+
}
352+
}
353+
354+
// Notify user of success
355+
uiService.showDialog("Java version updated!" +
356+
" Please restart to take advantage of the new Java.",
357+
DialogPrompt.MessageType.INFORMATION_MESSAGE);
358+
359+
// Update the app configuration file to use the newly downloaded JDK
360+
javaLoc = javaLoc.substring(0, javaLoc.length() - extensionLength);
361+
String exeName = System.getProperty("ij.executable");
362+
if (exeName != null && !exeName.trim().isEmpty()) {
363+
exeName = exeName.substring(exeName.lastIndexOf(File.separator));
364+
exeName = exeName.substring(0, exeName.indexOf("-"));
365+
final File appCfg = new File(imagejRoot + File.separator + exeName +
366+
".cfg");
367+
Map<String, String> appProps = appCfg.exists() ? PropertiesHelper.get(
368+
appCfg) : new HashMap<>();
369+
appProps.put("app-configured", javaLoc);
370+
PropertiesHelper.put(appProps, appCfg);
371+
}
372+
return true;
373+
}
374+
375+
/**
376+
* Helper method to extract an archive
377+
*/
378+
private void doExtraction(final File jdkDir, final ArchiveInputStream tarIn)
379+
throws IOException
380+
{
381+
ArchiveEntry entry;
382+
while ((entry = tarIn.getNextEntry()) != null) {
383+
if (entry.isDirectory()) {
384+
new File(jdkDir, entry.getName()).mkdirs();
385+
}
386+
else {
387+
byte[] buffer = new byte[1024];
388+
File outputFile = new File(jdkDir, entry.getName());
389+
OutputStream fos = new FileOutputStream(outputFile);
390+
int len;
391+
while ((len = tarIn.read(buffer)) != -1) {
392+
fos.write(buffer, 0, len);
393+
}
394+
fos.close();
395+
}
396+
}
397+
}
398+
399+
/**
400+
* Helper method that checks the remote JDK list and compares to a locally
401+
* cached version. If the remote list is newer an available Java update is
402+
* indicated. If the user agrees, the new JDK is downloaded and extracted to
403+
* the appropriate directory.
404+
*/
405+
private void updateJavaIfNecessary(final File imagejRoot) {
406+
final File jdkUrls = new File(imagejRoot.getAbsolutePath() +
407+
File.separator + "jdk-urls.txt");
408+
final String modifiedKey = "LAST_MODIFIED";
409+
final String jdkUrl = "https://downloads.imagej.net/java/jdk-urls.txt";
410+
long lastModifiedRemote;
411+
412+
// Get the last modified time on the remote JDK list
413+
try {
414+
HttpURLConnection connection = (HttpURLConnection) new URL(jdkUrl)
415+
.openConnection();
416+
connection.setRequestMethod("HEAD");
417+
lastModifiedRemote = connection.getLastModified();
418+
}
419+
catch (IOException e) {
420+
log.error("Unable to read remote JDK list", e);
421+
return;
422+
}
423+
424+
// Check if we've already cached a local version of the JDK list
425+
if (jdkUrls.exists()) {
426+
// check when the remote was last modified
427+
Map<String, String> jdkVersionProps = PropertiesHelper.get(jdkUrls);
428+
if (lastModifiedRemote == 0) { // 0 means "not provided"
429+
log.error("No modification date found in jdk-urls.txt");
430+
return;
431+
}
432+
long lastModifiedLocal = Long.parseLong(jdkVersionProps.getOrDefault(
433+
modifiedKey, "0"));
434+
435+
// return if up to date
436+
if (lastModifiedLocal == lastModifiedRemote) return;
437+
438+
// Otherwise delete the conf file and re-download
439+
jdkUrls.delete();
440+
}
441+
442+
// Download the new properties file
443+
try {
444+
Download dl = downloadService.download(locationService.resolve(jdkUrl),
445+
locationService.resolve(jdkUrls.toURI()));
446+
dl.task().waitFor();
447+
}
448+
catch (URISyntaxException e) {
449+
log.error("Failed to download the remote JDK url list: bad URI");
450+
return;
451+
}
452+
catch (ExecutionException | InterruptedException e) {
453+
log.error(
454+
"Failed to download the remote JDK url list: download task failed");
455+
return;
456+
}
457+
458+
// Inject the last modification date to the JDK list
459+
Map<String, String> jdkUrlMap = PropertiesHelper.get(jdkUrls);
460+
jdkUrlMap.put(modifiedKey, Long.toString(lastModifiedRemote));
461+
462+
// Ask the user if they would like to proceed with a Java update
463+
DialogPrompt.Result result = uiService.showDialog(
464+
"A newer version of Java is recommended.\n" +
465+
"Downloading this may take longer than normal updates, but will " +
466+
"eventually be required for continued updates.\n" +
467+
"Would you like to update now?", QUESTION_MESSAGE, YES_NO_OPTION);
468+
469+
// Do the update, if desired
470+
if (result == DialogPrompt.Result.YES_OPTION && updateJava(jdkUrlMap,
471+
imagejRoot))
472+
{
473+
// Store the current url list if we updated Java
474+
PropertiesHelper.put(jdkUrlMap, jdkUrls);
475+
}
476+
}
477+
267478
private void refreshUpdateSites(FilesCollection files)
268479
throws InterruptedException, InvocationTargetException
269480
{

0 commit comments

Comments
 (0)