Merge branch 'documentation/JAL-3111_release_211' into bug/JAL-2830_editManglesDatase...
[jalview.git] / getdown / src / getdown / core / src / main / java / com / threerings / getdown / data / EnvConfig.java
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
new file mode 100644 (file)
index 0000000..57b8d84
--- /dev/null
@@ -0,0 +1,245 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+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 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); }
+        public final Level level;
+        public final String message;
+        public Note (Level level, String message) {
+            this.level = level;
+            this.message = message;
+        }
+    }
+
+    /**
+     * Creates an environment config, obtaining information (in order) from the following sources:
+     *
+     * <ul>
+     * <li> A {@code bootstrap.properties} file bundled with the jar. </li>
+     * <li> System properties supplied to the JVM. </li>
+     * <li> The supplied command line arguments ({@code argv}). </li>
+     * </ul>
+     *
+     * If a later source supplies a configuration already provided by a prior source, a warning
+     * message will be logged to indicate the conflict, and the prior source will be used.
+     *
+     * @param notes a list into which notes are added, to be logged after the logging system has
+     * been initialized (which cannot happen until the appdir is known). If any {@code ERROR} notes
+     * are included, the app should terminate after reporting them.
+     * @return an env config instance, or {@code null} if no appdir could be located via any
+     * configuration source.
+     */
+    public static EnvConfig create (String[] argv, List<Note> notes) {
+        String appDir = null, appDirProv = null;
+        String appId = null, appIdProv = null;
+        String appBase = null, appBaseProv = null;
+
+        // start with bootstrap.properties config, if avaialble
+        try {
+            ResourceBundle bundle = ResourceBundle.getBundle("bootstrap");
+            if (bundle.containsKey("appdir")) {
+                appDir = bundle.getString("appdir");
+                appDir = appDir.replace(USER_HOME_KEY, System.getProperty("user.home"));
+                appDirProv = "bootstrap.properties";
+            }
+            if (bundle.containsKey("appid")) {
+                appId = bundle.getString("appid");
+                appIdProv = "bootstrap.properties";
+            }
+            if (bundle.containsKey("appbase")) {
+                appBase = bundle.getString("appbase");
+                appBaseProv = "bootstrap.properties";
+            }
+            // if any system properties are specified (keys prefixed with sys.), set those up
+            for (String key : bundle.keySet()) {
+                if (key.startsWith("sys.")) {
+                    String skey = key.substring(4);
+                    String svalue = bundle.getString(key);
+                    notes.add(Note.info("Setting system property from bundle: " +
+                                        skey + "='" + svalue + "'"));
+                    System.setProperty(skey, svalue);
+                }
+            }
+
+        } catch (MissingResourceException e) {
+            // bootstrap.properties is optional; no need for a warning
+        }
+
+        // next seek config from system properties
+        String spropsAppDir = SysProps.appDir();
+        if (!StringUtil.isBlank(spropsAppDir)) {
+            if (appDir == null) {
+                appDir = spropsAppDir;
+                appDirProv = "system property";
+            } else {
+                notes.add(Note.warn("Ignoring 'appdir' system property, have appdir via '" +
+                                    appDirProv + "'"));
+            }
+        }
+        String spropsAppId = SysProps.appId();
+        if (!StringUtil.isBlank(spropsAppId)) {
+            if (appId == null) {
+                appId = spropsAppId;
+                appIdProv = "system property";
+            } else {
+                notes.add(Note.warn("Ignoring 'appid' system property, have appid via '" +
+                                    appIdProv + "'"));
+            }
+        }
+        String spropsAppBase = SysProps.appBase();
+        if (!StringUtil.isBlank(spropsAppBase)) {
+            if (appBase == null) {
+                appBase = spropsAppBase;
+                appBaseProv = "system property";
+            } else {
+                notes.add(Note.warn("Ignoring 'appbase' system property, have appbase via '" +
+                                    appBaseProv + "'"));
+            }
+        }
+
+        // finally obtain config from command line arguments
+        String argvAppDir = argv.length > 0 ? argv[0] : null;
+        if (!StringUtil.isBlank(argvAppDir)) {
+            if (appDir == null) {
+                appDir = argvAppDir;
+                appDirProv = "command line";
+            } else {
+                notes.add(Note.warn("Ignoring 'appdir' command line arg, have appdir via '" +
+                                    appDirProv + "'"));
+            }
+        }
+        String argvAppId = argv.length > 1 ? argv[1] : null;
+        if (!StringUtil.isBlank(argvAppId)) {
+            if (appId == null) {
+                appId = argvAppId;
+                appIdProv = "command line";
+            } else {
+                notes.add(Note.warn("Ignoring 'appid' command line arg, have appid via '" +
+                                    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 (appDir == null) {
+            return null; // caller will report problem to user
+        }
+
+        notes.add(Note.info("Using appdir from " + appDirProv + ": " + appDir));
+        if (appId != null) notes.add(Note.info("Using appid from " + appIdProv + ": " + appId));
+        if (appBase != null) notes.add(
+            Note.info("Using appbase from " + appBaseProv + ": " + appBase));
+
+        // ensure that the appdir refers to a directory that exists
+        File appDirFile = new File(appDir);
+        if (!appDirFile.exists()) {
+            // if we have a bootstrap URL then we auto-create the app dir; this enables an
+            // installer to simply place a getdown.jar file somewhere and create an OS shortcut
+            // that runs getdown with an appdir and appbase specified, and have getdown create the
+            // appdir and download the app into it
+            if (!StringUtil.isBlank(appBase)) {
+                if (appDirFile.mkdirs()) {
+                    notes.add(Note.info("Auto-created app directory '" + appDir + "'"));
+                } else {
+                    notes.add(Note.warn("Unable to auto-create app dir: '" + appDir + "'"));
+                }
+            } else {
+                notes.add(Note.error("Invalid appdir '" + appDir + "': directory does not exist"));
+                return null;
+            }
+        } else if (!appDirFile.isDirectory()) {
+            notes.add(Note.error("Invalid appdir '" + appDir + "': refers to non-directory"));
+            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) :
+            Collections.<String>emptyList();
+
+        // load X.509 certificate if it exists
+        File crtFile = new File(appDirFile, Digest.digestFile(Digest.VERSION) + ".crt");
+        List<Certificate> certs = new ArrayList<>();
+        if (crtFile.exists()) {
+            try (FileInputStream fis = new FileInputStream(crtFile)) {
+                X509Certificate certificate = (X509Certificate)
+                    CertificateFactory.getInstance("X.509").generateCertificate(fis);
+                certs.add(certificate);
+            } catch (Exception e) {
+                notes.add(Note.error("Certificate error: " + e.getMessage()));
+            }
+        }
+
+        return new EnvConfig(appDirFile, appId, appBase, certs, appArgs);
+    }
+
+    /** The directory in which the application and metadata is stored. */
+    public final File appDir;
+
+    /** Either {@code null} or an identifier for a secondary application that should be
+      * launched. That app will use {@code appid.class} and {@code appid.apparg} to configure
+      * itself but all other parameters will be the same as the primary app. */
+    public final String appId;
+
+    /** Either {@code null} or fallback {@code appbase} to use if one cannot be read from a
+      * {@code getdown.txt} file during startup. */
+    public final String appBase;
+
+    /** Zero or more signing certificates used to verify the digest file. */
+    public final List<Certificate> certs;
+
+    /** Additional arguments to pass on to launched application. These will be added after the
+      * args in the getdown.txt file. */
+    public final List<String> appArgs;
+
+    public EnvConfig (File appDir) {
+        this(appDir, null, null, Collections.<Certificate>emptyList(),
+             Collections.<String>emptyList());
+    }
+
+    private EnvConfig (File appDir, String appId, String appBase, List<Certificate> certs,
+                       List<String> appArgs) {
+        this.appDir = appDir;
+        this.appId = appId;
+        this.appBase = appBase;
+        this.certs = certs;
+        this.appArgs = appArgs;
+    }
+
+    private static final String USER_HOME_KEY = "${user.home}";
+}