--- /dev/null
+diff --git a/getdown/src/getdown/CHANGELOG.md b/getdown/src/getdown/CHANGELOG.md
+index 098651eb1..ec7a923e1 100644
+--- a/getdown/src/getdown/CHANGELOG.md
++++ b/getdown/src/getdown/CHANGELOG.md
+@@ -1,16 +1,73 @@
+ # Getdown Releases
+
+-## 1.8.3 - Unreleased
++## 1.8.7 - Unreleased
++
++* Reinstated env var support in `appbase` property.
++
++* Fixed issue with `myIpAddress()` in PAC proxy support.
++
++## 1.8.6 - June 4, 2019
++
++* Fixed issues with PAC proxy support: added `myIpAddress()`, fixed `dnsResolve()`, fixed crash
++ when detecting PAC proxy.
++
++* Reverted env var support in `appbase` property. It's causing problems that need to be
++ investigated.
++
++## 1.8.5 - May 29, 2019
++
++* Fixed issues with proxy information not getting properly passed through to app.
++ Via [#216](//github.com/threerings/getdown/pull/216).
++
++* `appbase` and `latest` properties in `getdown.txt` now process env var subtitutions.
++
++* Added support for [Proxy Auto-config](https://en.wikipedia.org/wiki/Proxy_auto-config) via PAC
++ files.
++
++* Proxy handling can now recover from credentials going out of date. It will detect the error and
++ ask for updated credentials.
++
++* Added `try_no_proxy` system property. This instructs Getdown to always first try to run without a
++ proxy, regardless of whether it has been configured to use a proxy in the past. And if it can run
++ without a proxy, it does so for that session, but retains the proxy config for future sessions in
++ which the proxy may again be needed.
++
++* Added `revalidate_policy` config to control when Getdown revalidates resources (by hashing them
++ and comparing that hash to the values in `digest.txt`). The default, `after_update`, only
++ validates resources after the app is updated. A new mode, `always`, validates resources prior to
++ every application launch.
++
++## 1.8.4 - May 14, 2019
++
++* Added `verify_timeout` config to allow customization of the default (60 second) timeout during
++ the resource verification process. Apparently in some pathological situations, this is needed.
++ Woe betide the users who have to stare at an unmoving progress bar for more than 60 seconds.
++ Via [#198](//github.com/threerings/getdown/pull/198)
++ and [901682d](//github.com/threerings/getdown/commit/901682d).
++
++* Added `java_local_dir` config to allow custom location for Java if `java_location` is specified.
++ Via [#206](//github.com/threerings/getdown/pull/206).
++
++* `messages_XX.properties` files are now all maintained in UTF-8 encoding and then converted to
++ escaped ISO-8859-1 during the build process.
++
++* Resources and unpacked resources now support `.zip` files as well as `.jar` files.
++ Via [#210](//github.com/threerings/getdown/pull/210).
++
++* Fixed issue when path to JVM contained spaces. Via [#214](//github.com/threerings/getdown/pull/214).
++
++## 1.8.3 - Apr 10, 2019
+
+ * Added support for `nresource` resources which must be jar files that contain native libraries.
+ Prior to launching the application, these resources will be unpacked and their contents added to
+ the `java.library.path` system property.
+
+ * When the app is updated to require a new version of the JVM, that JVM will be downloaded and used
+- immediately during that app invocation (instead of one invocation later). Via PR#169.
++ immediately during that app invocation (instead of one invocation later).
++ Via [#169](//github.com/threerings/getdown/pull/169).
+
+-* When a custom JVM is installed, old JVM files will be deleted prior to unpacking the new JVM. Via
+- PR#170.
++* When a custom JVM is installed, old JVM files will be deleted prior to unpacking the new JVM.
++ Via [#170](//github.com/threerings/getdown/pull/170).
+
+ * Number of concurrent downloads now defaults to num-cores minus one. Though downloads are I/O
+ bound rather than CPU bound, this still turns out to be a decent default.
+@@ -23,6 +80,14 @@
+ credentials supplied by the user. Otherwise they will be requested every time Getdown runs, which
+ is not a viable user experience.
+
++* The Getdown window can be now closed by pressing the `ESC` key.
++ Via [#191](//github.com/threerings/getdown/pull/191).
++
++* If no `appdir` is specified via the command line or system property, the current working
++ directory will be used as the `appdir`. Via [8d59367](//github.com/threerings/getdown/commit/8d59367)
++
++* A basic Russian translation has been added. Thanks [@sergiorussia](//github.com/sergiorussia)!
++
+ ## 1.8.2 - Nov 27, 2018
+
+ * Fixed a data corruption bug introduced at last minute into 1.8.1 release. Oops.
+diff --git a/getdown/src/getdown/ant/.project-MOVED b/getdown/src/getdown/ant/.project-MOVED
+deleted file mode 100644
+index 097cb89db..000000000
+--- a/getdown/src/getdown/ant/.project-MOVED
++++ /dev/null
+@@ -1,23 +0,0 @@
+-<?xml version="1.0" encoding="UTF-8"?>
+-<projectDescription>
+- <name>getdown-ant</name>
+- <comment></comment>
+- <projects>
+- </projects>
+- <buildSpec>
+- <buildCommand>
+- <name>org.eclipse.jdt.core.javabuilder</name>
+- <arguments>
+- </arguments>
+- </buildCommand>
+- <buildCommand>
+- <name>org.eclipse.m2e.core.maven2Builder</name>
+- <arguments>
+- </arguments>
+- </buildCommand>
+- </buildSpec>
+- <natures>
+- <nature>org.eclipse.jdt.core.javanature</nature>
+- <nature>org.eclipse.m2e.core.maven2Nature</nature>
+- </natures>
+-</projectDescription>
+diff --git a/getdown/src/getdown/ant/.settings/org.eclipse.core.resources.prefs b/getdown/src/getdown/ant/.settings/org.eclipse.core.resources.prefs
+deleted file mode 100644
+index e9441bb12..000000000
+--- a/getdown/src/getdown/ant/.settings/org.eclipse.core.resources.prefs
++++ /dev/null
+@@ -1,3 +0,0 @@
+-eclipse.preferences.version=1
+-encoding//src/main/java=UTF-8
+-encoding/<project>=UTF-8
+diff --git a/getdown/src/getdown/ant/.settings/org.eclipse.jdt.core.prefs b/getdown/src/getdown/ant/.settings/org.eclipse.jdt.core.prefs
+deleted file mode 100644
+index 54e56721d..000000000
+--- a/getdown/src/getdown/ant/.settings/org.eclipse.jdt.core.prefs
++++ /dev/null
+@@ -1,6 +0,0 @@
+-eclipse.preferences.version=1
+-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
+-org.eclipse.jdt.core.compiler.compliance=1.7
+-org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+-org.eclipse.jdt.core.compiler.release=disabled
+-org.eclipse.jdt.core.compiler.source=1.7
+diff --git a/getdown/src/getdown/ant/.settings/org.eclipse.m2e.core.prefs b/getdown/src/getdown/ant/.settings/org.eclipse.m2e.core.prefs
+deleted file mode 100644
+index f897a7f1c..000000000
+--- a/getdown/src/getdown/ant/.settings/org.eclipse.m2e.core.prefs
++++ /dev/null
+@@ -1,4 +0,0 @@
+-activeProfiles=
+-eclipse.preferences.version=1
+-resolveWorkspaceProjects=true
+-version=1
+diff --git a/getdown/src/getdown/ant/pom.xml b/getdown/src/getdown/ant/pom.xml
+index f8231aa2e..a72d95d87 100644
+--- a/getdown/src/getdown/ant/pom.xml
++++ b/getdown/src/getdown/ant/pom.xml
+@@ -4,7 +4,7 @@
+ <parent>
+ <groupId>com.threerings.getdown</groupId>
+ <artifactId>getdown</artifactId>
+- <version>1.8.3-SNAPSHOT</version>
++ <version>1.8.7-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>getdown-ant</artifactId>
+diff --git a/getdown/src/getdown/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java b/getdown/src/getdown/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java
+index 48cc8d426..76212ae89 100644
+--- a/getdown/src/getdown/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java
++++ b/getdown/src/getdown/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java
+@@ -7,16 +7,13 @@ package com.threerings.getdown.tools;
+
+ import java.io.File;
+ import java.io.IOException;
+-
+ import java.security.GeneralSecurityException;
+
+ import org.apache.tools.ant.BuildException;
+ import org.apache.tools.ant.Task;
+
+-import com.threerings.getdown.data.Digest;
+-
+ /**
+- * An ant task used to create a <code>digest.txt</code> for a Getdown
++ * An ant task used to create a {@code digest.txt} for a Getdown
+ * application deployment.
+ */
+ public class DigesterTask extends Task
+diff --git a/getdown/src/getdown/core/.project-MOVED b/getdown/src/getdown/core/.project-MOVED
+deleted file mode 100644
+index 177252f5f..000000000
+--- a/getdown/src/getdown/core/.project-MOVED
++++ /dev/null
+@@ -1,23 +0,0 @@
+-<?xml version="1.0" encoding="UTF-8"?>
+-<projectDescription>
+- <name>getdown-core</name>
+- <comment></comment>
+- <projects>
+- </projects>
+- <buildSpec>
+- <buildCommand>
+- <name>org.eclipse.jdt.core.javabuilder</name>
+- <arguments>
+- </arguments>
+- </buildCommand>
+- <buildCommand>
+- <name>org.eclipse.m2e.core.maven2Builder</name>
+- <arguments>
+- </arguments>
+- </buildCommand>
+- </buildSpec>
+- <natures>
+- <nature>org.eclipse.jdt.core.javanature</nature>
+- <nature>org.eclipse.m2e.core.maven2Nature</nature>
+- </natures>
+-</projectDescription>
+diff --git a/getdown/src/getdown/core/.settings/org.eclipse.core.resources.prefs b/getdown/src/getdown/core/.settings/org.eclipse.core.resources.prefs
+deleted file mode 100644
+index 0a9bbb889..000000000
+--- a/getdown/src/getdown/core/.settings/org.eclipse.core.resources.prefs
++++ /dev/null
+@@ -1,6 +0,0 @@
+-eclipse.preferences.version=1
+-encoding//src/it/java=UTF-8
+-encoding//src/main/java=UTF-8
+-encoding//src/test/java=UTF-8
+-encoding//src/test/resources=UTF-8
+-encoding/<project>=UTF-8
+diff --git a/getdown/src/getdown/core/.settings/org.eclipse.jdt.core.prefs b/getdown/src/getdown/core/.settings/org.eclipse.jdt.core.prefs
+deleted file mode 100644
+index 54e56721d..000000000
+--- a/getdown/src/getdown/core/.settings/org.eclipse.jdt.core.prefs
++++ /dev/null
+@@ -1,6 +0,0 @@
+-eclipse.preferences.version=1
+-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
+-org.eclipse.jdt.core.compiler.compliance=1.7
+-org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+-org.eclipse.jdt.core.compiler.release=disabled
+-org.eclipse.jdt.core.compiler.source=1.7
+diff --git a/getdown/src/getdown/core/.settings/org.eclipse.m2e.core.prefs b/getdown/src/getdown/core/.settings/org.eclipse.m2e.core.prefs
+deleted file mode 100644
+index f897a7f1c..000000000
+--- a/getdown/src/getdown/core/.settings/org.eclipse.m2e.core.prefs
++++ /dev/null
+@@ -1,4 +0,0 @@
+-activeProfiles=
+-eclipse.preferences.version=1
+-resolveWorkspaceProjects=true
+-version=1
+diff --git a/getdown/src/getdown/core/pom.xml b/getdown/src/getdown/core/pom.xml
+index efec8b6ce..5e0f852ac 100644
+--- a/getdown/src/getdown/core/pom.xml
++++ b/getdown/src/getdown/core/pom.xml
+@@ -4,7 +4,7 @@
+ <parent>
+ <groupId>com.threerings.getdown</groupId>
+ <artifactId>getdown</artifactId>
+- <version>1.8.3-SNAPSHOT</version>
++ <version>1.8.7-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>getdown-core</artifactId>
+@@ -34,7 +34,7 @@
+ Wildcards can be used (*.mycompany.com) and multiple values can be
+ separated by commas (app1.foo.com,app2.bar.com,app3.baz.com). -->
+ <properties>
+- <getdown.host.whitelist>jalview.org,*.jalview.org</getdown.host.whitelist>
++ <getdown.host.whitelist />
+ </properties>
+
+ <build>
+diff --git a/getdown/src/getdown/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java b/getdown/src/getdown/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java
+index 52b4b5ee3..d2ddaf272 100644
+--- a/getdown/src/getdown/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java
++++ b/getdown/src/getdown/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java
+@@ -5,16 +5,16 @@
+
+ package com.threerings.getdown.tests;
+
+-import java.io.File;
+ import java.nio.charset.StandardCharsets;
+-import java.nio.file.*;
++import java.nio.file.Files;
++import java.nio.file.Path;
++import java.nio.file.Paths;
+ import java.util.Arrays;
+ import java.util.List;
+
+-import org.junit.*;
+-import static org.junit.Assert.*;
+-
+ import com.threerings.getdown.tools.Digester;
++import org.junit.Test;
++import static org.junit.Assert.assertEquals;
+
+ public class DigesterIT {
+
+@@ -32,23 +32,23 @@ public class DigesterIT {
+ Files.delete(digest2);
+
+ assertEquals(Arrays.asList(
+- "getdown.txt = 779c74fb4b251e18faf6e240a0667964",
++ "getdown.txt = 9c9b2494929c99d44ae51034d59e1a1b",
+ "testapp.jar = 404dafa55e78b25ec0e3a936357b1883",
+ "funny%test dir/some=file.txt = d8e8fca2dc0f896fd7cb4cb0031ba249",
+ "crazyhashfile#txt = f29d23fd5ab1781bd8d0760b3a516f16",
+ "foo.jar = 46ca4cc9079d9d019bb30cd21ebbc1ec",
+ "script.sh = f66e8ea25598e67e99c47d9b0b2a2cdf",
+- "digest.txt = f5561d85e4d80cc85883963897e58ff6"
++ "digest.txt = 11f9ba349cf9edacac4d72a3158447e5"
+ ), digestLines);
+
+ assertEquals(Arrays.asList(
+- "getdown.txt = 4f0c657895c3c3a35fa55bf5951c64fa9b0694f8fc685af3f1d8635c639e066b",
++ "getdown.txt = 1efecfae2a189002a6658f17d162b1922c7bde978944949276dc038a0df2461f",
+ "testapp.jar = c9cb1906afbf48f8654b416c3f831046bd3752a76137e5bf0a9af2f790bf48e0",
+ "funny%test dir/some=file.txt = f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2",
+ "crazyhashfile#txt = 6816889f922de38f145db215a28ad7c5e1badf7354b5cdab225a27486789fa3b",
+ "foo.jar = ea188b872e0496debcbe00aaadccccb12a8aa9b025bb62c130cd3d9b8540b062",
+ "script.sh = cca1c5c7628d9bf7533f655a9cfa6573d64afb8375f81960d1d832dc5135c988",
+- "digest2.txt = 70b442c9f56660561921da3368e1a206f05c379182fab3062750b7ddcf303407"
++ "digest2.txt = 41eacdabda8909bdbbf61e4f980867f4003c16a12f6770e6fc619b6af100e05b"
+ ), digest2Lines);
+ }
+ }
+diff --git a/getdown/src/getdown/core/src/it/resources/testapp/getdown.txt b/getdown/src/getdown/core/src/it/resources/testapp/getdown.txt
+index 3e0e5381a..ab0e47383 100644
+--- a/getdown/src/getdown/core/src/it/resources/testapp/getdown.txt
++++ b/getdown/src/getdown/core/src/it/resources/testapp/getdown.txt
+@@ -13,6 +13,18 @@ apparg = %APPDIR%
+ # test the %env% mechanism
+ jvmarg = -Dusername=\%ENV.USER%
+
++# test various java_*** configs, they are not interesting for digester
++java_local_dir = jre
++java_max_version = 1089999
++java_min_version = [windows] 1080111
++java_min_version = [!windows] 1080192
++java_exact_version_required = [linux] true
++java_location = [linux-amd64] /files/java/java_linux_64.zip
++java_location = [linux-i386] /files/java/java_linux_32.zip
++java_location = [mac] /files/java/java_mac_64.zip
++java_location = [windows-amd64] /files/java/java_windows_64.zip
++java_location = [windows-x86] /files/java/java_windows_32.zip
++
+ strict_comments = true
+ resource = funny%test dir/some=file.txt
+ resource = crazyhashfile#txt
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/Log.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/Log.java
+index 13b99564a..da98c9031 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/Log.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/Log.java
+@@ -15,7 +15,7 @@ import java.util.logging.*;
+ /**
+ * A placeholder class that contains a reference to the log object used by the Getdown code.
+ */
+-public class Log
++public final class Log
+ {
+ public static class Shim {
+ /**
+@@ -69,6 +69,13 @@ public class Log
+ /** We dispatch our log messages through this logging shim. */
+ public static final Shim log = new Shim();
+
++ /**
++ * Formats a message with key/value pairs. The pairs will be appended to the message as a
++ * comma separated list of {@code key=value} in square brackets.
++ * @param message the main log message.
++ * @param args the key/value pairs. Any trailing key with no value will be ignored.
++ * @return the formatted message, i.e. {@code Some log message [key=value, key=value]}.
++ */
+ public static String format (Object message, Object... args) {
+ if (args.length < 2) return String.valueOf(message);
+ StringBuilder buf = new StringBuilder(String.valueOf(message));
+@@ -76,13 +83,13 @@ public class Log
+ buf.append(' ');
+ }
+ buf.append('[');
+- for (int ii = 0; ii < args.length; ii += 2) {
++ for (int ii = 0, ll = args.length/2; ii < ll; ii += 1) {
+ if (ii > 0) {
+ buf.append(',').append(' ');
+ }
+- buf.append(args[ii]).append('=');
++ buf.append(args[2*ii]).append('=');
+ try {
+- buf.append(args[ii+1]);
++ buf.append(args[2*ii+1]);
+ } catch (Throwable t) {
+ buf.append("<toString() failure: ").append(t).append(">");
+ }
+@@ -136,6 +143,5 @@ public class Log
+ protected FieldPosition _fpos = new FieldPosition(SimpleDateFormat.DATE_FIELD);
+ }
+
+- protected static final String DATE_FORMAT = "{0,date} {0,time}";
+ protected static final Level[] LEVELS = {Level.FINE, Level.INFO, Level.WARNING, Level.SEVERE};
+ }
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java
+index 67ea64575..7e01e87c5 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java
+@@ -6,6 +6,8 @@
+ package com.threerings.getdown.cache;
+
+ import java.io.File;
++
++import com.threerings.getdown.data.Resource;
+ import com.threerings.getdown.util.FileUtil;
+
+ /**
+@@ -55,9 +57,9 @@ public class GarbageCollector
+ if (subdirs != null) {
+ for (File dir : subdirs) {
+ if (dir.isDirectory()) {
+- // Get all the native jars in the directory (there should only be one)
++ // Get all the native jars or zips in the directory (there should only be one)
+ for (File file : dir.listFiles()) {
+- if (!file.getName().endsWith(".jar")) {
++ if (!Resource.isJar(file) && !Resource.isZip(file)) {
+ continue;
+ }
+ File cachedFile = getCachedFile(file);
+@@ -94,6 +96,6 @@ public class GarbageCollector
+ private static File getCachedFile (File file)
+ {
+ return !isLastAccessedFile(file) ? file : new File(
+- file.getParentFile(), file.getName().substring(0, file.getName().lastIndexOf(".")));
++ file.getParentFile(), file.getName().substring(0, file.getName().lastIndexOf('.')));
+ }
+ }
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java
+index 0210e9a86..41f0c5f6d 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java
+@@ -69,7 +69,7 @@ public class ResourceCache
+
+ private String getFileSuffix (File fileToCache) {
+ String fileName = fileToCache.getName();
+- int index = fileName.lastIndexOf(".");
++ int index = fileName.lastIndexOf('.');
+
+ return index > -1 ? fileName.substring(index) : "";
+ }
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Application.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Application.java
+index 0de5c8ac8..a93122553 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Application.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Application.java
+@@ -22,24 +22,17 @@ import java.util.*;
+ import java.util.concurrent.*;
+ import java.util.regex.Matcher;
+ import java.util.regex.Pattern;
+-import java.util.zip.GZIPInputStream;
+-import com.sun.management.OperatingSystemMXBean;
+-import java.lang.management.ManagementFactory;
+-
+-import jalview.bin.MemorySetting;
+
++import com.threerings.getdown.net.Connector;
+ import com.threerings.getdown.util.*;
+ // avoid ambiguity with java.util.Base64 which we can't use as it's 1.8+
+ import com.threerings.getdown.util.Base64;
+
+-import com.threerings.getdown.data.EnvConfig;
+-import com.threerings.getdown.data.EnvConfig.Note;
+-
+ import static com.threerings.getdown.Log.log;
+ import static java.nio.charset.StandardCharsets.UTF_8;
+
+ /**
+- * Parses and provide access to the information contained in the <code>getdown.txt</code>
++ * Parses and provide access to the information contained in the {@code getdown.txt}
+ * configuration file.
+ */
+ public class Application
+@@ -218,10 +211,10 @@ public class Application
+ * Used by {@link #verifyMetadata} to communicate status in circumstances where it needs to
+ * take network actions.
+ */
+- public static interface StatusDisplay
++ public interface StatusDisplay
+ {
+ /** Requests that the specified status message be displayed. */
+- public void updateStatus (String message);
++ void updateStatus (String message);
+ }
+
+ /**
+@@ -239,19 +232,56 @@ public class Application
+ }
+ }
+
+- /** The proxy that should be used to do HTTP downloads. This must be configured prior to using
+- * the application instance. Yes this is a public mutable field, no I'm not going to create a
++ /**
++ * Reads the {@code getdown.txt} config file into a {@code Config} object and returns it.
++ */
++ public static Config readConfig (EnvConfig envc, boolean checkPlatform) throws IOException {
++ Config config = null;
++ File cfgfile = new File(envc.appDir, CONFIG_FILE);
++ Config.ParseOpts opts = Config.createOpts(checkPlatform);
++ try {
++ // if we have a configuration file, read the data from it
++ if (cfgfile.exists()) {
++ config = Config.parseConfig(cfgfile, opts);
++ }
++ // otherwise, try reading data from our backup config file; thanks to funny windows
++ // bullshit, we have to do this backup file fiddling in case we got screwed while
++ // updating getdown.txt during normal operation
++ else if ((cfgfile = new File(envc.appDir, Application.CONFIG_FILE + "_old")).exists()) {
++ config = Config.parseConfig(cfgfile, opts);
++ }
++ // otherwise, issue a warning that we found no getdown file
++ else {
++ log.info("Found no getdown.txt file", "appdir", envc.appDir);
++ }
++ } catch (Exception e) {
++ log.warning("Failure reading config file", "file", config, e);
++ }
++
++ // if we failed to read our config file, check for an appbase specified via a system
++ // property; we can use that to bootstrap ourselves back into operation
++ if (config == null) {
++ log.info("Using 'appbase' from bootstrap config", "appbase", envc.appBase);
++ Map<String, Object> cdata = new HashMap<>();
++ cdata.put("appbase", envc.appBase);
++ config = new Config(cdata);
++ }
++
++ return config;
++ }
++
++ /** A helper that is used to do HTTP downloads. This must be configured prior to using the
++ * application instance. Yes this is a public mutable field, no I'm not going to create a
+ * getter and setter just to pretend like that's not the case. */
+- public Proxy proxy = Proxy.NO_PROXY;
++ public Connector conn = Connector.DEFAULT;
+
+ /**
+- * Creates an application instance which records the location of the <code>getdown.txt</code>
++ * Creates an application instance which records the location of the {@code getdown.txt}
+ * configuration file from the supplied application directory.
+ *
+ */
+ public Application (EnvConfig envc) {
+ _envc = envc;
+- _config = getLocalPath(envc.appDir, CONFIG_FILE);
+ }
+
+ /**
+@@ -375,8 +405,7 @@ public class Application
+ */
+ public List<Resource> getActiveCodeResources ()
+ {
+- ArrayList<Resource> codes = new ArrayList<>();
+- codes.addAll(getCodeResources());
++ List<Resource> codes = new ArrayList<>(getCodeResources());
+ for (AuxGroup aux : getAuxGroups()) {
+ if (isAuxGroupActive(aux.name)) {
+ codes.addAll(aux.codes);
+@@ -404,8 +433,7 @@ public class Application
+ */
+ public List<Resource> getActiveResources ()
+ {
+- ArrayList<Resource> rsrcs = new ArrayList<>();
+- rsrcs.addAll(getResources());
++ List<Resource> rsrcs = new ArrayList<>(getResources());
+ for (AuxGroup aux : getAuxGroups()) {
+ if (isAuxGroupActive(aux.name)) {
+ rsrcs.addAll(aux.rsrcs);
+@@ -442,7 +470,15 @@ public class Application
+ }
+
+ /**
+- * Returns a resource for a zip file containing a Java VM that can be downloaded to use in
++ * @return directory into which a local VM installation should be unpacked.
++ */
++ public File getJavaLocalDir ()
++ {
++ return _javaLocalDir;
++ }
++
++ /**
++ * @return a resource for a zip file containing a Java VM that can be downloaded to use in
+ * place of the installed VM (in the case where the VM that launched Getdown does not meet the
+ * application's version requirements) or null if no VM is available for this platform.
+ */
+@@ -452,21 +488,16 @@ public class Application
+ return null;
+ }
+
+- String extension = (_javaLocation.endsWith(".tgz"))?".tgz":".jar";
+- String vmfile = LaunchUtil.LOCAL_JAVA_DIR + extension;
+- log.info("vmfile is '"+vmfile+"'");
+- System.out.println("vmfile is '"+vmfile+"'");
++ // take extension from java location
++ String vmfileExt = _javaLocation.substring(_javaLocation.lastIndexOf('.'));
++ String vmfile = _javaLocalDir.getName() + vmfileExt;
+ try {
+ URL remote = new URL(createVAppBase(_targetVersion), encodePath(_javaLocation));
+- log.info("Attempting to fetch jvm at "+remote.toString());
+- System.out.println("Attempting to fetch jvm at "+remote.toString());
+ return new Resource(vmfile, remote, getLocalPath(vmfile),
+ EnumSet.of(Resource.Attr.UNPACK, Resource.Attr.CLEAN));
+ } catch (Exception e) {
+ log.warning("Failed to create VM resource", "vmfile", vmfile, "appbase", _appbase,
+ "tvers", _targetVersion, "javaloc", _javaLocation, "error", e);
+- System.out.println("Failed to create VM resource: vmfile="+vmfile+", appbase="+_appbase+
+- ", tvers="+_targetVersion+", javaloc="+_javaLocation+", error="+e);
+ return null;
+ }
+ }
+@@ -547,88 +578,52 @@ public class Application
+ * @exception IOException thrown if there is an error reading the file or an error encountered
+ * during its parsing.
+ */
+- public Config init (boolean checkPlatform)
+- throws IOException
++ public Config init (boolean checkPlatform) throws IOException
+ {
+- Config config = null;
+- File cfgfile = _config;
+- Config.ParseOpts opts = Config.createOpts(checkPlatform);
+- try {
+- // if we have a configuration file, read the data from it
+- if (cfgfile.exists()) {
+- config = Config.parseConfig(_config, opts);
+- }
+- // otherwise, try reading data from our backup config file; thanks to funny windows
+- // bullshit, we have to do this backup file fiddling in case we got screwed while
+- // updating getdown.txt during normal operation
+- else if ((cfgfile = getLocalPath(CONFIG_FILE + "_old")).exists()) {
+- config = Config.parseConfig(cfgfile, opts);
+- }
+- // otherwise, issue a warning that we found no getdown file
+- else {
+- log.info("Found no getdown.txt file", "appdir", getAppDir());
+- }
+- } catch (Exception e) {
+- log.warning("Failure reading config file", "file", _config, e);
+- }
+-
+- // see if there's an override config from locator file
+- Config locatorConfig = createLocatorConfig(opts);
+-
+- // merge the locator file config into config (or replace config with)
+- if (locatorConfig != null) {
+- if (config == null || locatorConfig.getBoolean(LOCATOR_FILE_EXTENSION+"_replace")) {
+- config = locatorConfig;
+- } else {
+- config.mergeConfig(locatorConfig, locatorConfig.getBoolean(LOCATOR_FILE_EXTENSION+"_merge"));
+- }
+- }
++ Config config = readConfig(_envc, checkPlatform);
++ initBase(config);
++ initJava(config);
++ initTracking(config);
++ initResources(config);
++ initArgs(config);
++ return config;
++ }
+
+- // if we failed to read our config file, check for an appbase specified via a system
+- // property; we can use that to bootstrap ourselves back into operation
+- if (config == null) {
+- String appbase = _envc.appBase;
+- log.info("Using 'appbase' from bootstrap config", "appbase", appbase);
+- Map<String, Object> cdata = new HashMap<>();
+- cdata.put("appbase", appbase);
+- config = new Config(cdata);
+- }
++ /**
++ * Reads the basic config info from {@code config} into this instance. This includes things
++ * like the appbase and version.
++ */
++ public void initBase (Config config) throws IOException {
++ // first extract our version information
++ _version = config.getLong("version", -1L);
+
+- // first determine our application base, this way if anything goes wrong later in the
++ // determine our application base, this way if anything goes wrong later in the
+ // process, our caller can use the appbase to download a new configuration file
+ _appbase = config.getString("appbase");
+-
+- // see if locatorConfig override
+- if (locatorConfig != null && !StringUtil.isBlank(locatorConfig.getString("appbase"))) {
+- _appbase = locatorConfig.getString("appbase");
+- }
+-
+ if (_appbase == null) {
+ throw new RuntimeException("m.missing_appbase");
+ }
+
+- // check if we're overriding the domain in the appbase
+- _appbase = SysProps.overrideAppbase(_appbase);
++ // check if we're overriding the domain in the appbase, and sub envvars
++ _appbase = resolveEnvVars(SysProps.overrideAppbase(_appbase));
+
+ // make sure there's a trailing slash
+ if (!_appbase.endsWith("/")) {
+- _appbase = _appbase + "/";
++ _appbase += "/";
+ }
+
+- // extract our version information
+- _version = config.getLong("version", -1L);
+-
+ // if we are a versioned deployment, create a versioned appbase
+ try {
+ _vappbase = createVAppBase(_version);
+ } catch (MalformedURLException mue) {
+ String err = MessageUtil.tcompose("m.invalid_appbase", _appbase);
+- throw (IOException) new IOException(err).initCause(mue);
++ throw new IOException(err, mue);
+ }
+
+ // check for a latest config URL
+ String latest = config.getString("latest");
+ if (latest != null) {
++ latest = processArg(latest);
+ if (latest.startsWith(_appbase)) {
+ latest = _appbase + latest.substring(_appbase.length());
+ } else {
+@@ -641,20 +636,25 @@ public class Application
+ }
+ }
+
+- String appPrefix = _envc.appId == null ? "" : (_envc.appId + ".");
+-
+- // determine our application class name (use app-specific class _if_ one is provided)
+- _class = config.getString("class");
+- if (appPrefix.length() > 0) {
+- _class = config.getString(appPrefix + "class", _class);
+- }
+- if (_class == null) {
+- throw new IOException("m.missing_class");
+- }
+-
+- // determine whether we want strict comments
++ // read some miscellaneous configurations
+ _strictComments = config.getBoolean("strict_comments");
++ _allowOffline = config.getBoolean("allow_offline");
++ _revalidatePolicy = config.getEnum(
++ "revalidate_policy", RevalidatePolicy.class, RevalidatePolicy.AFTER_UPDATE);
++ int tpSize = SysProps.threadPoolSize();
++ _maxConcDownloads = Math.max(1, config.getInt("max_concurrent_downloads", tpSize));
++ _verifyTimeout = config.getInt("verify_timeout", 60);
++
++ // whether to cache code resources and launch from cache
++ _useCodeCache = config.getBoolean("use_code_cache");
++ _codeCacheRetentionDays = config.getInt("code_cache_retention_days", 7);
++ }
+
++ /**
++ * Reads the JVM requirements from {@code config} into this instance. This includes things like
++ * the min and max java version, location of a locally installed JRE, etc.
++ */
++ public void initJava (Config config) {
+ // check to see if we're using a custom java.version property and regex
+ _javaVersionProp = config.getString("java_version_prop", _javaVersionProp);
+ _javaVersionRegex = config.getString("java_version_regex", _javaVersionRegex);
+@@ -669,14 +669,16 @@ public class Application
+ // check to see if we require a particular JVM version and have a supplied JVM
+ _javaExactVersionRequired = config.getBoolean("java_exact_version_required");
+
+- // this is a little weird, but when we're run from the digester, we see a String[] which
+- // contains java locations for all platforms which we can't grok, but the digester doesn't
+- // need to know about that; when we're run in a real application there will be only one!
+- Object javaloc = config.getRaw("java_location");
+- if (javaloc instanceof String) {
+- _javaLocation = (String)javaloc;
+- }
++ _javaLocation = config.getString("java_location");
++
++ // used only in conjunction with java_location
++ _javaLocalDir = getLocalPath(config.getString("java_local_dir", LaunchUtil.LOCAL_JAVA_DIR));
++ }
+
++ /**
++ * Reads the install tracking info from {@code config} into this instance.
++ */
++ public void initTracking (Config config) {
+ // determine whether we have any tracking configuration
+ _trackingURL = config.getString("tracking_url");
+
+@@ -701,14 +703,16 @@ public class Application
+
+ // Some app may need to generate google analytics code
+ _trackingGAHash = config.getString("tracking_ga_hash");
++ }
+
++ /**
++ * Reads the app resource info from {@code config} into this instance.
++ */
++ public void initResources (Config config) throws IOException {
+ // clear our arrays as we may be reinitializing
+ _codes.clear();
+ _resources.clear();
+ _auxgroups.clear();
+- _jvmargs.clear();
+- _appargs.clear();
+- _txtJvmArgs.clear();
+
+ // parse our code resources
+ if (config.getMultiValue("code") == null &&
+@@ -727,10 +731,10 @@ public class Application
+
+ // parse our auxiliary resource groups
+ for (String auxgroup : config.getList("auxgroups")) {
+- ArrayList<Resource> codes = new ArrayList<>();
++ List<Resource> codes = new ArrayList<>();
+ parseResources(config, auxgroup + ".code", Resource.NORMAL, codes);
+ parseResources(config, auxgroup + ".ucode", Resource.UNPACK, codes);
+- ArrayList<Resource> rsrcs = new ArrayList<>();
++ List<Resource> rsrcs = new ArrayList<>();
+ parseResources(config, auxgroup + ".resource", Resource.NORMAL, rsrcs);
+ parseResources(config, auxgroup + ".xresource", Resource.EXEC, rsrcs);
+ parseResources(config, auxgroup + ".uresource", Resource.UNPACK, rsrcs);
+@@ -738,87 +742,48 @@ public class Application
+ parseResources(config, auxgroup + ".nresource", Resource.NATIVE, rsrcs);
+ _auxgroups.put(auxgroup, new AuxGroup(auxgroup, codes, rsrcs));
+ }
++ }
+
+- // transfer our JVM arguments (we include both "global" args and app_id-prefixed args)
+- String[] jvmargs = config.getMultiValue("jvmarg");
+- addAll(jvmargs, _jvmargs);
++ /**
++ * Reads the command line arg info from {@code config} into this instance.
++ */
++ public void initArgs (Config config) throws IOException {
++ _jvmargs.clear();
++ _appargs.clear();
++ _txtJvmArgs.clear();
++
++ String appPrefix = _envc.appId == null ? "" : (_envc.appId + ".");
++
++ // determine our application class name (use app-specific class _if_ one is provided)
++ _class = config.getString("class");
+ if (appPrefix.length() > 0) {
+- jvmargs = config.getMultiValue(appPrefix + "jvmarg");
+- addAll(jvmargs, _jvmargs);
++ _class = config.getString(appPrefix + "class", _class);
++ }
++ if (_class == null) {
++ throw new IOException("m.missing_class");
+ }
+
+- // see if a percentage of physical memory option exists
+- int jvmmempc = config.getInt("jvmmempc", -1);
+- // app_id prefixed setting overrides
++ // transfer our JVM arguments (we include both "global" args and app_id-prefixed args)
++ addAll(config.getMultiValue("jvmarg"), _jvmargs);
+ if (appPrefix.length() > 0) {
+- jvmmempc = config.getInt(appPrefix + "jvmmempc", jvmmempc);
+- }
+- if (0 <= jvmmempc && jvmmempc <= 100) {
+-
+- long maxMemLong = -1;
+-
+- try
+- {
+- maxMemLong = MemorySetting.memPercent(jvmmempc);
+- } catch (Exception e)
+- {
+- e.printStackTrace();
+- } catch (Throwable t)
+- {
+- t.printStackTrace();
+- }
+-
+- if (maxMemLong > 0)
+- {
+-
+- String[] maxMemHeapArg = new String[]{"-Xmx"+Long.toString(maxMemLong)};
+- // remove other max heap size arg
+- ARG: for (int i = 0; i < _jvmargs.size(); i++) {
+- if (_jvmargs.get(i) instanceof java.lang.String && _jvmargs.get(i).startsWith("-Xmx")) {
+- _jvmargs.remove(i);
+- }
+- }
+- addAll(maxMemHeapArg, _jvmargs);
+-
+- }
+-
+- } else if (jvmmempc != -1) {
+- System.out.println("'jvmmempc' value must be in range 0 to 100 (read as '"+Integer.toString(jvmmempc)+"')");
++ addAll(config.getMultiValue(appPrefix + "jvmarg"), _jvmargs);
+ }
+
+ // get the set of optimum JVM arguments
+ _optimumJvmArgs = config.getMultiValue("optimum_jvmarg");
+
+ // transfer our application arguments
+- String[] appargs = config.getMultiValue(appPrefix + "apparg");
+- addAll(appargs, _appargs);
++ addAll(config.getMultiValue(appPrefix + "apparg"), _appargs);
+
+ // add the launch specific application arguments
+ _appargs.addAll(_envc.appArgs);
+-
++
+ // look for custom arguments
+ fillAssignmentListFromPairs("extra.txt", _txtJvmArgs);
+
+- // determine whether we want to allow offline operation (defaults to false)
+- _allowOffline = config.getBoolean("allow_offline");
+-
+- // look for a debug.txt file which causes us to run in java.exe on Windows so that we can
+- // obtain a thread dump of the running JVM
+- _windebug = getLocalPath("debug.txt").exists();
+-
+- // whether to cache code resources and launch from cache
+- _useCodeCache = config.getBoolean("use_code_cache");
+- _codeCacheRetentionDays = config.getInt("code_cache_retention_days", 7);
+-
+- // maximum simultaneous downloads
+- _maxConcDownloads = Math.max(1, config.getInt("max_concurrent_downloads",
+- SysProps.threadPoolSize()));
+-
+ // extract some info used to configure our child process on macOS
+ _dockName = config.getString("ui.name");
+ _dockIconPath = config.getString("ui.mac_dock_icon", "../desktop.icns");
+-
+- return config;
+ }
+
+ /**
+@@ -847,8 +812,7 @@ public class Application
+ * Returns a URL from which the specified path can be fetched. Our application base URL is
+ * properly versioned and combined with the supplied path.
+ */
+- public URL getRemoteURL (String path)
+- throws MalformedURLException
++ public URL getRemoteURL (String path) throws MalformedURLException
+ {
+ return new URL(_vappbase, encodePath(path));
+ }
+@@ -858,7 +822,7 @@ public class Application
+ */
+ public File getLocalPath (String path)
+ {
+- return getLocalPath(getAppDir(), path);
++ return new File(getAppDir(), path);
+ }
+
+ /**
+@@ -881,8 +845,7 @@ public class Application
+ // if we have an unpacked VM, check the 'release' file for its version
+ Resource vmjar = getJavaVMResource();
+ if (vmjar != null && vmjar.isMarkedValid()) {
+- File vmdir = new File(getAppDir(), LaunchUtil.LOCAL_JAVA_DIR);
+- File relfile = new File(vmdir, "release");
++ File relfile = new File(_javaLocalDir, "release");
+ if (!relfile.exists()) {
+ log.warning("Unpacked JVM missing 'release' file. Assuming valid version.");
+ return true;
+@@ -939,7 +902,7 @@ public class Application
+ }
+
+ /**
+- * Attempts to redownload the <code>getdown.txt</code> file based on information parsed from a
++ * Attempts to redownload the {@code getdown.txt} file based on information parsed from a
+ * previous call to {@link #init}.
+ */
+ public void attemptRecovery (StatusDisplay status)
+@@ -950,7 +913,7 @@ public class Application
+ }
+
+ /**
+- * Downloads and replaces the <code>getdown.txt</code> and <code>digest.txt</code> files with
++ * Downloads and replaces the {@code getdown.txt} and {@code digest.txt} files with
+ * those for the target version of our application.
+ */
+ public void updateMetadata ()
+@@ -961,7 +924,7 @@ public class Application
+ _vappbase = createVAppBase(_targetVersion);
+ } catch (MalformedURLException mue) {
+ String err = MessageUtil.tcompose("m.invalid_appbase", _appbase);
+- throw (IOException) new IOException(err).initCause(mue);
++ throw new IOException(err, mue);
+ }
+
+ try {
+@@ -1000,7 +963,7 @@ public class Application
+ ArrayList<String> args = new ArrayList<>();
+
+ // reconstruct the path to the JVM
+- args.add(LaunchUtil.getJVMPath(getAppDir(), _windebug || optimum));
++ args.add(LaunchUtil.getJVMBinaryPath(_javaLocalDir, SysProps.debug() || optimum));
+
+ // check whether we're using -jar mode or -classpath mode
+ boolean dashJarMode = MANIFEST_CLASS.equals(_class);
+@@ -1018,14 +981,8 @@ public class Application
+ args.add("-Xdock:name=" + _dockName);
+ }
+
+- // pass along our proxy settings
+- String proxyHost;
+- if ((proxyHost = System.getProperty("http.proxyHost")) != null) {
+- args.add("-Dhttp.proxyHost=" + proxyHost);
+- args.add("-Dhttp.proxyPort=" + System.getProperty("http.proxyPort"));
+- args.add("-Dhttps.proxyHost=" + proxyHost);
+- args.add("-Dhttps.proxyPort=" + System.getProperty("http.proxyPort"));
+- }
++ // forward our proxy settings
++ conn.addProxyArgs(args);
+
+ // add the marker indicating the app is running in getdown
+ args.add("-D" + Properties.GETDOWN + "=true");
+@@ -1071,32 +1028,11 @@ public class Application
+ args.add(_class);
+ }
+
+- // almost finally check the startup file arguments
+- for (File f : _startupFiles) {
+- _appargs.add(f.getAbsolutePath());
+- break; // Only add one file to open
+- }
+-
+- // check if one arg with recognised extension
+- if ( _appargs.size() == 1 && _appargs.get(0) != null ) {
+- String filename = _appargs.get(0);
+- String ext = null;
+- int j = filename.lastIndexOf('.');
+- if (j > -1) {
+- ext = filename.substring(j+1);
+- }
+- if (LOCATOR_FILE_EXTENSION.equals(ext.toLowerCase())) {
+- // this file extension should have been dealt with in Getdown class
+- } else {
+- _appargs.add(0, "-open");
+- }
+- }
+-
+ // finally add the application arguments
+ for (String string : _appargs) {
+ args.add(processArg(string));
+ }
+-
++
+ String[] envp = createEnvironment();
+ String[] sargs = args.toArray(new String[args.size()]);
+ log.info("Running " + StringUtil.join(sargs, "\n "));
+@@ -1156,7 +1092,7 @@ public class Application
+ for (String jvmarg : _jvmargs) {
+ if (jvmarg.startsWith("-D")) {
+ jvmarg = processArg(jvmarg.substring(2));
+- int eqidx = jvmarg.indexOf("=");
++ int eqidx = jvmarg.indexOf('=');
+ if (eqidx == -1) {
+ log.warning("Bogus system property: '" + jvmarg + "'?");
+ } else {
+@@ -1194,32 +1130,36 @@ public class Application
+ }
+ }
+
+- /** Replaces the application directory and version in any argument. */
++ /** Replaces the application directory, version and env vars in any argument. */
+ protected String processArg (String arg)
+ {
+ arg = arg.replace("%APPDIR%", getAppDir().getAbsolutePath());
+ arg = arg.replace("%VERSION%", String.valueOf(_version));
++ arg = resolveEnvVars(arg);
++ return arg;
++ }
+
+- // if this argument contains %ENV.FOO% replace those with the associated values looked up
+- // from the environment
+- if (arg.contains(ENV_VAR_PREFIX)) {
++ /** Resolves env var substitutions in {@code text}. */
++ protected String resolveEnvVars (String text) {
++ // if the text contains %ENV.FOO% replace it with FOO looked up in the environment
++ if (text.contains(ENV_VAR_PREFIX)) {
+ StringBuffer sb = new StringBuffer();
+- Matcher matcher = ENV_VAR_PATTERN.matcher(arg);
++ Matcher matcher = ENV_VAR_PATTERN.matcher(text);
+ while (matcher.find()) {
+ String varName = matcher.group(1), varValue = System.getenv(varName);
+ String repValue = varValue == null ? "MISSING:"+varName : varValue;
+ matcher.appendReplacement(sb, Matcher.quoteReplacement(repValue));
+ }
+ matcher.appendTail(sb);
+- arg = sb.toString();
++ return sb.toString();
++ } else {
++ return text;
+ }
+-
+- return arg;
+ }
+
+ /**
+- * Loads the <code>digest.txt</code> file and verifies the contents of both that file and the
+- * <code>getdown.text</code> file. Then it loads the <code>version.txt</code> and decides
++ * Loads the {@code digest.txt} file and verifies the contents of both that file and the
++ * {@code getdown.text} file. Then it loads the {@code version.txt} and decides
+ * whether or not the application needs to be updated or whether we can proceed to verification
+ * and execution.
+ *
+@@ -1306,11 +1246,11 @@ public class Application
+ }
+
+ if (_latest != null) {
+- try (InputStream in = ConnectionUtil.open(proxy, _latest, 0, 0).getInputStream();
+- InputStreamReader reader = new InputStreamReader(in, UTF_8);
+- BufferedReader bin = new BufferedReader(reader)) {
+- for (String[] pair : Config.parsePairs(bin, Config.createOpts(false))) {
+- if (pair[0].equals("version")) {
++ try {
++ List<String[]> vdata = Config.parsePairs(
++ new StringReader(conn.fetch(_latest)), Config.createOpts(false));
++ for (String[] pair : vdata) {
++ if ("version".equals(pair[0])) {
+ _targetVersion = Math.max(Long.parseLong(pair[1]), _targetVersion);
+ if (fileVersion != -1 && _targetVersion > fileVersion) {
+ // replace the file with the newest version
+@@ -1404,7 +1344,10 @@ public class Application
+ while (completed[0] < rsrcs.size()) {
+ // we should be getting progress completion updates WAY more often than one every
+ // minute, so if things freeze up for 60 seconds, abandon ship
+- Runnable action = actions.poll(60, TimeUnit.SECONDS);
++ Runnable action = actions.poll(_verifyTimeout, TimeUnit.SECONDS);
++ if (action == null) {
++ throw new IllegalStateException("m.verify_timeout");
++ }
+ action.run();
+ }
+
+@@ -1415,14 +1358,14 @@ public class Application
+ unpacked.addAll(unpackedAsync);
+
+ long complete = System.currentTimeMillis();
+- log.info("Verified resources", "count", rsrcs.size(), "size", (totalSize/1024) + "k",
+- "duration", (complete-start) + "ms");
++ log.info("Verified resources", "count", rsrcs.size(), "alreadyValid", alreadyValid[0],
++ "size", (totalSize/1024) + "k", "duration", (complete-start) + "ms");
+ }
+
+ private void verifyResource (Resource rsrc, ProgressObserver obs, int[] alreadyValid,
+ Set<Resource> unpacked,
+ Set<Resource> toInstall, Set<Resource> toDownload) {
+- if (rsrc.isMarkedValid()) {
++ if (_revalidatePolicy != RevalidatePolicy.ALWAYS && rsrc.isMarkedValid()) {
+ if (alreadyValid != null) {
+ alreadyValid[0]++;
+ }
+@@ -1513,7 +1456,7 @@ public class Application
+ protected URL createVAppBase (long version)
+ throws MalformedURLException
+ {
+- String url = version < 0 ? _appbase : _appbase.replace("%VERSION%", "" + version);
++ String url = version < 0 ? _appbase : _appbase.replace("%VERSION%", String.valueOf(version));
+ return HostWhitelist.verify(new URL(url));
+ }
+
+@@ -1530,8 +1473,7 @@ public class Application
+ /**
+ * Downloads a new copy of CONFIG_FILE.
+ */
+- protected void downloadConfigFile ()
+- throws IOException
++ protected void downloadConfigFile () throws IOException
+ {
+ downloadControlFile(CONFIG_FILE, 0);
+ }
+@@ -1673,8 +1615,7 @@ public class Application
+ * Download a path to a temporary file, returning a {@link File} instance with the path
+ * contents.
+ */
+- protected File downloadFile (String path)
+- throws IOException
++ protected File downloadFile (String path) throws IOException
+ {
+ File target = getLocalPath(path + "_new");
+
+@@ -1684,30 +1625,11 @@ public class Application
+ } catch (Exception e) {
+ log.warning("Requested to download invalid control file",
+ "appbase", _vappbase, "path", path, "error", e);
+- throw (IOException) new IOException("Invalid path '" + path + "'.").initCause(e);
++ throw new IOException("Invalid path '" + path + "'.", e);
+ }
+
+ log.info("Attempting to refetch '" + path + "' from '" + targetURL + "'.");
+-
+- // stream the URL into our temporary file
+- URLConnection uconn = ConnectionUtil.open(proxy, targetURL, 0, 0);
+- // we have to tell Java not to use caches here, otherwise it will cache any request for
+- // same URL for the lifetime of this JVM (based on the URL string, not the URL object);
+- // if the getdown.txt file, for example, changes in the meanwhile, we would never hear
+- // about it; turning off caches is not a performance concern, because when Getdown asks
+- // to download a file, it expects it to come over the wire, not from a cache
+- uconn.setUseCaches(false);
+- uconn.setRequestProperty("Accept-Encoding", "gzip");
+- try (InputStream fin = uconn.getInputStream()) {
+- String encoding = uconn.getContentEncoding();
+- boolean gzip = "gzip".equalsIgnoreCase(encoding);
+- try (InputStream fin2 = (gzip ? new GZIPInputStream(fin) : fin)) {
+- try (FileOutputStream fout = new FileOutputStream(target)) {
+- StreamUtil.copy(fin2, fout);
+- }
+- }
+- }
+-
++ conn.download(targetURL, target); // stream the URL into our temporary file
+ return target;
+ }
+
+@@ -1721,9 +1643,7 @@ public class Application
+ /** Helper function to add all values in {@code values} (if non-null) to {@code target}. */
+ protected static void addAll (String[] values, List<String> target) {
+ if (values != null) {
+- for (String value : values) {
+- target.add(value);
+- }
++ Collections.addAll(target, values);
+ }
+ }
+
+@@ -1805,96 +1725,7 @@ public class Application
+ }
+ }
+
+- protected File getLocalPath (File appdir, String path)
+- {
+- return new File(appdir, path);
+- }
+-
+- public static void setStartupFilesFromParameterString(String p) {
+- // multiple files *might* be passed in as space separated quoted filenames
+- String q = "\"";
+- if (!StringUtil.isBlank(p)) {
+- String[] filenames;
+- // split quoted params or treat as single string array
+- if (p.startsWith(q) && p.endsWith(q)) {
+- // this fails if, e.g.
+- // p=q("stupidfilename\" " "otherfilename")
+- // let's hope no-one ever ends a filename with '" '
+- filenames = p.substring(q.length(),p.length()-q.length()).split(q+" "+q);
+- } else {
+- // single unquoted filename
+- filenames = new String[]{p};
+- }
+-
+- // check for locator file. Only allow one locator file to be double clicked (if multiple files opened, ignore locator files)
+- String locatorFilename = filenames.length >= 1 ? filenames[0] : null;
+- if (
+- !StringUtil.isBlank(locatorFilename)
+- && locatorFilename.toLowerCase().endsWith("."+Application.LOCATOR_FILE_EXTENSION)
+- ) {
+- setLocatorFile(locatorFilename);
+- // remove the locator filename from the filenames array
+- String[] otherFilenames = new String[filenames.length - 1];
+- System.arraycopy(filenames, 1, otherFilenames, 0, otherFilenames.length);
+- filenames = otherFilenames;
+- }
+-
+- for (int i = 0; i < filenames.length; i++) {
+- String filename = filenames[i];
+- // skip any other locator files in a multiple file list
+- if (! filename.toLowerCase().endsWith("."+Application.LOCATOR_FILE_EXTENSION)) {
+- addStartupFile(filename);
+- }
+- }
+- }
+- }
+-
+- public static void setLocatorFile(String filename) {
+- _locatorFile = new File(filename);
+- }
+-
+- public static void addStartupFile(String filename) {
+- _startupFiles.add(new File(filename));
+- }
+-
+- private Config createLocatorConfig(Config.ParseOpts opts) {
+- if (_locatorFile == null) {
+- return null;
+- }
+-
+- Config locatorConfig = null;
+-
+- try {
+- Config tmpConfig = null;
+- if (_locatorFile.exists()) {
+- tmpConfig = Config.parseConfig(_locatorFile, opts);
+- } else {
+- log.warning("Given locator file does not exist", "file", _locatorFile);
+- }
+-
+- // appbase is sanitised in HostWhitelist
+- Map<String, Object> tmpData = new HashMap<>();
+- for (Map.Entry<String, Object> entry : tmpConfig.getData().entrySet()) {
+- String key = entry.getKey();
+- Object value = entry.getValue();
+- String mkey = key.indexOf('.') > -1 ? key.substring(key.indexOf('.') + 1) : key;
+- if (Config.allowedReplaceKeys.contains(mkey) || Config.allowedMergeKeys.contains(mkey)) {
+- tmpData.put(key, value);
+- }
+- }
+- locatorConfig = new Config(tmpData);
+-
+- } catch (Exception e) {
+- log.warning("Failure reading locator file", "file", _locatorFile, e);
+- }
+-
+- log.info("Returning locatorConfig", locatorConfig);
+-
+- return locatorConfig;
+- }
+-
+ protected final EnvConfig _envc;
+- protected File _config;
+ protected Digest _digest;
+
+ protected long _version = -1;
+@@ -1906,7 +1737,6 @@ public class Application
+ protected String _dockName;
+ protected String _dockIconPath;
+ protected boolean _strictComments;
+- protected boolean _windebug;
+ protected boolean _allowOffline;
+ protected int _maxConcDownloads;
+
+@@ -1924,10 +1754,14 @@ public class Application
+ protected long _javaMinVersion, _javaMaxVersion;
+ protected boolean _javaExactVersionRequired;
+ protected String _javaLocation;
++ protected File _javaLocalDir;
+
+ protected List<Resource> _codes = new ArrayList<>();
+ protected List<Resource> _resources = new ArrayList<>();
+
++ protected int _verifyTimeout = 60;
++
++ protected RevalidatePolicy _revalidatePolicy = RevalidatePolicy.AFTER_UPDATE;
+ protected boolean _useCodeCache;
+ protected int _codeCacheRetentionDays;
+
+@@ -1941,9 +1775,6 @@ public class Application
+
+ protected List<String> _txtJvmArgs = new ArrayList<>();
+
+- /** If a warning has been issued about not being able to set modtimes. */
+- protected boolean _warnedAboutSetLastModified;
+-
+ /** Locks gettingdown.lock in the app dir. Held the entire time updating is going on.*/
+ protected FileLock _lock;
+
+@@ -1956,8 +1787,6 @@ public class Application
+
+ protected static final String ENV_VAR_PREFIX = "%ENV.";
+ protected static final Pattern ENV_VAR_PATTERN = Pattern.compile("%ENV\\.(.*?)%");
+-
+- protected static File _locatorFile;
+- protected static List<File> _startupFiles = new ArrayList<>();
+- public static final String LOCATOR_FILE_EXTENSION = "jvl";
++
++ protected static enum RevalidatePolicy { ALWAYS, AFTER_UPDATE }
+ }
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Digest.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Digest.java
+index bc8d14052..e310a52a2 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Digest.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Digest.java
+@@ -21,7 +21,7 @@ import static com.threerings.getdown.Log.log;
+ import static java.nio.charset.StandardCharsets.UTF_8;
+
+ /**
+- * Manages the <code>digest.txt</code> file and the computing and processing of digests for an
++ * Manages the {@code digest.txt} file and the computing and processing of digests for an
+ * application.
+ */
+ public class Digest
+@@ -72,8 +72,7 @@ public class Digest
+ digests.put(rsrc, rsrc.computeDigest(fversion, md, null));
+ completed.add(rsrc);
+ } catch (Throwable t) {
+- completed.add(new IOException("Error computing digest for: " + rsrc).
+- initCause(t));
++ completed.add(new IOException("Error computing digest for: " + rsrc, t));
+ }
+ }
+ });
+@@ -88,7 +87,7 @@ public class Digest
+ if (done instanceof IOException) {
+ throw (IOException)done;
+ } else if (done instanceof Resource) {
+- pending.remove((Resource)done);
++ pending.remove(done);
+ } else {
+ throw new AssertionError("What is this? " + done);
+ }
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/EnvConfig.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/EnvConfig.java
+index 57b8d8493..a14b02c63 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/EnvConfig.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/EnvConfig.java
+@@ -7,22 +7,19 @@ package com.threerings.getdown.data;
+
+ import java.io.File;
+ import java.io.FileInputStream;
+-import java.net.MalformedURLException;
+-import java.net.URL;
+ import java.security.cert.Certificate;
+ import java.security.cert.CertificateFactory;
+ import java.security.cert.X509Certificate;
+ import java.util.*;
+
+ import com.threerings.getdown.util.StringUtil;
+-import com.threerings.getdown.data.Application;
+
+ /** Configuration that comes from our "environment" (command line args, sys props, etc.). */
+ public final class EnvConfig {
+
+ /** Used to report problems or feedback by {@link #create}. */
+ public static final class Note {
+- public static enum Level { INFO, WARN, ERROR };
++ public enum Level { INFO, WARN, ERROR }
+ public static Note info (String msg) { return new Note(Level.INFO, msg); }
+ public static Note warn (String msg) { return new Note(Level.WARN, msg); }
+ public static Note error (String msg) { return new Note(Level.ERROR, msg); }
+@@ -141,23 +138,11 @@ public final class EnvConfig {
+ appIdProv + "'"));
+ }
+ }
+-
+- int skipArgs = 2;
+- // Look for locator file, pass to Application and remove from appArgs
+- String argvLocatorFilename = argv.length > 2 ? argv[2] : null;
+- if (
+- !StringUtil.isBlank(argvLocatorFilename)
+- && argvLocatorFilename.toLowerCase().endsWith("."+Application.LOCATOR_FILE_EXTENSION)
+- ) {
+- notes.add(Note.info("locatorFilename in args: '"+argv[2]+"'"));
+- Application.setLocatorFile(argvLocatorFilename);
+-
+- skipArgs++;
+- }
+
+- // ensure that we were able to find an app dir
++ // if no appdir was provided, default to the current working directory
+ if (appDir == null) {
+- return null; // caller will report problem to user
++ appDir = System.getProperty("user.dir");
++ appDirProv = "default (cwd)";
+ }
+
+ notes.add(Note.info("Using appdir from " + appDirProv + ": " + appDir));
+@@ -187,9 +172,9 @@ public final class EnvConfig {
+ return null;
+ }
+
+- // pass along anything after the first two (or three) args as extra app args
+- List<String> appArgs = argv.length > skipArgs ?
+- Arrays.asList(argv).subList(skipArgs, argv.length) :
++ // pass along anything after the first two args as extra app args
++ List<String> appArgs = argv.length > 2 ?
++ Arrays.asList(argv).subList(2, argv.length) :
+ Collections.<String>emptyList();
+
+ // load X.509 certificate if it exists
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/PathBuilder.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/PathBuilder.java
+index b0a1dc920..57e9275be 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/PathBuilder.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/PathBuilder.java
+@@ -10,7 +10,7 @@ import java.io.IOException;
+ import java.util.LinkedHashSet;
+ import java.util.List;
+ import java.util.concurrent.TimeUnit;
+-import java.util.jar.JarFile;
++import java.util.zip.ZipFile;
+
+ import com.threerings.getdown.cache.GarbageCollector;
+ import com.threerings.getdown.cache.ResourceCache;
+@@ -112,7 +112,7 @@ public class PathBuilder
+
+ if (!unpackedIndicator.exists()) {
+ try {
+- FileUtil.unpackJar(new JarFile(cachedFile), cachedParent, false);
++ FileUtil.unpackJar(new ZipFile(cachedFile), cachedParent, false);
+ unpackedIndicator.createNewFile();
+ } catch (IOException ioe) {
+ log.warning("Failed to unpack native jar",
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Resource.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Resource.java
+index adc2d4f21..d1ccba3ba 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Resource.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Resource.java
+@@ -7,25 +7,14 @@ package com.threerings.getdown.data;
+
+ import java.io.*;
+ import java.net.URL;
+-import java.nio.file.Files;
+-import java.nio.file.Paths;
+ import java.security.MessageDigest;
+-import java.util.Collections;
+-import java.util.Comparator;
+-import java.util.EnumSet;
+-import java.util.List;
+-import java.util.Locale;
+-import java.util.jar.JarEntry;
+-import java.util.jar.JarFile;
+-
+-import org.apache.commons.compress.archivers.ArchiveInputStream;
+-import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+-import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
++import java.util.*;
++import java.util.zip.ZipEntry;
++import java.util.zip.ZipFile;
+
+ import com.threerings.getdown.util.FileUtil;
+ import com.threerings.getdown.util.ProgressObserver;
+ import com.threerings.getdown.util.StringUtil;
+-
+ import static com.threerings.getdown.Log.log;
+
+ /**
+@@ -34,7 +23,7 @@ import static com.threerings.getdown.Log.log;
+ public class Resource implements Comparable<Resource>
+ {
+ /** Defines special attributes for resources. */
+- public static enum Attr {
++ public enum Attr {
+ /** Indicates that the resource should be unpacked. */
+ UNPACK,
+ /** If present, when unpacking a resource, any directories created by the newly
+@@ -46,7 +35,7 @@ public class Resource implements Comparable<Resource>
+ PRELOAD,
+ /** Indicates that the resource is a jar containing native libs. */
+ NATIVE
+- };
++ }
+
+ public static final EnumSet<Attr> NORMAL = EnumSet.noneOf(Attr.class);
+ public static final EnumSet<Attr> UNPACK = EnumSet.of(Attr.UNPACK);
+@@ -66,29 +55,30 @@ public class Resource implements Comparable<Resource>
+ byte[] buffer = new byte[DIGEST_BUFFER_SIZE];
+ int read;
+
+- boolean isJar = isJar(target.getPath());
+- boolean isPacked200Jar = isPacked200Jar(target.getPath());
++ boolean isZip = isJar(target) || isZip(target); // jar is a zip too
++ boolean isPacked200Jar = isPacked200Jar(target);
+
+ // if this is a jar, we need to compute the digest in a "timestamp and file order" agnostic
+ // manner to properly correlate jardiff patched jars with their unpatched originals
+- if (isJar || isPacked200Jar){
++ if (isPacked200Jar || isZip){
+ File tmpJarFile = null;
+- JarFile jar = null;
++ ZipFile zip = null;
+ try {
+- // if this is a compressed jar file, uncompress it to compute the jar file digest
++ // if this is a compressed zip file, uncompress it to compute the zip file digest
+ if (isPacked200Jar){
+ tmpJarFile = new File(target.getPath() + ".tmp");
++ tmpJarFile.deleteOnExit();
+ FileUtil.unpackPacked200Jar(target, tmpJarFile);
+- jar = new JarFile(tmpJarFile);
++ zip = new ZipFile(tmpJarFile);
+ } else{
+- jar = new JarFile(target);
++ zip = new ZipFile(target);
+ }
+
+- List<JarEntry> entries = Collections.list(jar.entries());
++ List<? extends ZipEntry> entries = Collections.list(zip.entries());
+ Collections.sort(entries, ENTRY_COMP);
+
+ int eidx = 0;
+- for (JarEntry entry : entries) {
++ for (ZipEntry entry : entries) {
+ // old versions of the digest code skipped metadata
+ if (version < 2) {
+ if (entry.getName().startsWith("META-INF")) {
+@@ -97,7 +87,7 @@ public class Resource implements Comparable<Resource>
+ }
+ }
+
+- try (InputStream in = jar.getInputStream(entry)) {
++ try (InputStream in = zip.getInputStream(entry)) {
+ while ((read = in.read(buffer)) != -1) {
+ md.update(buffer, 0, read);
+ }
+@@ -107,11 +97,11 @@ public class Resource implements Comparable<Resource>
+ }
+
+ } finally {
+- if (jar != null) {
++ if (zip != null) {
+ try {
+- jar.close();
++ zip.close();
+ } catch (IOException ioe) {
+- log.warning("Error closing jar", "path", target, "jar", jar, "error", ioe);
++ log.warning("Error closing", "path", target, "zip", zip, "error", ioe);
+ }
+ }
+ if (tmpJarFile != null) {
+@@ -132,6 +122,34 @@ public class Resource implements Comparable<Resource>
+ return StringUtil.hexlate(md.digest());
+ }
+
++ /**
++ * Returns whether {@code file} is a {@code zip} file.
++ */
++ public static boolean isZip (File file)
++ {
++ String path = file.getName();
++ return path.endsWith(".zip") || path.endsWith(".zip_new");
++ }
++
++ /**
++ * Returns whether {@code file} is a {@code jar} file.
++ */
++ public static boolean isJar (File file)
++ {
++ String path = file.getName();
++ return path.endsWith(".jar") || path.endsWith(".jar_new");
++ }
++
++ /**
++ * Returns whether {@code file} is a {@code jar.pack} file.
++ */
++ public static boolean isPacked200Jar (File file)
++ {
++ String path = file.getName();
++ return path.endsWith(".jar.pack") || path.endsWith(".jar.pack_new") ||
++ path.endsWith(".jar.pack.gz") || path.endsWith(".jar.pack.gz_new");
++ }
++
+ /**
+ * Creates a resource with the supplied remote URL and local path.
+ */
+@@ -141,17 +159,13 @@ public class Resource implements Comparable<Resource>
+ _remote = remote;
+ _local = local;
+ _localNew = new File(local.toString() + "_new");
+- String lpath = _local.getPath();
+- _marker = new File(lpath + "v");
++ _marker = new File(_local.getPath() + "v");
+
+ _attrs = attrs;
+- _isTgz = isTgz(lpath);
+- _isJar = isJar(lpath);
+- _isPacked200Jar = isPacked200Jar(lpath);
++ _isZip = isJar(local) || isZip(local);
++ _isPacked200Jar = isPacked200Jar(local);
+ boolean unpack = attrs.contains(Attr.UNPACK);
+- if (unpack && _isJar) {
+- _unpacked = _local.getParentFile();
+- } else if(unpack && _isTgz) {
++ if (unpack && _isZip) {
+ _unpacked = _local.getParentFile();
+ } else if(unpack && _isPacked200Jar) {
+ String dotJar = ".jar", lname = _local.getName();
+@@ -307,20 +321,13 @@ public class Resource implements Comparable<Resource>
+ public void unpack () throws IOException
+ {
+ // sanity check
+- if (!_isJar && !_isPacked200Jar && !_isTgz) {
+- throw new IOException("Requested to unpack non-jar/tgz file '" + _local + "'.");
++ if (!_isZip && !_isPacked200Jar) {
++ throw new IOException("Requested to unpack non-jar file '" + _local + "'.");
+ }
+- if (_isJar) {
+- try (JarFile jar = new JarFile(_local)) {
++ if (_isZip) {
++ try (ZipFile jar = new ZipFile(_local)) {
+ FileUtil.unpackJar(jar, _unpacked, _attrs.contains(Attr.CLEAN));
+ }
+- } else if (_isTgz) {
+- try (InputStream fi = Files.newInputStream(_local.toPath());
+- InputStream bi = new BufferedInputStream(fi);
+- InputStream gzi = new GzipCompressorInputStream(bi);
+- TarArchiveInputStream tgz = new TarArchiveInputStream(gzi)) {
+- FileUtil.unpackTgz(tgz, _unpacked, _attrs.contains(Attr.CLEAN));
+- }
+ } else {
+ FileUtil.unpackPacked200Jar(_local, _unpacked);
+ }
+@@ -382,31 +389,15 @@ public class Resource implements Comparable<Resource>
+ }
+ }
+
+- protected static boolean isJar (String path)
+- {
+- return path.endsWith(".jar") || path.endsWith(".jar_new");
+- }
+-
+- protected static boolean isTgz (String path)
+- {
+- return path.endsWith(".tgz") || path.endsWith(".tgz_new");
+- }
+-
+- protected static boolean isPacked200Jar (String path)
+- {
+- return path.endsWith(".jar.pack") || path.endsWith(".jar.pack_new") ||
+- path.endsWith(".jar.pack.gz")|| path.endsWith(".jar.pack.gz_new");
+- }
+-
+ protected String _path;
+ protected URL _remote;
+ protected File _local, _localNew, _marker, _unpacked;
+ protected EnumSet<Attr> _attrs;
+- protected boolean _isJar, _isPacked200Jar, _isTgz;
++ protected boolean _isZip, _isPacked200Jar;
+
+ /** Used to sort the entries in a jar file. */
+- protected static final Comparator<JarEntry> ENTRY_COMP = new Comparator<JarEntry>() {
+- @Override public int compare (JarEntry e1, JarEntry e2) {
++ protected static final Comparator<ZipEntry> ENTRY_COMP = new Comparator<ZipEntry>() {
++ @Override public int compare (ZipEntry e1, ZipEntry e2) {
+ return e1.getName().compareTo(e2.getName());
+ }
+ };
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/SysProps.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/SysProps.java
+index 0d96ecb71..b36d40021 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/SysProps.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/SysProps.java
+@@ -16,7 +16,7 @@ import com.threerings.getdown.util.VersionUtil;
+ * accessor so that it's easy to see all of the secret system property arguments that Getdown makes
+ * use of.
+ */
+-public class SysProps
++public final class SysProps
+ {
+ /** Configures the appdir (in lieu of passing it in argv). Usage: {@code -Dappdir=foo}. */
+ public static String appDir () {
+@@ -40,6 +40,14 @@ public class SysProps
+ return System.getProperty("no_log_redir") != null;
+ }
+
++ /** Used to debug Getdown's launching of an app. When set, it disables redirection of stdout
++ * and stderr into a log file, and on Windows it uses {@code java.exe} to launch the app so
++ * that its console output can be observed.
++ * Usage: {@code -Ddebug}. */
++ public static boolean debug () {
++ return System.getProperty("debug") != null;
++ }
++
+ /** Overrides the domain on {@code appbase}. Usage: {@code -Dappbase_domain=foo}. */
+ public static String appbaseDomain () {
+ return System.getProperty("appbase_domain");
+@@ -101,6 +109,16 @@ public class SysProps
+ return Boolean.getBoolean("direct");
+ }
+
++ /** If true, Getdown will always try to connect without proxy settings even it a proxy is set
++ * in {@code proxy.txt}. If direct access is possible it will not clear {@code proxy.txt}, it
++ * will preserve the settings. This is to support cases where a user uses a workstation in two
++ * different networks, one with proxy the other one without. They should not be asked for
++ * proxy settings again each time they switch back to the proxy network.
++ * Usage: {@code -Dtry_no_proxy}. */
++ public static boolean tryNoProxyFirst () {
++ return Boolean.getBoolean("try_no_proxy");
++ }
++
+ /** Specifies the connection timeout (in seconds) to use when downloading control files from
+ * the server. This is chiefly useful when you are running in versionless mode and want Getdown
+ * to more quickly timeout its startup update check if the server with which it is
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/Downloader.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/Downloader.java
+index 6033e2f6e..2298d6099 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/Downloader.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/Downloader.java
+@@ -6,7 +6,13 @@
+ package com.threerings.getdown.net;
+
+ import java.io.File;
++import java.io.FileOutputStream;
+ import java.io.IOException;
++import java.io.InputStream;
++import java.net.HttpURLConnection;
++import java.net.URLConnection;
++import java.nio.channels.Channels;
++import java.nio.channels.ReadableByteChannel;
+
+ import java.util.Collection;
+ import java.util.HashMap;
+@@ -26,8 +32,13 @@ import static com.threerings.getdown.Log.log;
+ * implementors must take care to only execute thread-safe code or simply pass a message to the AWT
+ * thread, for example.
+ */
+-public abstract class Downloader
++public class Downloader
+ {
++ public Downloader (Connector conn)
++ {
++ _conn = conn;
++ }
++
+ /**
+ * Start the downloading process.
+ * @param resources the resources to download.
+@@ -129,10 +140,31 @@ public abstract class Downloader
+ */
+ protected void downloadFailed (Resource rsrc, Exception cause) {}
+
++ /**
++ * Called when a to-be-downloaded resource returns a 404 not found.
++ */
++ protected void resourceMissing (Resource rsrc) {}
++
+ /**
+ * Performs the protocol-specific portion of checking download size.
+ */
+- protected abstract long checkSize (Resource rsrc) throws IOException;
++ protected long checkSize (Resource rsrc) throws IOException {
++ URLConnection conn = _conn.open(rsrc.getRemote(), 0, 0);
++ try {
++ // if we're accessing our data via HTTP, we only need a HEAD request
++ if (conn instanceof HttpURLConnection) {
++ ((HttpURLConnection)conn).setRequestMethod("HEAD");
++ }
++ // if we get a satisfactory response code, return a size; ignore errors as we'll report
++ // those when we actually attempt to download the resource
++ int code = _conn.checkConnectStatus(conn);
++ return code == HttpURLConnection.HTTP_OK ? conn.getContentLength() : 0;
++
++ } finally {
++ // let it be known that we're done with this connection
++ conn.getInputStream().close();
++ }
++ }
+
+ /**
+ * Periodically called by the protocol-specific downloaders to update their progress. This
+@@ -203,13 +235,68 @@ public abstract class Downloader
+ * protocol-specific code. This method should periodically check whether {@code _state} is set
+ * to aborted and abort any in-progress download if so.
+ */
+- protected abstract void download (Resource rsrc) throws IOException;
++ protected void download (Resource rsrc) throws IOException {
++ URLConnection conn = _conn.open(rsrc.getRemote(), 0, 0);
++ // make sure we got a satisfactory response code
++ int code = _conn.checkConnectStatus(conn);
++ if (code == HttpURLConnection.HTTP_NOT_FOUND) {
++ resourceMissing(rsrc);
++ } else if (code != HttpURLConnection.HTTP_OK) {
++ throw new IOException(
++ "Resource returned HTTP error " + rsrc.getRemote() + " [code=" + code + "]");
++ }
++
++ // TODO: make FileChannel download impl (below) robust and allow apps to opt-into it via a
++ // system property
++ if (true) {
++ // download the resource from the specified URL
++ long actualSize = conn.getContentLength();
++ log.info("Downloading resource", "url", rsrc.getRemote(), "size", actualSize);
++ long currentSize = 0L;
++ byte[] buffer = new byte[4*4096];
++ try (InputStream in = conn.getInputStream();
++ FileOutputStream out = new FileOutputStream(rsrc.getLocalNew())) {
++
++ // TODO: look to see if we have a download info file
++ // containing info on potentially partially downloaded data;
++ // if so, use a "Range: bytes=HAVE-" header.
++
++ // read in the file data
++ int read;
++ while ((read = in.read(buffer)) != -1) {
++ // abort the download if the downloader is aborted
++ if (_state == State.ABORTED) {
++ break;
++ }
++ // write it out to our local copy
++ out.write(buffer, 0, read);
++ // note that we've downloaded some data
++ currentSize += read;
++ reportProgress(rsrc, currentSize, actualSize);
++ }
++ }
++
++ } else {
++ log.info("Downloading resource", "url", rsrc.getRemote(), "size", "unknown");
++ File localNew = rsrc.getLocalNew();
++ try (ReadableByteChannel rbc = Channels.newChannel(conn.getInputStream());
++ FileOutputStream fos = new FileOutputStream(localNew)) {
++ // TODO: more work is needed here, transferFrom can fail to transfer the entire
++ // file, in which case it's not clear what we're supposed to do.. call it again?
++ // will it repeatedly fail?
++ fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
++ reportProgress(rsrc, localNew.length(), localNew.length());
++ }
++ }
++ }
++
++ protected final Connector _conn;
+
+ /** The reported sizes of our resources. */
+- protected Map<Resource, Long> _sizes = new HashMap<>();
++ protected final Map<Resource, Long> _sizes = new HashMap<>();
+
+ /** The bytes downloaded for each resource. */
+- protected Map<Resource, Long> _downloaded = new HashMap<>();
++ protected final Map<Resource, Long> _downloaded = new HashMap<>();
+
+ /** The time at which the file transfer began. */
+ protected long _start;
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/HTTPDownloader.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/HTTPDownloader.java
+deleted file mode 100644
+index a7a3287a9..000000000
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/HTTPDownloader.java
++++ /dev/null
+@@ -1,115 +0,0 @@
+-//
+-// Getdown - application installer, patcher and launcher
+-// Copyright (C) 2004-2018 Getdown authors
+-// https://github.com/threerings/getdown/blob/master/LICENSE
+-
+-package com.threerings.getdown.net;
+-
+-import java.io.File;
+-import java.io.FileOutputStream;
+-import java.io.IOException;
+-import java.io.InputStream;
+-import java.net.HttpURLConnection;
+-import java.net.Proxy;
+-import java.net.URL;
+-import java.net.URLConnection;
+-import java.nio.channels.Channels;
+-import java.nio.channels.ReadableByteChannel;
+-
+-import com.threerings.getdown.data.Resource;
+-import com.threerings.getdown.util.ConnectionUtil;
+-
+-import static com.threerings.getdown.Log.log;
+-
+-/**
+- * Implements downloading files over HTTP
+- */
+-public class HTTPDownloader extends Downloader
+-{
+- public HTTPDownloader (Proxy proxy)
+- {
+- _proxy = proxy;
+- }
+-
+- @Override protected long checkSize (Resource rsrc) throws IOException
+- {
+- URLConnection conn = ConnectionUtil.open(_proxy, rsrc.getRemote(), 0, 0);
+- try {
+- // if we're accessing our data via HTTP, we only need a HEAD request
+- if (conn instanceof HttpURLConnection) {
+- HttpURLConnection hcon = (HttpURLConnection)conn;
+- hcon.setRequestMethod("HEAD");
+- hcon.connect();
+- // make sure we got a satisfactory response code
+- if (hcon.getResponseCode() != HttpURLConnection.HTTP_OK) {
+- throw new IOException("Unable to check up-to-date for " +
+- rsrc.getRemote() + ": " + hcon.getResponseCode());
+- }
+- }
+- return conn.getContentLength();
+-
+- } finally {
+- // let it be known that we're done with this connection
+- conn.getInputStream().close();
+- }
+- }
+-
+- @Override protected void download (Resource rsrc) throws IOException
+- {
+- // TODO: make FileChannel download impl (below) robust and allow apps to opt-into it via a
+- // system property
+- if (true) {
+- // download the resource from the specified URL
+- URLConnection conn = ConnectionUtil.open(_proxy, rsrc.getRemote(), 0, 0);
+- conn.connect();
+-
+- // make sure we got a satisfactory response code
+- if (conn instanceof HttpURLConnection) {
+- HttpURLConnection hcon = (HttpURLConnection)conn;
+- if (hcon.getResponseCode() != HttpURLConnection.HTTP_OK) {
+- throw new IOException("Unable to download resource " + rsrc.getRemote() + ": " +
+- hcon.getResponseCode());
+- }
+- }
+- long actualSize = conn.getContentLength();
+- log.info("Downloading resource", "url", rsrc.getRemote(), "size", actualSize);
+- long currentSize = 0L;
+- byte[] buffer = new byte[4*4096];
+- try (InputStream in = conn.getInputStream();
+- FileOutputStream out = new FileOutputStream(rsrc.getLocalNew())) {
+-
+- // TODO: look to see if we have a download info file
+- // containing info on potentially partially downloaded data;
+- // if so, use a "Range: bytes=HAVE-" header.
+-
+- // read in the file data
+- int read;
+- while ((read = in.read(buffer)) != -1) {
+- // abort the download if the downloader is aborted
+- if (_state == State.ABORTED) {
+- break;
+- }
+- // write it out to our local copy
+- out.write(buffer, 0, read);
+- // note that we've downloaded some data
+- currentSize += read;
+- reportProgress(rsrc, currentSize, actualSize);
+- }
+- }
+-
+- } else {
+- log.info("Downloading resource", "url", rsrc.getRemote(), "size", "unknown");
+- File localNew = rsrc.getLocalNew();
+- try (ReadableByteChannel rbc = Channels.newChannel(rsrc.getRemote().openStream());
+- FileOutputStream fos = new FileOutputStream(localNew)) {
+- // TODO: more work is needed here, transferFrom can fail to transfer the entire
+- // file, in which case it's not clear what we're supposed to do.. call it again?
+- // will it repeatedly fail?
+- fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+- reportProgress(rsrc, localNew.length(), localNew.length());
+- }
+- }
+- }
+-
+- protected final Proxy _proxy;
+-}
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java
+index 22446ec0a..8c6d84160 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java
+@@ -11,7 +11,7 @@ package com.threerings.getdown.spi;
+ public interface ProxyAuth
+ {
+ /** Credentials for a proxy server. */
+- public static class Credentials {
++ class Credentials {
+ public final String username;
+ public final String password;
+ public Credentials (String username, String password) {
+@@ -23,10 +23,10 @@ public interface ProxyAuth
+ /**
+ * Loads the credentials for the app installed in {@code appDir}.
+ */
+- public Credentials loadCredentials (String appDir);
++ Credentials loadCredentials (String appDir);
+
+ /**
+ * Encrypts and saves the credentials for the app installed in {@code appDir}.
+ */
+- public void saveCredentials (String appDir, String username, String password);
++ void saveCredentials (String appDir, String username, String password);
+ }
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Differ.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Differ.java
+index c2e740b6e..4f8e50ebd 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Differ.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Differ.java
+@@ -11,15 +11,14 @@ import java.io.FileInputStream;
+ import java.io.FileOutputStream;
+ import java.io.IOException;
+ import java.io.InputStream;
+-
++import java.io.OutputStream;
++import java.security.MessageDigest;
+ import java.util.ArrayList;
+ import java.util.Enumeration;
+-import java.util.jar.JarEntry;
+-import java.util.jar.JarFile;
+-import java.util.jar.JarOutputStream;
++import java.util.List;
+ import java.util.zip.ZipEntry;
+-
+-import java.security.MessageDigest;
++import java.util.zip.ZipFile;
++import java.util.zip.ZipOutputStream;
+
+ import com.threerings.getdown.data.Application;
+ import com.threerings.getdown.data.Digest;
+@@ -39,8 +38,8 @@ public class Differ
+ /**
+ * Creates a single patch file that contains the differences between
+ * the two specified application directories. The patch file will be
+- * created in the <code>nvdir</code> directory with name
+- * <code>patchV.dat</code> where V is the old application version.
++ * created in the {@code nvdir} directory with name
++ * {@code patchV.dat} where V is the old application version.
+ */
+ public void createDiff (File nvdir, File ovdir, boolean verbose)
+ throws IOException
+@@ -61,13 +60,13 @@ public class Differ
+
+ Application oapp = new Application(new EnvConfig(ovdir));
+ oapp.init(false);
+- ArrayList<Resource> orsrcs = new ArrayList<>();
++ List<Resource> orsrcs = new ArrayList<>();
+ orsrcs.addAll(oapp.getCodeResources());
+ orsrcs.addAll(oapp.getResources());
+
+ Application napp = new Application(new EnvConfig(nvdir));
+ napp.init(false);
+- ArrayList<Resource> nrsrcs = new ArrayList<>();
++ List<Resource> nrsrcs = new ArrayList<>();
+ nrsrcs.addAll(napp.getCodeResources());
+ nrsrcs.addAll(napp.getResources());
+
+@@ -91,15 +90,15 @@ public class Differ
+ }
+ }
+
+- protected void createPatch (File patch, ArrayList<Resource> orsrcs,
+- ArrayList<Resource> nrsrcs, boolean verbose)
++ protected void createPatch (File patch, List<Resource> orsrcs,
++ List<Resource> nrsrcs, boolean verbose)
+ throws IOException
+ {
+ int version = Digest.VERSION;
+ MessageDigest md = Digest.getMessageDigest(version);
+ try (FileOutputStream fos = new FileOutputStream(patch);
+ BufferedOutputStream buffered = new BufferedOutputStream(fos);
+- JarOutputStream jout = new JarOutputStream(buffered)) {
++ ZipOutputStream jout = new ZipOutputStream(buffered)) {
+
+ // for each file in the new application, it either already exists
+ // in the old application, or it is new
+@@ -172,13 +171,13 @@ public class Differ
+ throws IOException
+ {
+ File temp = File.createTempFile("differ", "jar");
+- try (JarFile jar = new JarFile(target);
++ try (ZipFile jar = new ZipFile(target);
+ FileOutputStream tempFos = new FileOutputStream(temp);
+ BufferedOutputStream tempBos = new BufferedOutputStream(tempFos);
+- JarOutputStream jout = new JarOutputStream(tempBos)) {
++ ZipOutputStream jout = new ZipOutputStream(tempBos)) {
+ byte[] buffer = new byte[4096];
+- for (Enumeration< JarEntry > iter = jar.entries(); iter.hasMoreElements();) {
+- JarEntry entry = iter.nextElement();
++ for (Enumeration<? extends ZipEntry> iter = jar.entries(); iter.hasMoreElements();) {
++ ZipEntry entry = iter.nextElement();
+ entry.setCompressedSize(-1);
+ jout.putNextEntry(entry);
+ try (InputStream in = jar.getInputStream(entry)) {
+@@ -193,8 +192,7 @@ public class Differ
+ return temp;
+ }
+
+- protected void jarDiff (File ofile, File nfile, JarOutputStream jout)
+- throws IOException
++ protected void jarDiff (File ofile, File nfile, ZipOutputStream jout) throws IOException
+ {
+ JarDiff.createPatch(ofile.getPath(), nfile.getPath(), jout, false);
+ }
+@@ -209,7 +207,7 @@ public class Differ
+ Differ differ = new Differ();
+ boolean verbose = false;
+ int aidx = 0;
+- if (args[0].equals("-verbose")) {
++ if ("-verbose".equals(args[0])) {
+ verbose = true;
+ aidx++;
+ }
+@@ -222,11 +220,10 @@ public class Differ
+ }
+ }
+
+- protected static void pipe (File file, JarOutputStream jout)
+- throws IOException
++ protected static void pipe (File file, OutputStream out) throws IOException
+ {
+ try (FileInputStream fin = new FileInputStream(file)) {
+- StreamUtil.copy(fin, jout);
++ StreamUtil.copy(fin, out);
+ }
+ }
+ }
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Digester.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Digester.java
+index b04a6539b..ae61b4333 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Digester.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Digester.java
+@@ -23,6 +23,7 @@ import com.threerings.getdown.data.Digest;
+ import com.threerings.getdown.data.EnvConfig;
+ import com.threerings.getdown.data.Resource;
+ import com.threerings.getdown.util.Base64;
++import com.threerings.getdown.util.Config;
+
+ import static java.nio.charset.StandardCharsets.UTF_8;
+
+@@ -74,8 +75,11 @@ public class Digester
+ System.out.println("Generating digest file '" + target + "'...");
+
+ // create our application and instruct it to parse its business
+- Application app = new Application(new EnvConfig(appdir));
+- app.init(false);
++ EnvConfig envc = new EnvConfig(appdir);
++ Application app = new Application(envc);
++ Config config = Application.readConfig(envc, false);
++ app.initBase(config);
++ app.initResources(config);
+
+ List<Resource> rsrcs = new ArrayList<>();
+ rsrcs.add(app.getConfigResource());
+@@ -86,6 +90,9 @@ public class Digester
+ rsrcs.addAll(ag.rsrcs);
+ }
+
++ // reinit app just to verify that getdown.txt has valid format
++ app.init(true);
++
+ // now generate the digest file
+ Digest.createDigest(version, rsrcs, target);
+ }
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiff.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiff.java
+index 1cea0eacd..f0db8ac03 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiff.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiff.java
+@@ -41,15 +41,24 @@
+
+ package com.threerings.getdown.tools;
+
+-import java.io.*;
++import java.io.Closeable;
++import java.io.File;
++import java.io.IOException;
++import java.io.InputStream;
++import java.io.OutputStream;
++import java.io.StringWriter;
++import java.io.Writer;
+ import java.util.*;
+-import java.util.jar.*;
++import java.util.zip.ZipEntry;
++import java.util.zip.ZipFile;
++import java.util.zip.ZipOutputStream;
+
+ import static java.nio.charset.StandardCharsets.UTF_8;
+
+ /**
+- * JarDiff is able to create a jar file containing the delta between two jar files (old and new).
+- * The delta jar file can then be applied to the old jar file to reconstruct the new jar file.
++ * JarDiff is able to create a zip file containing the delta between two jar or zip files (old
++ * and new). The delta file can then be applied to the old archive file to reconstruct the new
++ * archive file.
+ *
+ * <p> Refer to the JNLP spec for details on how this is done.
+ *
+@@ -58,39 +67,37 @@ import static java.nio.charset.StandardCharsets.UTF_8;
+ public class JarDiff implements JarDiffCodes
+ {
+ private static final int DEFAULT_READ_SIZE = 2048;
+- private static byte[] newBytes = new byte[DEFAULT_READ_SIZE];
+- private static byte[] oldBytes = new byte[DEFAULT_READ_SIZE];
++ private static final byte[] newBytes = new byte[DEFAULT_READ_SIZE];
++ private static final byte[] oldBytes = new byte[DEFAULT_READ_SIZE];
+
+ // The JARDiff.java is the stand-alone jardiff.jar tool. Thus, we do not depend on Globals.java
+ // and other stuff here. Instead, we use an explicit _debug flag.
+ private static boolean _debug;
+
+ /**
+- * Creates a patch from the two passed in files, writing the result to <code>os</code>.
++ * Creates a patch from the two passed in files, writing the result to {@code os}.
+ */
+ public static void createPatch (String oldPath, String newPath,
+ OutputStream os, boolean minimal) throws IOException
+ {
+- try (JarFile2 oldJar = new JarFile2(oldPath);
+- JarFile2 newJar = new JarFile2(newPath)) {
++ try (ZipFile2 oldArchive = new ZipFile2(oldPath);
++ ZipFile2 newArchive = new ZipFile2(newPath)) {
+
+- HashMap<String,String> moved = new HashMap<>();
+- HashSet<String> implicit = new HashSet<>();
+- HashSet<String> moveSrc = new HashSet<>();
+- HashSet<String> newEntries = new HashSet<>();
++ Map<String,String> moved = new HashMap<>();
++ Set<String> implicit = new HashSet<>();
++ Set<String> moveSrc = new HashSet<>();
++ Set<String> newEntries = new HashSet<>();
+
+ // FIRST PASS
+- // Go through the entries in new jar and
+- // determine which files are candidates for implicit moves
+- // ( files that has the same filename and same content in old.jar
+- // and new.jar )
+- // and for files that cannot be implicitly moved, we will either
+- // find out whether it is moved or new (modified)
+- for (JarEntry newEntry : newJar) {
++ // Go through the entries in new archive and determine which files are candidates for
++ // implicit moves (files that have the same filename and same content in old and new)
++ // and for files that cannot be implicitly moved, we will either find out whether it is
++ // moved or new (modified)
++ for (ZipEntry newEntry : newArchive) {
+ String newname = newEntry.getName();
+
+ // Return best match of contents, will return a name match if possible
+- String oldname = oldJar.getBestMatch(newJar, newEntry);
++ String oldname = oldArchive.getBestMatch(newArchive, newEntry);
+ if (oldname == null) {
+ // New or modified entry
+ if (_debug) {
+@@ -101,7 +108,7 @@ public class JarDiff implements JarDiffCodes
+ // Content already exist - need to do a move
+
+ // Should do implicit move? Yes, if names are the same, and
+- // no move command already exist from oldJar
++ // no move command already exist from oldArchive
+ if (oldname.equals(newname) && !moveSrc.contains(oldname)) {
+ if (_debug) {
+ System.out.println(newname + " added to implicit set!");
+@@ -117,12 +124,9 @@ public class JarDiff implements JarDiffCodes
+ // JarDiffPatcher also.
+ if (!minimal && (implicit.contains(oldname) ||
+ moveSrc.contains(oldname) )) {
+-
+ // generate non-minimal jardiff
+ // for backward compatibility
+-
+ if (_debug) {
+-
+ System.out.println("NEW: "+ newname);
+ }
+ newEntries.add(newname);
+@@ -153,8 +157,8 @@ public class JarDiff implements JarDiffCodes
+
+ // SECOND PASS: <deleted files> = <oldjarnames> - <implicitmoves> -
+ // <source of move commands> - <new or modified entries>
+- ArrayList<String> deleted = new ArrayList<>();
+- for (JarEntry oldEntry : oldJar) {
++ List<String> deleted = new ArrayList<>();
++ for (ZipEntry oldEntry : oldArchive) {
+ String oldName = oldEntry.getName();
+ if (!implicit.contains(oldName) && !moveSrc.contains(oldName)
+ && !newEntries.contains(oldName)) {
+@@ -180,7 +184,7 @@ public class JarDiff implements JarDiffCodes
+ }
+ }
+
+- JarOutputStream jos = new JarOutputStream(os);
++ ZipOutputStream jos = new ZipOutputStream(os);
+
+ // Write out all the MOVEs and REMOVEs
+ createIndex(jos, deleted, moved);
+@@ -190,7 +194,7 @@ public class JarDiff implements JarDiffCodes
+ if (_debug) {
+ System.out.println("New File: " + newName);
+ }
+- writeEntry(jos, newJar.getEntryByName(newName), newJar);
++ writeEntry(jos, newArchive.getEntryByName(newName), newArchive);
+ }
+
+ jos.finish();
+@@ -199,11 +203,11 @@ public class JarDiff implements JarDiffCodes
+ }
+
+ /**
+- * Writes the index file out to <code>jos</code>.
+- * <code>oldEntries</code> gives the names of the files that were removed,
+- * <code>movedMap</code> maps from the new name to the old name.
++ * Writes the index file out to {@code jos}.
++ * {@code oldEntries} gives the names of the files that were removed,
++ * {@code movedMap} maps from the new name to the old name.
+ */
+- private static void createIndex (JarOutputStream jos, List<String> oldEntries,
++ private static void createIndex (ZipOutputStream jos, List<String> oldEntries,
+ Map<String,String> movedMap)
+ throws IOException
+ {
+@@ -220,17 +224,17 @@ public class JarDiff implements JarDiffCodes
+ }
+
+ // And those that have moved
+- for (String newName : movedMap.keySet()) {
+- String oldName = movedMap.get(newName);
++ for (Map.Entry<String, String> entry : movedMap.entrySet()) {
++ String oldName = entry.getValue();
+ writer.write(MOVE_COMMAND);
+ writer.write(" ");
+ writeEscapedString(writer, oldName);
+ writer.write(" ");
+- writeEscapedString(writer, newName);
++ writeEscapedString(writer, entry.getKey());
+ writer.write("\r\n");
+ }
+
+- jos.putNextEntry(new JarEntry(INDEX_NAME));
++ jos.putNextEntry(new ZipEntry(INDEX_NAME));
+ byte[] bytes = writer.toString().getBytes(UTF_8);
+ jos.write(bytes, 0, bytes.length);
+ }
+@@ -264,10 +268,10 @@ public class JarDiff implements JarDiffCodes
+ return writer;
+ }
+
+- private static void writeEntry (JarOutputStream jos, JarEntry entry, JarFile2 file)
++ private static void writeEntry (ZipOutputStream jos, ZipEntry entry, ZipFile2 file)
+ throws IOException
+ {
+- try (InputStream data = file.getJarFile().getInputStream(entry)) {
++ try (InputStream data = file.getArchive().getInputStream(entry)) {
+ jos.putNextEntry(entry);
+ int size = data.read(newBytes);
+ while (size != -1) {
+@@ -278,31 +282,31 @@ public class JarDiff implements JarDiffCodes
+ }
+
+ /**
+- * JarFile2 wraps a JarFile providing some convenience methods.
++ * ZipFile2 wraps a ZipFile providing some convenience methods.
+ */
+- private static class JarFile2 implements Iterable<JarEntry>, Closeable
++ private static class ZipFile2 implements Iterable<ZipEntry>, Closeable
+ {
+- private JarFile _jar;
+- private List<JarEntry> _entries;
+- private HashMap<String,JarEntry> _nameToEntryMap;
+- private HashMap<Long,LinkedList<JarEntry>> _crcToEntryMap;
++ private final ZipFile _archive;
++ private List<ZipEntry> _entries;
++ private HashMap<String,ZipEntry> _nameToEntryMap;
++ private HashMap<Long,LinkedList<ZipEntry>> _crcToEntryMap;
+
+- public JarFile2 (String path) throws IOException {
+- _jar = new JarFile(new File(path));
++ public ZipFile2 (String path) throws IOException {
++ _archive = new ZipFile(new File(path));
+ index();
+ }
+
+- public JarFile getJarFile () {
+- return _jar;
++ public ZipFile getArchive () {
++ return _archive;
+ }
+
+- // from interface Iterable<JarEntry>
++ // from interface Iterable<ZipEntry>
+ @Override
+- public Iterator<JarEntry> iterator () {
++ public Iterator<ZipEntry> iterator () {
+ return _entries.iterator();
+ }
+
+- public JarEntry getEntryByName (String name) {
++ public ZipEntry getEntryByName (String name) {
+ return _nameToEntryMap.get(name);
+ }
+
+@@ -350,7 +354,7 @@ public class JarDiff implements JarDiffCodes
+ return retVal;
+ }
+
+- public String getBestMatch (JarFile2 file, JarEntry entry) throws IOException {
++ public String getBestMatch (ZipFile2 file, ZipEntry entry) throws IOException {
+ // check for same name and same content, return name if found
+ if (contains(file, entry)) {
+ return (entry.getName());
+@@ -360,11 +364,10 @@ public class JarDiff implements JarDiffCodes
+ return (hasSameContent(file,entry));
+ }
+
+- public boolean contains (JarFile2 f, JarEntry e) throws IOException {
+-
+- JarEntry thisEntry = getEntryByName(e.getName());
++ public boolean contains (ZipFile2 f, ZipEntry e) throws IOException {
++ ZipEntry thisEntry = getEntryByName(e.getName());
+
+- // Look up name in 'this' Jar2File - if not exist return false
++ // Look up name in 'this' ZipFile2 - if not exist return false
+ if (thisEntry == null)
+ return false;
+
+@@ -373,26 +376,26 @@ public class JarDiff implements JarDiffCodes
+ return false;
+
+ // Check contents - if no match - return false
+- try (InputStream oldIS = getJarFile().getInputStream(thisEntry);
+- InputStream newIS = f.getJarFile().getInputStream(e)) {
++ try (InputStream oldIS = getArchive().getInputStream(thisEntry);
++ InputStream newIS = f.getArchive().getInputStream(e)) {
+ return !differs(oldIS, newIS);
+ }
+ }
+
+- public String hasSameContent (JarFile2 file, JarEntry entry) throws IOException {
++ public String hasSameContent (ZipFile2 file, ZipEntry entry) throws IOException {
+ String thisName = null;
+- Long crcL = Long.valueOf(entry.getCrc());
+- // check if this jar contains files with the passed in entry's crc
++ Long crcL = entry.getCrc();
++ // check if this archive contains files with the passed in entry's crc
+ if (_crcToEntryMap.containsKey(crcL)) {
+ // get the Linked List with files with the crc
+- LinkedList<JarEntry> ll = _crcToEntryMap.get(crcL);
++ LinkedList<ZipEntry> ll = _crcToEntryMap.get(crcL);
+ // go through the list and check for content match
+- ListIterator<JarEntry> li = ll.listIterator(0);
++ ListIterator<ZipEntry> li = ll.listIterator(0);
+ while (li.hasNext()) {
+- JarEntry thisEntry = li.next();
++ ZipEntry thisEntry = li.next();
+ // check for content match
+- try (InputStream oldIS = getJarFile().getInputStream(thisEntry);
+- InputStream newIS = file.getJarFile().getInputStream(entry)) {
++ try (InputStream oldIS = getArchive().getInputStream(thisEntry);
++ InputStream newIS = file.getArchive().getInputStream(entry)) {
+ if (!differs(oldIS, newIS)) {
+ thisName = thisEntry.getName();
+ return thisName;
+@@ -404,19 +407,19 @@ public class JarDiff implements JarDiffCodes
+ }
+
+ private void index () throws IOException {
+- Enumeration<JarEntry> entries = _jar.entries();
++ Enumeration<? extends ZipEntry> entries = _archive.entries();
+
+ _nameToEntryMap = new HashMap<>();
+ _crcToEntryMap = new HashMap<>();
+ _entries = new ArrayList<>();
+ if (_debug) {
+- System.out.println("indexing: " + _jar.getName());
++ System.out.println("indexing: " + _archive.getName());
+ }
+ if (entries != null) {
+ while (entries.hasMoreElements()) {
+- JarEntry entry = entries.nextElement();
++ ZipEntry entry = entries.nextElement();
+ long crc = entry.getCrc();
+- Long crcL = Long.valueOf(crc);
++ Long crcL = crc;
+ if (_debug) {
+ System.out.println("\t" + entry.getName() + " CRC " + crc);
+ }
+@@ -427,13 +430,13 @@ public class JarDiff implements JarDiffCodes
+ // generate the CRC to entries map
+ if (_crcToEntryMap.containsKey(crcL)) {
+ // key exist, add the entry to the correcponding linked list
+- LinkedList<JarEntry> ll = _crcToEntryMap.get(crcL);
++ LinkedList<ZipEntry> ll = _crcToEntryMap.get(crcL);
+ ll.add(entry);
+ _crcToEntryMap.put(crcL, ll);
+
+ } else {
+ // create a new entry in the hashmap for the new key
+- LinkedList<JarEntry> ll = new LinkedList<JarEntry>();
++ LinkedList<ZipEntry> ll = new LinkedList<>();
+ ll.add(entry);
+ _crcToEntryMap.put(crcL, ll);
+ }
+@@ -443,7 +446,7 @@ public class JarDiff implements JarDiffCodes
+
+ @Override
+ public void close() throws IOException {
+- _jar.close();
++ _archive.close();
+ }
+ }
+ }
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java
+index b5a0a1763..e55034bca 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java
+@@ -16,27 +16,25 @@ import java.util.ArrayList;
+ import java.util.Enumeration;
+ import java.util.HashMap;
+ import java.util.HashSet;
+-import java.util.Iterator;
+ import java.util.List;
+ import java.util.Map;
+ import java.util.Set;
+-
+-import java.util.jar.JarEntry;
+-import java.util.jar.JarFile;
+ import java.util.jar.JarOutputStream;
++import java.util.zip.ZipEntry;
++import java.util.zip.ZipFile;
++import java.util.zip.ZipOutputStream;
+
+ import com.threerings.getdown.util.ProgressObserver;
+-
+ import static java.nio.charset.StandardCharsets.UTF_8;
+
+ /**
+- * Applies a jardiff patch to a jar file.
++ * Applies a jardiff patch to a jar/zip file.
+ */
+ public class JarDiffPatcher implements JarDiffCodes
+ {
+ /**
+- * Patches the specified jar file using the supplied patch file and writing
+- * the new jar file to the supplied target.
++ * Patches the specified jar file using the supplied patch file and writing the new jar file to
++ * the supplied target.
+ *
+ * @param jarPath the path to the original jar file.
+ * @param diffPath the path to the jardiff patch file.
+@@ -49,11 +47,9 @@ public class JarDiffPatcher implements JarDiffCodes
+ throws IOException
+ {
+ File oldFile = new File(jarPath), diffFile = new File(diffPath);
+-
+- try (JarFile oldJar = new JarFile(oldFile);
+- JarFile jarDiff = new JarFile(diffFile);
+- JarOutputStream jos = new JarOutputStream(new FileOutputStream(target))) {
+-
++ try (ZipFile oldJar = new ZipFile(oldFile);
++ ZipFile jarDiff = new ZipFile(diffFile);
++ ZipOutputStream jos = makeOutputStream(oldFile, target)) {
+ Set<String> ignoreSet = new HashSet<>();
+ Map<String, String> renameMap = new HashMap<>();
+ determineNameMapping(jarDiff, ignoreSet, renameMap);
+@@ -63,7 +59,7 @@ public class JarDiffPatcher implements JarDiffCodes
+
+ // Files to implicit move
+ Set<String> oldjarNames = new HashSet<>();
+- Enumeration<JarEntry> oldEntries = oldJar.entries();
++ Enumeration<? extends ZipEntry> oldEntries = oldJar.entries();
+ if (oldEntries != null) {
+ while (oldEntries.hasMoreElements()) {
+ oldjarNames.add(oldEntries.nextElement().getName());
+@@ -83,10 +79,10 @@ public class JarDiffPatcher implements JarDiffCodes
+ size -= ignoreSet.size();
+
+ // Add content from JARDiff
+- Enumeration<JarEntry> entries = jarDiff.entries();
++ Enumeration<? extends ZipEntry> entries = jarDiff.entries();
+ if (entries != null) {
+ while (entries.hasMoreElements()) {
+- JarEntry entry = entries.nextElement();
++ ZipEntry entry = entries.nextElement();
+ if (!INDEX_NAME.equals(entry.getName())) {
+ updateObserver(observer, currentEntry, size);
+ currentEntry++;
+@@ -114,15 +110,15 @@ public class JarDiffPatcher implements JarDiffCodes
+ // Apply move <oldName> <newName> command
+ String oldName = renameMap.get(newName);
+
+- // Get source JarEntry
+- JarEntry oldEntry = oldJar.getJarEntry(oldName);
++ // Get source ZipEntry
++ ZipEntry oldEntry = oldJar.getEntry(oldName);
+ if (oldEntry == null) {
+ String moveCmd = MOVE_COMMAND + oldName + " " + newName;
+ throw new IOException("error.badmove: " + moveCmd);
+ }
+
+- // Create dest JarEntry
+- JarEntry newEntry = new JarEntry(newName);
++ // Create dest ZipEntry
++ ZipEntry newEntry = new ZipEntry(newName);
+ newEntry.setTime(oldEntry.getTime());
+ newEntry.setSize(oldEntry.getSize());
+ newEntry.setCompressedSize(oldEntry.getCompressedSize());
+@@ -149,19 +145,15 @@ public class JarDiffPatcher implements JarDiffCodes
+ }
+
+ // implicit move
+- Iterator<String> iEntries = oldjarNames.iterator();
+- if (iEntries != null) {
+- while (iEntries.hasNext()) {
+- String name = iEntries.next();
+- JarEntry entry = oldJar.getJarEntry(name);
+- if (entry == null) {
+- // names originally retrieved from the JAR, so this should never happen
+- throw new AssertionError("JAR entry not found: " + name);
+- }
+- updateObserver(observer, currentEntry, size);
+- currentEntry++;
+- writeEntry(jos, entry, oldJar);
++ for (String name : oldjarNames) {
++ ZipEntry entry = oldJar.getEntry(name);
++ if (entry == null) {
++ // names originally retrieved from the archive, so this should never happen
++ throw new AssertionError("Archive entry not found: " + name);
+ }
++ updateObserver(observer, currentEntry, size);
++ currentEntry++;
++ writeEntry(jos, entry, oldJar);
+ }
+ updateObserver(observer, currentEntry, size);
+ }
+@@ -175,7 +167,7 @@ public class JarDiffPatcher implements JarDiffCodes
+ }
+
+ protected void determineNameMapping (
+- JarFile jarDiff, Set<String> ignoreSet, Map<String, String> renameMap)
++ ZipFile jarDiff, Set<String> ignoreSet, Map<String, String> renameMap)
+ throws IOException
+ {
+ InputStream is = jarDiff.getInputStream(jarDiff.getEntry(INDEX_NAME));
+@@ -223,7 +215,7 @@ public class JarDiffPatcher implements JarDiffCodes
+ {
+ int index = 0;
+ int length = path.length();
+- ArrayList<String> sub = new ArrayList<>();
++ List<String> sub = new ArrayList<>();
+
+ while (index < length) {
+ while (index < length && Character.isWhitespace
+@@ -231,9 +223,8 @@ public class JarDiffPatcher implements JarDiffCodes
+ index++;
+ }
+ if (index < length) {
+- int start = index;
+- int last = start;
+- String subString = null;
++ int last = index;
++ StringBuilder subString = null;
+
+ while (index < length) {
+ char aChar = path.charAt(index);
+@@ -241,9 +232,9 @@ public class JarDiffPatcher implements JarDiffCodes
+ path.charAt(index + 1) == ' ') {
+
+ if (subString == null) {
+- subString = path.substring(last, index);
++ subString = new StringBuilder(path.substring(last, index));
+ } else {
+- subString += path.substring(last, index);
++ subString.append(path, last, index);
+ }
+ last = ++index;
+ } else if (Character.isWhitespace(aChar)) {
+@@ -253,18 +244,20 @@ public class JarDiffPatcher implements JarDiffCodes
+ }
+ if (last != index) {
+ if (subString == null) {
+- subString = path.substring(last, index);
++ subString = new StringBuilder(path.substring(last, index));
+ } else {
+- subString += path.substring(last, index);
++ subString.append(path, last, index);
+ }
+ }
+- sub.add(subString);
++ if (subString != null) {
++ sub.add(subString.toString());
++ }
+ }
+ }
+ return sub;
+ }
+
+- protected void writeEntry (JarOutputStream jos, JarEntry entry, JarFile file)
++ protected void writeEntry (ZipOutputStream jos, ZipEntry entry, ZipFile file)
+ throws IOException
+ {
+ try (InputStream data = file.getInputStream(entry)) {
+@@ -272,10 +265,10 @@ public class JarDiffPatcher implements JarDiffCodes
+ }
+ }
+
+- protected void writeEntry (JarOutputStream jos, JarEntry entry, InputStream data)
++ protected void writeEntry (ZipOutputStream jos, ZipEntry entry, InputStream data)
+ throws IOException
+ {
+- jos.putNextEntry(new JarEntry(entry.getName()));
++ jos.putNextEntry(new ZipEntry(entry.getName()));
+
+ // Read the entry
+ int size = data.read(newBytes);
+@@ -285,6 +278,15 @@ public class JarDiffPatcher implements JarDiffCodes
+ }
+ }
+
++ protected static ZipOutputStream makeOutputStream (File source, File target)
++ throws IOException
++ {
++ FileOutputStream out = new FileOutputStream(target);
++ if (source.getName().endsWith(".jar")) return new JarOutputStream(out);
++ else if (source.getName().endsWith(".zip")) return new ZipOutputStream(out);
++ else throw new AssertionError("Unsupported source file '" + source + "'. Not a .jar or .zip?");
++ }
++
+ protected static final int DEFAULT_READ_SIZE = 2048;
+
+ protected static byte[] newBytes = new byte[DEFAULT_READ_SIZE];
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Patcher.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Patcher.java
+index 4ead59bb3..5c8baaaab 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Patcher.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Patcher.java
+@@ -9,16 +9,13 @@ import java.io.File;
+ import java.io.FileOutputStream;
+ import java.io.IOException;
+ import java.io.InputStream;
+-
+ import java.util.Enumeration;
+-import java.util.jar.JarEntry;
+-import java.util.jar.JarFile;
+ import java.util.zip.ZipEntry;
++import java.util.zip.ZipFile;
+
+ import com.threerings.getdown.util.FileUtil;
+ import com.threerings.getdown.util.ProgressObserver;
+ import com.threerings.getdown.util.StreamUtil;
+-
+ import static com.threerings.getdown.Log.log;
+
+ /**
+@@ -55,10 +52,10 @@ public class Patcher
+ _obs = obs;
+ _plength = patch.length();
+
+- try (JarFile file = new JarFile(patch)) {
+- Enumeration<JarEntry> entries = file.entries(); // old skool!
++ try (ZipFile file = new ZipFile(patch)) {
++ Enumeration<? extends ZipEntry> entries = file.entries();
+ while (entries.hasMoreElements()) {
+- JarEntry entry = entries.nextElement();
++ ZipEntry entry = entries.nextElement();
+ String path = entry.getName();
+ long elength = entry.getCompressedSize();
+
+@@ -96,7 +93,7 @@ public class Patcher
+ return path.substring(0, path.length() - suffix.length());
+ }
+
+- protected void createFile (JarFile file, ZipEntry entry, File target)
++ protected void createFile (ZipFile file, ZipEntry entry, File target)
+ {
+ // create our copy buffer if necessary
+ if (_buffer == null) {
+@@ -124,8 +121,7 @@ public class Patcher
+ }
+ }
+
+- protected void patchFile (JarFile file, ZipEntry entry,
+- File appdir, String path)
++ protected void patchFile (ZipFile file, ZipEntry entry, File appdir, String path)
+ {
+ File target = new File(appdir, path);
+ File patch = new File(appdir, entry.getName());
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Base64.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Base64.java
+index 2a5db79ec..233f1e739 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Base64.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Base64.java
+@@ -24,7 +24,7 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
+ * href="http://www.ietf.org/rfc/rfc2045.txt">2045</a> and <a
+ * href="http://www.ietf.org/rfc/rfc3548.txt">3548</a>.
+ */
+-public class Base64 {
++public final class Base64 {
+ /**
+ * Default values for encoder/decoder flags.
+ */
+@@ -68,7 +68,7 @@ public class Base64 {
+ // shared code
+ // --------------------------------------------------------
+
+- /* package */ static abstract class Coder {
++ /* package */ abstract static class Coder {
+ public byte[] output;
+ public int op;
+
+@@ -178,7 +178,7 @@ public class Base64 {
+ * Lookup table for turning bytes into their position in the
+ * Base64 alphabet.
+ */
+- private static final int DECODE[] = {
++ private static final int[] DECODE = {
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
+@@ -201,7 +201,7 @@ public class Base64 {
+ * Decode lookup table for the "web safe" variant (RFC 3548
+ * sec. 4) where - and _ replace + and /.
+ */
+- private static final int DECODE_WEBSAFE[] = {
++ private static final int[] DECODE_WEBSAFE = {
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1,
+@@ -236,7 +236,7 @@ public class Base64 {
+ private int state; // state number (0 to 6)
+ private int value;
+
+- final private int[] alphabet;
++ private final int[] alphabet;
+
+ public Decoder(int flags, byte[] output) {
+ this.output = output;
+@@ -541,7 +541,7 @@ public class Base64 {
+ * Lookup table for turning Base64 alphabet positions (6 bits)
+ * into output bytes.
+ */
+- private static final byte ENCODE[] = {
++ private static final byte[] ENCODE = {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
+ 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
+ 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
+@@ -552,21 +552,21 @@ public class Base64 {
+ * Lookup table for turning Base64 alphabet positions (6 bits)
+ * into output bytes.
+ */
+- private static final byte ENCODE_WEBSAFE[] = {
++ private static final byte[] ENCODE_WEBSAFE = {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
+ 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
+ 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
+ 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_',
+ };
+
+- final private byte[] tail;
++ private final byte[] tail;
+ /* package */ int tailLen;
+ private int count;
+
+- final public boolean do_padding;
+- final public boolean do_newline;
+- final public boolean do_cr;
+- final private byte[] alphabet;
++ public final boolean do_padding;
++ public final boolean do_newline;
++ public final boolean do_cr;
++ private final byte[] alphabet;
+
+ public Encoder(int flags, byte[] output) {
+ this.output = output;
+@@ -618,7 +618,7 @@ public class Base64 {
+ ((input[p++] & 0xff) << 8) |
+ (input[p++] & 0xff);
+ tailLen = 0;
+- };
++ }
+ break;
+
+ case 2:
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Color.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Color.java
+index 047cead76..416f77e58 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Color.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Color.java
+@@ -8,11 +8,11 @@ package com.threerings.getdown.util;
+ /**
+ * Utilities for handling ARGB colors.
+ */
+-public class Color
++public final class Color
+ {
+- public final static int CLEAR = 0x00000000;
+- public final static int WHITE = 0xFFFFFFFF;
+- public final static int BLACK = 0xFF000000;
++ public static final int CLEAR = 0x00000000;
++ public static final int WHITE = 0xFFFFFFFF;
++ public static final int BLACK = 0xFF000000;
+
+ public static float brightness (int argb) {
+ // TODO: we're ignoring alpha here...
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Config.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Config.java
+index 6ad2b4fd9..75357e882 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Config.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Config.java
+@@ -14,7 +14,6 @@ import java.net.MalformedURLException;
+ import java.net.URL;
+ import java.nio.charset.StandardCharsets;
+ import java.util.ArrayList;
+-import java.util.Arrays;
+ import java.util.HashMap;
+ import java.util.List;
+ import java.util.Locale;
+@@ -58,33 +57,17 @@ public class Config
+ }
+
+ /**
+- * Parses a configuration file containing key/value pairs. The file must be in the UTF-8
+- * encoding.
+- *
++ * Parses configuration text containing key/value pairs.
+ * @param opts options that influence the parsing. See {@link #createOpts}.
+- *
+- * @return a list of <code>String[]</code> instances containing the key/value pairs in the
++ * @return a list of {@code String[]} instances containing the key/value pairs in the
+ * order they were parsed from the file.
+ */
+- public static List<String[]> parsePairs (File source, ParseOpts opts)
+- throws IOException
+- {
+- // annoyingly FileReader does not allow encoding to be specified (uses platform default)
+- try (FileInputStream fis = new FileInputStream(source);
+- InputStreamReader input = new InputStreamReader(fis, StandardCharsets.UTF_8)) {
+- return parsePairs(input, opts);
+- }
+- }
+-
+- /**
+- * See {@link #parsePairs(File,ParseOpts)}.
+- */
+ public static List<String[]> parsePairs (Reader source, ParseOpts opts) throws IOException
+ {
+ List<String[]> pairs = new ArrayList<>();
+ for (String line : FileUtil.readLines(source)) {
+ // nix comments
+- int cidx = line.indexOf("#");
++ int cidx = line.indexOf('#');
+ if (opts.strictComments ? cidx == 0 : cidx != -1) {
+ line = line.substring(0, cidx);
+ }
+@@ -98,7 +81,7 @@ public class Config
+ // parse our key/value pair
+ String[] pair = new String[2];
+ // if we're biasing toward key, put all the extra = in the key rather than the value
+- int eidx = opts.biasToKey ? line.lastIndexOf("=") : line.indexOf("=");
++ int eidx = opts.biasToKey ? line.lastIndexOf('=') : line.indexOf('=');
+ if (eidx != -1) {
+ pair[0] = line.substring(0, eidx).trim();
+ pair[1] = line.substring(eidx+1).trim();
+@@ -109,7 +92,7 @@ public class Config
+
+ // if the pair has an os qualifier, we need to process it
+ if (pair[1].startsWith("[")) {
+- int qidx = pair[1].indexOf("]");
++ int qidx = pair[1].indexOf(']');
+ if (qidx == -1) {
+ log.warning("Bogus platform specifier", "key", pair[0], "value", pair[1]);
+ continue; // omit the pair entirely
+@@ -132,6 +115,21 @@ public class Config
+ return pairs;
+ }
+
++ /**
++ * Parses configuration file containing key/value pairs.
++ * @param source the file containing the config text. Must be in the UTF-8 encoding.
++ * @param opts options that influence the parsing. See {@link #createOpts}.
++ */
++ public static List<String[]> parsePairs (File source, ParseOpts opts)
++ throws IOException
++ {
++ // annoyingly FileReader does not allow encoding to be specified (uses platform default)
++ try (FileInputStream fis = new FileInputStream(source);
++ InputStreamReader input = new InputStreamReader(fis, StandardCharsets.UTF_8)) {
++ return parsePairs(input, opts);
++ }
++ }
++
+ /**
+ * Takes a comma-separated String of four integers and returns a rectangle using those ints as
+ * the its x, y, width, and height.
+@@ -167,13 +165,10 @@ public class Config
+ }
+
+ /**
+- * Parses a configuration file containing key/value pairs. The file must be in the UTF-8
+- * encoding.
+- *
+- * @return a map from keys to values, where a value will be an array of strings if more than
+- * one key/value pair in the config file was associated with the same key.
++ * Parses the data for a config instance from the supplied {@code source} reader.
++ * @return a map that can be used to create a {@link #Config}.
+ */
+- public static Config parseConfig (File source, ParseOpts opts)
++ public static Map<String, Object> parseData (Reader source, ParseOpts opts)
+ throws IOException
+ {
+ Map<String, Object> data = new HashMap<>();
+@@ -196,15 +191,34 @@ public class Config
+ }
+ }
+
+- // special magic for the getdown.txt config: if the parsed data contains 'strict_comments =
+- // true' then we reparse the file with strict comments (i.e. # is only assumed to start a
+- // comment in column 0)
+- if (!opts.strictComments && Boolean.parseBoolean((String)data.get("strict_comments"))) {
+- opts.strictComments = true;
+- return parseConfig(source, opts);
+- }
++ return data;
++ }
++
++ /**
++ * Parses a configuration file containing key/value pairs. The file must be in the UTF-8
++ * encoding.
++ *
++ * @return a map from keys to values, where a value will be an array of strings if more than
++ * one key/value pair in the config file was associated with the same key.
++ */
++ public static Config parseConfig (File source, ParseOpts opts)
++ throws IOException
++ {
++ // annoyingly FileReader does not allow encoding to be specified (uses platform default)
++ try (FileInputStream fis = new FileInputStream(source);
++ InputStreamReader input = new InputStreamReader(fis, StandardCharsets.UTF_8)) {
++ Map<String, Object> data = parseData(input, opts);
++
++ // special magic for the getdown.txt config: if the parsed data contains
++ // 'strict_comments = true' then we reparse the file with strict comments (i.e. # is
++ // only assumed to start a comment in column 0)
++ if (!opts.strictComments && Boolean.parseBoolean((String)data.get("strict_comments"))) {
++ opts.strictComments = true;
++ return parseConfig(source, opts);
++ }
+
+- return new Config(data);
++ return new Config(data);
++ }
+ }
+
+ public Config (Map<String, Object> data) {
+@@ -248,6 +262,20 @@ public class Config
+ return Boolean.parseBoolean(getString(name));
+ }
+
++ /**
++ * Returns the specified config value as an enum value. The string value of the config is
++ * converted to all upper case and then turned into an enum via {@link Enum#valueOf}.
++ */
++ public <T extends Enum<T>> T getEnum (String name, Class<T> eclass, T defval) {
++ String value = getString(name, defval.toString());
++ try {
++ return Enum.valueOf(eclass, value.toUpperCase());
++ } catch (Exception e) {
++ log.warning("Invalid value for '" + name + "' config: '" + value + "'.");
++ return defval;
++ }
++ }
++
+ /**
+ * Massages a single string into an array and leaves existing array values as is. Simplifies
+ * access to parameters that are expected to be arrays.
+@@ -255,9 +283,6 @@ public class Config
+ public String[] getMultiValue (String name)
+ {
+ Object value = _data.get(name);
+- if (value == null) {
+- return new String[] {};
+- }
+ if (value instanceof String) {
+ return new String[] { (String)value };
+ } else {
+@@ -355,7 +380,7 @@ public class Config
+ protected static boolean checkQualifiers (String quals, String osname, String osarch)
+ {
+ if (quals.startsWith("!")) {
+- if (quals.indexOf(",") != -1) { // sanity check
++ if (quals.contains(",")) { // sanity check
+ log.warning("Multiple qualifiers cannot be used when one of the qualifiers " +
+ "is negative", "quals", quals);
+ return false;
+@@ -375,103 +400,8 @@ public class Config
+ {
+ String[] bits = qual.trim().toLowerCase(Locale.ROOT).split("-");
+ String os = bits[0], arch = (bits.length > 1) ? bits[1] : "";
+- return (osname.indexOf(os) != -1) && (osarch.indexOf(arch) != -1);
+- }
+-
+- public void mergeConfig(Config newValues, boolean merge) {
+-
+- for (Map.Entry<String, Object> entry : newValues.getData().entrySet()) {
+-
+- String key = entry.getKey();
+- Object nvalue = entry.getValue();
+-
+- String mkey = key.indexOf('.') > -1 ? key.substring(key.indexOf('.') + 1) : key;
+- if (merge && allowedMergeKeys.contains(mkey)) {
+-
+- // merge multi values
+-
+- Object value = _data.get(key);
+-
+- if (value == null) {
+- _data.put(key, nvalue);
+- } else if (value instanceof String) {
+- if (nvalue instanceof String) {
+-
+- // value is String, nvalue is String
+- _data.put(key, new String[] { (String)value, (String)nvalue });
+-
+- } else if (nvalue instanceof String[]) {
+-
+- // value is String, nvalue is String[]
+- String[] nvalues = (String[])nvalue;
+- String[] newvalues = new String[nvalues.length+1];
+- newvalues[0] = (String)value;
+- System.arraycopy(nvalues, 0, newvalues, 1, nvalues.length);
+- _data.put(key, newvalues);
+-
+- }
+- } else if (value instanceof String[]) {
+- if (nvalue instanceof String) {
+-
+- // value is String[], nvalue is String
+- String[] values = (String[])value;
+- String[] newvalues = new String[values.length+1];
+- System.arraycopy(values, 0, newvalues, 0, values.length);
+- newvalues[values.length] = (String)nvalue;
+- _data.put(key, newvalues);
+-
+- } else if (nvalue instanceof String[]) {
+-
+- // value is String[], nvalue is String[]
+- String[] values = (String[])value;
+- String[] nvalues = (String[])nvalue;
+- String[] newvalues = new String[values.length + nvalues.length];
+- System.arraycopy(values, 0, newvalues, 0, values.length);
+- System.arraycopy(nvalues, 0, newvalues, values.length, newvalues.length);
+- _data.put(key, newvalues);
+-
+- }
+- }
+-
+- } else if (allowedReplaceKeys.contains(mkey)){
+-
+- // replace value
+- _data.put(key, nvalue);
+-
+- } else {
+- log.warning("Not merging key '"+key+"' into config");
+- }
+-
+- }
+-
+- }
+-
+- public String toString() {
+- StringBuilder sb = new StringBuilder();
+- for (Map.Entry<String, Object> entry : getData().entrySet()) {
+- String key = entry.getKey();
+- Object val = entry.getValue();
+- sb.append(key);
+- sb.append("=");
+- if (val instanceof String) {
+- sb.append((String)val);
+- } else if (val instanceof String[]) {
+- sb.append(Arrays.toString((String[])val));
+- } else {
+- sb.append("Value not String or String[]");
+- }
+- sb.append("\n");
+- }
+- return sb.toString();
+- }
+-
+- public Map<String, Object> getData() {
+- return _data;
++ return (osname.contains(os)) && (osarch.contains(arch));
+ }
+
+ private final Map<String, Object> _data;
+-
+- public static final List<String> allowedReplaceKeys = Arrays.asList("appbase","apparg","jvmarg"); // these are the ones we might use
+- public static final List<String> allowedMergeKeys = Arrays.asList("apparg","jvmarg"); // these are the ones we might use
+- //private final List<String> allowedMergeKeys = Arrays.asList("apparg","jvmarg","resource","code","java_location"); // (not exhaustive list here)
+ }
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java
+deleted file mode 100644
+index 21b056932..000000000
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java
++++ /dev/null
+@@ -1,73 +0,0 @@
+-//
+-// Getdown - application installer, patcher and launcher
+-// Copyright (C) 2004-2018 Getdown authors
+-// https://github.com/threerings/getdown/blob/master/LICENSE
+-
+-package com.threerings.getdown.util;
+-
+-import java.io.IOException;
+-import java.net.HttpURLConnection;
+-import java.net.Proxy;
+-import java.net.URL;
+-import java.net.URLConnection;
+-import java.net.URLDecoder;
+-
+-import com.threerings.getdown.data.SysProps;
+-
+-import static java.nio.charset.StandardCharsets.UTF_8;
+-
+-public class ConnectionUtil
+-{
+- /**
+- * Opens a connection to a URL, setting the authentication header if user info is present.
+- * @param proxy the proxy via which to perform HTTP connections.
+- * @param url the URL to which to open a connection.
+- * @param connectTimeout if {@code > 0} then a timeout, in seconds, to use when opening the
+- * connection. If {@code 0} is supplied, the connection timeout specified via system properties
+- * will be used instead.
+- * @param readTimeout if {@code > 0} then a timeout, in seconds, to use while reading data from
+- * the connection. If {@code 0} is supplied, the read timeout specified via system properties
+- * will be used instead.
+- */
+- public static URLConnection open (Proxy proxy, URL url, int connectTimeout, int readTimeout)
+- throws IOException
+- {
+- URLConnection conn = url.openConnection(proxy);
+-
+- // configure a connect timeout, if requested
+- int ctimeout = connectTimeout > 0 ? connectTimeout : SysProps.connectTimeout();
+- if (ctimeout > 0) {
+- conn.setConnectTimeout(ctimeout * 1000);
+- }
+-
+- // configure a read timeout, if requested
+- int rtimeout = readTimeout > 0 ? readTimeout : SysProps.readTimeout();
+- if (rtimeout > 0) {
+- conn.setReadTimeout(rtimeout * 1000);
+- }
+-
+- // If URL has a username:password@ before hostname, use HTTP basic auth
+- String userInfo = url.getUserInfo();
+- if (userInfo != null) {
+- // Remove any percent-encoding in the username/password
+- userInfo = URLDecoder.decode(userInfo, "UTF-8");
+- // Now base64 encode the auth info and make it a single line
+- String encoded = Base64.encodeToString(userInfo.getBytes(UTF_8), Base64.DEFAULT).
+- replaceAll("\\n","").replaceAll("\\r", "");
+- conn.setRequestProperty("Authorization", "Basic " + encoded);
+- }
+-
+- return conn;
+- }
+-
+- /**
+- * Opens a connection to a http or https URL, setting the authentication header if user info is
+- * present. Throws a class cast exception if the connection returned is not the right type. See
+- * {@link #open} for parameter documentation.
+- */
+- public static HttpURLConnection openHttp (
+- Proxy proxy, URL url, int connectTimeout, int readTimeout) throws IOException
+- {
+- return (HttpURLConnection)open(proxy, url, connectTimeout, readTimeout);
+- }
+-}
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/FileUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/FileUtil.java
+index 67d033086..d7de78bbd 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/FileUtil.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/FileUtil.java
+@@ -6,25 +6,17 @@
+ package com.threerings.getdown.util;
+
+ import java.io.*;
+-import java.nio.file.Files;
+-import java.nio.file.Paths;
+ import java.util.*;
+ import java.util.jar.*;
+-import java.util.zip.GZIPInputStream;
++import java.util.zip.*;
+
+-import org.apache.commons.compress.archivers.ArchiveEntry;
+-import org.apache.commons.compress.archivers.ArchiveInputStream;
+-import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+-import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+-
+-import com.threerings.getdown.util.StreamUtil;
+ import com.threerings.getdown.Log;
+ import static com.threerings.getdown.Log.log;
+
+ /**
+ * File related utilities.
+ */
+-public class FileUtil
++public final class FileUtil
+ {
+ /**
+ * Gets the specified source file to the specified destination file by hook or crook. Windows
+@@ -122,13 +114,13 @@ public class FileUtil
+ * @param cleanExistingDirs if true, all files in all directories contained in {@code jar} will
+ * be deleted prior to unpacking the jar.
+ */
+- public static void unpackJar (JarFile jar, File target, boolean cleanExistingDirs)
++ public static void unpackJar (ZipFile jar, File target, boolean cleanExistingDirs)
+ throws IOException
+ {
+ if (cleanExistingDirs) {
+- Enumeration<?> entries = jar.entries();
++ Enumeration<? extends ZipEntry> entries = jar.entries();
+ while (entries.hasMoreElements()) {
+- JarEntry entry = (JarEntry)entries.nextElement();
++ ZipEntry entry = entries.nextElement();
+ if (entry.isDirectory()) {
+ File efile = new File(target, entry.getName());
+ if (efile.exists()) {
+@@ -141,9 +133,9 @@ public class FileUtil
+ }
+ }
+
+- Enumeration<?> entries = jar.entries();
++ Enumeration<? extends ZipEntry> entries = jar.entries();
+ while (entries.hasMoreElements()) {
+- JarEntry entry = (JarEntry)entries.nextElement();
++ ZipEntry entry = entries.nextElement();
+ File efile = new File(target, entry.getName());
+
+ // if we're unpacking a normal jar file, it will have special path
+@@ -164,7 +156,7 @@ public class FileUtil
+ }
+
+ try (BufferedOutputStream fout = new BufferedOutputStream(new FileOutputStream(efile));
+- InputStream jin = jar.getInputStream(entry)) {
++ InputStream jin = jar.getInputStream(entry)) {
+ StreamUtil.copy(jin, fout);
+ } catch (Exception e) {
+ throw new IOException(
+@@ -174,82 +166,13 @@ public class FileUtil
+ }
+
+ /**
+- * Unpacks the specified tgz file into the specified target directory.
+- * @param cleanExistingDirs if true, all files in all directories contained in {@code tgz} will
+- * be deleted prior to unpacking the tgz.
+- */
+- public static void unpackTgz (TarArchiveInputStream tgz, File target, boolean cleanExistingDirs)
+- throws IOException
+- {
+- TarArchiveEntry entry;
+- while ((entry = tgz.getNextTarEntry()) != null) {
+- // sanitize the entry name
+- String entryName = entry.getName();
+- if (entryName.startsWith(File.separator))
+- {
+- entryName = entryName.substring(File.separator.length());
+- }
+- File efile = new File(target, entryName);
+-
+- // if we're unpacking a normal tgz file, it will have special path
+- // entries that allow us to create our directories first
+- if (entry.isDirectory()) {
+-
+- if (cleanExistingDirs) {
+- if (efile.exists()) {
+- for (File f : efile.listFiles()) {
+- if (!f.isDirectory())
+- f.delete();
+- }
+- }
+- }
+-
+- if (!efile.exists() && !efile.mkdir()) {
+- log.warning("Failed to create tgz entry path", "tgz", tgz, "entry", entry);
+- }
+- continue;
+- }
+-
+- // but some do not, so we want to ensure that our directories exist
+- // prior to getting down and funky
+- File parent = new File(efile.getParent());
+- if (!parent.exists() && !parent.mkdirs()) {
+- log.warning("Failed to create tgz entry parent", "tgz", tgz, "parent", parent);
+- continue;
+- }
+-
+- if (entry.isLink())
+- {
+- System.out.println("Creating hard link "+efile.getName()+" -> "+entry.getLinkName());
+- Files.createLink(efile.toPath(), Paths.get(entry.getLinkName()));
+- continue;
+- }
+-
+- if (entry.isSymbolicLink())
+- {
+- System.out.println("Creating symbolic link "+efile.getName()+" -> "+entry.getLinkName());
+- Files.createSymbolicLink(efile.toPath(), Paths.get(entry.getLinkName()));
+- continue;
+- }
+-
+- try (BufferedOutputStream fout = new BufferedOutputStream(new FileOutputStream(efile));
+- InputStream tin = tgz;) {
+- StreamUtil.copy(tin, fout);
+- } catch (Exception e) {
+- throw new IOException(
+- Log.format("Failure unpacking", "tgz", tgz, "entry", efile), e);
+- }
+- }
+- }
+-
+- /**
+- * Unpacks a pack200 packed jar file from {@code packedJar} into {@code target}. If {@code
+- * packedJar} has a {@code .gz} extension, it will be gunzipped first.
++ * Unpacks a pack200 packed jar file from {@code packedJar} into {@code target}.
++ * If {@code packedJar} has a {@code .gz} extension, it will be gunzipped first.
+ */
+ public static void unpackPacked200Jar (File packedJar, File target) throws IOException
+ {
+ try (InputStream packJarIn = new FileInputStream(packedJar);
+- JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(target))) {
++ JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(target))) {
+ boolean gz = (packedJar.getName().endsWith(".gz") ||
+ packedJar.getName().endsWith(".gz_new"));
+ try (InputStream packJarIn2 = (gz ? new GZIPInputStream(packJarIn) : packJarIn)) {
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/HostWhitelist.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/HostWhitelist.java
+index f2f7ef39f..c05992abd 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/HostWhitelist.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/HostWhitelist.java
+@@ -25,8 +25,6 @@ public final class HostWhitelist
+ */
+ public static URL verify (URL url) throws MalformedURLException
+ {
+-
+-
+ return verify(Build.hostWhitelist(), url);
+ }
+
+@@ -43,12 +41,6 @@ public final class HostWhitelist
+ }
+
+ String urlHost = url.getHost();
+- String protocol = url.getProtocol();
+-
+- if (ALLOW_LOCATOR_FILE_PROTOCOL && protocol.equals("file") && urlHost.equals("")) {
+- return url;
+- }
+-
+ for (String host : hosts) {
+ String regex = host.replace(".", "\\.").replace("*", ".*");
+ if (urlHost.matches(regex)) {
+@@ -59,5 +51,4 @@ public final class HostWhitelist
+ throw new MalformedURLException(
+ "The host for the specified URL (" + url + ") is not in the host whitelist: " + hosts);
+ }
+- private static boolean ALLOW_LOCATOR_FILE_PROTOCOL = true;
+ }
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java
+index f2cd5736e..cc51794ef 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java
+@@ -17,21 +17,23 @@ import static com.threerings.getdown.Log.log;
+ * Useful routines for launching Java applications from within other Java
+ * applications.
+ */
+-public class LaunchUtil
++public final class LaunchUtil
+ {
+- /** The directory into which a local VM installation should be unpacked. */
+- public static final String LOCAL_JAVA_DIR = "jre";
++ /** The default directory into which a local VM installation should be unpacked. */
++ public static final String LOCAL_JAVA_DIR = "java_vm";
+
+ /**
+- * Writes a <code>version.txt</code> file into the specified application directory and
++ * Writes a {@code version.txt} file into the specified application directory and
+ * attempts to relaunch Getdown in that directory which will cause it to upgrade to the newly
+ * specified version and relaunch the application.
+ *
+ * @param appdir the directory in which the application is installed.
+ * @param getdownJarName the name of the getdown jar file in the application directory. This is
+- * probably <code>getdown-pro.jar</code> or <code>getdown-retro-pro.jar</code> if you are using
++ * probably {@code getdown-pro.jar} or {@code getdown-retro-pro.jar} if you are using
+ * the results of the standard build.
+ * @param newVersion the new version to which Getdown will update when it is executed.
++ * @param javaLocalDir the name of the directory (inside {@code appdir}) that contains a
++ * locally installed JRE. Defaults to {@link #LOCAL_JAVA_DIR} if null is passed.
+ *
+ * @return true if the relaunch succeeded, false if we were unable to relaunch due to being on
+ * Windows 9x where we cannot launch subprocesses without waiting around for them to exit,
+@@ -39,13 +41,13 @@ public class LaunchUtil
+ * after making this call as it will be upgraded and restarted. If false is returned, the
+ * application should tell the user that they must restart the application manually.
+ *
+- * @exception IOException thrown if we were unable to create the <code>version.txt</code> file
++ * @exception IOException thrown if we were unable to create the {@code version.txt} file
+ * in the supplied application directory. If the version.txt file cannot be created, restarting
+ * Getdown will not cause the application to be upgraded, so the application will have to
+ * resort to telling the user that it is in a bad way.
+ */
+ public static boolean updateVersionAndRelaunch (
+- File appdir, String getdownJarName, String newVersion)
++ File appdir, String getdownJarName, String newVersion, String javaLocalDir)
+ throws IOException
+ {
+ // create the file that instructs Getdown to upgrade
+@@ -61,9 +63,9 @@ public class LaunchUtil
+ }
+
+ // do the deed
+- String[] args = new String[] {
+- getJVMPath(appdir), "-jar", pro.toString(), appdir.getPath()
+- };
++ String javaDir = StringUtil.isBlank(javaLocalDir) ? LOCAL_JAVA_DIR : javaLocalDir;
++ String javaBin = getJVMBinaryPath(new File(appdir, javaDir), false);
++ String[] args = { javaBin, "-jar", pro.toString(), appdir.getPath() };
+ log.info("Running " + StringUtil.join(args, "\n "));
+ try {
+ Runtime.getRuntime().exec(args, null);
+@@ -75,25 +77,15 @@ public class LaunchUtil
+ }
+
+ /**
+- * Reconstructs the path to the JVM used to launch this process.
+- */
+- public static String getJVMPath (File appdir)
+- {
+- return getJVMPath(appdir, false);
+- }
+-
+- /**
+- * Reconstructs the path to the JVM used to launch this process.
+- *
++ * Resolves a path to a JVM binary.
++ * @param javaLocalDir JRE location within appdir.
+ * @param windebug if true we will use java.exe instead of javaw.exe on Windows.
++ * @return the path to the JVM binary used to launch this process.
+ */
+- public static String getJVMPath (File appdir, boolean windebug)
++ public static String getJVMBinaryPath (File javaLocalDir, boolean windebug)
+ {
+ // first look in our application directory for an installed VM
+- String vmpath = checkJVMPath(new File(appdir, LOCAL_JAVA_DIR).getAbsolutePath(), windebug);
+- if (vmpath == null && isMacOS()) {
+- vmpath = checkJVMPath(new File(appdir, LOCAL_JAVA_DIR + "/Contents/Home").getAbsolutePath(), windebug);
+- }
++ String vmpath = checkJVMPath(javaLocalDir.getAbsolutePath(), windebug);
+
+ // then fall back to the VM in which we're already running
+ if (vmpath == null) {
+@@ -102,7 +94,7 @@ public class LaunchUtil
+
+ // then throw up our hands and hope for the best
+ if (vmpath == null) {
+- log.warning("Unable to find java [appdir=" + appdir +
++ log.warning("Unable to find java [local=" + javaLocalDir +
+ ", java.home=" + System.getProperty("java.home") + "]!");
+ vmpath = "java";
+ }
+@@ -153,7 +145,7 @@ public class LaunchUtil
+ if (newgd.renameTo(curgd)) {
+ FileUtil.deleteHarder(oldgd); // yay!
+ try {
+- // copy the moved file back to getdown-dop-new.jar so that we don't end up
++ // copy the moved file back to newgd so that we don't end up
+ // downloading another copy next time
+ FileUtil.copy(curgd, newgd);
+ } catch (IOException e) {
+@@ -185,23 +177,23 @@ public class LaunchUtil
+ public static boolean mustMonitorChildren ()
+ {
+ String osname = System.getProperty("os.name", "").toLowerCase(Locale.ROOT);
+- return (osname.indexOf("windows 98") != -1 || osname.indexOf("windows me") != -1);
++ return (osname.contains("windows 98") || osname.contains("windows me"));
+ }
+
+ /**
+ * Returns true if we're running in a JVM that identifies its operating system as Windows.
+ */
+- public static final boolean isWindows () { return _isWindows; }
++ public static boolean isWindows () { return _isWindows; }
+
+ /**
+ * Returns true if we're running in a JVM that identifies its operating system as MacOS.
+ */
+- public static final boolean isMacOS () { return _isMacOS; }
++ public static boolean isMacOS () { return _isMacOS; }
+
+ /**
+ * Returns true if we're running in a JVM that identifies its operating system as Linux.
+ */
+- public static final boolean isLinux () { return _isLinux; }
++ public static boolean isLinux () { return _isLinux; }
+
+ /**
+ * Checks whether a Java Virtual Machine can be located in the supplied path.
+@@ -240,10 +232,9 @@ public class LaunchUtil
+ try {
+ String osname = System.getProperty("os.name");
+ osname = (osname == null) ? "" : osname;
+- _isWindows = (osname.indexOf("Windows") != -1);
+- _isMacOS = (osname.indexOf("Mac OS") != -1 ||
+- osname.indexOf("MacOS") != -1);
+- _isLinux = (osname.indexOf("Linux") != -1);
++ _isWindows = (osname.contains("Windows"));
++ _isMacOS = (osname.contains("Mac OS") || osname.contains("MacOS"));
++ _isLinux = (osname.contains("Linux"));
+ } catch (Exception e) {
+ // can't grab system properties; we'll just pretend we're not on any of these OSes
+ }
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/MessageUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/MessageUtil.java
+index 28dbdcff5..f2ee9a265 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/MessageUtil.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/MessageUtil.java
+@@ -5,7 +5,7 @@
+
+ package com.threerings.getdown.util;
+
+-public class MessageUtil {
++public final class MessageUtil {
+
+ /**
+ * Returns whether or not the provided string is tainted. See {@link #taint}. Null strings
+@@ -101,11 +101,11 @@ public class MessageUtil {
+ }
+
+ /**
+- * Used to escape single quotes so that they are not interpreted by <code>MessageFormat</code>.
++ * Used to escape single quotes so that they are not interpreted by {@code MessageFormat}.
+ * As we assume all single quotes are to be escaped, we cannot use the characters
+ * <code>{</code> and <code>}</code> in our translation strings, but this is a small price to
+ * pay to have to differentiate between messages that will and won't eventually be parsed by a
+- * <code>MessageFormat</code> instance.
++ * {@code MessageFormat} instance.
+ */
+ public static String escape (String message)
+ {
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java
+index ad4c560a4..ac3e0a3de 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java
+@@ -14,5 +14,5 @@ public interface ProgressObserver
+ * Informs the observer that we have completed the specified
+ * percentage of the process.
+ */
+- public void progress (int percent);
++ void progress (int percent);
+ }
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StreamUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StreamUtil.java
+index 373cfff00..5cb1a405b 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StreamUtil.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StreamUtil.java
+@@ -11,11 +11,10 @@ import java.io.InputStream;
+ import java.io.OutputStream;
+ import java.io.Reader;
+ import java.io.Writer;
+-import java.nio.charset.Charset;
+
+ import static com.threerings.getdown.Log.log;
+
+-public class StreamUtil {
++public final class StreamUtil {
+ /**
+ * Convenient close for a stream. Use in a finally clause and love life.
+ */
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StringUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StringUtil.java
+index 03d3c9ccd..86277c881 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StringUtil.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StringUtil.java
+@@ -7,14 +7,14 @@ package com.threerings.getdown.util;
+
+ import java.util.StringTokenizer;
+
+-public class StringUtil {
++public final class StringUtil {
+
+ /**
+ * @return true if the specified string could be a valid URL (contains no illegal characters)
+ */
+ public static boolean couldBeValidUrl (String url)
+ {
+- return url.matches("[A-Za-z0-9\\-\\._~:/\\?#\\[\\]@!$&'\\(\\)\\*\\+,;=%]+");
++ return url.matches("[A-Za-z0-9\\-._~:/?#\\[\\]@!$&'()*+,;=%]+");
+ }
+
+ /**
+@@ -84,7 +84,7 @@ public class StringUtil {
+ source = source.replace(",,", "%COMMA%");
+
+ // count up the number of tokens
+- while ((tpos = source.indexOf(",", tpos+1)) != -1) {
++ while ((tpos = source.indexOf(',', tpos+1)) != -1) {
+ tcount++;
+ }
+
+@@ -92,7 +92,7 @@ public class StringUtil {
+ tpos = -1; tcount = 0;
+
+ // do the split
+- while ((tpos = source.indexOf(",", tpos+1)) != -1) {
++ while ((tpos = source.indexOf(',', tpos+1)) != -1) {
+ tokens[tcount] = source.substring(tstart, tpos);
+ tokens[tcount] = tokens[tcount].trim().replace("%COMMA%", ",");
+ if (intern) {
+@@ -119,7 +119,7 @@ public class StringUtil {
+
+ /**
+ * Generates a string from the supplied bytes that is the HEX encoded representation of those
+- * bytes. Returns the empty string for a <code>null</code> or empty byte array.
++ * bytes. Returns the empty string for a {@code null} or empty byte array.
+ *
+ * @param bytes the bytes for which we want a string representation.
+ * @param count the number of bytes to stop at (which will be coerced into being {@code <=} the
+@@ -185,7 +185,7 @@ public class StringUtil {
+ }
+
+ /**
+- * Helper function for the various <code>join</code> methods.
++ * Helper function for the various {@code join} methods.
+ */
+ protected static String join (Object[] values, String separator, boolean escape)
+ {
+@@ -201,6 +201,6 @@ public class StringUtil {
+ return buf.toString();
+ }
+
+- /** Used by {@link #hexlate} and {@link #unhexlate}. */
++ /** Used by {@link #hexlate}. */
+ protected static final String XLATE = "0123456789abcdef";
+ }
+diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/VersionUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/VersionUtil.java
+index 49e4e6e0e..b2f289415 100644
+--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/VersionUtil.java
++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/VersionUtil.java
+@@ -22,7 +22,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
+ /**
+ * Version related utilities.
+ */
+-public class VersionUtil
++public final class VersionUtil
+ {
+ /**
+ * Reads a version number from a file.
+diff --git a/getdown/src/getdown/core/src/main/java/jalview/bin/MemorySetting.java b/getdown/src/getdown/core/src/main/java/jalview/bin/MemorySetting.java
+deleted file mode 100644
+index 8af09da6d..000000000
+--- a/getdown/src/getdown/core/src/main/java/jalview/bin/MemorySetting.java
++++ /dev/null
+@@ -1,51 +0,0 @@
+-package jalview.bin;
+-
+-import java.lang.management.ManagementFactory;
+-import java.lang.management.OperatingSystemMXBean;
+-
+-public class MemorySetting
+-{
+- public static final long leaveFreeMinMemory = 536870912; // 0.5 GB
+-
+- public static final long applicationMinMemory = 536870912; // 0.5 GB
+-
+- protected static long getPhysicalMemory()
+- {
+- final OperatingSystemMXBean o = ManagementFactory
+- .getOperatingSystemMXBean();
+-
+- try
+- {
+- if (o instanceof com.sun.management.OperatingSystemMXBean)
+- {
+- final com.sun.management.OperatingSystemMXBean osb = (com.sun.management.OperatingSystemMXBean) o;
+- return osb.getTotalPhysicalMemorySize();
+- }
+- } catch (NoClassDefFoundError e)
+- {
+- // com.sun.management.OperatingSystemMXBean doesn't exist in this JVM
+- System.err.println("No com.sun.management.OperatingSystemMXBean");
+- }
+-
+- // We didn't get a com.sun.management.OperatingSystemMXBean.
+- return -1;
+- }
+-
+- public static long memPercent(int percent)
+- {
+- long memPercent = -1;
+-
+- long physicalMem = getPhysicalMemory();
+- if (physicalMem > applicationMinMemory)
+- {
+- // try and set at least applicationMinMemory and thereafter ensure
+- // leaveFreeMinMemory is left for the OS
+- memPercent = Math.max(applicationMinMemory,
+- physicalMem - Math.max(physicalMem * (100 - percent) / 100,
+- leaveFreeMinMemory));
+- }
+-
+- return memPercent;
+- }
+-
+-}
+diff --git a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/GarbageCollectorTest.java b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/GarbageCollectorTest.java
+index d5a393745..c7fcf7271 100644
+--- a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/GarbageCollectorTest.java
++++ b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/GarbageCollectorTest.java
+@@ -7,23 +7,36 @@ package com.threerings.getdown.cache;
+
+ import java.io.File;
+ import java.io.IOException;
++import java.util.Arrays;
++import java.util.Collection;
+ import java.util.concurrent.TimeUnit;
+
+ import org.junit.*;
+ import org.junit.rules.TemporaryFolder;
+-
++import org.junit.runner.RunWith;
++import org.junit.runners.Parameterized;
+ import static org.junit.Assert.*;
+ import static org.junit.Assume.assumeTrue;
+
+ /**
+ * Validates that cache garbage is collected and deleted correctly.
+ */
++@RunWith(Parameterized.class)
+ public class GarbageCollectorTest
+ {
++ @Parameterized.Parameters(name = "{0}")
++ public static Collection<Object[]> data() {
++ return Arrays.asList(new Object[][] {{ ".jar" }, { ".zip" }});
++ }
++
++ @Parameterized.Parameter
++ public String extension;
++
+ @Before public void setupFiles () throws IOException
+ {
+- _cachedFile = _folder.newFile("abc123.jar");
+- _lastAccessedFile = _folder.newFile("abc123.jar" + ResourceCache.LAST_ACCESSED_FILE_SUFFIX);
++ _cachedFile = _folder.newFile("abc123" + extension);
++ _lastAccessedFile = _folder.newFile(
++ "abc123" + extension + ResourceCache.LAST_ACCESSED_FILE_SUFFIX);
+ }
+
+ @Test public void shouldDeleteCacheEntryIfRetentionPeriodIsReached ()
+diff --git a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/ResourceCacheTest.java b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/ResourceCacheTest.java
+index 860c72a37..6acffda32 100644
+--- a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/ResourceCacheTest.java
++++ b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/ResourceCacheTest.java
+@@ -7,26 +7,38 @@ package com.threerings.getdown.cache;
+
+ import java.io.File;
+ import java.io.IOException;
++import java.util.Arrays;
++import java.util.Collection;
+ import java.util.concurrent.TimeUnit;
+
+ import org.junit.*;
+ import org.junit.rules.TemporaryFolder;
+-
++import org.junit.runner.RunWith;
++import org.junit.runners.Parameterized;
+ import static org.junit.Assert.*;
+
+ /**
+ * Asserts the correct functionality of the {@link ResourceCache}.
+ */
++@RunWith(Parameterized.class)
+ public class ResourceCacheTest
+ {
++ @Parameterized.Parameters(name = "{0}")
++ public static Collection<Object[]> data() {
++ return Arrays.asList(new Object[][] {{ ".jar" }, { ".zip" }});
++ }
++
++ @Parameterized.Parameter
++ public String extension;
++
+ @Before public void setupCache () throws IOException {
+- _fileToCache = _folder.newFile("filetocache.jar");
++ _fileToCache = _folder.newFile("filetocache" + extension);
+ _cache = new ResourceCache(_folder.newFolder(".cache"));
+ }
+
+ @Test public void shouldCacheFile () throws IOException
+ {
+- assertEquals("abc123.jar", cacheFile().getName());
++ assertEquals("abc123" + extension, cacheFile().getName());
+ }
+
+ private File cacheFile() throws IOException
+@@ -36,7 +48,7 @@ public class ResourceCacheTest
+
+ @Test public void shouldTrackFileUsage () throws IOException
+ {
+- String name = "abc123.jar" + ResourceCache.LAST_ACCESSED_FILE_SUFFIX;
++ String name = "abc123" + extension + ResourceCache.LAST_ACCESSED_FILE_SUFFIX;
+ File lastAccessedFile = new File(cacheFile().getParentFile(), name);
+ assertTrue(lastAccessedFile.exists());
+ }
+diff --git a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/EnvConfigTest.java b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/EnvConfigTest.java
+index 61786518c..04a73d38b 100644
+--- a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/EnvConfigTest.java
++++ b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/EnvConfigTest.java
+@@ -19,13 +19,13 @@ public class EnvConfigTest {
+ static String TESTID = "testid";
+ static String TESTBASE = "https://test.com/test";
+
+- private void debugNotes(List<EnvConfig.Note> notes) {
++ private void debugNotes (List<EnvConfig.Note> notes) {
+ for (EnvConfig.Note note : notes) {
+ System.out.println(note.message);
+ }
+ }
+
+- private void checkNoNotes (List<EnvConfig.Note> notes) {
++ static void checkNoNotes (List<EnvConfig.Note> notes) {
+ StringBuilder msg = new StringBuilder();
+ for (EnvConfig.Note note : notes) {
+ if (note.level != EnvConfig.Note.Level.INFO) {
+diff --git a/getdown/src/getdown/launcher/.project-MOVED b/getdown/src/getdown/launcher/.project-MOVED
+deleted file mode 100644
+index d77a6e8db..000000000
+--- a/getdown/src/getdown/launcher/.project-MOVED
++++ /dev/null
+@@ -1,23 +0,0 @@
+-<?xml version="1.0" encoding="UTF-8"?>
+-<projectDescription>
+- <name>getdown-launcher</name>
+- <comment></comment>
+- <projects>
+- </projects>
+- <buildSpec>
+- <buildCommand>
+- <name>org.eclipse.jdt.core.javabuilder</name>
+- <arguments>
+- </arguments>
+- </buildCommand>
+- <buildCommand>
+- <name>org.eclipse.m2e.core.maven2Builder</name>
+- <arguments>
+- </arguments>
+- </buildCommand>
+- </buildSpec>
+- <natures>
+- <nature>org.eclipse.jdt.core.javanature</nature>
+- <nature>org.eclipse.m2e.core.maven2Nature</nature>
+- </natures>
+-</projectDescription>
+diff --git a/getdown/src/getdown/launcher/.settings/org.eclipse.core.resources.prefs b/getdown/src/getdown/launcher/.settings/org.eclipse.core.resources.prefs
+deleted file mode 100644
+index abdea9ac0..000000000
+--- a/getdown/src/getdown/launcher/.settings/org.eclipse.core.resources.prefs
++++ /dev/null
+@@ -1,4 +0,0 @@
+-eclipse.preferences.version=1
+-encoding//src/main/java=UTF-8
+-encoding//src/main/resources=UTF-8
+-encoding/<project>=UTF-8
+diff --git a/getdown/src/getdown/launcher/.settings/org.eclipse.jdt.core.prefs b/getdown/src/getdown/launcher/.settings/org.eclipse.jdt.core.prefs
+deleted file mode 100644
+index 54e56721d..000000000
+--- a/getdown/src/getdown/launcher/.settings/org.eclipse.jdt.core.prefs
++++ /dev/null
+@@ -1,6 +0,0 @@
+-eclipse.preferences.version=1
+-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
+-org.eclipse.jdt.core.compiler.compliance=1.7
+-org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+-org.eclipse.jdt.core.compiler.release=disabled
+-org.eclipse.jdt.core.compiler.source=1.7
+diff --git a/getdown/src/getdown/launcher/.settings/org.eclipse.m2e.core.prefs b/getdown/src/getdown/launcher/.settings/org.eclipse.m2e.core.prefs
+deleted file mode 100644
+index f897a7f1c..000000000
+--- a/getdown/src/getdown/launcher/.settings/org.eclipse.m2e.core.prefs
++++ /dev/null
+@@ -1,4 +0,0 @@
+-activeProfiles=
+-eclipse.preferences.version=1
+-resolveWorkspaceProjects=true
+-version=1
+diff --git a/getdown/src/getdown/launcher/pom.xml b/getdown/src/getdown/launcher/pom.xml
+index e77061a3b..f55234977 100644
+--- a/getdown/src/getdown/launcher/pom.xml
++++ b/getdown/src/getdown/launcher/pom.xml
+@@ -4,7 +4,7 @@
+ <parent>
+ <groupId>com.threerings.getdown</groupId>
+ <artifactId>getdown</artifactId>
+- <version>1.8.3-SNAPSHOT</version>
++ <version>1.8.7-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>getdown-launcher</artifactId>
+@@ -24,11 +24,13 @@
+ <groupId>com.threerings.getdown</groupId>
+ <artifactId>getdown-core</artifactId>
+ <version>${project.version}</version>
++ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>com.samskivert</groupId>
+ <artifactId>samskivert</artifactId>
+ <version>1.2</version>
++ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>jregistrykey</groupId>
+@@ -36,22 +38,47 @@
+ <version>1.0</version>
+ <optional>true</optional>
+ </dependency>
++ <dependency>
++ <groupId>junit</groupId>
++ <artifactId>junit</artifactId>
++ <version>4.12</version>
++ <scope>test</scope>
++ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+- <!--
++ <plugin>
++ <groupId>org.codehaus.mojo</groupId>
++ <artifactId>native2ascii-maven-plugin</artifactId>
++ <version>2.0.1</version>
++ <executions>
++ <execution>
++ <id>utf8-to-latin1</id>
++ <goals>
++ <goal>inplace</goal>
++ </goals>
++ <phase>process-resources</phase>
++ <configuration>
++ <dir>${project.build.outputDirectory}</dir>
++ <encoding>${project.build.sourceEncoding}</encoding>
++ <includes>
++ <include>**/*.properties</include>
++ </includes>
++ </configuration>
++ </execution>
++ </executions>
++ </plugin>
++
+ <plugin>
+ <groupId>com.github.wvengen</groupId>
+ <artifactId>proguard-maven-plugin</artifactId>
+ <version>2.0.14</version>
+ <executions>
+- <execution>
+- <phase>package</phase>
+- <goals>
+- <goal>proguard</goal>
+- </goals>
+- </execution>
++ <execution>
++ <phase>package</phase>
++ <goals><goal>proguard</goal></goals>
++ </execution>
+ </executions>
+ <dependencies>
+ <dependency>
+@@ -109,7 +136,7 @@
+ <addMavenDescriptor>false</addMavenDescriptor>
+ </configuration>
+ </plugin>
+- -->
++
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+@@ -130,40 +157,6 @@
+ </archive>
+ </configuration>
+ </plugin>
+-
+- <plugin>
+- <groupId>org.apache.maven.plugins</groupId>
+- <artifactId>maven-shade-plugin</artifactId>
+- <version>3.2.1</version>
+- <configuration>
+- <!-- put your configurations here -->
+- </configuration>
+- <executions>
+- <execution>
+- <phase>package</phase>
+- <goals>
+- <goal>shade</goal>
+- </goals>
+- <!--
+- <configuration>
+- <minimizeJar>true</minimizeJar>
+- <filters>
+- <filter>
+- <artifact>install4j-runtime</artifact>
+- <includes>
+- <include>**</include>
+- </includes>
+- </filter>
+- </filters>
+- </configuration>
+- -->
+- </execution>
+- </executions>
+- </plugin>
+-
+-
+-
+-
+ </plugins>
+ </build>
+
+@@ -202,6 +195,7 @@
+ <lib>${java.home}/jmods/java.base.jmod</lib>
+ <lib>${java.home}/jmods/java.desktop.jmod</lib>
+ <lib>${java.home}/jmods/java.logging.jmod</lib>
++ <lib>${java.home}/jmods/java.scripting.jmod</lib>
+ <lib>${java.home}/jmods/jdk.jsobject.jmod</lib>
+ </libs>
+ </configuration>
+diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java
+index dc1e54e46..4bd9f90b0 100644
+--- a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java
++++ b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java
+@@ -72,7 +72,7 @@ public final class AbortPanel extends JFrame
+ public void actionPerformed (ActionEvent e)
+ {
+ String cmd = e.getActionCommand();
+- if (cmd.equals("ok")) {
++ if ("ok".equals(cmd)) {
+ System.exit(0);
+ } else {
+ setVisible(false);
+diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java
+index 5750fce66..1cc6922c8 100644
+--- a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java
++++ b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java
+@@ -9,51 +9,73 @@ import java.awt.BorderLayout;
+ import java.awt.Container;
+ import java.awt.Dimension;
+ import java.awt.EventQueue;
+-import java.awt.Graphics;
+ import java.awt.GraphicsEnvironment;
+ import java.awt.Image;
+ import java.awt.event.ActionEvent;
+ import java.awt.image.BufferedImage;
+ import java.io.BufferedReader;
+ import java.io.File;
+-import java.io.FileInputStream;
+ import java.io.FileNotFoundException;
+-import java.io.FileReader;
+ import java.io.IOException;
+ import java.io.InputStream;
+ import java.io.InputStreamReader;
+ import java.io.PrintStream;
+ import java.net.HttpURLConnection;
+-import java.net.MalformedURLException;
+ import java.net.URL;
+-import java.util.*;
++import java.util.ArrayList;
++import java.util.Arrays;
++import java.util.Collection;
++import java.util.HashSet;
++import java.util.List;
++import java.util.Locale;
++import java.util.ResourceBundle;
++import java.util.Set;
++import java.util.concurrent.TimeUnit;
+
+ import javax.imageio.ImageIO;
+ import javax.swing.AbstractAction;
+ import javax.swing.JButton;
+ import javax.swing.JFrame;
+ import javax.swing.JLayeredPane;
+-import javax.swing.JPanel;
+
+ import com.samskivert.swing.util.SwingUtil;
+-import com.threerings.getdown.data.*;
++import com.threerings.getdown.data.Application;
+ import com.threerings.getdown.data.Application.UpdateInterface.Step;
++import com.threerings.getdown.data.Build;
++import com.threerings.getdown.data.EnvConfig;
++import com.threerings.getdown.data.Resource;
++import com.threerings.getdown.data.SysProps;
+ import com.threerings.getdown.net.Downloader;
+-import com.threerings.getdown.net.HTTPDownloader;
+ import com.threerings.getdown.tools.Patcher;
+-import com.threerings.getdown.util.*;
+-
++import com.threerings.getdown.util.Config;
++import com.threerings.getdown.util.FileUtil;
++import com.threerings.getdown.util.LaunchUtil;
++import com.threerings.getdown.util.MessageUtil;
++import com.threerings.getdown.util.ProgressAggregator;
++import com.threerings.getdown.util.ProgressObserver;
++import com.threerings.getdown.util.StringUtil;
++import com.threerings.getdown.util.VersionUtil;
+ import static com.threerings.getdown.Log.log;
+
+ /**
+ * Manages the main control for the Getdown application updater and deployment system.
+ */
+-public abstract class Getdown extends Thread
++public abstract class Getdown
+ implements Application.StatusDisplay, RotatingBackgrounds.ImageLoader
+ {
++ /**
++ * Starts a thread to run Getdown and ultimately (hopefully) launch the target app.
++ */
++ public static void run (final Getdown getdown) {
++ new Thread("Getdown") {
++ @Override public void run () {
++ getdown.run();
++ }
++ }.start();
++ }
++
+ public Getdown (EnvConfig envc)
+ {
+- super("Getdown");
+ try {
+ // If the silent property exists, install without bringing up any gui. If it equals
+ // launch, start the application after installing. Otherwise, just install and exit.
+@@ -80,7 +102,7 @@ public abstract class Getdown extends Thread
+ // welcome to hell, where java can't cope with a classpath that contains jars that live
+ // in a directory that contains a !, at least the same bug happens on all platforms
+ String dir = envc.appDir.toString();
+- if (dir.equals(".")) {
++ if (".".equals(dir)) {
+ dir = System.getProperty("user.dir");
+ }
+ String errmsg = "The directory in which this application is installed:\n" + dir +
+@@ -107,7 +129,7 @@ public abstract class Getdown extends Thread
+ {
+ if (SysProps.noInstall()) {
+ log.info("Skipping install due to 'no_install' sysprop.");
+- } else if (_readyToInstall) {
++ } else if (isUpdateAvailable()) {
+ log.info("Installing " + _toInstallResources.size() + " downloaded resources:");
+ for (Resource resource : _toInstallResources) {
+ resource.install(true);
+@@ -120,8 +142,28 @@ public abstract class Getdown extends Thread
+ }
+ }
+
+- @Override
+- public void run ()
++ /**
++ * Configures our proxy settings (called by {@link ProxyPanel}) and fires up the launcher.
++ */
++ public void configProxy (String host, String port, String username, String password)
++ {
++ log.info("User configured proxy", "host", host, "port", port);
++ ProxyUtil.configProxy(_app, host, port, username, password);
++
++ // clear out our UI
++ disposeContainer();
++ _container = null;
++
++ // fire up a new thread
++ run(this);
++ }
++
++ /**
++ * The main entry point of Getdown: does some sanity checks and preparation, then delegates the
++ * actual getting down to {@link #getdown}. This is not called directly, but rather via the
++ * static {@code run} method as Getdown does its main work on a separate thread.
++ */
++ protected void run ()
+ {
+ // if we have no messages, just bail because we're hosed; the error message will be
+ // displayed to the user already
+@@ -135,95 +177,48 @@ public abstract class Getdown extends Thread
+ File instdir = _app.getLocalPath("");
+ if (!instdir.canWrite()) {
+ String path = instdir.getPath();
+- if (path.equals(".")) {
++ if (".".equals(path)) {
+ path = System.getProperty("user.dir");
+ }
+ fail(MessageUtil.tcompose("m.readonly_error", path));
+ return;
+ }
+
+- try {
+- _dead = false;
+- // if we fail to detect a proxy, but we're allowed to run offline, then go ahead and
+- // run the app anyway because we're prepared to cope with not being able to update
+- if (detectProxy() || _app.allowOffline()) {
+- getdown();
+- } else if (_silent) {
+- log.warning("Need a proxy, but we don't want to bother anyone. Exiting.");
+- } else {
+- // create a panel they can use to configure the proxy settings
+- _container = createContainer();
+- // allow them to close the window to abort the proxy configuration
+- _dead = true;
+- configureContainer();
+- ProxyPanel panel = new ProxyPanel(this, _msgs);
+- // set up any existing configured proxy
+- String[] hostPort = ProxyUtil.loadProxy(_app);
+- panel.setProxy(hostPort[0], hostPort[1]);
+- _container.add(panel, BorderLayout.CENTER);
+- showContainer();
+- }
+-
+- } catch (Exception e) {
+- log.warning("run() failed.", e);
+- String msg = e.getMessage();
+- if (msg == null) {
+- msg = MessageUtil.compose("m.unknown_error", _ifc.installError);
+- } else if (!msg.startsWith("m.")) {
+- // try to do something sensible based on the type of error
+- if (e instanceof FileNotFoundException) {
+- msg = MessageUtil.compose(
+- "m.missing_resource", MessageUtil.taint(msg), _ifc.installError);
+- } else {
+- msg = MessageUtil.compose(
+- "m.init_error", MessageUtil.taint(msg), _ifc.installError);
+- }
+- }
+- fail(msg);
+- }
+- }
+-
+- /**
+- * Configures our proxy settings (called by {@link ProxyPanel}) and fires up the launcher.
+- */
+- public void configProxy (String host, String port, String username, String password)
+- {
+- log.info("User configured proxy", "host", host, "port", port);
+-
+- if (!StringUtil.isBlank(host)) {
+- ProxyUtil.configProxy(_app, host, port, username, password);
+- }
+-
+- // clear out our UI
+- disposeContainer();
+- _container = null;
+-
+- // fire up a new thread
+- new Thread(this).start();
++ _dead = false;
++ // if we fail to detect a proxy, but we're allowed to run offline, then go ahead and
++ // run the app anyway because we're prepared to cope with not being able to update
++ if (detectProxy() || _app.allowOffline()) getdown();
++ else requestProxyInfo(false);
+ }
+
+ protected boolean detectProxy () {
+- if (ProxyUtil.autoDetectProxy(_app)) {
+- return true;
+- }
+-
+- // otherwise see if we actually need a proxy; first we have to initialize our application
+- // to get some sort of interface configuration and the appbase URL
++ // first we have to initialize our application to get the appbase URL, etc.
+ log.info("Checking whether we need to use a proxy...");
+ try {
+ readConfig(true);
+ } catch (IOException ioe) {
+ // no worries
+ }
++
++ boolean tryNoProxy = SysProps.tryNoProxyFirst();
++ if (!tryNoProxy && ProxyUtil.autoDetectProxy(_app)) {
++ return true;
++ }
++
++ // see if we actually need a proxy
+ updateStatus("m.detecting_proxy");
+- if (!ProxyUtil.canLoadWithoutProxy(_app.getConfigResource().getRemote())) {
+- return false;
++ URL configURL = _app.getConfigResource().getRemote();
++ if (!ProxyUtil.canLoadWithoutProxy(configURL, tryNoProxy ? 2 : 5)) {
++ // if we didn't auto-detect proxy first thing, do auto-detect now
++ return tryNoProxy ? ProxyUtil.autoDetectProxy(_app) : false;
+ }
+
+- // we got through, so we appear not to require a proxy; make a blank proxy config so that
+- // we don't go through this whole detection process again next time
+ log.info("No proxy appears to be needed.");
+- ProxyUtil.saveProxy(_app, null, null);
++ if (!tryNoProxy) {
++ // we got through, so we appear not to require a proxy; make a blank proxy config so
++ // that we don't go through this whole detection process again next time
++ ProxyUtil.saveProxy(_app, null, null);
++ }
+ return true;
+ }
+
+@@ -233,6 +228,25 @@ public abstract class Getdown extends Thread
+ _ifc = new Application.UpdateInterface(config);
+ }
+
++ protected void requestProxyInfo (boolean reinitAuth) {
++ if (_silent) {
++ log.warning("Need a proxy, but we don't want to bother anyone. Exiting.");
++ return;
++ }
++
++ // create a panel they can use to configure the proxy settings
++ _container = createContainer();
++ // allow them to close the window to abort the proxy configuration
++ _dead = true;
++ configureContainer();
++ ProxyPanel panel = new ProxyPanel(this, _msgs, reinitAuth);
++ // set up any existing configured proxy
++ String[] hostPort = ProxyUtil.loadProxy(_app);
++ panel.setProxy(hostPort[0], hostPort[1]);
++ _container.add(panel, BorderLayout.CENTER);
++ showContainer();
++ }
++
+ /**
+ * Downloads and installs (without verifying) any resources that are marked with a
+ * {@code PRELOAD} attribute.
+@@ -247,13 +261,15 @@ public abstract class Getdown extends Thread
+ }
+ }
+
+- try {
+- download(predownloads);
+- for (Resource rsrc : predownloads) {
+- rsrc.install(false); // install but don't validate yet
++ if (!predownloads.isEmpty()) {
++ try {
++ download(predownloads);
++ for (Resource rsrc : predownloads) {
++ rsrc.install(false); // install but don't validate yet
++ }
++ } catch (IOException ioe) {
++ log.warning("Failed to predownload resources. Continuing...", ioe);
+ }
+- } catch (IOException ioe) {
+- log.warning("Failed to predownload resources. Continuing...", ioe);
+ }
+ }
+
+@@ -263,7 +279,7 @@ public abstract class Getdown extends Thread
+ protected void getdown ()
+ {
+ try {
+- // first parses our application deployment file
++ // first parse our application deployment file
+ try {
+ readConfig(true);
+ } catch (IOException ioe) {
+@@ -278,7 +294,7 @@ public abstract class Getdown extends Thread
+ throw new MultipleGetdownRunning();
+ }
+
+- // Update the config modtime so a sleeping getdown will notice the change.
++ // update the config modtime so a sleeping getdown will notice the change
+ File config = _app.getLocalPath(Application.CONFIG_FILE);
+ if (!config.setLastModified(System.currentTimeMillis())) {
+ log.warning("Unable to set modtime on config file, will be unable to check for " +
+@@ -290,7 +306,7 @@ public abstract class Getdown extends Thread
+ // Store the config modtime before waiting the delay amount of time
+ long lastConfigModtime = config.lastModified();
+ log.info("Waiting " + _delay + " minutes before beginning actual work.");
+- Thread.sleep(_delay * 60 * 1000);
++ TimeUnit.MINUTES.sleep(_delay);
+ if (lastConfigModtime < config.lastModified()) {
+ log.warning("getdown.txt was modified while getdown was waiting.");
+ throw new MultipleGetdownRunning();
+@@ -339,11 +355,7 @@ public abstract class Getdown extends Thread
+
+ if (toDownload.size() > 0) {
+ // we have resources to download, also note them as to-be-installed
+- for (Resource r : toDownload) {
+- if (!_toInstallResources.contains(r)) {
+- _toInstallResources.add(r);
+- }
+- }
++ _toInstallResources.addAll(toDownload);
+
+ try {
+ // if any of our resources have already been marked valid this is not a
+@@ -427,24 +439,20 @@ public abstract class Getdown extends Thread
+ throw new IOException("m.unable_to_repair");
+
+ } catch (Exception e) {
+- log.warning("getdown() failed.", e);
+- String msg = e.getMessage();
+- if (msg == null) {
+- msg = MessageUtil.compose("m.unknown_error", _ifc.installError);
+- } else if (!msg.startsWith("m.")) {
+- // try to do something sensible based on the type of error
+- if (e instanceof FileNotFoundException) {
+- msg = MessageUtil.compose(
+- "m.missing_resource", MessageUtil.taint(msg), _ifc.installError);
+- } else {
+- msg = MessageUtil.compose(
+- "m.init_error", MessageUtil.taint(msg), _ifc.installError);
+- }
++ // if we failed due to proxy errors, ask for proxy info
++ switch (_app.conn.state) {
++ case NEED_PROXY:
++ requestProxyInfo(false);
++ break;
++ case NEED_PROXY_AUTH:
++ requestProxyInfo(true);
++ break;
++ default:
++ log.warning("getdown() failed.", e);
++ fail(e);
++ _app.releaseLock();
++ break;
+ }
+- // Since we're dead, clear off the 'time remaining' label along with displaying the
+- // error message
+- fail(msg);
+- _app.releaseLock();
+ }
+ }
+
+@@ -499,6 +507,18 @@ public abstract class Getdown extends Thread
+ throw new IOException("m.java_download_failed");
+ }
+
++ // on Windows, if the local JVM is in use, we will not be able to replace it with an
++ // updated JVM; we detect this by attempting to rename the java.dll to its same name, which
++ // will fail on Windows for in use files; hackery!
++ File javaLocalDir = _app.getJavaLocalDir();
++ File javaDll = new File(javaLocalDir, "bin" + File.separator + "java.dll");
++ if (javaDll.exists()) {
++ if (!javaDll.renameTo(javaDll)) {
++ log.info("Cannot update local Java VM as it is in use.");
++ return;
++ }
++ }
++
+ reportTrackingEvent("jvm_start", -1);
+
+ updateStatus("m.downloading_java");
+@@ -507,21 +527,24 @@ public abstract class Getdown extends Thread
+ download(list);
+
+ reportTrackingEvent("jvm_unpack", -1);
+-
+ updateStatus("m.unpacking_java");
+- vmjar.install(true);
++ try {
++ vmjar.install(true);
++ } catch (IOException ioe) {
++ throw new IOException("m.java_unpack_failed", ioe);
++ }
+
+ // these only run on non-Windows platforms, so we use Unix file separators
+- String localJavaDir = LaunchUtil.LOCAL_JAVA_DIR + "/";
+- FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "bin/java"));
+- FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "lib/jspawnhelper"));
+- FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "lib/amd64/jspawnhelper"));
++ FileUtil.makeExecutable(new File(javaLocalDir, "bin/java"));
++ FileUtil.makeExecutable(new File(javaLocalDir, "lib/jspawnhelper"));
++ FileUtil.makeExecutable(new File(javaLocalDir, "lib/amd64/jspawnhelper"));
+
+ // lastly regenerate the .jsa dump file that helps Java to start up faster
+- String vmpath = LaunchUtil.getJVMPath(_app.getLocalPath(""));
++ String vmpath = LaunchUtil.getJVMBinaryPath(javaLocalDir, false);
++ String[] command = { vmpath, "-Xshare:dump" };
+ try {
+ log.info("Regenerating classes.jsa for " + vmpath + "...");
+- Runtime.getRuntime().exec(vmpath + " -Xshare:dump");
++ Runtime.getRuntime().exec(command);
+ } catch (Exception e) {
+ log.warning("Failed to regenerate .jsa dump file", "error", e);
+ }
+@@ -578,6 +601,8 @@ public abstract class Getdown extends Thread
+ int ii = 0; for (Resource prsrc : list) {
+ ProgressObserver pobs = pragg.startElement(ii++);
+ try {
++ // if this patch file failed to download, skip it
++ if (!prsrc.getLocalNew().exists()) continue;
+ // install the patch file (renaming them from _new)
+ prsrc.install(false);
+ // now apply the patch
+@@ -613,7 +638,7 @@ public abstract class Getdown extends Thread
+ // create our user interface
+ createInterfaceAsync(false);
+
+- Downloader dl = new HTTPDownloader(_app.proxy) {
++ Downloader dl = new Downloader(_app.conn) {
+ @Override protected void resolvingDownloads () {
+ updateStatus("m.resolving");
+ }
+@@ -640,6 +665,10 @@ public abstract class Getdown extends Thread
+ log.warning("Download failed", "rsrc", rsrc, e);
+ }
+
++ @Override protected void resourceMissing (Resource rsrc) {
++ log.warning("Resource missing (got 404)", "rsrc", rsrc);
++ }
++
+ /** The last percentage at which we checked for another getdown running, or -1 for not
+ * having checked at all. */
+ protected int _lastCheck = -1;
+@@ -725,7 +754,7 @@ public abstract class Getdown extends Thread
+ long minshow = _ifc.minShowSeconds * 1000L;
+ if (_container != null && uptime < minshow) {
+ try {
+- Thread.sleep(minshow - uptime);
++ TimeUnit.MILLISECONDS.sleep(minshow - uptime);
+ } catch (Exception e) {
+ }
+ }
+@@ -750,10 +779,9 @@ public abstract class Getdown extends Thread
+ if (_silent || (_container != null && !reinit)) {
+ return;
+ }
+-/*
++
+ EventQueue.invokeLater(new Runnable() {
+ public void run () {
+-*/
+ if (_container == null || reinit) {
+ if (_container == null) {
+ _container = createContainer();
+@@ -762,42 +790,6 @@ public abstract class Getdown extends Thread
+ }
+ configureContainer();
+ _layers = new JLayeredPane();
+-
+-
+-
+- // added in the instant display of a splashscreen
+- try {
+- readConfig(false);
+- Graphics g = _container.getGraphics();
+- String imageFile = _ifc.backgroundImage;
+- BufferedImage bgImage = loadImage(_ifc.backgroundImage);
+- int bwidth = bgImage.getWidth();
+- int bheight = bgImage.getHeight();
+-
+- instantSplashPane = new JPanel() {
+- @Override
+- protected void paintComponent(Graphics g)
+- {
+- super.paintComponent(g);
+- // attempt to draw a background image...
+- if (bgImage != null) {
+- g.drawImage(bgImage, 0, 0, this);
+- }
+- }
+- };
+-
+- instantSplashPane.setSize(bwidth,bheight);
+- instantSplashPane.setPreferredSize(new Dimension(bwidth,bheight));
+-
+- _layers.add(instantSplashPane, Integer.valueOf(0));
+-
+- _container.setPreferredSize(new Dimension(bwidth,bheight));
+- } catch (Exception e) {
+- log.warning("Failed to set instant background image", "bg", _ifc.backgroundImage);
+- }
+-
+-
+-
+ _container.add(_layers, BorderLayout.CENTER);
+ _patchNotes = new JButton(new AbstractAction(_msgs.getString("m.patch_notes")) {
+ @Override public void actionPerformed (ActionEvent event) {
+@@ -807,14 +799,12 @@ public abstract class Getdown extends Thread
+ _patchNotes.setFont(StatusPanel.FONT);
+ _layers.add(_patchNotes);
+ _status = new StatusPanel(_msgs);
+- _layers.add(_status, Integer.valueOf(10));
++ _layers.add(_status);
+ initInterface();
+ }
+ showContainer();
+-/*
+ }
+ });
+-*/
+ }
+
+ /**
+@@ -845,12 +835,13 @@ public abstract class Getdown extends Thread
+
+ protected RotatingBackgrounds getBackground ()
+ {
+- if (_ifc.rotatingBackgrounds != null && _ifc.rotatingBackgrounds.size() > 0) {
++ if (_ifc.rotatingBackgrounds != null) {
+ if (_ifc.backgroundImage != null) {
+ log.warning("ui.background_image and ui.rotating_background were both specified. " +
+- "The background image is being used.");
++ "The rotating images are being used.");
+ }
+- return new RotatingBackgrounds(_ifc.rotatingBackgrounds, _ifc.errorBackground, Getdown.this);
++ return new RotatingBackgrounds(_ifc.rotatingBackgrounds, _ifc.errorBackground,
++ Getdown.this);
+ } else if (_ifc.backgroundImage != null) {
+ return new RotatingBackgrounds(loadImage(_ifc.backgroundImage));
+ } else {
+@@ -879,8 +870,23 @@ public abstract class Getdown extends Thread
+ }
+ }
+
++ private void fail (Exception e) {
++ String msg = e.getMessage();
++ if (msg == null) {
++ msg = MessageUtil.compose("m.unknown_error", _ifc.installError);
++ } else if (!msg.startsWith("m.")) {
++ // try to do something sensible based on the type of error
++ msg = MessageUtil.taint(msg);
++ msg = e instanceof FileNotFoundException ?
++ MessageUtil.compose("m.missing_resource", msg, _ifc.installError) :
++ MessageUtil.compose("m.init_error", msg, _ifc.installError);
++ }
++ // since we're dead, clear off the 'time remaining' label along with displaying the error
++ fail(msg);
++ }
++
+ /**
+- * Update the status to indicate getdown has failed for the reason in <code>message</code>.
++ * Update the status to indicate getdown has failed for the reason in {@code message}.
+ */
+ protected void fail (String message)
+ {
+@@ -961,14 +967,14 @@ public abstract class Getdown extends Thread
+ do {
+ URL url = _app.getTrackingProgressURL(++_reportedProgress);
+ if (url != null) {
+- new ProgressReporter(url).start();
++ reportProgress(url);
+ }
+ } while (_reportedProgress <= progress);
+
+ } else {
+ URL url = _app.getTrackingURL(event);
+ if (url != null) {
+- new ProgressReporter(url).start();
++ reportProgress(url);
+ }
+ }
+ }
+@@ -1031,44 +1037,40 @@ public abstract class Getdown extends Thread
+ }
+
+ /** Used to fetch a progress report URL. */
+- protected class ProgressReporter extends Thread
+- {
+- public ProgressReporter (URL url) {
+- setDaemon(true);
+- _url = url;
+- }
+-
+- @Override
+- public void run () {
+- try {
+- HttpURLConnection ucon = ConnectionUtil.openHttp(_app.proxy, _url, 0, 0);
+-
+- // if we have a tracking cookie configured, configure the request with it
+- if (_app.getTrackingCookieName() != null &&
+- _app.getTrackingCookieProperty() != null) {
+- String val = System.getProperty(_app.getTrackingCookieProperty());
+- if (val != null) {
+- ucon.setRequestProperty("Cookie", _app.getTrackingCookieName() + "=" + val);
++ protected void reportProgress (final URL url) {
++ Thread reporter = new Thread("Progress reporter") {
++ public void run () {
++ try {
++ HttpURLConnection ucon = _app.conn.openHttp(url, 0, 0);
++
++ // if we have a tracking cookie configured, configure the request with it
++ if (_app.getTrackingCookieName() != null &&
++ _app.getTrackingCookieProperty() != null) {
++ String val = System.getProperty(_app.getTrackingCookieProperty());
++ if (val != null) {
++ ucon.setRequestProperty(
++ "Cookie", _app.getTrackingCookieName() + "=" + val);
++ }
+ }
+- }
+
+- // now request our tracking URL and ensure that we get a non-error response
+- ucon.connect();
+- try {
+- if (ucon.getResponseCode() != HttpURLConnection.HTTP_OK) {
+- log.warning("Failed to report tracking event",
+- "url", _url, "rcode", ucon.getResponseCode());
++ // now request our tracking URL and ensure that we get a non-error response
++ ucon.connect();
++ try {
++ if (ucon.getResponseCode() != HttpURLConnection.HTTP_OK) {
++ log.warning("Failed to report tracking event",
++ "url", url, "rcode", ucon.getResponseCode());
++ }
++ } finally {
++ ucon.disconnect();
+ }
+- } finally {
+- ucon.disconnect();
+- }
+
+- } catch (IOException ioe) {
+- log.warning("Failed to report tracking event", "url", _url, "error", ioe);
++ } catch (IOException ioe) {
++ log.warning("Failed to report tracking event", "url", url, "error", ioe);
++ }
+ }
+- }
+-
+- protected URL _url;
++ };
++ reporter.setDaemon(true);
++ reporter.start();
+ }
+
+ /** Used to pass progress on to our user interface. */
+@@ -1084,7 +1086,6 @@ public abstract class Getdown extends Thread
+ protected ResourceBundle _msgs;
+ protected Container _container;
+ protected JLayeredPane _layers;
+- protected JPanel instantSplashPane;
+ protected StatusPanel _status;
+ protected JButton _patchNotes;
+ protected AbortPanel _abort;
+@@ -1112,5 +1113,4 @@ public abstract class Getdown extends Thread
+
+ protected static final int MAX_LOOPS = 5;
+ protected static final long FALLBACK_CHECK_TIME = 1000L;
+-
+ }
+diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java
+index 2d1908904..3859f6a09 100644
+--- a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java
++++ b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java
+@@ -28,258 +28,225 @@ import javax.swing.KeyStroke;
+ import javax.swing.WindowConstants;
+
+ import com.samskivert.swing.util.SwingUtil;
+-import com.threerings.getdown.data.Application;
+ import com.threerings.getdown.data.EnvConfig;
+ import com.threerings.getdown.data.SysProps;
+ import com.threerings.getdown.util.LaunchUtil;
+ import com.threerings.getdown.util.StringUtil;
+ import static com.threerings.getdown.Log.log;
+-import jalview.bin.StartupNotificationListener;
+
+ /**
+ * The main application entry point for Getdown.
+ */
+ public class GetdownApp
+ {
+- public static String startupFilesParameterString = "";
+- /**
+- * The main entry point of the Getdown launcher application.
+- */
+- public static void main (String[] argv) {
+- try {
+- start(argv);
+- } catch (Exception e) {
+- log.warning("main() failed.", e);
+- }
+- }
+-
+- /**
+- * Runs Getdown as an application, using the arguments supplie as {@code argv}.
+- * @return the {@code Getdown} instance that is running. {@link Getdown#start} will have been
+- * called on it.
+- * @throws Exception if anything goes wrong starting Getdown.
+- */
+- public static Getdown start (String[] argv) throws Exception {
+- List<EnvConfig.Note> notes = new ArrayList<>();
+- EnvConfig envc = EnvConfig.create(argv, notes);
+- if (envc == null) {
+- if (!notes.isEmpty()) for (EnvConfig.Note n : notes) System.err.println(n.message);
+- else System.err.println("Usage: java -jar getdown.jar [app_dir] [app_id] [app args]");
+- System.exit(-1);
+- }
+-
+- // pipe our output into a file in the application directory
+- if (!SysProps.noLogRedir()) {
+- File logFile = new File(envc.appDir, "launcher.log");
+- try {
+- PrintStream logOut = new PrintStream(
+- new BufferedOutputStream(new FileOutputStream(logFile)), true);
+- System.setOut(logOut);
+- System.setErr(logOut);
+- } catch (IOException ioe) {
+- log.warning("Unable to redirect output to '" + logFile + "': " + ioe);
+- }
+- }
+-
+- // report any notes from reading our env config, and abort if necessary
+- boolean abort = false;
+- for (EnvConfig.Note note : notes) {
+- switch (note.level) {
+- case INFO: log.info(note.message); break;
+- case WARN: log.warning(note.message); break;
+- case ERROR: log.error(note.message); abort = true; break;
+- }
++ /**
++ * The main entry point of the Getdown launcher application.
++ */
++ public static void main (String[] argv) {
++ try {
++ start(argv);
++ } catch (Exception e) {
++ log.warning("main() failed.", e);
++ }
+ }
+- if (abort) System.exit(-1);
+
+- try
+- {
+- jalview.bin.StartupNotificationListener.setListener();
+- } catch (Exception e)
+- {
+- e.printStackTrace();
+- } catch (NoClassDefFoundError e)
+- {
+- log.warning("Starting without install4j classes");
+- } catch (Throwable t)
+- {
+- t.printStackTrace();
+- }
+-
+- // record a few things for posterity
+- log.info("------------------ VM Info ------------------");
+- log.info("-- OS Name: " + System.getProperty("os.name"));
+- log.info("-- OS Arch: " + System.getProperty("os.arch"));
+- log.info("-- OS Vers: " + System.getProperty("os.version"));
+- log.info("-- Java Vers: " + System.getProperty("java.version"));
+- log.info("-- Java Home: " + System.getProperty("java.home"));
+- log.info("-- User Name: " + System.getProperty("user.name"));
+- log.info("-- User Home: " + System.getProperty("user.home"));
+- log.info("-- Cur dir: " + System.getProperty("user.dir"));
+- log.info("-- startupFilesParameterString: " + startupFilesParameterString);
+- log.info("---------------------------------------------");
++ /**
++ * Runs Getdown as an application, using the arguments supplie as {@code argv}.
++ * @return the {@code Getdown} instance that is running. {@link Getdown#run} will have been
++ * called on it.
++ * @throws Exception if anything goes wrong starting Getdown.
++ */
++ public static Getdown start (String[] argv) throws Exception {
++ List<EnvConfig.Note> notes = new ArrayList<>();
++ EnvConfig envc = EnvConfig.create(argv, notes);
++ if (envc == null) {
++ if (!notes.isEmpty()) for (EnvConfig.Note n : notes) System.err.println(n.message);
++ else System.err.println("Usage: java -jar getdown.jar [app_dir] [app_id] [app args]");
++ System.exit(-1);
++ }
+
+- Getdown app = new Getdown(envc) {
+- @Override
+- protected Container createContainer () {
+- // create our user interface, and display it
+- if (_frame == null) {
+- _frame = new JFrame("");
+- _frame.addWindowListener(new WindowAdapter() {
+- @Override
+- public void windowClosing (WindowEvent evt) {
+- handleWindowClose();
+- }
+- });
+- // handle close on ESC
+- String cancelId = "Cancel"; // $NON-NLS-1$
+- _frame.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
+- KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), cancelId);
+- _frame.getRootPane().getActionMap().put(cancelId, new AbstractAction() {
+- public void actionPerformed (ActionEvent e) {
+- handleWindowClose();
++ // pipe our output into a file in the application directory
++ if (!SysProps.noLogRedir() && !SysProps.debug()) {
++ File logFile = new File(envc.appDir, "launcher.log");
++ try {
++ PrintStream logOut = new PrintStream(
++ new BufferedOutputStream(new FileOutputStream(logFile)), true);
++ System.setOut(logOut);
++ System.setErr(logOut);
++ } catch (IOException ioe) {
++ log.warning("Unable to redirect output to '" + logFile + "': " + ioe);
+ }
+- });
+- // this cannot be called in configureContainer as it is only allowed before the
+- // frame has been displayed for the first time
+- _frame.setUndecorated(_ifc.hideDecorations);
+- _frame.setResizable(false);
+- } else {
+- _frame.getContentPane().removeAll();
+ }
+- _frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
+- return _frame.getContentPane();
+- }
+-
+- @Override
+- protected void configureContainer () {
+- if (_frame == null) return;
+-
+- _frame.setTitle(_ifc.name);
+
+- try {
+- _frame.setBackground(new Color(_ifc.background, true));
+- } catch (Exception e) {
+- log.warning("Failed to set background", "bg", _ifc.background, e);
++ // report any notes from reading our env config, and abort if necessary
++ boolean abort = false;
++ for (EnvConfig.Note note : notes) {
++ switch (note.level) {
++ case INFO: log.info(note.message); break;
++ case WARN: log.warning(note.message); break;
++ case ERROR: log.error(note.message); abort = true; break;
++ }
+ }
++ if (abort) System.exit(-1);
++
++ // record a few things for posterity
++ log.info("------------------ VM Info ------------------");
++ log.info("-- OS Name: " + System.getProperty("os.name"));
++ log.info("-- OS Arch: " + System.getProperty("os.arch"));
++ log.info("-- OS Vers: " + System.getProperty("os.version"));
++ log.info("-- Java Vers: " + System.getProperty("java.version"));
++ log.info("-- Java Home: " + System.getProperty("java.home"));
++ log.info("-- User Name: " + System.getProperty("user.name"));
++ log.info("-- User Home: " + System.getProperty("user.home"));
++ log.info("-- Cur dir: " + System.getProperty("user.dir"));
++ log.info("---------------------------------------------");
++
++ Getdown getdown = new Getdown(envc) {
++ @Override
++ protected Container createContainer () {
++ // create our user interface, and display it
++ if (_frame == null) {
++ _frame = new JFrame("");
++ _frame.addWindowListener(new WindowAdapter() {
++ @Override
++ public void windowClosing (WindowEvent evt) {
++ handleWindowClose();
++ }
++ });
++ // handle close on ESC
++ String cancelId = "Cancel"; // $NON-NLS-1$
++ _frame.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
++ KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), cancelId);
++ _frame.getRootPane().getActionMap().put(cancelId, new AbstractAction() {
++ public void actionPerformed (ActionEvent e) {
++ handleWindowClose();
++ }
++ });
++ // this cannot be called in configureContainer as it is only allowed before the
++ // frame has been displayed for the first time
++ _frame.setUndecorated(_ifc.hideDecorations);
++ _frame.setResizable(false);
++ } else {
++ _frame.getContentPane().removeAll();
++ }
++ _frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
++ return _frame.getContentPane();
++ }
+
+- if (_ifc.iconImages != null && _ifc.iconImages.size() > 0) {
+- ArrayList<Image> icons = new ArrayList<>();
+- for (String path : _ifc.iconImages) {
+- Image img = loadImage(path);
+- if (img == null) {
+- log.warning("Error loading icon image", "path", path);
+- } else {
+- icons.add(img);
++ @Override
++ protected void configureContainer () {
++ if (_frame == null) return;
++
++ _frame.setTitle(_ifc.name);
++
++ try {
++ _frame.setBackground(new Color(_ifc.background, true));
++ } catch (Exception e) {
++ log.warning("Failed to set background", "bg", _ifc.background, e);
++ }
++
++ if (_ifc.iconImages != null) {
++ List<Image> icons = new ArrayList<>();
++ for (String path : _ifc.iconImages) {
++ Image img = loadImage(path);
++ if (img == null) {
++ log.warning("Error loading icon image", "path", path);
++ } else {
++ icons.add(img);
++ }
++ }
++ if (icons.isEmpty()) {
++ log.warning("Failed to load any icons", "iconImages", _ifc.iconImages);
++ } else {
++ _frame.setIconImages(icons);
++ }
++ }
+ }
+- }
+- if (icons.isEmpty()) {
+- log.warning("Failed to load any icons", "iconImages", _ifc.iconImages);
+- } else {
+- _frame.setIconImages(icons);
+- }
+- }
+- }
+
+- @Override
+- protected void showContainer () {
+- if (_frame != null) {
+- _frame.pack();
+- SwingUtil.centerWindow(_frame);
+- _frame.setVisible(true);
+- }
+- }
++ @Override
++ protected void showContainer () {
++ if (_frame != null) {
++ _frame.pack();
++ SwingUtil.centerWindow(_frame);
++ _frame.setVisible(true);
++ }
++ }
+
+- @Override
+- protected void disposeContainer () {
+- if (_frame != null) {
+- _frame.dispose();
+- _frame = null;
+- }
+- }
++ @Override
++ protected void disposeContainer () {
++ if (_frame != null) {
++ _frame.dispose();
++ _frame = null;
++ }
++ }
+
+- @Override
+- protected void showDocument (String url) {
+- if (!StringUtil.couldBeValidUrl(url)) {
+- // command injection would be possible if we allowed e.g. spaces and double quotes
+- log.warning("Invalid document URL.", "url", url);
+- return;
+- }
+- String[] cmdarray;
+- if (LaunchUtil.isWindows()) {
+- String osName = System.getProperty("os.name", "");
+- if (osName.indexOf("9") != -1 || osName.indexOf("Me") != -1) {
+- cmdarray = new String[] {
+- "command.com", "/c", "start", "\"" + url + "\"" };
+- } else {
+- cmdarray = new String[] {
+- "cmd.exe", "/c", "start", "\"\"", "\"" + url + "\"" };
+- }
+- } else if (LaunchUtil.isMacOS()) {
+- cmdarray = new String[] { "open", url };
+- } else { // Linux, Solaris, etc.
+- cmdarray = new String[] { "firefox", url };
+- }
+- try {
+- Runtime.getRuntime().exec(cmdarray);
+- } catch (Exception e) {
+- log.warning("Failed to open browser.", "cmdarray", cmdarray, e);
+- }
+- }
++ @Override
++ protected void showDocument (String url) {
++ if (!StringUtil.couldBeValidUrl(url)) {
++ // command injection would be possible if we allowed e.g. spaces and double quotes
++ log.warning("Invalid document URL.", "url", url);
++ return;
++ }
++ String[] cmdarray;
++ if (LaunchUtil.isWindows()) {
++ String osName = System.getProperty("os.name", "");
++ if (osName.contains("9") || osName.contains("Me")) {
++ cmdarray = new String[] {
++ "command.com", "/c", "start", "\"" + url + "\"" };
++ } else {
++ cmdarray = new String[] {
++ "cmd.exe", "/c", "start", "\"\"", "\"" + url + "\"" };
++ }
++ } else if (LaunchUtil.isMacOS()) {
++ cmdarray = new String[] { "open", url };
++ } else { // Linux, Solaris, etc.
++ cmdarray = new String[] { "firefox", url };
++ }
++ try {
++ Runtime.getRuntime().exec(cmdarray);
++ } catch (Exception e) {
++ log.warning("Failed to open browser.", "cmdarray", cmdarray, e);
++ }
++ }
+
+- @Override
+- protected void exit (int exitCode) {
+- // if we're running the app in the same JVM, don't call System.exit, but do
+- // make double sure that the download window is closed.
+- if (invokeDirect()) {
+- disposeContainer();
+- } else {
+- System.exit(exitCode);
+- }
+- }
++ @Override
++ protected void exit (int exitCode) {
++ // if we're running the app in the same JVM, don't call System.exit, but do
++ // make double sure that the download window is closed.
++ if (invokeDirect()) {
++ disposeContainer();
++ } else {
++ System.exit(exitCode);
++ }
++ }
+
+- @Override
+- protected void fail (String message) {
+- super.fail(message);
+- // super.fail causes the UI to be created (if needed) on the next UI tick, so we
+- // want to wait until that happens before we attempt to redecorate the window
+- EventQueue.invokeLater(new Runnable() {
+- @Override
+- public void run() {
+- // if the frame was set to be undecorated, make window decoration available
+- // to allow the user to close the window
+- if (_frame != null && _frame.isUndecorated()) {
+- _frame.dispose();
+- Color bg = _frame.getBackground();
+- if (bg != null && bg.getAlpha() < 255) {
+- // decorated windows do not allow alpha backgrounds
+- _frame.setBackground(
+- new Color(bg.getRed(), bg.getGreen(), bg.getBlue()));
+- }
+- _frame.setUndecorated(false);
+- showContainer();
++ @Override
++ protected void fail (String message) {
++ super.fail(message);
++ // super.fail causes the UI to be created (if needed) on the next UI tick, so we
++ // want to wait until that happens before we attempt to redecorate the window
++ EventQueue.invokeLater(new Runnable() {
++ @Override public void run () {
++ // if the frame was set to be undecorated, make window decoration available
++ // to allow the user to close the window
++ if (_frame != null && _frame.isUndecorated()) {
++ _frame.dispose();
++ Color bg = _frame.getBackground();
++ if (bg != null && bg.getAlpha() < 255) {
++ // decorated windows do not allow alpha backgrounds
++ _frame.setBackground(
++ new Color(bg.getRed(), bg.getGreen(), bg.getBlue()));
++ }
++ _frame.setUndecorated(false);
++ showContainer();
++ }
++ }
++ });
+ }
+- }
+- });
+- }
+
+- protected JFrame _frame;
+- };
+-
+- String startupFile = getStartupFilesParameterString();
+- if (!StringUtil.isBlank(startupFile)) {
+- Application.setStartupFilesFromParameterString(startupFile);
++ protected JFrame _frame;
++ };
++ Getdown.run(getdown);
++ return getdown;
+ }
+-
+- app.start();
+- return app;
+- }
+-
+- public static void setStartupFilesParameterString(String parameters) {
+- startupFilesParameterString = parameters;
+- }
+-
+- public static String getStartupFilesParameterString() {
+- return startupFilesParameterString;
+- }
+ }
+diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java
+index 217827364..5f18896c4 100644
+--- a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java
++++ b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java
+@@ -35,14 +35,16 @@ import static com.threerings.getdown.Log.log;
+ */
+ public final class ProxyPanel extends JPanel implements ActionListener
+ {
+- public ProxyPanel (Getdown getdown, ResourceBundle msgs)
++ public ProxyPanel (Getdown getdown, ResourceBundle msgs, boolean updateAuth)
+ {
+ _getdown = getdown;
+ _msgs = msgs;
++ _updateAuth = updateAuth;
+
+ setLayout(new VGroupLayout());
+ setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+- add(new SaneLabelField(get("m.configure_proxy")));
++ String title = get(updateAuth ? "m.update_proxy_auth" : "m.configure_proxy");
++ add(new SaneLabelField(title));
+ add(new Spacer(5, 5));
+
+ JPanel row = new JPanel(new GridLayout());
+@@ -61,19 +63,20 @@ public final class ProxyPanel extends JPanel implements ActionListener
+ row.add(new SaneLabelField(get("m.proxy_auth_required")), BorderLayout.WEST);
+ _useAuth = new JCheckBox();
+ row.add(_useAuth);
++ _useAuth.setSelected(updateAuth);
+ add(row);
+
+ row = new JPanel(new GridLayout());
+ row.add(new SaneLabelField(get("m.proxy_username")), BorderLayout.WEST);
+ _username = new SaneTextField();
+- _username.setEnabled(false);
++ _username.setEnabled(updateAuth);
+ row.add(_username);
+ add(row);
+
+ row = new JPanel(new GridLayout());
+ row.add(new SaneLabelField(get("m.proxy_password")), BorderLayout.WEST);
+ _password = new SanePasswordField();
+- _password.setEnabled(false);
++ _password.setEnabled(updateAuth);
+ row.add(_password);
+ add(row);
+
+@@ -112,7 +115,13 @@ public final class ProxyPanel extends JPanel implements ActionListener
+ public void addNotify ()
+ {
+ super.addNotify();
+- _host.requestFocusInWindow();
++ if (_updateAuth) {
++ // we are asking the user to update the credentials for an existing proxy
++ // configuration, so focus that instead of the proxy host config
++ _username.requestFocusInWindow();
++ } else {
++ _host.requestFocusInWindow();
++ }
+ }
+
+ // documentation inherited
+@@ -131,7 +140,7 @@ public final class ProxyPanel extends JPanel implements ActionListener
+ public void actionPerformed (ActionEvent e)
+ {
+ String cmd = e.getActionCommand();
+- if (cmd.equals("ok")) {
++ if ("ok".equals(cmd)) {
+ String user = null, pass = null;
+ if (_useAuth.isSelected()) {
+ user = _username.getText();
+@@ -184,8 +193,9 @@ public final class ProxyPanel extends JPanel implements ActionListener
+ return dim;
+ }
+
+- protected Getdown _getdown;
+- protected ResourceBundle _msgs;
++ protected final Getdown _getdown;
++ protected final ResourceBundle _msgs;
++ protected final boolean _updateAuth;
+
+ protected JTextField _host;
+ protected JTextField _port;
+diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java
+index a36b5fa67..8962d35b9 100644
+--- a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java
++++ b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java
+@@ -8,31 +8,41 @@ package com.threerings.getdown.launcher;
+ import java.io.File;
+ import java.io.FileOutputStream;
+ import java.io.IOException;
++import java.io.InputStreamReader;
+ import java.io.PrintStream;
++import java.io.Reader;
+ import java.net.Authenticator;
+ import java.net.HttpURLConnection;
++import java.net.InetAddress;
+ import java.net.InetSocketAddress;
+ import java.net.PasswordAuthentication;
+ import java.net.Proxy;
+ import java.net.URL;
+ import java.net.URLConnection;
++import java.net.UnknownHostException;
+ import java.util.Iterator;
+ import java.util.ServiceLoader;
+
++import javax.script.Bindings;
++import javax.script.Invocable;
++import javax.script.ScriptContext;
++import javax.script.ScriptEngine;
++import javax.script.ScriptEngineManager;
++
+ import ca.beq.util.win32.registry.RegistryKey;
+ import ca.beq.util.win32.registry.RegistryValue;
+ import ca.beq.util.win32.registry.RootKey;
+
+ import com.threerings.getdown.data.Application;
++import com.threerings.getdown.net.Connector;
+ import com.threerings.getdown.spi.ProxyAuth;
+ import com.threerings.getdown.util.Config;
+-import com.threerings.getdown.util.ConnectionUtil;
+ import com.threerings.getdown.util.LaunchUtil;
+ import com.threerings.getdown.util.StringUtil;
+
+ import static com.threerings.getdown.Log.log;
+
+-public class ProxyUtil {
++public final class ProxyUtil {
+
+ public static boolean autoDetectProxy (Application app)
+ {
+@@ -57,25 +67,40 @@ public class ProxyUtil {
+ RegistryKey r = new RegistryKey(RootKey.HKEY_CURRENT_USER, PROXY_REGISTRY);
+ for (Iterator<?> iter = r.values(); iter.hasNext(); ) {
+ RegistryValue value = (RegistryValue)iter.next();
+- if (value.getName().equals("ProxyEnable")) {
+- enabled = value.getStringValue().equals("1");
++ if ("ProxyEnable".equals(value.getName())) {
++ enabled = "1".equals(value.getStringValue());
+ }
+ if (value.getName().equals("ProxyServer")) {
+- String strval = value.getStringValue();
+- int cidx = strval.indexOf(":");
+- if (cidx != -1) {
+- rport = strval.substring(cidx+1);
+- strval = strval.substring(0, cidx);
++ String[] hostPort = splitHostPort(value.getStringValue());
++ rhost = hostPort[0];
++ rport = hostPort[1];
++ }
++ if (value.getName().equals("AutoConfigURL")) {
++ String acurl = value.getStringValue();
++ Reader acjs = new InputStreamReader(new URL(acurl).openStream());
++ // technically we should be returning all this info and trying each proxy
++ // in succession, but that's complexity we'll leave for another day
++ URL configURL = app.getConfigResource().getRemote();
++ for (String proxy : findPACProxiesForURL(acjs, configURL)) {
++ if (proxy.startsWith("PROXY ")) {
++ String[] hostPort = splitHostPort(proxy.substring(6));
++ rhost = hostPort[0];
++ rport = hostPort[1];
++ // TODO: is this valid? Does AutoConfigURL imply proxy enabled?
++ enabled = true;
++ break;
++ }
+ }
+- rhost = strval;
+ }
+ }
++
+ if (enabled) {
+ host = rhost;
+ port = rport;
+ } else {
+ log.info("Detected no proxy settings in the registry.");
+ }
++
+ } catch (Throwable t) {
+ log.info("Failed to find proxy settings in Windows registry", "error", t);
+ }
+@@ -97,32 +122,31 @@ public class ProxyUtil {
+ return true;
+ }
+
+- public static boolean canLoadWithoutProxy (URL rurl)
++ public static boolean canLoadWithoutProxy (URL rurl, int timeoutSeconds)
+ {
+- log.info("Testing whether proxy is needed, via: " + rurl);
++ log.info("Attempting to fetch without proxy: " + rurl);
+ try {
+- // try to make a HEAD request for this URL (use short connect and read timeouts)
+- URLConnection conn = ConnectionUtil.open(Proxy.NO_PROXY, rurl, 5, 5);
+- if (conn instanceof HttpURLConnection) {
+- HttpURLConnection hcon = (HttpURLConnection)conn;
+- try {
+- hcon.setRequestMethod("HEAD");
+- hcon.connect();
+- // make sure we got a satisfactory response code
+- int rcode = hcon.getResponseCode();
+- if (rcode == HttpURLConnection.HTTP_PROXY_AUTH ||
+- rcode == HttpURLConnection.HTTP_FORBIDDEN) {
+- log.warning("Got an 'HTTP credentials needed' response", "code", rcode);
+- } else {
+- return true;
+- }
+- } finally {
+- hcon.disconnect();
+- }
+- } else {
+- // if the appbase is not an HTTP/S URL (like file:), then we don't need a proxy
++ URLConnection conn = Connector.DEFAULT.open(rurl, timeoutSeconds, timeoutSeconds);
++ // if the appbase is not an HTTP/S URL (like file:), then we don't need a proxy
++ if (!(conn instanceof HttpURLConnection)) {
+ return true;
+ }
++ // otherwise, try to make a HEAD request for this URL
++ HttpURLConnection hcon = (HttpURLConnection)conn;
++ try {
++ hcon.setRequestMethod("HEAD");
++ hcon.connect();
++ // make sure we got a satisfactory response code
++ int rcode = hcon.getResponseCode();
++ if (rcode == HttpURLConnection.HTTP_PROXY_AUTH ||
++ rcode == HttpURLConnection.HTTP_FORBIDDEN) {
++ log.warning("Got an 'HTTP credentials needed' response", "code", rcode);
++ } else {
++ return true;
++ }
++ } finally {
++ hcon.disconnect();
++ }
+ } catch (IOException ioe) {
+ log.info("Failed to HEAD " + rurl + ": " + ioe);
+ log.info("We probably need a proxy, but auto-detection failed.");
+@@ -190,9 +214,14 @@ public class ProxyUtil {
+ }
+ boolean haveCreds = !StringUtil.isBlank(username) && !StringUtil.isBlank(password);
+
+- int pport = StringUtil.isBlank(port) ? 80 : Integer.valueOf(port);
+- log.info("Using proxy", "host", host, "port", pport, "haveCreds", haveCreds);
+- app.proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, pport));
++ if (StringUtil.isBlank(host)) {
++ log.info("Using no proxy");
++ app.conn = new Connector(Proxy.NO_PROXY);
++ } else {
++ int pp = StringUtil.isBlank(port) ? 80 : Integer.valueOf(port);
++ log.info("Using proxy", "host", host, "port", pp, "haveCreds", haveCreds);
++ app.conn = new Connector(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, pp)));
++ }
+
+ if (haveCreds) {
+ final String fuser = username;
+@@ -205,6 +234,61 @@ public class ProxyUtil {
+ }
+ }
+
++ public static class Resolver {
++ public String dnsResolve (String host) {
++ try {
++ return InetAddress.getByName(host).getHostAddress();
++ } catch (UnknownHostException uhe) {
++ return null;
++ }
++ }
++ public String myIpAddress () {
++ try {
++ return InetAddress.getLocalHost().getHostAddress();
++ } catch (UnknownHostException uhe) {
++ return null;
++ }
++ }
++ }
++
++ public static String[] findPACProxiesForURL (Reader pac, URL url) {
++ ScriptEngineManager manager = new ScriptEngineManager();
++ ScriptEngine engine = manager.getEngineByName("javascript");
++ Bindings globals = engine.createBindings();
++ globals.put("resolver", new Resolver());
++ engine.setBindings(globals, ScriptContext.GLOBAL_SCOPE);
++ try {
++ URL utils = ProxyUtil.class.getResource("PacUtils.js");
++ if (utils == null) {
++ log.error("Unable to load PacUtils.js");
++ return new String[0];
++ }
++ engine.eval(new InputStreamReader(utils.openStream()));
++ Object res = engine.eval(pac);
++ if (engine instanceof Invocable) {
++ Object[] args = new Object[] { url.toString(), url.getHost() };
++ res = ((Invocable) engine).invokeFunction("FindProxyForURL", args);
++ }
++ String[] proxies = res.toString().split(";");
++ for (int ii = 0; ii < proxies.length; ii += 1) {
++ proxies[ii] = proxies[ii].trim();
++ }
++ return proxies;
++ } catch (Exception e) {
++ log.warning("Failed to resolve PAC proxy", e);
++ }
++ return new String[0];
++ }
++
++ private static String[] splitHostPort (String hostPort) {
++ int cidx = hostPort.indexOf(":");
++ if (cidx == -1) {
++ return new String[] { hostPort, null};
++ } else {
++ return new String[] { hostPort.substring(0, cidx), hostPort.substring(cidx+1) };
++ }
++ }
++
+ protected static final String PROXY_REGISTRY =
+ "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings";
+ }
+diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java
+index d3aa2bd25..d64e5f02d 100644
+--- a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java
++++ b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java
+@@ -14,7 +14,7 @@ public final class RotatingBackgrounds
+ {
+ public interface ImageLoader {
+ /** Loads and returns the image with the supplied path. */
+- public Image loadImage (String path);
++ Image loadImage (String path);
+ }
+
+ /**
+@@ -35,7 +35,7 @@ public final class RotatingBackgrounds
+ }
+
+ /**
+- * Create a sequence of images to be rotated through from <code>backgrounds</code>.
++ * Create a sequence of images to be rotated through from {@code backgrounds}.
+ *
+ * Each String in backgrounds should be the path to the image, a semicolon, and the minimum
+ * amount of time to display the image in seconds. Each image will be active for an equal
+diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java
+index 99f44ca51..197dc9170 100644
+--- a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java
++++ b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java
+@@ -26,12 +26,9 @@ import com.samskivert.swing.Label;
+ import com.samskivert.swing.LabelStyleConstants;
+ import com.samskivert.swing.util.SwingUtil;
+ import com.samskivert.util.Throttle;
+-
+ import com.threerings.getdown.data.Application.UpdateInterface;
+ import com.threerings.getdown.util.MessageUtil;
+ import com.threerings.getdown.util.Rectangle;
+-import com.threerings.getdown.util.StringUtil;
+-
+ import static com.threerings.getdown.Log.log;
+
+ /**
+@@ -344,7 +341,7 @@ public final class StatusPanel extends JComponent
+ {
+ String msg = get(key);
+ if (msg != null) return MessageFormat.format(MessageUtil.escape(msg), (Object[])args);
+- return key + String.valueOf(Arrays.asList(args));
++ return key + Arrays.asList(args);
+ }
+
+ /** Used by {@link #setStatus}, and {@link #setProgress}. */
+diff --git a/getdown/src/getdown/launcher/src/main/java/jalview/bin/StartupNotificationListener.java b/getdown/src/getdown/launcher/src/main/java/jalview/bin/StartupNotificationListener.java
+deleted file mode 100644
+index 5c6c7c393..000000000
+--- a/getdown/src/getdown/launcher/src/main/java/jalview/bin/StartupNotificationListener.java
++++ /dev/null
+@@ -1,29 +0,0 @@
+-package jalview.bin;
+-
+-import com.threerings.getdown.launcher.GetdownApp;
+-import static com.threerings.getdown.Log.log;
+-
+-public class StartupNotificationListener {
+-
+- public static void setListener() {
+-
+-
+- try {
+- com.install4j.api.launcher.StartupNotification.registerStartupListener(
+- new com.install4j.api.launcher.StartupNotification.Listener() {
+- @Override
+- public void startupPerformed(String parameters) {
+- log.info("StartupNotification.Listener.startupPerformed: '"+parameters+"'");
+- GetdownApp.setStartupFilesParameterString(parameters);
+- }
+- }
+- );
+- } catch (Exception e) {
+- e.printStackTrace();
+- } catch (NoClassDefFoundError t) {
+- log.warning("Starting without install4j classes");
+- }
+-
+- }
+-
+-}
+diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages.properties
+index 19b2999e1..7a33ca0e4 100644
+--- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages.properties
++++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages.properties
+@@ -12,11 +12,14 @@ m.abort_cancel = Continue installation
+ m.detecting_proxy = Trying to auto-detect proxy settings
+
+ m.configure_proxy = <html>We were unable to connect to the application server to download data. \
+- <p> Please make sure that no virus scanner or firewall is blocking network communicaton with \
++ <p> Please make sure that no virus scanner or firewall is blocking network communication with \
+ the server. \
+ <p> Your computer may access the Internet through a proxy and we were unable to automatically \
+ detect your proxy settings. If you know your proxy settings, you can enter them below.</html>
+
++m.update_proxy_auth = <html>The stored proxy user/password is wrong or obsolete. \
++ <p>Please provide an updated user/password combination.</html>
++
+ m.proxy_extra = <html>If you are sure that you don't use a proxy then \
+ perhaps there is a temporary Internet outage that is preventing us from \
+ communicating with the servers. In this case, you can cancel and try \
+@@ -41,10 +44,7 @@ m.checking = Checking for update
+ m.validating = Validating
+ m.patching = Patching
+ m.launching = Launching
+-
+ m.patch_notes = Patch Notes
+-m.play_again = Play Again
+-
+ m.complete = {0}% complete
+ m.remain = {0} remaining
+
+@@ -99,7 +99,7 @@ m.default_install_error = the support section of the website
+ m.another_getdown_running = Multiple instances of this application's \
+ installer are running. This one will stop and let another complete.
+
+-m.applet_stopped = Getdown's applet was told to stop working.
++m.verify_timeout = Verifying resources took too long.
+
+ # application/digest errors
+ m.missing_appbase = The configuration file is missing the 'appbase'.
+diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_de.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_de.properties
+index 8e3683594..db35593b2 100644
+--- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_de.properties
++++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_de.properties
+@@ -1,13 +1,9 @@
+ #
+-# $Id$
+-#
+-# Getdown translation messages
++# Getdown German translation messages
+
+ m.abort_title = Installation abbrechen?
+-m.abort_confirm = <html>Bist du sicher, dass du die Installation abbrechen \
+-m\u00f6chtest? \
+- Du kannst sp\u00e4ter fortfahren, indem du die Anwendung erneut \
+-ausf\u00fchrst.</html>
++m.abort_confirm = <html>Bist du sicher, dass du die Installation abbrechen möchtest? \
++ Du kannst später fortfahren, indem du die Anwendung erneut ausführst.</html>
+ m.abort_ok = Beenden
+ m.abort_cancel = Installation fortsetzen
+
+@@ -17,9 +13,12 @@ m.configure_proxy = <html>Es konnte keine Verbindung zum Applikations-Server auf
+ <p>Bitte kontrollieren Sie die Proxyeinstellungen und stellen Sie sicher, dass keine lokal oder \
+ im Netzwerk betriebene Sicherheitsanwendung (Virenscanner, Firewall, etc.) die Kommunikation \
+ mit dem Server blockiert.<br> \
+- Wenn kein Proxy verwendet werden soll, l\u00f6schen Sie bitte alle Eintr\u00e4ge in den unten \
++ Wenn kein Proxy verwendet werden soll, löschen Sie bitte alle Einträge in den unten \
+ stehenden Feldern und klicken sie auf OK.</html>
+
++m.update_proxy_auth = <html>Gespeicherte Proxy User/Passwort Kombination ungültig oder obsolet. \
++ <p>Bitte geben Sie die korrekte User/Passwort Kombination ein.</html>
++
+ m.proxy_extra = <html>Sollten Sie keine Proxyeinstellungen gesetzt haben wenden Sie sich bitte \
+ an Ihren Administrator.</html>
+
+@@ -46,71 +45,68 @@ m.launching = Starte
+ m.patch_notes = Patchnotes
+
+ m.complete = {0}% abgeschlossen
+-m.remain = {0} \u00fcbrig
++m.remain = {0} übrig
+
+ m.updating_metadata = Lade Steuerungsdateien herunter
+
+-m.init_failed = Unsere Konfigurationsdatei fehlt oder ist besch\u00e4digt. \
+-Versuche, eine neue Kopie herunterzuladen...
++m.init_failed = Unsere Konfigurationsdatei fehlt oder ist beschädigt. \
++ Versuche, eine neue Kopie herunterzuladen...
+
+-m.java_download_failed = Wir konnten die notwendige Javaversion f\u00fcr deinen \
+-Computer nicht automatisch herunterladen. \n\n \
+-Bitte auf www.java.com die aktuelle Javaversion herunterladen und dann die \
+-Anwendung erneut starten.
++m.java_download_failed = Wir konnten die notwendige Javaversion für deinen \
++ Computer nicht automatisch herunterladen. \n\n \
++ Bitte auf www.java.com die aktuelle Javaversion herunterladen und dann die \
++ Anwendung erneut starten.
+
+ m.java_unpack_failed = Wir konnten die aktualisierte Javaversion nicht \
+-entpacken. Bitte stelle sicher, dass wenigstens 100MB Platz auf der \
+-Festplatte frei sind und versuche dann die Anwendung erneut zu \
+-starten.\n\n\ \
+-Falls das das Problem nicht beseitigt, bitte auf www.java.com die aktuelle \
+-Javaversion herunterladen und installieren und dann erneut versuchen.
++ entpacken. Bitte stelle sicher, dass wenigstens 100MB Platz auf der \
++ Festplatte frei sind und versuche dann die Anwendung erneut zu \
++ starten.\n\n\ \
++ Falls das das Problem nicht beseitigt, bitte auf www.java.com die aktuelle \
++ Javaversion herunterladen und installieren und dann erneut versuchen.
+
+ m.unable_to_repair = Wir konnten die notwendigen Dateien nach 5 Versuchen \
+-nicht herunterladen. Du kannst versuchen, die Anwendung erneut zu starten, \
+-aber wenn dies erneut fehlschl\u00e4gt, musst du die Anwendung deinstallieren \
+-und erneut installieren.
++ nicht herunterladen. Du kannst versuchen, die Anwendung erneut zu starten, \
++ aber wenn dies erneut fehlschlägt, musst du die Anwendung deinstallieren \
++ und erneut installieren.
+
+ m.unknown_error = Die Anwendung konnte wegen eines unbekannten Fehlers \
+-nicht gestartet werden. Bitte auf \n{0} weiterlesen.
++ nicht gestartet werden. Bitte auf \n{0} weiterlesen.
+
+ m.init_error = Die Anwendung konnte wegen folgendem Fehler nicht gestartet \
+-werden:\n{0}\n\n Bitte auf \n{1} weiterlesen, um zu erfahren, wie bei \
+-solchen Problemen vorzugehen ist.
++ werden:\n{0}\n\n Bitte auf \n{1} weiterlesen, um zu erfahren, wie bei \
++ solchen Problemen vorzugehen ist.
+
+-m.readonly_error = Das Verzeichnis, in dem die Anwendung installiert ist: \
+- \n{0}\nist nicht schreibberechtigt. Bitte in ein Verzeichnis mit \
+-Schreibzugriff installieren.
++m.readonly_error = Das Verzeichnis, in dem die Anwendung installiert ist:\n{0}\n \
++ ist nicht schreibberechtigt. Bitte in ein Verzeichnis mit Schreibzugriff installieren.
+
+ m.missing_resource = Die Anwendung konnte nicht gestartet werden, da die \
+-folgende Quelle nicht gefunden wurde:\n{0}\n\n\ Bitte auf \n{1} \
+-weiterlesen, um zu erfahren, wie bei solchen Problemen vorzugehen ist.
++ folgende Quelle nicht gefunden wurde:\n{0}\n\n\ Bitte auf \n{1} \
++ weiterlesen, um zu erfahren, wie bei solchen Problemen vorzugehen ist.
+
+ m.insufficient_permissions_error = Du hast die digitale Signatur dieser \
+-Anwendung nicht akzeptiert. Falls du diese Anwendung benutzen willst, \
+-musst du ihre digitale Signatur akzeptieren. \n\Um das zu tun, musst du \
+-deinen Browser beenden, neu starten und erneut die Anwendung von dieser \
+-Webseite aus starten. Wenn die Sicherheitsabfrage erscheint, bitte die \
+-digitale Signatur akzeptieren, um der Anwendung die n\u00f6tigen Rechte zu \
+-geben, die sie braucht, um zu laufen.
++ Anwendung nicht akzeptiert. Falls du diese Anwendung benutzen willst, \
++ musst du ihre digitale Signatur akzeptieren. \n\Um das zu tun, musst du \
++ deinen Browser beenden, neu starten und erneut die Anwendung von dieser \
++ Webseite aus starten. Wenn die Sicherheitsabfrage erscheint, bitte die \
++ digitale Signatur akzeptieren, um der Anwendung die nötigen Rechte zu \
++ geben, die sie braucht, um zu laufen.
+
+ m.corrupt_digest_signature_error = Wir konnten die digitale Signatur \
+-dieser Anwendung nicht \u00fcberpr\u00fcfen.\nBitte \u00fcberpr\u00fcfe, ob du die Anwendung \
+-von der richtigen Webseite aus startest.
++ dieser Anwendung nicht überprüfen.\nBitte überprüfe, ob du die Anwendung \
++ von der richtigen Webseite aus startest.
+
+ m.default_install_error = der Support-Webseite
+
+-m.another_getdown_running = Diese Installationsanwendung l\u00e4uft in mehreren \
+-Instanzen. Diese Instanz wird sich beenden und eine andere Instanz den \
+-Vorgang erledigen lassen.
+-
+-m.applet_stopped = Die Anwendung wurde beendet.
++m.another_getdown_running = Diese Installationsanwendung läuft in mehreren \
++ Instanzen. Diese Instanz wird sich beenden und eine andere Instanz den \
++ Vorgang erledigen lassen.
+
++m.verify_timeout = Timeout beim Verifizieren der Resourcen.
+
+ # application/digest errors
+ m.missing_appbase = In der Konfigurationsdatei fehlt die 'appbase'.
+ m.invalid_version = In der Konfigurationsdatei steht die falsche Version.
+ m.invalid_appbase = In der Konfigurationsdatei steht die falsche 'appbase'.
+ m.missing_class = In der Konfigurationsdatei fehlt die Anwendungsklasse.
+-m.missing_code = Die Konfigurationsdatei enth\u00e4lt keine Codequellen.
+-m.invalid_digest_file = Die Hashwertedatei ist ung\u00fcltig.
+-
++m.missing_code = Die Konfigurationsdatei enthält keine Codequellen.
++m.invalid_digest_file = Die Hashwertedatei ist ungültig.
+diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_es.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_es.properties
+index 609b02524..46cd64ac9 100644
+--- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_es.properties
++++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_es.properties
+@@ -1,36 +1,34 @@
+ #
+-# $Id$
+-#
+-# Getdown translation messages
++# Getdown Spanish translation messages
+
+-m.abort_title = \u00bfCancelar la instalaci\u00f3n?
+-m.abort_confirm = <html>\u00bfEst\u00e1s seguro de querer cancelar la instalaci\u00f3n? \
+- Puedes continuarla despu\u00e9s si corres de nuevo la aplicaci\u00f3n.</html>
++m.abort_title = ¿Cancelar la instalación?
++m.abort_confirm = <html>¿Estás seguro de querer cancelar la instalación? \
++ Puedes continuarla después si corres de nuevo la aplicación.</html>
+ m.abort_ok = Cancelar
+-m.abort_cancel = Continuar la instalaci\u00f3n
++m.abort_cancel = Continuar la instalación
+
+-m.detecting_proxy = Detectando autom\u00e1ticamente la configuraci\u00f3n proxy
++m.detecting_proxy = Detectando automáticamente la configuración proxy
+
+ m.configure_proxy = <html>No ha sido posible conectar con nuestros servidores para \
+ descargar los datos del juego. \
+ <ul><li> Si el cortafuegos de Windows o Norton Internet Security tiene instrucciones \
+- de bloquear <code>javaw.exe</code> no podemos descargar el juego. Necesitar\u00e1s \
++ de bloquear <code>javaw.exe</code> no podemos descargar el juego. Necesitarás \
+ permitir que <code>javaw.exe</code> tenga acceso al Internet. Puedes intentar \
+ correr el juego de nuevo, pero es posible que debas dar permisos a javaw.exe en la \
+- configuraci\u00f3n de tu cortafuegos ( Inicio -> Panel de control -> Firewall de Windows ).</ul> \
++ configuración de tu cortafuegos ( Inicio -> Panel de control -> Firewall de Windows ).</ul> \
+ <p> Es posible que tu computadora tenga acceso al Internet por medio de un proxy por lo que \
+- no ha sido posible detectar autom\u00e1ticamente tu configuraci\u00f3n. Si conoces tu \
+- configuraci\u00f3n proxy, puedes anotarla abajo.</html>
++ no ha sido posible detectar automáticamente tu configuración. Si conoces tu \
++ configuración proxy, puedes anotarla abajo.</html>
+
+-m.proxy_extra = <html>Si est\u00e1s seguro de que no tienes un proxy entonces \
+- tal vez exista un falla temporal en el Internet que est\u00e1 evitando que podamos \
++m.proxy_extra = <html>Si estás seguro de que no tienes un proxy entonces \
++ tal vez exista un falla temporal en el Internet que está evitando que podamos \
+ comunicarnos con los servidores. En este caso, puedes cancelar e intentar \
+- instalarla de nuevo m\u00e1s tarde.</html>
++ instalarla de nuevo más tarde.</html>
+
+ m.proxy_host = IP proxy
+ m.proxy_port = Puerto proxy
+ m.proxy_username = Nombre de usuario
+-m.proxy_password = Contrase\u00f1a
++m.proxy_password = Contraseña
+ m.proxy_auth_required = Autenticacion requerida
+ m.proxy_ok = OK
+ m.proxy_cancel = Cancelar
+@@ -54,62 +52,59 @@ m.remain = {0} restante
+
+ m.updating_metadata = Descargando los archivos de control
+
+-m.init_failed = Un archivo de configuraci\u00f3n est\u00e1 faltante o est\u00e1 corrupto. Intentando \
++m.init_failed = Un archivo de configuración está faltante o está corrupto. Intentando \
+ descargar una nueva copia...
+
+-m.java_download_failed = No ha sido posible descargar autom\u00e1ticamente la \
+- versi\u00f3n de Java necesaria para tu computadora.\n\n\
+- Por favor ve a www.java.com y descarga la \u00faltima versi\u00f3n de \
+- Java, despu\u00e9s intenta correr de nuevo la aplicaci\u00f3n.
++m.java_download_failed = No ha sido posible descargar automáticamente la \
++ versión de Java necesaria para tu computadora.\n\n\
++ Por favor ve a www.java.com y descarga la última versión de \
++ Java, después intenta correr de nuevo la aplicación.
+
+-m.java_unpack_failed = No ha sido posible desempacar una versi\u00f3n actualizada de \
+- Java. Por favor aseg\u00farate de tener al menos 100 MB de espacio libre en tu \
+- disco duro e intenta correr de nuevo la aplicaci\u00f3n.\n\n\
++m.java_unpack_failed = No ha sido posible desempacar una versión actualizada de \
++ Java. Por favor asegúrate de tener al menos 100 MB de espacio libre en tu \
++ disco duro e intenta correr de nuevo la aplicación.\n\n\
+ Si eso no soluciona el problema, ve a www.java.com y descarga e \
+- instala la \u00faltima versi\u00f3n de Java e intenta de nuevo.
++ instala la última versión de Java e intenta de nuevo.
+
+-m.unable_to_repair = No ha sido posible descargar los archivos necesarios despu\u00e9s de \
+- cinco intentos. Puedes intentar correr de nuevo la aplicaci\u00f3n, pero si falla \
+- de nuevo podr\u00edas necesitar desinstalar y reinstalar.
++m.unable_to_repair = No ha sido posible descargar los archivos necesarios después de \
++ cinco intentos. Puedes intentar correr de nuevo la aplicación, pero si falla \
++ de nuevo podrías necesitar desinstalar y reinstalar.
+
+-m.unknown_error = La aplicaci\u00f3n no ha podido iniciar debido a un extra\u00f1o \
+- error del que no se pudo recobrar. Por favor visita\n{0} para ver informaci\u00f3n acerca \
++m.unknown_error = La aplicación no ha podido iniciar debido a un extraño \
++ error del que no se pudo recobrar. Por favor visita\n{0} para ver información acerca \
+ de como recuperarla.
+-m.init_error = La aplicaci\u00f3n no ha podido iniciar debido al siguiente \
++m.init_error = La aplicación no ha podido iniciar debido al siguiente \
+ error:\n{0}\n\nPor favor visita\n{1} para \
+- ver informaci\u00f3n acerca de como manejar ese tipo de problemas.
++ ver información acerca de como manejar ese tipo de problemas.
+
+-m.readonly_error = El directorio en el que esta aplicaci\u00f3n est\u00e1 instalada: \
+- \n{0}\nes solo lectura. Por favor instala la aplicaci\u00f3n en un directorio en el cual \
++m.readonly_error = El directorio en el que esta aplicación está instalada: \
++ \n{0}\nes solo lectura. Por favor instala la aplicación en un directorio en el cual \
+ tengas acceso de escritura.
+
+-m.missing_resource = La aplicaci\u00f3n no ha podido iniciar debido a un recurso \
+- faltante:\n{0}\n\nPor favor visita\n{1} para informaci\u00f3n acerca de como solucionar \
++m.missing_resource = La aplicación no ha podido iniciar debido a un recurso \
++ faltante:\n{0}\n\nPor favor visita\n{1} para información acerca de como solucionar \
+ estos problemas.
+
+ m.insufficient_permissions_error = No aceptaste la firma digital de \
+- esta aplicaci\u00f3n. Si quieres correr la aplicaci\u00f3n, necesitas aceptar \
++ esta aplicación. Si quieres correr la aplicación, necesitas aceptar \
+ su firma digital.\n\nPara hacerlo, necesitas cerrar tu navegador, \
+- reiniciarlo, y regresar a esta p\u00e1gina web para reiniciar la aplicaci\u00f3n. Cuando se muestre \
+- el di\u00e1logo de seguridad, haz clic en el bot\u00f3n para aceptar la firmar digital \
+- y otorgar a esta aplicaci\u00f3n los privilegios que necesita para correr.
++ reiniciarlo, y regresar a esta página web para reiniciar la aplicación. Cuando se muestre \
++ el diálogo de seguridad, haz clic en el botón para aceptar la firmar digital \
++ y otorgar a esta aplicación los privilegios que necesita para correr.
+
+ m.corrupt_digest_signature_error = No pudimos verificar la firma digital \
+- de la aplicaci\u00f3n.\nPor favor revisa que est\u00e9s lanzando la aplicaci\u00f3n desde\nel \
++ de la aplicación.\nPor favor revisa que estés lanzando la aplicación desde\nel \
+ sitio web correcto.
+
+-m.default_install_error = la secci\u00f3n de asistencia de este sitio web
+-
+-m.another_getdown_running = Est\u00e1n corriendo m\u00faltiples instancias de \
+- este instalador. Este se detendr\u00e1 para permitir que otra contin\u00fae.
++m.default_install_error = la sección de asistencia de este sitio web
+
+-m.applet_stopped = Se le dijo al applet de Getdown que dejara de trabajar.
++m.another_getdown_running = Están corriendo múltiples instancias de \
++ este instalador. Este se detendrá para permitir que otra continúe.
+
+ # application/digest errors
+-m.missing_appbase = Al archivo de configuraci\u00f3n le falta el 'appbase'.
+-m.invalid_version = El archivo de configuraci\u00f3n especifica una versi\u00f3n no v\u00e1lida.
+-m.invalid_appbase = El archivo de configuraci\u00f3n especifica un 'appbase' no v\u00e1lido.
+-m.missing_class = Al archivo de configuraci\u00f3n le falta la clase de aplicaci\u00f3n.
+-m.missing_code = El archivo de configuraci\u00f3n especifica que no hay recursos de c\u00f3digo.
+-m.invalid_digest_file = El archivo digest no es v\u00e1lido.
+-
++m.missing_appbase = Al archivo de configuración le falta el 'appbase'.
++m.invalid_version = El archivo de configuración especifica una versión no válida.
++m.invalid_appbase = El archivo de configuración especifica un 'appbase' no válido.
++m.missing_class = Al archivo de configuración le falta la clase de aplicación.
++m.missing_code = El archivo de configuración especifica que no hay recursos de código.
++m.invalid_digest_file = El archivo digest no es válido.
+diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_fr.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_fr.properties
+index 3666204e2..5eb8ec9cd 100644
+--- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_fr.properties
++++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_fr.properties
+@@ -1,29 +1,27 @@
+ #
+-# $Id: messages.properties 485 2012-03-08 22:05:30Z ray.j.greenwell $
+-#
+-# Getdown translation messages
++# Getdown French translation messages
+
+ m.abort_title = Annuler l'installation?
+-m.abort_confirm =<html>\u00cates-vous s\u00fbr de vouloir annuler l'installation? \
+- Vous pourrez reprendre l'installation en ex\u00e9cutant l'application de nouveau.</html>
++m.abort_confirm =<html>Êtes-vous sûr de vouloir annuler l'installation? \
++ Vous pourrez reprendre l'installation en exécutant l'application de nouveau.</html>
+ m.abort_ok = Quitter
+ m.abort_cancel = Continuer l'installation
+
+-m.detecting_proxy = D\u00e9tection automatique des r\u00e9glages proxy
++m.detecting_proxy = Détection automatique des réglages proxy
+
+ m.configure_proxy =<html>Connexion au serveur impossible. \
+- <ul><li> Veuillez v\u00e9rifier que <code>javaw.exe</code> n'est bloqu\u00e9 \
++ <ul><li> Veuillez vérifier que <code>javaw.exe</code> n'est bloqué \
+ par aucun pare-feu ou antivirus. \
+ Vous pouvez vous rendre sur la configuration du pare-feu windows via \
+- (D\u00e9marrer -> Panneau de Configuration -> Pare-feu Windows ).</ul> \
+- <p> Il est \u00e9galement possible que vous soyez derri\u00e8re un proxy que l'application \
+- est incapable de d\u00e9tecter automatiquement. \
+- Si tel est le cas, veuillez saisir les r\u00e9glages proxy ci-dessous.</html>
++ (Démarrer -> Panneau de Configuration -> Pare-feu Windows ).</ul> \
++ <p> Il est également possible que vous soyez derrière un proxy que l'application \
++ est incapable de détecter automatiquement. \
++ Si tel est le cas, veuillez saisir les réglages proxy ci-dessous.</html>
+
+-m.proxy_extra =<html>Si vous \u00eates certain de ne pas utiliser de proxy, il est \
+- possible qu'une interruption temporaire de la connexion internet emp\u00fbche la \
++m.proxy_extra =<html>Si vous êtes certain de ne pas utiliser de proxy, il est \
++ possible qu'une interruption temporaire de la connexion internet empûche la \
+ communication avec les serveurs. Dans ce cas, vous pouvez relancer \
+- l'installation ult\u00e9rieurement.</html>
++ l'installation ultérieurement.</html>
+
+ m.proxy_host = Proxy IP
+ m.proxy_port = Proxy port
+@@ -33,79 +31,77 @@ m.proxy_auth_required = Identification requise
+ m.proxy_ok = OK
+ m.proxy_cancel = Annuler
+
+-m.downloading_java = T\u00e9l\u00e9chargement en cours de la Machine Virtuelle Java
+-m.unpacking_java = D\u00e9compression en cours de la Machine Virtuelle Java
++m.downloading_java = Téléchargement en cours de la Machine Virtuelle Java
++m.unpacking_java = Décompression en cours de la Machine Virtuelle Java
+
+-m.resolving = R\u00e9solution des t\u00e9l\u00e9chargements en cours
+-m.downloading = T\u00e9l\u00e9chargement des donn\u00e9es en cours
+-m.failure = \u00c9chec du t\u00e9l\u00e9chargement: {0}
++m.resolving = Résolution des téléchargements en cours
++m.downloading = Téléchargement des données en cours
++m.failure = Échec du téléchargement: {0}
+
+-m.checking = V\u00e9rification de la mise-\u00e0-jour en cours
++m.checking = Vérification de la mise-à-jour en cours
+ m.validating = Validation en cours
+ m.patching = Modification en cours
+ m.launching = Lancement en cours
+
+-m.patch_notes = Notes de mise-\u00e0-jour
++m.patch_notes = Notes de mise-à-jour
+
+-m.complete = Complet \u00e0 {0}%
++m.complete = Complet à {0}%
+ m.remain = {0} restant
+
+-m.updating_metadata = T\u00e9l\u00e9chargement des fichiers de contr\u00f4les en cours
++m.updating_metadata = Téléchargement des fichiers de contrôles en cours
+
+-m.init_failed = Notre fichier de configuration est perdu ou corrompu. T\u00e9l\u00e9chargement \
++m.init_failed = Notre fichier de configuration est perdu ou corrompu. Téléchargement \
+ d'une nouvelle copie en cours ...
+
+-m.java_download_failed = Impossible de t\u00e9l\u00e9charger automatiquement la \
+- version de Java n\u00e9cessaire.\n\n\
+- Veuillez vous rendre sur www.java.com et t\u00e9l\u00e9charger et installer la version \
+- la plus r\u00e9cente de Java, avant d'ex\u00e9cuter l'application \u00e0 nouveau.
++m.java_download_failed = Impossible de télécharger automatiquement la \
++ version de Java nécessaire.\n\n\
++ Veuillez vous rendre sur www.java.com et télécharger et installer la version \
++ la plus récente de Java, avant d'exécuter l'application à nouveau.
+
+-m.java_unpack_failed = Impossible de d\u00e9compresser la version de \
+- Java n\u00e9cessaire. Veuillez v\u00e9rifier que vous avez au moins 100 MB d'espace libre \
+- sur votre disque dur puis tenter d'ex\u00e9cuter l'application \u00e0 nouveau.\n\n\
+- Si le probl\u00e8me persiste, rendez vous www.java.com et t\u00e9l\u00e9chargez et \
+- installez la version plus r\u00e9cente de Java puis essayez de nouveau.
++m.java_unpack_failed = Impossible de décompresser la version de \
++ Java nécessaire. Veuillez vérifier que vous avez au moins 100 MB d'espace libre \
++ sur votre disque dur puis tenter d'exécuter l'application à nouveau.\n\n\
++ Si le problème persiste, rendez vous www.java.com et téléchargez et \
++ installez la version plus récente de Java puis essayez de nouveau.
+
+-m.unable_to_repair = Impossible de t\u00e9l\u00e9charger les fichiers n\u00e9cessaires apr\u00e8s \
+- cinq tentatives. Vous pouvez tenter d'ex\u00e9cuter l'application \u00e0 nouveau, mais il est \
+- possible qu'une d\u00e9sinstallation / r\u00e9installation soit n\u00e9cessaire.
++m.unable_to_repair = Impossible de télécharger les fichiers nécessaires après \
++ cinq tentatives. Vous pouvez tenter d'exécuter l'application à nouveau, mais il est \
++ possible qu'une désinstallation / réinstallation soit nécessaire.
+
+-m.unknown_error = Une erreur inconnue a fait \u00e9chouer le lancement de l'application. \
++m.unknown_error = Une erreur inconnue a fait échouer le lancement de l'application. \
+ Veuillez visiter\n{0} pour plus d'informations.
+-m.init_error = Le lancement de l'application a \u00e9chou\u00e9 \u00e0 cause de l'erreur \
++m.init_error = Le lancement de l'application a échoué à cause de l'erreur \
+ suivante:\n{0}\n\nVeuillez visiter\n{1} pour plus d'informations.
+
+-m.readonly_error = Le r\u00e9pertoire d'installation de cette application: \
+- \n{0}\nest en lecture seule. Veuillez installer l'application dans un r\u00e9pertoire avec \
+- un acc\u00e8s en \u00e9criture.
++m.readonly_error = Le répertoire d'installation de cette application: \
++ \n{0}\nest en lecture seule. Veuillez installer l'application dans un répertoire avec \
++ un accès en écriture.
+
+-m.missing_resource = Le lancement de l'application a \u00e9chou\u00e9 \u00e0 cause d'une \
++m.missing_resource = Le lancement de l'application a échoué à cause d'une \
+ ressource manquante:\n{0}\n\nVeuillez visiter\n{1} pour plus d'informations.
+
+ m.insufficient_permissions_error = Vous n'avez pas accepter la signature \
+- num\u00e9rique de cette application. Si vous souhaitez ex\u00e9cuter cette application, vous \
+- devez accepter sa signature num\u00e9rique.\n\nAfin de le faire, vous devez quitter votre \
+- navigateur, le red\u00e9marrer, retourner \u00e0 cette page puis relancer l'application. \
+- Une fois la bo\u00eete de dialogue de s\u00e9curit\u00e9 affich\u00e9e, cliquez sur le bouton \
+- pour accepter la signature num\u00e9rique et accorder les permissions n\u00e9cessaires au bon \
++ numérique de cette application. Si vous souhaitez exécuter cette application, vous \
++ devez accepter sa signature numérique.\n\nAfin de le faire, vous devez quitter votre \
++ navigateur, le redémarrer, retourner à cette page puis relancer l'application. \
++ Une fois la boîte de dialogue de sécurité affichée, cliquez sur le bouton \
++ pour accepter la signature numérique et accorder les permissions nécessaires au bon \
+ fonctionnement de l'application.
+
+-m.corrupt_digest_signature_error = Nous ne pouvons pas v\u00e9rifier la signature num\u00e9rique \
+- de l'application.\nVeuillez v\u00e9rifier que vous lancez l'application \ndepuis \
++m.corrupt_digest_signature_error = Nous ne pouvons pas vérifier la signature numérique \
++ de l'application.\nVeuillez vérifier que vous lancez l'application \ndepuis \
+ la bonne adresse internet.
+
+ m.default_install_error = la section de support du site
+
+ m.another_getdown_running = Plusieurs instances d'installation de cette \
+- application sont d\u00e9j\u00e0 en cours d'ex\u00e9cution. Cette instance va s'arr\u00eater \
++ application sont déjà en cours d'exécution. Cette instance va s'arrêter \
+ afin de permettre aux autres d'aboutir.
+
+-m.applet_stopped = L'appelet Getdown a \u00e9t\u00e9 stopp\u00e9e.
+-
+ # application/digest errors
+ m.missing_appbase = Le fichier de configuration ne contient pas 'appbase'.
+-m.invalid_version = Le fichier de configuration sp\u00e9cifie une version invalide.
+-m.invalid_appbase = Le fichier de configuration sp\u00e9cifie un 'appbase' invalide.
++m.invalid_version = Le fichier de configuration spécifie une version invalide.
++m.invalid_appbase = Le fichier de configuration spécifie un 'appbase' invalide.
+ m.missing_class = Le fichier de configuration ne contient pas la classe de l'application.
+-m.missing_code = Le fichier de configuration ne sp\u00e9cifie aucune ressource de code.
++m.missing_code = Le fichier de configuration ne spécifie aucune ressource de code.
+ m.invalid_digest_file = Le fichier digest est invalide.
+diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_it.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_it.properties
+index 33b3260ce..aea9e9017 100644
+--- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_it.properties
++++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_it.properties
+@@ -1,7 +1,5 @@
+ #
+-# $Id$
+-#
+-# Getdown translation messages
++# Getdown Italian translation messages
+
+ m.abort_title = Annullare l'installazione?
+ m.abort_confirm = <html>Sei sicuro di voler annullare l'installazione? \
+@@ -22,7 +20,11 @@ m.configure_proxy = <html>Impossibile collegarsi al server per \
+ questo potrebbe non essere stato riconosciuto automaticamente. Se conosci le \
+ tue impostazioni del proxy, puoi inserirle di seguito.</html>
+
+-m.proxy_extra = <html>Se sei sicuro di non usare proxy \
++m.update_proxy_auth = <html>Combinazione User/Password salvata per il proxy non è valida \
++ oppure obsoleta. \
++ <p>Perfavore inserire una combinazione User/Password valida.</html>
++
++m.proxy_extra = <html>Se sei sicuro di non usare proxy \
+ potrebbe essere un problema di internet o di collegamento con il server. \
+ In questo caso puoi annullare e ripetere l'installazione più tardi.</html>
+
+@@ -47,8 +49,6 @@ m.patching = Applico le patch
+ m.launching = Avvio
+
+ m.patch_notes = Note delle Patch
+-m.play_again = Avvia Nuovamente
+-
+ m.complete = {0}% completato
+ m.remain = {0} rimasto
+
+@@ -103,8 +103,6 @@ m.default_install_error = la sezione di supporto del sito
+ m.another_getdown_running = E' già in esecuzione un'istanza del programma. \
+ Questa verrà chiusa.
+
+-m.applet_stopped = L'applet di Getdown è stata interrotta.
+-
+ # application/digest errors
+ m.missing_appbase = Il tag "appbase" è mancante.
+ m.invalid_version = Il file di configurazione non contiene una versione valida (tag "version").
+diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ja.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ja.properties
+index c344c16e0..f3538d0ac 100644
+--- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ja.properties
++++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ja.properties
+@@ -1,107 +1,105 @@
+ #
+-# $Id$
+-#
+-# Getdown translation messages
+-
+-m.abort_title = \u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u4e2d\u6b62\u3057\u307e\u3059\u304b\uff1f
+-m.abort_confirm = <html>\u672c\u5f53\u306b\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u4e2d\u6b62\u3057\u307e\u3059\u304b\uff1f \
+- \u5f8c\u3067\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u8d77\u52d5\u3057\u305f\u969b\u306b\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u518d\u958b\u3067\u304d\u307e\u3059\u3002</html>
+-m.abort_ok = \u4e2d\u6b62
+-m.abort_cancel = \u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306e\u7d9a\u884c
+-
+-m.detecting_proxy = \u81ea\u52d5\u30d7\u30ed\u30ad\u30b7\u8a2d\u5b9a\u5b9f\u884c\u4e2d
+-
+-m.configure_proxy = <html>\u30b5\u30fc\u30d0\u306b\u63a5\u7d9a\u3067\u304d\u306a\u3044\u305f\u3081\u3001\u30b2\u30fc\u30e0\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u306b \
+- \u5931\u6557\u3057\u307e\u3057\u305f\u3002 \
+- <ul><li>\u30a6\u30a3\u30f3\u30c9\u30a6\u30ba\u30d5\u30a1\u30a4\u30a2\u30a6\u30a9\u30fc\u30eb\u307e\u305f\u306f\u30ce\u30fc\u30c8\u30f3\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u304c \
+- <code>javaw.exe</code>\u3092\u30d6\u30ed\u30c3\u30af\u3059\u308b\u3088\u3046\u8a2d\u5b9a\u3057\u3066\u3042\u308b\u5834\u5408\u306f\u3001\u30b2\u30fc\u30e0\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3067\u304d\u307e\u305b\u3093\u3002 \u8a2d\u5b9a\u3092 \
+- <code>javaw.exe</code>\u7d4c\u7531\u3067\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u306b\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u3088\u3046\u306b\u5909\u66f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \u30b2\u30fc\u30e0\u3092\u518d\u8d77\u52d5 \
+- \u3057\u305f\u5f8c\u3001\u30d5\u30a1\u30a4\u30a2\u30a6\u30a9\u30fc\u30eb\u306e\u8a2d\u5b9a\u304b\u3089javaw.exe \u3092\u524a\u9664 \
+- \u3057\u3066\u304f\u3060\u3055\u3044\uff08\u30b9\u30bf\u30fc\u30c8\u2192\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d1\u30cd\u30eb\u2192\u30d5\u30a1\u30a4\u30a2\u30a6\u30a9\u30fc\u30eb\uff09\u3002</ul> \
+- <p>\u30d7\u30ed\u30ad\u30b7\u8a2d\u5b9a\u306e\u81ea\u52d5\u691c\u51fa\u304c\u3067\u304d\u307e\u305b\u3093\u3002\u304a\u4f7f\u3044\u306e\u30b3\u30f3\u30d4\u30e5\u30fc\u30bf\u30fc\u306f \
+- \u30d7\u30ed\u30ad\u30b7\u3092\u4f7f\u7528\u3057\u3066\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u3078\u30a2\u30af\u30bb\u30b9\u3057\u3066\u3044\u307e\u3059\u3002 \u30d7\u30ed\u30ad\u30b7\u8a2d\u5b9a\u306e\u8a73\u7d30\u304c \
+- \u308f\u304b\u3063\u3066\u3044\u308b\u5834\u5408\u306f\u3001\u4e0b\u306b\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002</html>
+-
+-m.proxy_extra = <html>\u30d7\u30ed\u30ad\u30b7\u3092\u4f7f\u7528\u3057\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u4e00\u6642\u7684\u306a \
+- \u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u306e\u4e0d\u5177\u5408\u306b\u3088\u308a\u3001\u30b5\u30fc\u30d0\u3068\u4ea4\u4fe1\u3067\u304d\u306a\u3044\u72b6\u614b\u306b\u3042\u308b \
+- \u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002 \u305d\u306e\u5834\u5408\u306f\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u30ad\u30e3\u30f3\u30bb\u30eb\u3057\u3066\u3001 \
+- \u5f8c\u307b\u3069\u6539\u3081\u3066\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002</html>
+-
+-m.proxy_host = \u30d7\u30ed\u30ad\u30b7IP
+-m.proxy_port = \u30d7\u30ed\u30ad\u30b7\u30dd\u30fc\u30c8
++# Getdown Japanese translation messages
++
++m.abort_title = インストールを中止しますか?
++m.abort_confirm = <html>本当にインストールを中止しますか? \
++ 後でアプリケーションを起動した際にインストールを再開できます。</html>
++m.abort_ok = 中止
++m.abort_cancel = インストールの続行
++
++m.detecting_proxy = 自動プロキシ設定実行中
++
++m.configure_proxy = <html>サーバに接続できないため、ゲームのダウンロードに \
++ 失敗しました。 \
++ <ul><li>ウィンドウズファイアウォールまたはノートンインターネットセキュリティが \
++ <code>javaw.exe</code>をブロックするよう設定してある場合は、ゲームをダウンロードできません。 設定を \
++ <code>javaw.exe</code>経由でインターネットにアクセスできるように変更してください。 ゲームを再起動 \
++ した後、ファイアウォールの設定からjavaw.exe を削除 \
++ してください(スタート→コントロールパネル→ファイアウォール)。</ul> \
++ <p>プロキシ設定の自動検出ができません。お使いのコンピューターは \
++ プロキシを使用してインターネットへアクセスしています。 プロキシ設定の詳細が \
++ わかっている場合は、下に入力してください。</html>
++
++m.proxy_extra = <html>プロキシを使用していない場合は、一時的な \
++ インターネットの不具合により、サーバと交信できない状態にある \
++ 可能性があります。 その場合はインストールをキャンセルして、 \
++ 後ほど改めて実行してください。</html>
++
++m.proxy_host = プロキシIP
++m.proxy_port = プロキシポート
+ m.proxy_username = Username
+ m.proxy_password = Password
+ m.proxy_auth_required = Authentication required
+-m.proxy_ok = OK
+-m.proxy_cancel = \u30ad\u30e3\u30f3\u30bb\u30eb
++m.proxy_ok = OK
++m.proxy_cancel = キャンセル
+
+-m.downloading_java = Java\u30d0\u30fc\u30c1\u30e3\u30eb\u30de\u30b7\u30f3\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u4e2d
+-m.unpacking_java = Java\u30d0\u30fc\u30c1\u30e3\u30eb\u30de\u30b7\u30f3\u306e\u89e3\u51cd\u4e2d
++m.downloading_java = Javaバーチャルマシンのダウンロード中
++m.unpacking_java = Javaバーチャルマシンの解凍中
+
+-m.resolving = \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u306e\u8a2d\u5b9a\u4e2d
+-m.downloading = \u30c7\u30fc\u30bf\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u4e2d
+-m.failure = \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u5931\u6557\uff1a {0}
++m.resolving = ダウンロードの設定中
++m.downloading = データのダウンロード中
++m.failure = ダウンロード失敗: {0}
+
+-m.checking = \u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u306e\u78ba\u8a8d\u4e2d
+-m.validating = \u8a8d\u8a3c\u4e2d
+-m.patching = \u4fee\u6b63\u30d7\u30ed\u30b0\u30e9\u30e0\u306e\u5b9f\u884c\u4e2d
+-m.launching = \u5b9f\u884c\u4e2d
++m.checking = アップデートの確認中
++m.validating = 認証中
++m.patching = 修正プログラムの実行中
++m.launching = 実行中
+
+-m.complete = {0}\uff05\u5b8c\u4e86
+-m.remain = \u3000\u6b8b\u308a{0}
++m.complete = {0}%完了
++m.remain = 残り{0}
+
+-m.updating_metadata = \u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d5\u30a1\u30a4\u30eb\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u4e2d
++m.updating_metadata = コントロールファイルのダウンロード中
+
+-m.init_failed = \u74b0\u5883\u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u304c\u5b58\u5728\u3057\u306a\u3044\u304b\u3001\u307e\u305f\u306f\u58ca\u308c\u3066\u3044\u307e\u3059\u3002 \u65b0\u30d0\u30fc\u30b8\u30e7\u30f3\u3092 \
+- \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u4e2d\u2026
++m.init_failed = 環境設定ファイルが存在しないか、または壊れています。 新バージョンを \
++ ダウンロード中…
+
+-m.java_download_failed = \u304a\u4f7f\u3044\u306e\u30b3\u30f3\u30d4\u30e5\u30fc\u30bf\u30fc\u306b\u3001Java\u30d7\u30ed\u30b0\u30e9\u30e0\u306e\u6700\u65b0 \
+- \u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u81ea\u52d5\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\n\n \
+- www.java.com \u304b\u3089\u6700\u65b0\u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u624b\u52d5\u3067\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3057\u3066\u3001 \
+- \u518d\u5ea6\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u8d77\u52d5\u3057\u3066\u304f\u3060\u3055\u3044\u3002
++m.java_download_failed = お使いのコンピューターに、Javaプログラムの最新 \
++ バージョンを自動インストールできませんでした。\n\n \
++ www.java.com から最新バージョンを手動でダウンロードして、 \
++ 再度アプリケーションを起動してください。
+
+-m.java_unpack_failed = Java\u306e\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u89e3\u51cd \
+- \u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002 \u30cf\u30fc\u30c9\u30c9\u30e9\u30a4\u30d6\u306e\u30e1\u30e2\u30ea\u304c100MB\u4ee5\u4e0a\u3042\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304b\u3089 \
+- \u518d\u5ea6\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u8d77\u52d5\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n \
+- \u554f\u984c\u304c\u89e3\u6c7a\u3057\u306a\u3044\u5834\u5408\u306f\u3001www.java.com \u304b\u3089Java\u306e\u6700\u65b0\u30d0\u30fc\u30b8\u30e7\u30f3\u3092 \
+- \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3057\u3066\u304b\u3089\u3001\u518d\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002
++m.java_unpack_failed = Javaのアップデートバージョンが解凍 \
++ できませんでした。 ハードドライブのメモリが100MB以上あることを確認してから \
++ 再度アプリケーションを起動してください。\n\n \
++ 問題が解決しない場合は、www.java.com からJavaの最新バージョンを \
++ ダウンロードしてから、再度お試しください。
+
+-m.unable_to_repair = 5\u56de\u8a66\u884c\u3057\u307e\u3057\u305f\u304c\u3001\u5fc5\u8981\u306a\u30d5\u30a1\u30a4\u30eb\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9 \
+- \u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002 \u5f8c\u307b\u3069\u6539\u3081\u3066\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \
+- \u518d\u5ea6\u5931\u6557\u3057\u305f\u5834\u5408\u306f\u3001\u30a2\u30f3\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u5f8c\u306b\u518d\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3057\u3066\u304f\u3060\u3055\u3044\u3002
++m.unable_to_repair = 5回試行しましたが、必要なファイルをダウンロード \
++ できませんでした。 後ほど改めてアプリケーションを実行してください。 \
++ 再度失敗した場合は、アンインストール後に再インストールしてください。
+
+-m.unknown_error = \u539f\u56e0\u4e0d\u660e\u306e\u30a8\u30e9\u30fc\u306b\u3088\u308a\u3001\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u304c \
+- \u5b9f\u884c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002 \u89e3\u6c7a\u65b9\u6cd5\u3092\n{0}\u3067 \
+- \u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002
+-m.init_error = \u6b21\u306e\u30a8\u30e9\u30fc\u306b\u3088\u308a\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3067\u304d\u307e\u305b\u3093 \
+- \u3067\u3057\u305f\u3002\n{0}\n\n\u5bfe\u51e6\u65b9\u6cd5\u3092\n{1}\u3067 \
+- \u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002
++m.unknown_error = 原因不明のエラーにより、アプリケーションが \
++ 実行できませんでした。 解決方法を\n{0}で \
++ 確認してください。
++m.init_error = 次のエラーによりアプリケーションを実行できません \
++ でした。\n{0}\n\n対処方法を\n{1}で \
++ 確認してください。
+
+-m.readonly_error = \u3053\u306e\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u304c\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3055\u308c\u305f\u30d5\u30a9\u30eb\u30c0\u306f \
+- \n{0}\n\u8aad\u307f\u53d6\u308a\u5c02\u7528\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002 \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u66f8\u304d\u8fbc\u307f\u304c\u3067\u304d\u308b\u30d5\u30a9\u30eb\u30c0\u306b \
+- \u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3057\u3066\u304f\u3060\u3055\u3044\u3002
++m.readonly_error = このアプリケーションがインストールされたフォルダは \
++ \n{0}\n読み取り専用に設定されています。 アプリケーションを書き込みができるフォルダに \
++ インストールしてください。
+
+-m.missing_resource = \u30ea\u30bd\u30fc\u30b9\u4e0d\u660e\u306e\u305f\u3081\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3067\u304d\u307e\u305b\u3093 \
+- \u3067\u3057\u305f\u3002\n{0}\n\n\u5bfe\u51e6\u65b9\u6cd5\u3092\n{1}\u3067\u78ba\u8a8d \
+- \u3057\u3066\u304f\u3060\u3055\u3044\u3002
++m.missing_resource = リソース不明のためアプリケーションを実行できません \
++ でした。\n{0}\n\n対処方法を\n{1}で確認 \
++ してください。
+
+-m.insufficient_permissions_error = \u3053\u306e\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u30c7\u30b8\u30bf\u30eb\u7f72\u540d\u304c\u62d2\u5426 \
+- \u3055\u308c\u307e\u3057\u305f\u3002 \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3059\u308b\u5834\u5408\u306f\u3001\u30c7\u30b8\u30bf\u30eb\u7f72\u540d\u306e\u627f\u8a8d\u304c \
+- \u5fc5\u8981\u3067\u3059\u3002\n\n\u627f\u8a8d\u306b\u306f\u3001\u30d6\u30e9\u30a6\u30b6\u3092\u9589\u3058\u3066\u304b\u3089\u518d\u5ea6\u958b\u304d\u3001 \
+- \u672c\u30db\u30fc\u30e0\u30da\u30fc\u30b8\u3092\u518d\u8868\u793a\u3057\u3066\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u518d\u5ea6\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044 \u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u306e \
+- \u8b66\u544a\u304c\u8868\u793a\u3055\u308c\u305f\u6642\u306f\u3001\u5b9f\u884c\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u30c7\u30b8\u30bf\u30eb\u7f72\u540d\u3092\u627f\u8a8d\u3057\u3001 \
+- \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002
++m.insufficient_permissions_error = このアプリケーションのデジタル署名が拒否 \
++ されました。 アプリケーションを実行する場合は、デジタル署名の承認が \
++ 必要です。\n\n承認には、ブラウザを閉じてから再度開き、 \
++ 本ホームページを再表示してアプリケーションを再度実行してください セキュリティの \
++ 警告が表示された時は、実行をクリックしてデジタル署名を承認し、 \
++ アプリケーションを実行してください。
+
+-m.corrupt_digest_signature_error = \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u30c7\u30b8\u30bf\u30eb\u7f72\u540d\u304c\u8a8d\u8a3c \
+- \u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\n\u6307\u5b9a\u30db\u30fc\u30e0\u30da\u30fc\u30b8\u304b\u3089\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3057\u3066\u3044\u308b\u304b\n \
+- \u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002
++m.corrupt_digest_signature_error = アプリケーションのデジタル署名が認証 \
++ できませんでした。\n指定ホームページからアプリケーションを実行しているか\n \
++ 確認してください。
+
+-m.default_install_error = \u30db\u30fc\u30e0\u30da\u30fc\u30b8\u3067\u306e\u30b5\u30dd\u30fc\u30c8\u8868\u793a
++m.default_install_error = ホームページでのサポート表示
+
+ # application/digest errors
+-m.missing_appbase = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u306eappbase\u304c\u4e0d\u660e\u3067\u3059\u3002
+-m.invalid_version = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u306f\u7121\u52b9\u306a\u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u6307\u5b9a\u3057\u3066\u3044\u307e\u3059\u3002
+-m.invalid_appbase = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u304c\u7121\u52b9\u306aappbase\u3092\u6307\u5b9a\u3057\u3066\u3044\u307e\u3059\u3002
+-m.missing_class = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u306e\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u30af\u30e9\u30b9\u304c\u4e0d\u660e\u3067\u3059\u3002
+-m.missing_code = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u3067\u30b3\u30fc\u30c9\u30ea\u30bd\u30fc\u30b9\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002
+-m.invalid_digest_file = \u30c0\u30a4\u30b8\u30a7\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u304c\u7121\u52b9\u3067\u3059\u3002
++m.missing_appbase = 設定ファイルのappbaseが不明です。
++m.invalid_version = 設定ファイルは無効なバージョンを指定しています。
++m.invalid_appbase = 設定ファイルが無効なappbaseを指定しています。
++m.missing_class = 設定ファイルのアプリケーションクラスが不明です。
++m.missing_code = 設定ファイルでコードリソースが指定されていません。
++m.invalid_digest_file = ダイジェストファイルが無効です。
+diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ko.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ko.properties
+index 3f8a47f35..05700d363 100644
+--- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ko.properties
++++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ko.properties
+@@ -1,102 +1,96 @@
+ #
+-# $Id$
+-#
+-# Getdown translation messages
++# Getdown Korean translation messages
+
+-m.abort_title = \uC124\uCE58\uB97C \uCDE8\uC18C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?
+-m.abort_confirm = <html>\uC815\uB9D0\uB85C \uC124\uCE58\uB97C \uCDE8\uC18C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C? \
+- \uB098\uC911\uC5D0 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC2E4\uD589\uD558\uC5EC \uC124\uCE58\uB97C \uC7AC\uAC1C\uD558\uC5EC \uC8FC\uC2ED\uC2DC\uC624.</html>
+-m.abort_ok = \uC911\uC9C0
+-m.abort_cancel = \uACC4\uC18D\uD558\uC5EC \uC124\uCE58
++m.abort_title = 설치를 취소하시겠습니까?
++m.abort_confirm = <html>정말로 설치를 취소하시겠습니까? \
++ 나중에 어플리케이션을 실행하여 설치를 재개하여 주십시오.</html>
++m.abort_ok = 중지
++m.abort_cancel = 계속하여 설치
+
+-m.detecting_proxy = \uC790\uB3D9 \uD504\uB85D\uC2DC\uB97C \uC124\uC815\uC744 \uC2DC\uB3C4
++m.detecting_proxy = 자동 프록시를 설정을 시도
+
+-m.configure_proxy = <html>\uAC8C\uC784 \uB370\uC774\uD130\uB97C \uBC1B\uAE30 \uC704\uD55C \uC11C\uBC84 \uC811\uC18D\uC5D0 \uC2E4\uD328\uD558\uC600\uC2B5\uB2C8\uB2E4.\
+- <ul><li>\uC708\uB3C4\uC6B0 \uBC29\uD654\uBCBD \uB610\uB294 \uB178\uD134 \uC778\uD130\uB137 \uC2DC\uD050\uB9AC\uD2F0\uAC00 <code>javaw.exe</code>\uC774 \uC124\uC815\uC5D0\uC11C \uCC28\uB2E8\uB418\uC5B4 \uC788\uC744 \uACBD\uC6B0, \
+- \uAC8C\uC784 \uB370\uC774\uD130\uB97C \uB2E4\uC6B4\uB85C\uB4DC \uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \
+- <code>javaw.exe</code>\uAC00 \uC778\uD130\uB137 \uC5F0\uACB0\uC744 \uD560 \uC218 \uC788\uB3C4\uB85D \uC124\uC815\uC744 \uBCC0\uACBD\uD558\uC5EC \uC8FC\uC2ED\uC2DC\uC624. \
+- \uAC8C\uC784\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD55C \uD6C4, \uBC29\uD654\uBCBD \uC124\uC815\uC5D0\uC11C javaw.exe\uB97C \uC0AD\uC81C\uD558\uC5EC \uC8FC\uC2ED\uC2DC\uC624. \
+- ( \uC2DC\uC791 -> \uC81C\uC5B4\uD310 -> \uC708\uB3C4\uC6B0 \uBC29\uD654\uBCBD )</ul> \
+- <p> \uCEF4\uD4E8\uD130\uAC00 \uD504\uB85D\uC2DC \uC11C\uBC84\uB97C \uD1B5\uD574 \uC778\uD130\uB137\uC5D0 \uC5F0\uACB0\uB418\uC5B4 \uC788\uB2E4\uBA74, \uD504\uB85D\uC2DC \uC124\uC815\uC758 \uC790\uB3D9 \uAD6C\uC131\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC73C\uBBC0\uB85C, \
+- \uC0AC\uC6A9\uD558\uB294 \uD504\uB85D\uC2DC \uC124\uC815\uC744 \uC54C\uACE0 \uC788\uC744 \uACBD\uC6B0 \uC544\uB798\uC5D0 \uC785\uB825\uD558\uC5EC \uC8FC\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4.</html>
++m.configure_proxy = <html>게임 데이터를 받기 위한 서버 접속에 실패하였습니다.\
++ <ul><li>윈도우 방화벽 또는 노턴 인터넷 시큐리티가 <code>javaw.exe</code>이 설정에서 차단되어 있을 경우, \
++ 게임 데이터를 다운로드 할 수 없습니다. \
++ <code>javaw.exe</code>가 인터넷 연결을 할 수 있도록 설정을 변경하여 주십시오. \
++ 게임을 다시 실행한 후, 방화벽 설정에서 javaw.exe를 삭제하여 주십시오. \
++ ( 시작 -> 제어판 -> 윈도우 방화벽 )</ul> \
++ <p> 컴퓨터가 프록시 서버를 통해 인터넷에 연결되어 있다면, 프록시 설정의 자동 구성을 사용할 수 없으므로, \
++ 사용하는 프록시 설정을 알고 있을 경우 아래에 입력하여 주시길 바랍니다.</html>
+
+-m.proxy_extra = \uC790\uB3D9 \uD504\uB85D\uC2DC\uB97C \uC124\uC815\uC744 \uC2DC\uB3C4
++m.proxy_extra = 자동 프록시를 설정을 시도
+
+-m.proxy_host = \uD504\uB85D\uC2DC IP
+-m.proxy_port = \uD504\uB85D\uC2DC \uD3EC\uD2B8
++m.proxy_host = 프록시 IP
++m.proxy_port = 프록시 포트
+ m.proxy_username = Username
+ m.proxy_password = Password
+ m.proxy_auth_required = Authentication required
+ m.proxy_ok = OK
+-m.proxy_cancel = \uCDE8\uC18C
+-
+-m.downloading_java = \uC790\uBC14 \uAC00\uC0C1 \uBA38\uC2E0(JVM) \uB2E4\uC6B4\uB85C\uB4DC \uC911
+-m.unpacking_java = \uC790\uBC14 \uAC00\uC0C1 \uBA38\uC2E0(JVM) \uC555\uCD95\uC744 \uD574\uC81C\uD558\uB294 \uC911
+-
+-m.resolving = \uB2E4\uC6B4\uB85C\uB4DC \uBD84\uC11D \uC911
+-m.downloading = \uB370\uC774\uD130 \uB2E4\uC6B4\uB85C\uB4DC \uC911
+-m.failure = \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: {0}
++m.proxy_cancel = 취소
+
+-m.checking = \uC5C5\uB370\uC774\uD2B8 \uCCB4\uD06C
+-m.validating = \uC720\uD6A8\uC131 \uAC80\uC0AC \uC911
+-m.patching = \uD328\uCE58 \uC911
+-m.launching = \uC2E4\uD589 \uC911
++m.downloading_java = 자바 가상 머신(JVM) 다운로드 중
++m.unpacking_java = 자바 가상 머신(JVM) 압축을 해제하는 중
+
+-m.patch_notes = \uD328\uCE58 \uB178\uD2B8
+-m.play_again = \uB2E4\uC2DC \uC2E4\uD589
++m.resolving = 다운로드 분석 중
++m.downloading = 데이터 다운로드 중
++m.failure = 다운로드 실패: {0}
+
+-m.complete = {0}% \uC644\uB8CC
+-m.remain = {0} \uB0A8\uC74C
++m.checking = 업데이트 체크
++m.validating = 유효성 검사 중
++m.patching = 패치 중
++m.launching = 실행 중
+
+-m.updating_metadata = \uCEE8\uD2B8\uB864 \uD30C\uC77C\uC744 \uB2E4\uC6B4\uB85C\uB4DC \uC911
++m.patch_notes = 패치 노트
++m.complete = {0}% 완료
++m.remain = {0} 남음
+
+-m.init_failed = \uC124\uC815 \uD30C\uC77C\uC774 \uB204\uB77D\uB418\uC5C8\uAC70\uB098 \uBCC0\uD615\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \
+- \uC0C8\uB85C\uC6B4 \uBCF5\uC0AC\uBCF8\uC744 \uB2E4\uC6B4\uB85C\uB4DC \uC911\uC785\uB2C8\uB2E4...
++m.updating_metadata = 컨트롤 파일을 다운로드 중
+
+-m.java_download_failed = \uC774 \uCEF4\uD4E8\uD130\uC5D0 \uD544\uC694\uD55C \uC0C8\uB85C\uC6B4 \uBC84\uC804\uC758 \uC790\uBC14\uB97C \uC790\uB3D9\uC73C\uB85C \uB2E4\uC6B4\uB85C\uB4DC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.\n\n\
+- \uC790\uBC14 \uC6F9\uC0AC\uC774\uD2B8(www.java.com)\uB85C \uAC00\uC11C \uCD5C\uC2E0\uC758 \uC790\uBC14\uB97C \uB2E4\uC6B4\uB85C\uB4DC \uBC1B\uC73C\uC2E0 \uD6C4, \
+- \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC2ED\uC2DC\uC624.
++m.init_failed = 설정 파일이 누락되었거나 변형되었습니다. \
++ 새로운 복사본을 다운로드 중입니다...
+
+-m.java_unpack_failed = \uC5C5\uB370\uC774\uD2B8\uB41C \uBC84\uC804\uC758 \uC790\uBC14\uC758 \uC555\uCD95\uC744 \uD480 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \
+- \uD558\uB4DC\uB4DC\uB77C\uC774\uBE0C\uC5D0 \uCD5C\uC18C\uD55C 100MB\uC758 \uC6A9\uB7C9\uC744 \uD655\uBCF4\uD55C \uC774\uD6C4, \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC2ED\uC2DC\uC624.\n\n\
+- \uB9CC\uC57D \uBB38\uC81C\uAC00 \uD574\uACB0\uB418\uC9C0 \uC54A\uB294\uB2E4\uBA74, \uC790\uBC14 \uC6F9\uC0AC\uC774\uD2B8(www.java.com)\uB85C \uAC00\uC11C \uCD5C\uC2E0\uC758 \uC790\uBC14\uB97C \uB2E4\uC6B4\uB85C\uB4DC \uBC1B\uC73C\uC2E0 \uD6C4, \
+- \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC2ED\uC2DC\uC624.
++m.java_download_failed = 이 컴퓨터에 필요한 새로운 버전의 자바를 자동으로 다운로드할 수 없습니다.\n\n\
++ 자바 웹사이트(www.java.com)로 가서 최신의 자바를 다운로드 받으신 후, \
++ 어플리케이션을 다시 실행해 주십시오.
+
+-m.unable_to_repair = \uB2E4\uC12F\uBC88\uC758 \uC2DC\uB3C4\uC5D0\uB3C4 \uD544\uC694\uD55C \uD30C\uC77C\uC744 \uB2E4\uC6B4\uB85C\uB4DC\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \
+- \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uB2E4\uC2DC \uC2DC\uC791\uD574\uBCF4\uC2DC\uACE0, \uADF8\uB798\uB3C4 \uB2E4\uC6B4\uB85C\uB4DC\uC5D0 \uC2E4\uD328\uD55C\uB2E4\uBA74, \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC81C\uAC70\uD55C \uD6C4, \uB2E4\uC2DC \uC2E4\uD589\uD574\uBCF4\uC2DC\uAE30 \uBC14\uB78D\uB2C8\uB2E4.
++m.java_unpack_failed = 업데이트된 버전의 자바의 압축을 풀 수 없습니다. \
++ 하드드라이브에 최소한 100MB의 용량을 확보한 이후, 어플리케이션을 다시 실행해 주십시오.\n\n\
++ 만약 문제가 해결되지 않는다면, 자바 웹사이트(www.java.com)로 가서 최신의 자바를 다운로드 받으신 후, \
++ 어플리케이션을 다시 실행해 주십시오.
+
+-m.unknown_error = \uBCF5\uAD6C\uB420 \uC218 \uC5C6\uB294 \uC624\uB958\uB85C \uC778\uD558\uC5EC \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC758 \uC2E4\uD589\uC774 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \
+- \n{0}\uC5D0 \uB300\uD55C \uBCF5\uAD6C \uBC29\uBC95\uC744 \uCC3E\uAE30 \uC704\uD574\uC11C \uBC29\uBB38\uD558\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4.
++m.unable_to_repair = 다섯번의 시도에도 필요한 파일을 다운로드하지 못했습니다. \
++ 어플리케이션을 다시 시작해보시고, 그래도 다운로드에 실패한다면, 어플리케이션을 제거한 후, 다시 실행해보시기 바랍니다.
+
+-m.init_error = \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC774 \uC544\uB798\uC640 \uAC19\uC740 \uC5D0\uB7EC\uB85C \uC2E4\uD589\uC774 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC5D0\uB7EC:\
+- \n{0}\n\n{1}\uC5D0 \uB300\uD55C \uBB38\uC81C \uD574\uACB0 \uBC29\uBC95\uC744 \uCC3E\uAE30 \uC704\uD574\uC11C \uBC29\uBB38\uD558\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4.
++m.unknown_error = 복구될 수 없는 오류로 인하여 어플리케이션의 실행이 중단되었습니다. \
++ \n{0}에 대한 복구 방법을 찾기 위해서 방문하시길 바랍니다.
+
+-m.readonly_error = \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC774 \uC124\uCE58\uB41C \uB514\uB809\uD1A0\uB9AC: \
+- \n{0}\n\uAC00 \uC77D\uAE30 \uC804\uC6A9\uC785\uB2C8\uB2E4. \uC77D\uAE30 \uAD8C\uD55C\uC774 \uC2B9\uC778\uB41C \uB809\uD1A0\uB9AC\uC5D0 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC124\uCE58\uD558\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4.
++m.init_error = 어플리케이션이 아래와 같은 에러로 실행이 중단되었습니다. 에러:\
++ \n{0}\n\n{1}에 대한 문제 해결 방법을 찾기 위해서 방문하시길 바랍니다.
+
+-m.missing_resource = \uB9AC\uC18C\uC2A4\uC758 \uC190\uC2E4\uB85C \uC778\uD558\uC5EC \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC758 \uC2E4\uD589\uC774 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4. : \
+- \n{0}\n\n{1}\uC5D0 \uB300\uD55C \uBB38\uC81C \uD574\uACB0 \uBC29\uBC95\uC744 \uCC3E\uAE30 \uC704\uD574\uC11C \uBC29\uBB38\uD558\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4.
++m.readonly_error = 어플리케이션이 설치된 디렉토리: \
++ \n{0}\n가 읽기 전용입니다. 읽기 권한이 승인된 렉토리에 어플리케이션을 설치하시길 바랍니다.
+
+-m.insufficient_permissions_error = \uC774 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC758 \uB514\uC9C0\uD0C8 \uC11C\uBA85\uC744 \uD655\uC778\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \
+- \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC2E4\uD589\uD558\uAE30 \uC704\uD574\uC11C \uB514\uC9C0\uD0C8 \uC11C\uBA85\uC744 \uD655\uC778\uD558\uC5EC \uC8FC\uC2ED\uC2DC\uC624. \
+- \n\n\uADF8\uB9AC\uACE0 \uB098\uC11C \uC6F9 \uBE0C\uB77C\uC6B0\uC800\uB97C \uB2EB\uACE0 \uB2E4\uC2DC \uC2DC\uC791\uD558\uC5EC \uC6F9\uD398\uC774\uC9C0\uB85C \uB3CC\uC544\uC640 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC7AC\uC2DC\uC791\uD574\uC8FC\uC2DC\uAE30 \uBC14\uB78D\uB2C8\uB2E4. \
+- \uBCF4\uC548\uC5D0 \uB300\uD55C \uB300\uD654\uC0C1\uC790\uAC00 \uBCF4\uC774\uBA74, \uB514\uC9C0\uD0C8 \uC11C\uBA85\uC5D0 \uB300\uD55C \uD655\uC778 \uBC84\uD2BC\uC744 \uD074\uB9AD\uD558\uACE0, \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC774 \uC2E4\uD589\uB418\uAE30 \uC704\uD55C \
+- \uAD8C\uD55C\uC744 \uBD80\uC5EC\uD574\uC8FC\uC2DC\uAE30 \uBC14\uB78D\uB2C8\uB2E4.
++m.missing_resource = 리소스의 손실로 인하여 어플리케이션의 실행이 중단되었습니다. : \
++ \n{0}\n\n{1}에 대한 문제 해결 방법을 찾기 위해서 방문하시길 바랍니다.
+
+-m.corrupt_digest_signature_error = \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC758 \uB514\uC9C0\uD0C8 \uC11C\uBA85\uC744 \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.\n \
+- \uC62C\uBC14\uB978 \uC6F9\uC0AC\uC774\uD2B8\uC5D0\uC11C \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC774 \uC2E4\uD589\uB418\uACE0 \uC788\uB294 \uC9C0 \uD655\uC778\uBC14\uB78D\uB2C8\uB2E4.
++m.insufficient_permissions_error = 이 어플리케이션의 디지탈 서명을 확인하지 않았습니다. \
++ 어플리케이션을 실행하기 위해서 디지탈 서명을 확인하여 주십시오. \
++ \n\n그리고 나서 웹 브라우저를 닫고 다시 시작하여 웹페이지로 돌아와 어플리케이션을 재시작해주시기 바랍니다. \
++ 보안에 대한 대화상자가 보이면, 디지탈 서명에 대한 확인 버튼을 클릭하고, 어플리케이션이 실행되기 위한 \
++ 권한을 부여해주시기 바랍니다.
+
+-m.default_install_error = \uC6F9\uC0AC\uC774\uD2B8\uC758 \uC9C0\uC6D0 \uBA54\uB274(support section)\uB97C \uD655\uC778\uD558\uC2DC\uAE30 \uBC14\uB78D\uB2C8\uB2E4.
++m.corrupt_digest_signature_error = 어플리케이션의 디지탈 서명을 확인할 수 없습니다.\n \
++ 올바른 웹사이트에서 어플리케이션이 실행되고 있는 지 확인바랍니다.
+
+-m.another_getdown_running = \uC774 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158 \uC778\uC2A4\uD1A8\uB7EC\uC758 \uB2E4\uC911 \uC778\uC2A4\uD134\uC2A4\uAC00 \uC2E4\uD589\uC911\uC785\uB2C8\uB2E4. \
+- \uD558\uB098\uAC00 \uC644\uB8CC\uB420 \uB54C\uAE4C\uC9C0 \uC911\uB2E8\uB429\uB2C8\uB2E4.
++m.default_install_error = 웹사이트의 지원 메뉴(support section)를 확인하시기 바랍니다.
+
+-m.applet_stopped = Getdown \uC560\uD50C\uB9BF \uC2E4\uD589\uC774 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
++m.another_getdown_running = 이 어플리케이션 인스톨러의 다중 인스턴스가 실행중입니다. \
++ 하나가 완료될 때까지 중단됩니다.
+
+ # application/digest errors
+-m.missing_appbase = \uC124\uC815 \uD30C\uC77C\uC5D0 'appbase' \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.
+-m.invalid_version = \uC124\uC815 \uD30C\uC77C\uC5D0 \uC798\uBABB\uB41C \uBC84\uC804\uC774 \uBA85\uC2DC\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.
+-m.invalid_appbase = \uC124\uC815 \uD30C\uC77C\uC5D0 \uC798\uBABB\uB41C 'appbase'\uAC00 \uBA85\uC2DC\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.
+-m.missing_class = \uC124\uC815 \uD30C\uC77C\uC5D0 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158 \uD074\uB798\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.
+-m.missing_code = \uC124\uC815 \uD30C\uC77C\uC5D0 \uB9AC\uC18C\uC2A4\uC5D0 \uB300\uD55C \uCF54\uB4DC\uAC00 \uBA85\uC2DC\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.
+-m.invalid_digest_file = \uB2E4\uC774\uC81C\uC2A4\uD2B8 \uD30C\uC77C\uC774 \uC798\uBABB\uB418\uC5C8\uC2B5\uB2C8\uB2E4.
++m.missing_appbase = 설정 파일에 'appbase' 가 없습니다.
++m.invalid_version = 설정 파일에 잘못된 버전이 명시되어 있습니다.
++m.invalid_appbase = 설정 파일에 잘못된 'appbase'가 명시되어 있습니다.
++m.missing_class = 설정 파일에 어플리케이션 클래스가 없습니다.
++m.missing_code = 설정 파일에 리소스에 대한 코드가 명시되어 있지 않습니다.
++m.invalid_digest_file = 다이제스트 파일이 잘못되었습니다.
+diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_pt.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_pt.properties
+index 47db91c90..e59ed20b2 100644
+--- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_pt.properties
++++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_pt.properties
+@@ -1,118 +1,112 @@
+ #
+-# $Id$
+-#
+-# Getdown translation messages
++# Getdown Portuguese translation messages
+
+-m.abort_title = Cancelar a instala\u00E7\u00E3o?
+-m.abort_confirm = <html>Tem certeza que deseja cancelar a instala\u00E7\u00E3o? \
+- Voc\u00EA pode continuar a instala\u00E7\u00E3o mais tarde, \
+- basta executar a aplica\u00E7\u00E3o novamente.</html>
++m.abort_title = Cancelar a instalação?
++m.abort_confirm = <html>Tem certeza que deseja cancelar a instalação? \
++ Você pode continuar a instalação mais tarde, \
++ basta executar a aplicação novamente.</html>
+ m.abort_ok = Sair
+-m.abort_cancel = Continuar a instala\u00E7\u00E3o
++m.abort_cancel = Continuar a instalação
+
+-m.detecting_proxy = Tentando detectar automaticamente as configura\u00E7\u00F5es de proxy
++m.detecting_proxy = Tentando detectar automaticamente as configurações de proxy
+
+-m.configure_proxy = <html>N\u00E3o foi poss\u00EDvel conectar aos nossos servidores para \
++m.configure_proxy = <html>Não foi possível conectar aos nossos servidores para \
+ fazer o download dos dados. \
+- <ul><li> Se o Firewall do Windows ou o Norton Internet Security est\u00E1 configurado \
+- para bloquear o programa <code>javaw.exe</code> n\u00E3o ser\u00E1 poss\u00EDvel realizar \
+- o download. Voc\u00EA ter\u00E1 que permitir que o programa <code>javaw.exe</code> acesse \
+- a internet. Voc\u00EA pode tentar executar o programa novamente, mas voc\u00EA precisa \
+- remover o programa javaw.exe das configura\u00E7\u00F5es do firewall (Iniciar -> Painel \
++ <ul><li> Se o Firewall do Windows ou o Norton Internet Security está configurado \
++ para bloquear o programa <code>javaw.exe</code> não será possível realizar \
++ o download. Você terá que permitir que o programa <code>javaw.exe</code> acesse \
++ a internet. Você pode tentar executar o programa novamente, mas você precisa \
++ remover o programa javaw.exe das configurações do firewall (Iniciar -> Painel \
+ de controle -> Firewall do Windows).</ul> \
+- <p> Seu computador pode estar acessando a internet atrav\u00E9s de um proxy e n\u00E3o foi \
+- capaz de detectar automaticamente as configura\u00E7\u00F5es de proxy. \
+- Voc\u00EA pode informar esses dados abaixo.</html>
++ <p> Seu computador pode estar acessando a internet através de um proxy e não foi \
++ capaz de detectar automaticamente as configurações de proxy. \
++ Você pode informar esses dados abaixo.</html>
+
+-m.proxy_extra = <html>Se voc\u00EA tem certeza que n\u00E3o usa um proxy, ent\u00E3o pode ser \
+- que exista um problema tempor\u00E1rio que est\u00E1 impedindo a comunica\u00E7\u00E3o \
+- com os nossos servidores. Neste caso voc\u00EA pode cancelar e tentar instalar novamente \
++m.proxy_extra = <html>Se você tem certeza que não usa um proxy, então pode ser \
++ que exista um problema temporário que está impedindo a comunicação \
++ com os nossos servidores. Neste caso você pode cancelar e tentar instalar novamente \
+ mais tarde.</html>
+
+ m.proxy_host = IP do Proxy
+ m.proxy_port = Porta do Proxy
+-m.proxy_username = Nome de usu\u00e1rio
++m.proxy_username = Nome de usuário
+ m.proxy_password = Senha
+-m.proxy_auth_required = Autentifica\u00e7\u00e3o requerida
++m.proxy_auth_required = Autentificação requerida
+ m.proxy_ok = OK
+ m.proxy_cancel = Cancelar
+
+-m.downloading_java = Fazendo o download da m\u00E1quina virtual Java
+-m.unpacking_java = Descompactando a m\u00E1quina virtual Java
++m.downloading_java = Fazendo o download da máquina virtual Java
++m.unpacking_java = Descompactando a máquina virtual Java
+
+ m.resolving = Resolvendo downloads
+ m.downloading = Transferindo dados
+ m.failure = Download falhou: {0}
+
+-m.checking = Verificando atualiza\u00E7\u00F5es
++m.checking = Verificando atualizações
+ m.validating = Validando
+ m.patching = Atualizando
+ m.launching = Executando
+
+ m.patch_notes = Corrigir notas
+-m.play_again = Jogar de novo
+-
+ m.complete = {0}% completo
+ m.remain = {0} Permanecer
+
+ m.updating_metadata = Transferindo arquivos de controle
+
+-m.init_failed = Nosso arquivo de configura\u00E7\u00E3o est\u00E1 ausente ou corrompido. Tente \
+- baixar uma nova c\u00F3pia...
++m.init_failed = Nosso arquivo de configuração está ausente ou corrompido. Tente \
++ baixar uma nova cópia...
+
+-m.java_download_failed = N\u00E3o conseguimos baixar automaticamente a\
+- vers\u00E3o necess\u00E1ria do Java para o seu computador.\n\n\
+- Por favor, acesse www.java.com, baixe e instale a \u00FAltima vers\u00E3o do \
++m.java_download_failed = Não conseguimos baixar automaticamente a\
++ versão necessária do Java para o seu computador.\n\n\
++ Por favor, acesse www.java.com, baixe e instale a última versão do \
+ Java, em seguida, tente executar o aplicativo novamente.
+
+-m.java_unpack_failed = N\u00E3o conseguimos descompactar uma vers\u00E3o atualizada do \
+- Java. Por favor, certifique-se de ter pelo menos 100 MB de espa\u00E7o livre em seu \
+- disco r\u00EDgido e tente executar o aplicativo novamente. \n\n\
+- Se isso n\u00E3o resolver o problema, acesse www.java.com,baixe e \
+- instale a \u00FAltima vers\u00E3o do Java e tente novamente.
++m.java_unpack_failed = Não conseguimos descompactar uma versão atualizada do \
++ Java. Por favor, certifique-se de ter pelo menos 100 MB de espaço livre em seu \
++ disco rígido e tente executar o aplicativo novamente. \n\n\
++ Se isso não resolver o problema, acesse www.java.com,baixe e \
++ instale a última versão do Java e tente novamente.
+
+-m.unable_to_repair = N\u00E3o conseguimos baixar os arquivos necess\u00E1rios depois de \
+- cinco tentativas. Voc\u00EA pode tentar executar o aplicativo novamente, mas se ele \
+- falhar pode ser necess\u00E1rio desinstalar e reinstalar.
++m.unable_to_repair = Não conseguimos baixar os arquivos necessários depois de \
++ cinco tentativas. Você pode tentar executar o aplicativo novamente, mas se ele \
++ falhar pode ser necessário desinstalar e reinstalar.
+
+-m.unknown_error = A aplica\u00E7\u00E3o falhou ao iniciar devido a algum erro estranho \
+- do qual n\u00E3o conseguimos recuperar. Por favor, visite \n{0} para obter \
+- informa\u00E7\u00F5es sobre como recuperar.
+-m.init_error = A aplica\u00E7\u00E3o falhou ao iniciar devido ao seguinte \
++m.unknown_error = A aplicação falhou ao iniciar devido a algum erro estranho \
++ do qual não conseguimos recuperar. Por favor, visite \n{0} para obter \
++ informações sobre como recuperar.
++m.init_error = A aplicação falhou ao iniciar devido ao seguinte \
+ erro:\n{0}\n\nPor favor visite \n{1} para \
+- informa\u00E7\u00F5es sobre como lidar com esse problema.
++ informações sobre como lidar com esse problema.
+
+-m.readonly_error =O diret\u00F3rio no qual este aplicativo est\u00E1 instalado: \
+- \n{0}\n \u00E9 somente leitura. Por favor, instale o aplicativo em um diret\u00F3rio onde \
+- voc\u00EA tem acesso de grava\u00E7\u00E3o.
++m.readonly_error =O diretório no qual este aplicativo está instalado: \
++ \n{0}\n é somente leitura. Por favor, instale o aplicativo em um diretório onde \
++ você tem acesso de gravação.
+
+-m.missing_resource = A aplica\u00E7\u00E3o falhou ao iniciar devido a uma falta \
+- de recurso:\n{0}\n\n Por favor, visite\n{1} para obter informa\u00E7\u00F5es sobre \
++m.missing_resource = A aplicação falhou ao iniciar devido a uma falta \
++ de recurso:\n{0}\n\n Por favor, visite\n{1} para obter informações sobre \
+ como lidar com tal problema.
+
+-m.insufficient_permissions_error = Voc\u00EA n\u00E3o aceitou a assinatura digital \
+- do aplicativo. Se voc\u00EA quiser executar o aplicativo, voc\u00EA ter\u00E1 que aceitar \
+- a assinatura digital. \n\nPara fazer isso, voc\u00EA ter\u00E1 que sair do seu navegador, \
+- reinici\u00E1-lo, e retornar a esta p\u00E1gina web para executar a aplica\u00E7\u00E3o. \
+- Quando o di\u00E1logo de seguran\u00E7a aparecer, clique no bot\u00E3o para aceitar a \
+- assinatura digital e conceder a este aplicativo os privil\u00E9gios necess\u00E1rios \
++m.insufficient_permissions_error = Você não aceitou a assinatura digital \
++ do aplicativo. Se você quiser executar o aplicativo, você terá que aceitar \
++ a assinatura digital. \n\nPara fazer isso, você terá que sair do seu navegador, \
++ reiniciá-lo, e retornar a esta página web para executar a aplicação. \
++ Quando o diálogo de segurança aparecer, clique no botão para aceitar a \
++ assinatura digital e conceder a este aplicativo os privilégios necessários \
+ para executar.
+
+-m.corrupt_digest_signature_error = N\u00E3o conseguimos verificar a assinatura digital \
+- do aplicativo.\nPor favor, verifique se voc\u00EA est\u00E1 utilizando o aplicativo \nde um \
++m.corrupt_digest_signature_error = Não conseguimos verificar a assinatura digital \
++ do aplicativo.\nPor favor, verifique se você está utilizando o aplicativo \nde um \
+ site correto.
+
+-m.default_install_error = a se\u00E7\u00E3o de suporte do site
+-
+-m.another_getdown_running = V\u00E1rias inst\u00E2ncias desta aplica\u00E7\u00E3o \
+- est\u00E3o em execu\u00E7\u00E3o. Esta ir\u00E1 parar e deixar outra completar suas atividades.
++m.default_install_error = a seção de suporte do site
+
+-m.applet_stopped = Foi solicitado ao miniaplicativo GetDow que parasse de trabalhar.
++m.another_getdown_running = Várias instâncias desta aplicação \
++ estão em execução. Esta irá parar e deixar outra completar suas atividades.
+
+ # application/digest errors
+-m.missing_appbase = O arquivo de configura\u00E7\u00E3o n\u00E3o possui o 'AppBase'.
+-m.invalid_version = O arquivo de configura\u00E7\u00E3o especifica uma vers\u00E3o inv\u00E1lida.
+-m.invalid_appbase = O arquivo de configura\u00E7\u00E3o especifica um 'AppBase' inv\u00E1lido.
+-m.missing_class = O arquivo de configura\u00E7\u00E3o n\u00E3o possui a classe de aplicativo.
+-m.missing_code = O arquivo de configura\u00E7\u00E3o n\u00E3o especifica um recurso de c\u00F3digo.
+-m.invalid_digest_file = O arquivo digest \u00E9 inv\u00E1lido.
++m.missing_appbase = O arquivo de configuração não possui o 'AppBase'.
++m.invalid_version = O arquivo de configuração especifica uma versão inválida.
++m.invalid_appbase = O arquivo de configuração especifica um 'AppBase' inválido.
++m.missing_class = O arquivo de configuração não possui a classe de aplicativo.
++m.missing_code = O arquivo de configuração não especifica um recurso de código.
++m.invalid_digest_file = O arquivo digest é inválido.
+diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_zh.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_zh.properties
+index 2c275437b..fa74fb293 100644
+--- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_zh.properties
++++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_zh.properties
+@@ -1,61 +1,57 @@
+ #
+-# $Id$
+-#
+-# Getdown translation messages
++# Getdown Chinese translation messages
+
+-m.detecting_proxy = \u641c\u5bfb\u4ee3\u7406\u670d\u52a1\u5668
++m.detecting_proxy = 搜寻代理服务器
+
+-m.configure_proxy = <html>\u6211\u4eec\u65e0\u6cd5\u8fde\u63a5\u5230\u670d\u52a1\u5668\u4e0b\u8f7d\u6e38\u620f\u6570\u636e\u3002\u8fd9\u53ef\u80fd\u662f\u7531\u4e8e \
+- \u60a8\u7684\u8ba1\u7b97\u673a\u662f\u901a\u8fc7\u4ee3\u7406\u670d\u52a1\u5668\u8fde\u63a5\u4e92\u8054\u7f51\u7684\uff0c\u5e76\u4e14\u6211\u4eec\u65e0\u6cd5\u81ea\u52a8\u83b7\u5f97\u4ee3\u7406\u670d\u52a1\u5668\u7684 \
+- \u8bbe\u7f6e\u3002\u5982\u679c\u60a8\u77e5\u9053\u60a8\u4ee3\u7406\u670d\u52a1\u5668\u7684\u8bbe\u7f6e\uff0c\u60a8\u53ef\u4ee5\u5728\u4e0b\u9762\u8f93\u5165\u3002</html>
++m.configure_proxy = <html>我们无法连接到服务器下载游戏数据。这可能是由于 \
++ 您的计算机是通过代理服务器连接互联网的,并且我们无法自动获得代理服务器的 \
++ 设置。如果您知道您代理服务器的设置,您可以在下面输入。</html>
+
+-m.proxy_extra = <html>\u5982\u679c\u60a8\u786e\u5b9a\u60a8\u6ca1\u6709\u4f7f\u7528\u4ee3\u7406\u670d\u52a1\u5668\uff0c\u8fd9\u53ef\u80fd\u662f\u7531\u4e8e\u6682\u65f6\u65e0\u6cd5 \
+- \u8fde\u63a5\u5230\u4e92\u8054\u7f51\uff0c\u5bfc\u81f4\u65e0\u6cd5\u548c\u670d\u52a1\u5668\u901a\u8baf\u3002\u8fd9\u79cd\u60c5\u51b5\uff0c\u60a8\u53ef\u4ee5\u53d6\u6d88\uff0c\u7a0d\u5019\u518d\u91cd\u65b0\u5b89\u88c5\u3002<br><br> \
+- \u5982\u679c\u60a8\u65e0\u6cd5\u786e\u5b9a\u60a8\u662f\u5426\u4f7f\u7528\u4e86\u4ee3\u7406\u670d\u52a1\u5668\uff0c\u8bf7\u8bbf\u95ee\u6211\u4eec\u7f51\u7ad9\u4e2d\u7684\u6280\u672f\u652f\u6301\u90e8\u4efd\uff0c \
+- \u4e86\u89e3\u5982\u4f55\u68c0\u6d4b\u60a8\u7684\u4ee3\u7406\u670d\u52a1\u5668\u8bbe\u7f6e\u3002</html>
++m.proxy_extra = <html>如果您确定您没有使用代理服务器,这可能是由于暂时无法 \
++ 连接到互联网,导致无法和服务器通讯。这种情况,您可以取消,稍候再重新安装。<br><br> \
++ 如果您无法确定您是否使用了代理服务器,请访问我们网站中的技术支持部份, \
++ 了解如何检测您的代理服务器设置。</html>
+
+-m.proxy_host = \u4ee3\u7406\u670d\u52a1\u5668\u7684IP\u5730\u5740
+-m.proxy_port = \u4ee3\u7406\u670d\u52a1\u5668\u7684\u7aef\u53e3\u53f7
++m.proxy_host = 代理服务器的IP地址
++m.proxy_port = 代理服务器的端口号
+ m.proxy_username = Username
+ m.proxy_password = Password
+ m.proxy_auth_required = Authentication required
+-m.proxy_ok = \u786e\u5b9a
+-m.proxy_cancel = \u53d6\u6d88
+-
+-m.resolving = \u5206\u6790\u9700\u4e0b\u8f7d\u5185\u5bb9
+-m.downloading = \u4e0b\u8f7d\u6570\u636e
+-m.failure = \u4e0b\u8f7d\u5931\u8d25: {0}
+-
+-m.checking = \u68c0\u67e5\u66f4\u65b0\u5185\u5bb9
+-m.validating = \u786e\u8ba4
+-m.patching = \u5347\u7ea7
+-m.launching = \u542f\u52a8
++m.proxy_ok = 确定
++m.proxy_cancel = 取消
+
+-m.complete = {0}% \u5b8c\u6210
+-m.remain = {0} \u5269\u4f59\u65f6\u95f4
++m.resolving = 分析需下载内容
++m.downloading = 下载数据
++m.failure = 下载失败: {0}
+
+-m.updating_metadata = \u4e0b\u8f7d\u63a7\u5236\u6587\u4ef6
++m.checking = 检查更新内容
++m.validating = 确认
++m.patching = 升级
++m.launching = 启动
+
+-m.init_failed = \u65e0\u6cd5\u627e\u5230\u914d\u7f6e\u6587\u4ef6\u6216\u5df2\u635f\u574f\u3002\u5c1d\u8bd5\u91cd\u65b0\u4e0b\u8f7d...
++m.complete = {0}% 完成
++m.remain = {0} 剩余时间
+
+-m.unable_to_repair = \u7ecf\u8fc75\u6b21\u5c1d\u8bd5\uff0c\u4f9d\u7136\u65e0\u6cd5\u4e0b\u8f7d\u6240\u9700\u7684\u6587\u4ef6\u3002\
+-\u60a8\u53ef\u4ee5\u91cd\u65b0\u8fd0\u884c\u7a0b\u5e8f\uff0c\u4f46\u662f\u5982\u679c\u4f9d\u7136\u5931\u8d25\uff0c\u60a8\u53ef\u80fd\u9700\u8981\u53cd\u5b89\u88c5\u5e76\u91cd\u65b0\u5b89\u88c5\u3002
++m.updating_metadata = 下载控制文件
+
++m.init_failed = 无法找到配置文件或已损坏。尝试重新下载...
+
+-m.unknown_error = \u7531\u4e8e\u4e00\u4e9b\u65e0\u6cd5\u56de\u590d\u7684\u4e25\u91cd\u9519\u8bef\uff0c\u7a0b\u5e8f\u542f\u52a8\u5931\u8d25\u3002\
+-\u8bf7\u8bbf\u95ee\u6211\u4eec\u7684\u7f51\u7ad9\u7684\u6280\u672f\u652f\u6301\u90e8\u4efd\uff0c\u4e86\u89e3\u5982\u4f55\u89e3\u51b3\u95ee\u9898\u3002
++m.unable_to_repair = 经过5次尝试,依然无法下载所需的文件。\
++您可以重新运行程序,但是如果依然失败,您可能需要反安装并重新安装。
+
+-m.init_error = \u7531\u4e8e\u4e0b\u5217\u9519\u8bef\uff0c\u7a0b\u5e8f\u542f\u52a8\u5931\u8d25\uff1a\n{0}\n\n \
+-\u8bf7\u8bbf\u95ee\u6211\u4eec\u7684\u7f51\u7ad9\u7684\u6280\u672f\u652f\u6301\u90e8\u4efd\uff0c\u4e86\u89e3\u5982\u4f55\u5904\u7406\u8fd9\u4e9b\u9519\u8bef\u3002
++m.unknown_error = 由于一些无法回复的严重错误,程序启动失败。\
++请访问我们的网站的技术支持部份,了解如何解决问题。
+
++m.init_error = 由于下列错误,程序启动失败:\n{0}\n\n \
++请访问我们的网站的技术支持部份,了解如何处理这些错误。
+
+-m.missing_resource = \u7531\u4e8e\u65e0\u6cd5\u627e\u5230\u4e0b\u5217\u8d44\u6e90\uff0c\u7a0b\u5e8f\u542f\u52a8\u5931\u8d25\uff1a\n{0}\n\n \
+-\u8bf7\u8bbf\u95ee\u6211\u4eec\u7684\u7f51\u7ad9\u7684\u6280\u672f\u652f\u6301\u90e8\u4efd\uff0c\u4e86\u89e3\u5982\u4f55\u5904\u7406\u8fd9\u4e9b\u95ee\u9898\u3002
++m.missing_resource = 由于无法找到下列资源,程序启动失败:\n{0}\n\n \
++请访问我们的网站的技术支持部份,了解如何处理这些问题。
+
+ # application/digest errors
+-m.missing_appbase = \u914d\u7f6e\u6587\u4ef6\u4e2d\u65e0\u6cd5\u627e\u5230 'appbase'\u3002
+-m.invalid_version = \u914d\u7f6e\u6587\u4ef6\u6307\u5b9a\u4e86\u65e0\u6548\u7684\u7248\u672c\u3002
+-m.invalid_appbase = \u914d\u7f6e\u6587\u4ef6\u6307\u5b9a\u4e86\u65e0\u6548\u7684 'appbase'\u3002
+-m.missing_class = \u914d\u7f6e\u6587\u4ef6\u4e2d\u65e0\u6cd5\u627e\u5230\u7a0b\u5e8f\u6587\u4ef6\u3002
+-m.missing_code = \u914d\u7f6e\u6587\u4ef6\u4e2d\u65e0\u6cd5\u627e\u5230\u6307\u5b9a\u7684\u8d44\u6e90\u3002
+-m.invalid_digest_file = \u65e0\u6548\u7684\u914d\u7f6e\u6587\u4ef6\u3002
++m.missing_appbase = 配置文件中无法找到 'appbase'。
++m.invalid_version = 配置文件指定了无效的版本。
++m.invalid_appbase = 配置文件指定了无效的 'appbase'。
++m.missing_class = 配置文件中无法找到程序文件。
++m.missing_code = 配置文件中无法找到指定的资源。
++m.invalid_digest_file = 无效的配置文件。
+diff --git a/getdown/src/getdown/lib/commons-compress-1.18.jar b/getdown/src/getdown/lib/commons-compress-1.18.jar
+deleted file mode 100644
+index e401046b5..000000000
+Binary files a/getdown/src/getdown/lib/commons-compress-1.18.jar and /dev/null differ
+diff --git a/getdown/src/getdown/mvn_cmd b/getdown/src/getdown/mvn_cmd
+deleted file mode 100644
+index 0ce786ff8..000000000
+--- a/getdown/src/getdown/mvn_cmd
++++ /dev/null
+@@ -1 +0,0 @@
+-mvn clean package -Dgetdown.host.whitelist="jalview.org,*.jalview.org" && cp launcher/target/getdown-launcher-1.8.3-SNAPSHOT.jar ../../../getdown/lib/getdown-launcher.jar && cp core/target/getdown-core-1.8.3-SNAPSHOT.jar ../../../getdown/lib/getdown-core-1.8.3-SNAPSHOT.jar && cp core/target/getdown-core-1.8.3-SNAPSHOT.jar ../../../j8lib/getdown-core.jar && cp core/target/getdown-core-1.8.3-SNAPSHOT.jar ../../../j11lib/getdown-core.jar
+diff --git a/getdown/src/getdown/pom.xml b/getdown/src/getdown/pom.xml
+index 78d67b0a5..ae1370dde 100644
+--- a/getdown/src/getdown/pom.xml
++++ b/getdown/src/getdown/pom.xml
+@@ -10,7 +10,7 @@
+ <groupId>com.threerings.getdown</groupId>
+ <artifactId>getdown</artifactId>
+ <packaging>pom</packaging>
+- <version>1.8.3-SNAPSHOT</version>
++ <version>1.8.7-SNAPSHOT</version>
+
+ <name>getdown</name>
+ <description>An application installer and updater.</description>
+@@ -35,6 +35,10 @@
+ </developer>
+ </developers>
+
++ <properties>
++ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
++ </properties>
++
+ <scm>
+ <connection>scm:git:git://github.com/threerings/getdown.git</connection>
+ <developerConnection>scm:git:git@github.com:threerings/getdown.git</developerConnection>
+@@ -47,28 +51,6 @@
+ <module>ant</module>
+ </modules>
+
+- <repositories>
+- <repository>
+- <id>ej-technologies</id>
+- <url>https://maven.ej-technologies.com/repository</url>
+- </repository>
+- </repositories>
+-
+-
+- <dependencies>
+- <dependency>
+- <groupId>org.apache.commons</groupId>
+- <artifactId>commons-compress</artifactId>
+- <version>1.18</version>
+- </dependency>
+- <dependency>
+- <groupId>com.install4j</groupId>
+- <artifactId>install4j-runtime</artifactId>
+- <version>7.0.11</version>
+- <scope>provided</scope>
+- </dependency>
+- </dependencies>
+-
+ <build>
+ <plugins>
+ <plugin>
+@@ -83,6 +65,20 @@
+ <stagingProfileId>aa555c46fc37d0</stagingProfileId>
+ </configuration>
+ </plugin>
++
++ <plugin>
++ <groupId>org.apache.maven.plugins</groupId>
++ <artifactId>maven-javadoc-plugin</artifactId>
++ <version>3.1.0</version>
++ <configuration>
++ <quiet>true</quiet>
++ <show>public</show>
++ <additionalOptions>
++ <additionalOption>-Xdoclint:all</additionalOption>
++ <additionalOption>-Xdoclint:-missing</additionalOption>
++ </additionalOptions>
++ </configuration>
++ </plugin>
+ </plugins>
+
+ <!-- Common plugin configuration for all children -->
+@@ -91,10 +87,10 @@
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+- <version>3.8.1</version>
++ <version>3.7.0</version>
+ <configuration>
+- <source>1.8</source>
+- <target>1.8</target>
++ <source>1.7</source>
++ <target>1.7</target>
+ <fork>true</fork>
+ <showDeprecation>true</showDeprecation>
+ <showWarnings>true</showWarnings>
+@@ -110,20 +106,12 @@
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-resources-plugin</artifactId>
+ <version>3.0.2</version>
+- <configuration>
+- <encoding>UTF-8</encoding>
+- </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+- <artifactId>maven-javadoc-plugin</artifactId>
+- <version>3.0.0-M1</version>
+- <configuration>
+- <quiet>true</quiet>
+- <show>public</show>
+- <additionalparam>-Xdoclint:all -Xdoclint:-missing</additionalparam>
+- </configuration>
++ <artifactId>maven-gpg-plugin</artifactId>
++ <version>1.6</version>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+@@ -181,7 +169,6 @@
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-gpg-plugin</artifactId>
+- <version>1.6</version>
+ <executions>
+ <execution>
+ <id>sign-artifacts</id>