JAL-3348 jvmmempc allowed in jvl file
[jalview.git] / getdown / src / getdown / core / src / main / java / com / threerings / getdown / util / Config.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.util;
7
8 import java.io.File;
9 import java.io.FileInputStream;
10 import java.io.IOException;
11 import java.io.InputStreamReader;
12 import java.io.Reader;
13 import java.net.MalformedURLException;
14 import java.net.URL;
15 import java.nio.charset.StandardCharsets;
16 import java.util.ArrayList;
17 import java.util.Arrays;
18 import java.util.HashMap;
19 import java.util.List;
20 import java.util.Locale;
21 import java.util.Map;
22
23 import static com.threerings.getdown.Log.log;
24
25 /**
26  * Handles parsing and runtime access for Getdown's config files (mainly {@code getdown.txt}).
27  * These files contain zero or more mappings for a particular string key. Config values can be
28  * fetched as single strings, lists of strings, or parsed into primitives or compound data types
29  * like colors and rectangles.
30  */
31 public class Config
32 {
33     /** Empty configuration. */
34     public static final Config EMPTY = new Config(new HashMap<String, Object>());
35
36     /** Options that control the {@link #parsePairs} function. */
37     public static class ParseOpts {
38         // these should be tweaked as desired by the caller
39         public boolean biasToKey = false;
40         public boolean strictComments = false;
41
42         // these are filled in by parseConfig
43         public String osname = null;
44         public String osarch = null;
45     }
46
47     /**
48      * Creates a parse configuration, filling in the platform filters (or not) depending on the
49      * value of {@code checkPlatform}.
50      */
51     public static ParseOpts createOpts (boolean checkPlatform) {
52         ParseOpts opts = new ParseOpts();
53         if (checkPlatform) {
54             opts.osname = StringUtil.deNull(System.getProperty("os.name")).toLowerCase(Locale.ROOT);
55             opts.osarch = StringUtil.deNull(System.getProperty("os.arch")).toLowerCase(Locale.ROOT);
56         }
57         return opts;
58     }
59
60     /**
61      * Parses a configuration file containing key/value pairs. The file must be in the UTF-8
62      * encoding.
63      *
64      * @param opts options that influence the parsing. See {@link #createOpts}.
65      *
66      * @return a list of <code>String[]</code> instances containing the key/value pairs in the
67      * order they were parsed from the file.
68      */
69     public static List<String[]> parsePairs (File source, ParseOpts opts)
70         throws IOException
71     {
72         // annoyingly FileReader does not allow encoding to be specified (uses platform default)
73         try (FileInputStream fis = new FileInputStream(source);
74              InputStreamReader input = new InputStreamReader(fis, StandardCharsets.UTF_8)) {
75             return parsePairs(input, opts);
76         }
77     }
78
79     /**
80      * See {@link #parsePairs(File,ParseOpts)}.
81      */
82     public static List<String[]> parsePairs (Reader source, ParseOpts opts) throws IOException
83     {
84         List<String[]> pairs = new ArrayList<>();
85         for (String line : FileUtil.readLines(source)) {
86             // nix comments
87             int cidx = line.indexOf("#");
88             if (opts.strictComments ? cidx == 0 : cidx != -1) {
89                 line = line.substring(0, cidx);
90             }
91
92             // trim whitespace and skip blank lines
93             line = line.trim();
94             if (StringUtil.isBlank(line)) {
95                 continue;
96             }
97
98             // parse our key/value pair
99             String[] pair = new String[2];
100             // if we're biasing toward key, put all the extra = in the key rather than the value
101             int eidx = opts.biasToKey ? line.lastIndexOf("=") : line.indexOf("=");
102             if (eidx != -1) {
103                 pair[0] = line.substring(0, eidx).trim();
104                 pair[1] = line.substring(eidx+1).trim();
105             } else {
106                 pair[0] = line;
107                 pair[1] = "";
108             }
109
110             // if the pair has an os qualifier, we need to process it
111             if (pair[1].startsWith("[")) {
112                 int qidx = pair[1].indexOf("]");
113                 if (qidx == -1) {
114                     log.warning("Bogus platform specifier", "key", pair[0], "value", pair[1]);
115                     continue; // omit the pair entirely
116                 }
117                 // if we're checking qualifiers and the os doesn't match this qualifier, skip it
118                 String quals = pair[1].substring(1, qidx);
119                 if (opts.osname != null && !checkQualifiers(quals, opts.osname, opts.osarch)) {
120                     log.debug("Skipping", "quals", quals,
121                               "osname", opts.osname, "osarch", opts.osarch,
122                               "key", pair[0], "value", pair[1]);
123                     continue;
124                 }
125                 // otherwise filter out the qualifier text
126                 pair[1] = pair[1].substring(qidx+1).trim();
127             }
128
129             pairs.add(pair);
130         }
131
132         return pairs;
133     }
134
135     /**
136      * Takes a comma-separated String of four integers and returns a rectangle using those ints as
137      * the its x, y, width, and height.
138      */
139     public static Rectangle parseRect (String name, String value)
140     {
141         if (!StringUtil.isBlank(value)) {
142             int[] v = StringUtil.parseIntArray(value);
143             if (v != null && v.length == 4) {
144                 return new Rectangle(v[0], v[1], v[2], v[3]);
145             }
146             log.warning("Ignoring invalid rect '" + name + "' config '" + value + "'.");
147         }
148         return null;
149     }
150
151     /**
152      * Parses the given hex color value (e.g. FFCC99) and returns an {@code Integer} with that
153      * value. If the given value is null or not a valid hexadecimal number, this will return null.
154      */
155     public static Integer parseColor (String hexValue)
156     {
157         if (!StringUtil.isBlank(hexValue)) {
158             try {
159                 // if no alpha channel is specified, use 255 (full alpha)
160                 int alpha = hexValue.length() > 6 ? 0 : 0xFF000000;
161                 return Integer.parseInt(hexValue, 16) | alpha;
162             } catch (NumberFormatException e) {
163                 log.warning("Ignoring invalid color", "hexValue", hexValue, "exception", e);
164             }
165         }
166         return null;
167     }
168
169     /**
170      * Parses a configuration file containing key/value pairs. The file must be in the UTF-8
171      * encoding.
172      *
173      * @return a map from keys to values, where a value will be an array of strings if more than
174      * one key/value pair in the config file was associated with the same key.
175      */
176     public static Config parseConfig (File source, ParseOpts opts)
177         throws IOException
178     {
179         Map<String, Object> data = new HashMap<>();
180
181         // I thought that we could use HashMap<String, String[]> and put new String[] {pair[1]} for
182         // the null case, but it mysteriously dies on launch, so leaving it as HashMap<String,
183         // Object> for now
184         for (String[] pair : parsePairs(source, opts)) {
185             Object value = data.get(pair[0]);
186             if (value == null) {
187                 data.put(pair[0], pair[1]);
188             } else if (value instanceof String) {
189                 data.put(pair[0], new String[] { (String)value, pair[1] });
190             } else if (value instanceof String[]) {
191                 String[] values = (String[])value;
192                 String[] nvalues = new String[values.length+1];
193                 System.arraycopy(values, 0, nvalues, 0, values.length);
194                 nvalues[values.length] = pair[1];
195                 data.put(pair[0], nvalues);
196             }
197         }
198
199         // special magic for the getdown.txt config: if the parsed data contains 'strict_comments =
200         // true' then we reparse the file with strict comments (i.e. # is only assumed to start a
201         // comment in column 0)
202         if (!opts.strictComments && Boolean.parseBoolean((String)data.get("strict_comments"))) {
203             opts.strictComments = true;
204             return parseConfig(source, opts);
205         }
206
207         return new Config(data);
208     }
209
210     public Config (Map<String,  Object> data) {
211         _data = data;
212     }
213
214     /**
215      * Returns whether {@code name} has a value in this config.
216      */
217     public boolean hasValue (String name) {
218         return _data.containsKey(name);
219     }
220
221     /**
222      * Returns the raw-value for {@code name}. This may be a {@code String}, {@code String[]}, or
223      * {@code null}.
224      */
225     public Object getRaw (String name) {
226         return _data.get(name);
227     }
228
229     /**
230      * Returns the specified config value as a string, or {@code null}.
231      */
232     public String getString (String name) {
233         return (String)_data.get(name);
234     }
235
236     /**
237      * Returns the specified config value as a string, or {@code def}.
238      */
239     public String getString (String name, String def) {
240         String value = (String)_data.get(name);
241         return value == null ? def : value;
242     }
243
244     /**
245      * Returns the specified config value as a boolean.
246      */
247     public boolean getBoolean (String name) {
248         return Boolean.parseBoolean(getString(name));
249     }
250
251     /**
252      * Massages a single string into an array and leaves existing array values as is. Simplifies
253      * access to parameters that are expected to be arrays.
254      */
255     public String[] getMultiValue (String name)
256     {
257         Object value = _data.get(name);
258         if (value == null) {
259           return new String[] {};
260         }
261         if (value instanceof String) {
262             return new String[] { (String)value };
263         } else {
264             return (String[])value;
265         }
266     }
267
268     /** Used to parse rectangle specifications from the config file. */
269     public Rectangle getRect (String name, Rectangle def)
270     {
271         String value = getString(name);
272         Rectangle rect = parseRect(name, value);
273         return (rect == null) ? def : rect;
274     }
275
276     /**
277      * Parses and returns the config value for {@code name} as an int. If no value is provided,
278      * {@code def} is returned. If the value is invalid, a warning is logged and {@code def} is
279      * returned.
280      */
281     public int getInt (String name, int def) {
282         String value = getString(name);
283         try {
284             return value == null ? def : Integer.parseInt(value);
285         } catch (Exception e) {
286             log.warning("Ignoring invalid int '" + name + "' config '" + value + "',");
287             return def;
288         }
289     }
290
291     /**
292      * Parses and returns the config value for {@code name} as a long. If no value is provided,
293      * {@code def} is returned. If the value is invalid, a warning is logged and {@code def} is
294      * returned.
295      */
296     public long getLong (String name, long def) {
297         String value = getString(name);
298         try {
299             return value == null ? def : Long.parseLong(value);
300         } catch (Exception e) {
301             log.warning("Ignoring invalid long '" + name + "' config '" + value + "',");
302             return def;
303         }
304     }
305
306     /** Used to parse color specifications from the config file. */
307     public int getColor (String name, int def)
308     {
309         String value = getString(name);
310         Integer color = parseColor(value);
311         return (color == null) ? def : color;
312     }
313
314     /** Parses a list of strings from the config file. */
315     public String[] getList (String name)
316     {
317         String value = getString(name);
318         return (value == null) ? new String[0] : StringUtil.parseStringArray(value);
319     }
320
321     /**
322      * Parses a URL from the config file, checking first for a localized version.
323      */
324     public String getUrl (String name, String def)
325     {
326         String value = getString(name + "." + Locale.getDefault().getLanguage());
327         if (StringUtil.isBlank(value)) {
328             value = getString(name);
329         }
330         if (StringUtil.isBlank(value)) {
331             value = def;
332         }
333         if (!StringUtil.isBlank(value)) {
334             try {
335                 HostWhitelist.verify(new URL(value));
336             } catch (MalformedURLException e) {
337                 log.warning("Invalid URL.", "url", value, e);
338                 value = null;
339             }
340         }
341         return value;
342     }
343
344     /**
345      * A helper function for {@link #parsePairs(Reader,ParseOpts)}. Qualifiers have the following
346      * form:
347      * <pre>
348      * id = os[-arch]
349      * ids = id | id,ids
350      * quals = !id | ids
351      * </pre>
352      * Examples: [linux-amd64,linux-x86_64], [windows], [mac os x], [!windows]. Negative qualifiers
353      * must appear alone, they cannot be used with other qualifiers (positive or negative).
354      */
355     protected static boolean checkQualifiers (String quals, String osname, String osarch)
356     {
357         if (quals.startsWith("!")) {
358             if (quals.indexOf(",") != -1) { // sanity check
359                 log.warning("Multiple qualifiers cannot be used when one of the qualifiers " +
360                             "is negative", "quals", quals);
361                 return false;
362             }
363             return !checkQualifier(quals.substring(1), osname, osarch);
364         }
365         for (String qual : quals.split(",")) {
366             if (checkQualifier(qual, osname, osarch)) {
367                 return true; // if we have a positive match, we can immediately return true
368             }
369         }
370         return false; // we had no positive matches, so return false
371     }
372
373     /** A helper function for {@link #checkQualifiers}. */
374     protected static boolean checkQualifier (String qual, String osname, String osarch)
375     {
376         String[] bits = qual.trim().toLowerCase(Locale.ROOT).split("-");
377         String os = bits[0], arch = (bits.length > 1) ? bits[1] : "";
378         return (osname.indexOf(os) != -1) && (osarch.indexOf(arch) != -1);
379     }
380     
381     public void mergeConfig(Config newValues, boolean merge) {
382       
383       for (Map.Entry<String, Object> entry : newValues.getData().entrySet()) {
384         
385         String key = entry.getKey();
386         Object nvalue = entry.getValue();
387
388         String mkey = key.indexOf('.') > -1 ? key.substring(key.indexOf('.') + 1) : key;
389         if (merge && allowedMergeKeys.contains(mkey)) {
390           
391           // merge multi values
392           
393           Object value = _data.get(key);
394           
395           if (value == null) {
396             _data.put(key, nvalue);
397           } else if (value instanceof String) {
398             if (nvalue instanceof String) {
399               
400               // value is String, nvalue is String
401               _data.put(key, new String[] { (String)value, (String)nvalue });
402               
403             } else if (nvalue instanceof String[]) {
404               
405               // value is String, nvalue is String[]
406               String[] nvalues = (String[])nvalue;
407               String[] newvalues = new String[nvalues.length+1];
408               newvalues[0] = (String)value;
409               System.arraycopy(nvalues, 0, newvalues, 1, nvalues.length);
410               _data.put(key, newvalues);
411               
412             }
413           } else if (value instanceof String[]) {
414             if (nvalue instanceof String) {
415               
416               // value is String[], nvalue is String
417               String[] values = (String[])value;
418               String[] newvalues = new String[values.length+1];
419               System.arraycopy(values, 0, newvalues, 0, values.length);
420               newvalues[values.length] = (String)nvalue;
421               _data.put(key, newvalues);
422               
423             } else if (nvalue instanceof String[]) {
424               
425               // value is String[], nvalue is String[]
426               String[] values = (String[])value;
427               String[] nvalues = (String[])nvalue;
428               String[] newvalues = new String[values.length + nvalues.length];
429               System.arraycopy(values, 0, newvalues, 0, values.length);
430               System.arraycopy(nvalues, 0, newvalues, values.length, newvalues.length);
431               _data.put(key, newvalues);
432               
433             }
434           }
435           
436         } else if (allowedReplaceKeys.contains(mkey)){
437           
438           // replace value
439           _data.put(key, nvalue);
440           
441         } else {
442           log.warning("Not merging key '"+key+"' into config");
443         }
444
445       }
446       
447     }
448     
449     public String toString() {
450       StringBuilder sb = new StringBuilder();
451       for (Map.Entry<String, Object> entry : getData().entrySet()) {
452         String key = entry.getKey();
453         Object val = entry.getValue();
454         sb.append(key);
455         sb.append("=");
456         if (val instanceof String) {
457           sb.append((String)val);
458         } else if (val instanceof String[]) {
459           sb.append(Arrays.toString((String[])val));
460         } else {
461           sb.append("Value not String or String[]");
462         }
463         sb.append("\n");
464       }
465       return sb.toString();
466     }
467     
468     public Map<String, Object> getData() {
469       return _data;
470     }
471
472     private final Map<String, Object> _data;
473  
474     public static final List<String> allowedReplaceKeys = Arrays.asList("appbase","apparg","jvmarg","jvmmempc"); // these are the ones we might use
475     public static final List<String> allowedMergeKeys = Arrays.asList("apparg","jvmarg","jvmmempc"); // these are the ones we might use
476     //private final List<String> allowedMergeKeys = Arrays.asList("apparg","jvmarg","resource","code","java_location"); // (not exhaustive list here)
477 }