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