JAL-4409 Added remote JVL files for Linux/Windows
[jalview.git] / getdown / src / getdown / core / src / main / java / com / threerings / getdown / data / EnvConfig.java
1 //
2 // Getdown - application installer, patcher and launcher
3 // Copyright (C) 2004-2018 Getdown authors
4 // https://github.com/threerings/getdown/blob/master/LICENSE
5
6 package com.threerings.getdown.data;
7
8 import java.io.File;
9 import java.io.FileInputStream;
10 import java.net.MalformedURLException;
11 import java.net.URL;
12 import java.security.cert.Certificate;
13 import java.security.cert.CertificateFactory;
14 import java.security.cert.X509Certificate;
15 import java.util.*;
16
17 import com.threerings.getdown.util.StringUtil;
18
19 import jalview.util.HttpUtils;
20
21 import com.threerings.getdown.data.Application;
22
23 /** Configuration that comes from our "environment" (command line args, sys props, etc.). */
24 public final class EnvConfig {
25
26     /** Used to report problems or feedback by {@link #create}. */
27     public static final class Note {
28         public static enum Level { INFO, WARN, ERROR };
29         public static Note info (String msg) { return new Note(Level.INFO, msg); }
30         public static Note warn (String msg) { return new Note(Level.WARN, msg); }
31         public static Note error (String msg) { return new Note(Level.ERROR, msg); }
32         public final Level level;
33         public final String message;
34         public Note (Level level, String message) {
35             this.level = level;
36             this.message = message;
37         }
38     }
39
40     /**
41      * Creates an environment config, obtaining information (in order) from the following sources:
42      *
43      * <ul>
44      * <li> A {@code bootstrap.properties} file bundled with the jar. </li>
45      * <li> System properties supplied to the JVM. </li>
46      * <li> The supplied command line arguments ({@code argv}). </li>
47      * </ul>
48      *
49      * If a later source supplies a configuration already provided by a prior source, a warning
50      * message will be logged to indicate the conflict, and the prior source will be used.
51      *
52      * @param notes a list into which notes are added, to be logged after the logging system has
53      * been initialized (which cannot happen until the appdir is known). If any {@code ERROR} notes
54      * are included, the app should terminate after reporting them.
55      * @return an env config instance, or {@code null} if no appdir could be located via any
56      * configuration source.
57      */
58     public static EnvConfig create (String[] argv, List<Note> notes) {
59         String appDir = null, appDirProv = null;
60         String appId = null, appIdProv = null;
61         String appBase = null, appBaseProv = null;
62
63         // start with bootstrap.properties config, if avaialble
64         try {
65             ResourceBundle bundle = ResourceBundle.getBundle("bootstrap");
66             if (bundle.containsKey("appdir")) {
67                 appDir = bundle.getString("appdir");
68                 appDir = appDir.replace(USER_HOME_KEY, System.getProperty("user.home"));
69                 appDirProv = "bootstrap.properties";
70             }
71             if (bundle.containsKey("appid")) {
72                 appId = bundle.getString("appid");
73                 appIdProv = "bootstrap.properties";
74             }
75             if (bundle.containsKey("appbase")) {
76                 appBase = bundle.getString("appbase");
77                 appBaseProv = "bootstrap.properties";
78             }
79             // if any system properties are specified (keys prefixed with sys.), set those up
80             for (String key : bundle.keySet()) {
81                 if (key.startsWith("sys.")) {
82                     String skey = key.substring(4);
83                     String svalue = bundle.getString(key);
84                     notes.add(Note.info("Setting system property from bundle: " +
85                                         skey + "='" + svalue + "'"));
86                     System.setProperty(skey, svalue);
87                 }
88             }
89
90         } catch (MissingResourceException e) {
91             // bootstrap.properties is optional; no need for a warning
92         }
93
94         // next seek config from system properties
95         String spropsAppDir = SysProps.appDir();
96         if (!StringUtil.isBlank(spropsAppDir)) {
97             if (appDir == null) {
98                 appDir = spropsAppDir;
99                 appDirProv = "system property";
100             } else {
101                 notes.add(Note.warn("Ignoring 'appdir' system property, have appdir via '" +
102                                     appDirProv + "'"));
103             }
104         }
105         String spropsAppId = SysProps.appId();
106         if (!StringUtil.isBlank(spropsAppId)) {
107             if (appId == null) {
108                 appId = spropsAppId;
109                 appIdProv = "system property";
110             } else {
111                 notes.add(Note.warn("Ignoring 'appid' system property, have appid via '" +
112                                     appIdProv + "'"));
113             }
114         }
115         String spropsAppBase = SysProps.appBase();
116         if (!StringUtil.isBlank(spropsAppBase)) {
117             if (appBase == null) {
118                 appBase = spropsAppBase;
119                 appBaseProv = "system property";
120             } else {
121                 notes.add(Note.warn("Ignoring 'appbase' system property, have appbase via '" +
122                                     appBaseProv + "'"));
123             }
124         }
125
126         // finally obtain config from command line arguments
127         String argvAppDir = argv.length > 0 ? argv[0] : null;
128         if (!StringUtil.isBlank(argvAppDir)) {
129             if (appDir == null) {
130                 appDir = argvAppDir;
131                 appDirProv = "command line";
132             } else {
133                 notes.add(Note.warn("Ignoring 'appdir' command line arg, have appdir via '" +
134                                     appDirProv + "'"));
135             }
136         }
137         String argvAppId = argv.length > 1 ? argv[1] : null;
138         if (!StringUtil.isBlank(argvAppId)) {
139             if (appId == null) {
140                 appId = argvAppId;
141                 appIdProv = "command line";
142             } else {
143                 notes.add(Note.warn("Ignoring 'appid' command line arg, have appid via '" +
144                                     appIdProv + "'"));
145             }
146         }
147         
148         int skipArgs = 2;
149         // Look for locator file, pass to Application and remove from appArgs
150         String argvLocatorFilename = argv.length > 2 ? argv[2] : null;
151         if (!StringUtil.isBlank(argvLocatorFilename)
152               && argvLocatorFilename.toLowerCase(Locale.ROOT).endsWith("."+Application.LOCATOR_FILE_EXTENSION)) {
153           if (HttpUtils.isJalviewSchemeUri(argvLocatorFilename)) {
154             argvLocatorFilename = HttpUtils.equivalentJalviewUrl(argvLocatorFilename);
155           }
156           notes.add(Note.info("locatorFilename in args: '"+argv[2]+"'"));
157           Application.setLocatorFile(argvLocatorFilename);
158           
159           skipArgs++;
160         }
161
162         // ensure that we were able to find an app dir
163         if (appDir == null) {
164             return null; // caller will report problem to user
165         }
166
167         notes.add(Note.info("Using appdir from " + appDirProv + ": " + appDir));
168         if (appId != null) notes.add(Note.info("Using appid from " + appIdProv + ": " + appId));
169         if (appBase != null) notes.add(
170             Note.info("Using appbase from " + appBaseProv + ": " + appBase));
171
172         // ensure that the appdir refers to a directory that exists
173         File appDirFile = new File(appDir);
174         if (!appDirFile.exists()) {
175             // if we have a bootstrap URL then we auto-create the app dir; this enables an
176             // installer to simply place a getdown.jar file somewhere and create an OS shortcut
177             // that runs getdown with an appdir and appbase specified, and have getdown create the
178             // appdir and download the app into it
179             if (!StringUtil.isBlank(appBase)) {
180                 if (appDirFile.mkdirs()) {
181                     notes.add(Note.info("Auto-created app directory '" + appDir + "'"));
182                 } else {
183                     notes.add(Note.warn("Unable to auto-create app dir: '" + appDir + "'"));
184                 }
185             } else {
186                 notes.add(Note.error("Invalid appdir '" + appDir + "': directory does not exist"));
187                 return null;
188             }
189         } else if (!appDirFile.isDirectory()) {
190             notes.add(Note.error("Invalid appdir '" + appDir + "': refers to non-directory"));
191             return null;
192         }
193
194         // pass along anything after the first two (or three) args as extra app args
195         List<String> appArgs = argv.length > skipArgs ?
196             Arrays.asList(argv).subList(skipArgs, argv.length) :
197             Collections.<String>emptyList();
198
199         // load X.509 certificate if it exists
200         File crtFile = new File(appDirFile, Digest.digestFile(Digest.VERSION) + ".crt");
201         List<Certificate> certs = new ArrayList<>();
202         if (crtFile.exists()) {
203             try (FileInputStream fis = new FileInputStream(crtFile)) {
204                 X509Certificate certificate = (X509Certificate)
205                     CertificateFactory.getInstance("X.509").generateCertificate(fis);
206                 certs.add(certificate);
207             } catch (Exception e) {
208                 notes.add(Note.error("Certificate error: " + e.getMessage()));
209             }
210         }
211
212         return new EnvConfig(appDirFile, appId, appBase, certs, appArgs);
213     }
214
215     /** The directory in which the application and metadata is stored. */
216     public final File appDir;
217
218     /** Either {@code null} or an identifier for a secondary application that should be
219       * launched. That app will use {@code appid.class} and {@code appid.apparg} to configure
220       * itself but all other parameters will be the same as the primary app. */
221     public final String appId;
222
223     /** Either {@code null} or fallback {@code appbase} to use if one cannot be read from a
224       * {@code getdown.txt} file during startup. */
225     public final String appBase;
226
227     /** Zero or more signing certificates used to verify the digest file. */
228     public final List<Certificate> certs;
229
230     /** Additional arguments to pass on to launched application. These will be added after the
231       * args in the getdown.txt file. */
232     public final List<String> appArgs;
233
234     public EnvConfig (File appDir) {
235         this(appDir, null, null, Collections.<Certificate>emptyList(),
236              Collections.<String>emptyList());
237     }
238
239     private EnvConfig (File appDir, String appId, String appBase, List<Certificate> certs,
240                        List<String> appArgs) {
241         this.appDir = appDir;
242         this.appId = appId;
243         this.appBase = appBase;
244         this.certs = certs;
245         this.appArgs = appArgs;
246     }
247
248     private static final String USER_HOME_KEY = "${user.home}";
249 }