2 // Getdown - application installer, patcher and launcher
3 // Copyright (C) 2004-2018 Getdown authors
4 // https://github.com/threerings/getdown/blob/master/LICENSE
6 package com.threerings.getdown.data;
9 import java.io.FileInputStream;
10 import java.net.MalformedURLException;
12 import java.security.cert.Certificate;
13 import java.security.cert.CertificateFactory;
14 import java.security.cert.X509Certificate;
17 import com.threerings.getdown.util.StringUtil;
19 import jalview.util.HttpUtils;
21 import com.threerings.getdown.data.Application;
23 /** Configuration that comes from our "environment" (command line args, sys props, etc.). */
24 public final class EnvConfig {
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) {
36 this.message = message;
41 * Creates an environment config, obtaining information (in order) from the following sources:
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>
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.
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.
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;
63 // start with bootstrap.properties config, if avaialble
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";
71 if (bundle.containsKey("appid")) {
72 appId = bundle.getString("appid");
73 appIdProv = "bootstrap.properties";
75 if (bundle.containsKey("appbase")) {
76 appBase = bundle.getString("appbase");
77 appBaseProv = "bootstrap.properties";
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);
90 } catch (MissingResourceException e) {
91 // bootstrap.properties is optional; no need for a warning
94 // next seek config from system properties
95 String spropsAppDir = SysProps.appDir();
96 if (!StringUtil.isBlank(spropsAppDir)) {
98 appDir = spropsAppDir;
99 appDirProv = "system property";
101 notes.add(Note.warn("Ignoring 'appdir' system property, have appdir via '" +
105 String spropsAppId = SysProps.appId();
106 if (!StringUtil.isBlank(spropsAppId)) {
109 appIdProv = "system property";
111 notes.add(Note.warn("Ignoring 'appid' system property, have appid via '" +
115 String spropsAppBase = SysProps.appBase();
116 if (!StringUtil.isBlank(spropsAppBase)) {
117 if (appBase == null) {
118 appBase = spropsAppBase;
119 appBaseProv = "system property";
121 notes.add(Note.warn("Ignoring 'appbase' system property, have appbase via '" +
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) {
131 appDirProv = "command line";
133 notes.add(Note.warn("Ignoring 'appdir' command line arg, have appdir via '" +
137 String argvAppId = argv.length > 1 ? argv[1] : null;
138 if (!StringUtil.isBlank(argvAppId)) {
141 appIdProv = "command line";
143 notes.add(Note.warn("Ignoring 'appid' command line arg, have appid via '" +
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 argvLocatorFilename = HttpUtils.equivalentJalviewUrl(argvLocatorFilename);
154 notes.add(Note.info("locatorFilename in args: '"+argv[2]+"'"));
155 Application.setLocatorFile(argvLocatorFilename);
160 // ensure that we were able to find an app dir
161 if (appDir == null) {
162 return null; // caller will report problem to user
165 notes.add(Note.info("Using appdir from " + appDirProv + ": " + appDir));
166 if (appId != null) notes.add(Note.info("Using appid from " + appIdProv + ": " + appId));
167 if (appBase != null) notes.add(
168 Note.info("Using appbase from " + appBaseProv + ": " + appBase));
170 // ensure that the appdir refers to a directory that exists
171 File appDirFile = new File(appDir);
172 if (!appDirFile.exists()) {
173 // if we have a bootstrap URL then we auto-create the app dir; this enables an
174 // installer to simply place a getdown.jar file somewhere and create an OS shortcut
175 // that runs getdown with an appdir and appbase specified, and have getdown create the
176 // appdir and download the app into it
177 if (!StringUtil.isBlank(appBase)) {
178 if (appDirFile.mkdirs()) {
179 notes.add(Note.info("Auto-created app directory '" + appDir + "'"));
181 notes.add(Note.warn("Unable to auto-create app dir: '" + appDir + "'"));
184 notes.add(Note.error("Invalid appdir '" + appDir + "': directory does not exist"));
187 } else if (!appDirFile.isDirectory()) {
188 notes.add(Note.error("Invalid appdir '" + appDir + "': refers to non-directory"));
192 // pass along anything after the first two (or three) args as extra app args
193 List<String> appArgs = argv.length > skipArgs ?
194 Arrays.asList(argv).subList(skipArgs, argv.length) :
195 Collections.<String>emptyList();
197 // load X.509 certificate if it exists
198 File crtFile = new File(appDirFile, Digest.digestFile(Digest.VERSION) + ".crt");
199 List<Certificate> certs = new ArrayList<>();
200 if (crtFile.exists()) {
201 try (FileInputStream fis = new FileInputStream(crtFile)) {
202 X509Certificate certificate = (X509Certificate)
203 CertificateFactory.getInstance("X.509").generateCertificate(fis);
204 certs.add(certificate);
205 } catch (Exception e) {
206 notes.add(Note.error("Certificate error: " + e.getMessage()));
210 return new EnvConfig(appDirFile, appId, appBase, certs, appArgs);
213 /** The directory in which the application and metadata is stored. */
214 public final File appDir;
216 /** Either {@code null} or an identifier for a secondary application that should be
217 * launched. That app will use {@code appid.class} and {@code appid.apparg} to configure
218 * itself but all other parameters will be the same as the primary app. */
219 public final String appId;
221 /** Either {@code null} or fallback {@code appbase} to use if one cannot be read from a
222 * {@code getdown.txt} file during startup. */
223 public final String appBase;
225 /** Zero or more signing certificates used to verify the digest file. */
226 public final List<Certificate> certs;
228 /** Additional arguments to pass on to launched application. These will be added after the
229 * args in the getdown.txt file. */
230 public final List<String> appArgs;
232 public EnvConfig (File appDir) {
233 this(appDir, null, null, Collections.<Certificate>emptyList(),
234 Collections.<String>emptyList());
237 private EnvConfig (File appDir, String appId, String appBase, List<Certificate> certs,
238 List<String> appArgs) {
239 this.appDir = appDir;
241 this.appBase = appBase;
243 this.appArgs = appArgs;
246 private static final String USER_HOME_KEY = "${user.home}";