Options.java revision 1204:9597425b6b38
1/*
2 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package jdk.nashorn.internal.runtime.options;
27
28import java.io.PrintWriter;
29import java.security.AccessControlContext;
30import java.security.AccessController;
31import java.security.Permissions;
32import java.security.PrivilegedAction;
33import java.security.ProtectionDomain;
34import java.text.MessageFormat;
35import java.util.ArrayList;
36import java.util.Collection;
37import java.util.Collections;
38import java.util.Enumeration;
39import java.util.HashMap;
40import java.util.LinkedList;
41import java.util.List;
42import java.util.Locale;
43import java.util.Map;
44import java.util.MissingResourceException;
45import java.util.Objects;
46import java.util.PropertyPermission;
47import java.util.ResourceBundle;
48import java.util.StringTokenizer;
49import java.util.TimeZone;
50import java.util.TreeMap;
51import java.util.TreeSet;
52import jdk.nashorn.internal.runtime.QuotedStringTokenizer;
53
54/**
55 * Manages global runtime options.
56 */
57public final class Options {
58    // permission to just read nashorn.* System properties
59    private static AccessControlContext createPropertyReadAccCtxt() {
60        final Permissions perms = new Permissions();
61        perms.add(new PropertyPermission("nashorn.*", "read"));
62        return new AccessControlContext(new ProtectionDomain[] { new ProtectionDomain(null, perms) });
63    }
64
65    private static final AccessControlContext READ_PROPERTY_ACC_CTXT = createPropertyReadAccCtxt();
66
67    /** Resource tag. */
68    private final String resource;
69
70    /** Error writer. */
71    private final PrintWriter err;
72
73    /** File list. */
74    private final List<String> files;
75
76    /** Arguments list */
77    private final List<String> arguments;
78
79    /** The options map of enabled options */
80    private final TreeMap<String, Option<?>> options;
81
82    /** System property that can be used to prepend options to the explicitly specified command line. */
83    private static final String NASHORN_ARGS_PREPEND_PROPERTY = "nashorn.args.prepend";
84
85    /** System property that can be used to append options to the explicitly specified command line. */
86    private static final String NASHORN_ARGS_PROPERTY = "nashorn.args";
87
88    /**
89     * Constructor
90     *
91     * Options will use System.err as the output stream for any errors
92     *
93     * @param resource resource prefix for options e.g. "nashorn"
94     */
95    public Options(final String resource) {
96        this(resource, new PrintWriter(System.err, true));
97    }
98
99    /**
100     * Constructor
101     *
102     * @param resource resource prefix for options e.g. "nashorn"
103     * @param err      error stream for reporting parse errors
104     */
105    public Options(final String resource, final PrintWriter err) {
106        this.resource  = resource;
107        this.err       = err;
108        this.files     = new ArrayList<>();
109        this.arguments = new ArrayList<>();
110        this.options   = new TreeMap<>();
111
112        // set all default values
113        for (final OptionTemplate t : Options.validOptions) {
114            if (t.getDefaultValue() != null) {
115                // populate from system properties
116                final String v = getStringProperty(t.getKey(), null);
117                if (v != null) {
118                    set(t.getKey(), createOption(t, v));
119                } else if (t.getDefaultValue() != null) {
120                    set(t.getKey(), createOption(t, t.getDefaultValue()));
121                 }
122            }
123        }
124    }
125
126    /**
127     * Get the resource for this Options set, e.g. "nashorn"
128     * @return the resource
129     */
130    public String getResource() {
131        return resource;
132    }
133
134    @Override
135    public String toString() {
136        return options.toString();
137    }
138
139    /**
140     * Convenience function for getting system properties in a safe way
141
142     * @param name of boolean property
143     * @param defValue default value of boolean property
144     * @return true if set to true, default value if unset or set to false
145     */
146    public static boolean getBooleanProperty(final String name, final Boolean defValue) {
147        Objects.requireNonNull(name);
148        if (!name.startsWith("nashorn.")) {
149            throw new IllegalArgumentException(name);
150        }
151
152        return AccessController.doPrivileged(
153                new PrivilegedAction<Boolean>() {
154                    @Override
155                    public Boolean run() {
156                        try {
157                            final String property = System.getProperty(name);
158                            if (property == null && defValue != null) {
159                                return defValue;
160                            }
161                            return property != null && !"false".equalsIgnoreCase(property);
162                        } catch (final SecurityException e) {
163                            // if no permission to read, assume false
164                            return false;
165                        }
166                    }
167                }, READ_PROPERTY_ACC_CTXT);
168    }
169
170    /**
171     * Convenience function for getting system properties in a safe way
172
173     * @param name of boolean property
174     * @return true if set to true, false if unset or set to false
175     */
176    public static boolean getBooleanProperty(final String name) {
177        return getBooleanProperty(name, null);
178    }
179
180    /**
181     * Convenience function for getting system properties in a safe way
182     *
183     * @param name of string property
184     * @param defValue the default value if unset
185     * @return string property if set or default value
186     */
187    public static String getStringProperty(final String name, final String defValue) {
188        Objects.requireNonNull(name);
189        if (! name.startsWith("nashorn.")) {
190            throw new IllegalArgumentException(name);
191        }
192
193        return AccessController.doPrivileged(
194                new PrivilegedAction<String>() {
195                    @Override
196                    public String run() {
197                        try {
198                            return System.getProperty(name, defValue);
199                        } catch (final SecurityException e) {
200                            // if no permission to read, assume the default value
201                            return defValue;
202                        }
203                    }
204                }, READ_PROPERTY_ACC_CTXT);
205    }
206
207    /**
208     * Convenience function for getting system properties in a safe way
209     *
210     * @param name of integer property
211     * @param defValue the default value if unset
212     * @return integer property if set or default value
213     */
214    public static int getIntProperty(final String name, final int defValue) {
215        Objects.requireNonNull(name);
216        if (! name.startsWith("nashorn.")) {
217            throw new IllegalArgumentException(name);
218        }
219
220        return AccessController.doPrivileged(
221                new PrivilegedAction<Integer>() {
222                    @Override
223                    public Integer run() {
224                        try {
225                            return Integer.getInteger(name, defValue);
226                        } catch (final SecurityException e) {
227                            // if no permission to read, assume the default value
228                            return defValue;
229                        }
230                    }
231                }, READ_PROPERTY_ACC_CTXT);
232    }
233
234    /**
235     * Return an option given its resource key. If the key doesn't begin with
236     * {@literal <resource>}.option it will be completed using the resource from this
237     * instance
238     *
239     * @param key key for option
240     * @return an option value
241     */
242    public Option<?> get(final String key) {
243        return options.get(key(key));
244    }
245
246    /**
247     * Return an option as a boolean
248     *
249     * @param key key for option
250     * @return an option value
251     */
252    public boolean getBoolean(final String key) {
253        final Option<?> option = get(key);
254        return option != null ? (Boolean)option.getValue() : false;
255    }
256
257    /**
258     * Return an option as a integer
259     *
260     * @param key key for option
261     * @return an option value
262     */
263    public int getInteger(final String key) {
264        final Option<?> option = get(key);
265        return option != null ? (Integer)option.getValue() : 0;
266    }
267
268    /**
269     * Return an option as a String
270     *
271     * @param key key for option
272     * @return an option value
273     */
274    public String getString(final String key) {
275        final Option<?> option = get(key);
276        if (option != null) {
277            final String value = (String)option.getValue();
278            if(value != null) {
279                return value.intern();
280            }
281        }
282        return null;
283    }
284
285    /**
286     * Set an option, overwriting an existing state if one exists
287     *
288     * @param key    option key
289     * @param option option
290     */
291    public void set(final String key, final Option<?> option) {
292        options.put(key(key), option);
293    }
294
295    /**
296     * Set an option as a boolean value, overwriting an existing state if one exists
297     *
298     * @param key    option key
299     * @param option option
300     */
301    public void set(final String key, final boolean option) {
302        set(key, new Option<>(option));
303    }
304
305    /**
306     * Set an option as a String value, overwriting an existing state if one exists
307     *
308     * @param key    option key
309     * @param option option
310     */
311    public void set(final String key, final String option) {
312        set(key, new Option<>(option));
313    }
314
315    /**
316     * Return the user arguments to the program, i.e. those trailing "--" after
317     * the filename
318     *
319     * @return a list of user arguments
320     */
321    public List<String> getArguments() {
322        return Collections.unmodifiableList(this.arguments);
323    }
324
325    /**
326     * Return the JavaScript files passed to the program
327     *
328     * @return a list of files
329     */
330    public List<String> getFiles() {
331        return Collections.unmodifiableList(files);
332    }
333
334    /**
335     * Return the option templates for all the valid option supported.
336     *
337     * @return a collection of OptionTemplate objects.
338     */
339    public static Collection<OptionTemplate> getValidOptions() {
340        return Collections.unmodifiableCollection(validOptions);
341    }
342
343    /**
344     * Make sure a key is fully qualified for table lookups
345     *
346     * @param shortKey key for option
347     * @return fully qualified key
348     */
349    private String key(final String shortKey) {
350        String key = shortKey;
351        while (key.startsWith("-")) {
352            key = key.substring(1, key.length());
353        }
354        key = key.replace("-", ".");
355        final String keyPrefix = this.resource + ".option.";
356        if (key.startsWith(keyPrefix)) {
357            return key;
358        }
359        return keyPrefix + key;
360    }
361
362    static String getMsg(final String msgId, final String... args) {
363        try {
364            final String msg = Options.bundle.getString(msgId);
365            if (args.length == 0) {
366                return msg;
367            }
368            return new MessageFormat(msg).format(args);
369        } catch (final MissingResourceException e) {
370            throw new IllegalArgumentException(e);
371        }
372    }
373
374    /**
375     * Display context sensitive help
376     *
377     * @param e  exception that caused a parse error
378     */
379    public void displayHelp(final IllegalArgumentException e) {
380        if (e instanceof IllegalOptionException) {
381            final OptionTemplate template = ((IllegalOptionException)e).getTemplate();
382            if (template.isXHelp()) {
383                // display extended help information
384                displayHelp(true);
385            } else {
386                err.println(((IllegalOptionException)e).getTemplate());
387            }
388            return;
389        }
390
391        if (e != null && e.getMessage() != null) {
392            err.println(getMsg("option.error.invalid.option",
393                    e.getMessage(),
394                    helpOptionTemplate.getShortName(),
395                    helpOptionTemplate.getName()));
396            err.println();
397            return;
398        }
399
400        displayHelp(false);
401    }
402
403    /**
404     * Display full help
405     *
406     * @param extended show the extended help for all options, including undocumented ones
407     */
408    public void displayHelp(final boolean extended) {
409        for (final OptionTemplate t : Options.validOptions) {
410            if ((extended || !t.isUndocumented()) && t.getResource().equals(resource)) {
411                err.println(t);
412                err.println();
413            }
414        }
415    }
416
417    /**
418     * Processes the arguments and stores their information. Throws
419     * IllegalArgumentException on error. The message can be analyzed by the
420     * displayHelp function to become more context sensitive
421     *
422     * @param args arguments from command line
423     */
424    public void process(final String[] args) {
425        final LinkedList<String> argList = new LinkedList<>();
426        addSystemProperties(NASHORN_ARGS_PREPEND_PROPERTY, argList);
427        Collections.addAll(argList, args);
428        addSystemProperties(NASHORN_ARGS_PROPERTY, argList);
429
430        while (!argList.isEmpty()) {
431            final String arg = argList.remove(0);
432            Objects.requireNonNull(arg);
433
434            // skip empty args
435            if (arg.isEmpty()) {
436                continue;
437            }
438
439            // user arguments to the script
440            if ("--".equals(arg)) {
441                arguments.addAll(argList);
442                argList.clear();
443                continue;
444            }
445
446            // If it doesn't start with -, it's a file. But, if it is just "-",
447            // then it is a file representing standard input.
448            if (!arg.startsWith("-") || arg.length() == 1) {
449                files.add(arg);
450                continue;
451            }
452
453            if (arg.startsWith(definePropPrefix)) {
454                final String value = arg.substring(definePropPrefix.length());
455                final int eq = value.indexOf('=');
456                if (eq != -1) {
457                    // -Dfoo=bar Set System property "foo" with value "bar"
458                    System.setProperty(value.substring(0, eq), value.substring(eq + 1));
459                } else {
460                    // -Dfoo is fine. Set System property "foo" with "" as it's value
461                    if (!value.isEmpty()) {
462                        System.setProperty(value, "");
463                    } else {
464                        // do not allow empty property name
465                        throw new IllegalOptionException(definePropTemplate);
466                    }
467                }
468                continue;
469            }
470
471            // it is an argument,  it and assign key, value and template
472            final ParsedArg parg = new ParsedArg(arg);
473
474            // check if the value of this option is passed as next argument
475            if (parg.template.isValueNextArg()) {
476                if (argList.isEmpty()) {
477                    throw new IllegalOptionException(parg.template);
478                }
479                parg.value = argList.remove(0);
480            }
481
482            // -h [args...]
483            if (parg.template.isHelp()) {
484                // check if someone wants help on an explicit arg
485                if (!argList.isEmpty()) {
486                    try {
487                        final OptionTemplate t = new ParsedArg(argList.get(0)).template;
488                        throw new IllegalOptionException(t);
489                    } catch (final IllegalArgumentException e) {
490                        throw e;
491                    }
492                }
493                throw new IllegalArgumentException(); // show help for
494                // everything
495            }
496
497            if (parg.template.isXHelp()) {
498                throw new IllegalOptionException(parg.template);
499            }
500
501            set(parg.template.getKey(), createOption(parg.template, parg.value));
502
503            // Arg may have a dependency to set other args, e.g.
504            // scripting->anon.functions
505            if (parg.template.getDependency() != null) {
506                argList.addFirst(parg.template.getDependency());
507            }
508        }
509    }
510
511    private static void addSystemProperties(final String sysPropName, final List<String> argList) {
512        final String sysArgs = getStringProperty(sysPropName, null);
513        if (sysArgs != null) {
514            final StringTokenizer st = new StringTokenizer(sysArgs);
515            while (st.hasMoreTokens()) {
516                argList.add(st.nextToken());
517            }
518        }
519    }
520
521    private static OptionTemplate getOptionTemplate(final String key) {
522        for (final OptionTemplate t : Options.validOptions) {
523            if (t.matches(key)) {
524                return t;
525            }
526        }
527        return null;
528    }
529
530    private static Option<?> createOption(final OptionTemplate t, final String value) {
531        switch (t.getType()) {
532        case "string":
533            // default value null
534            return new Option<>(value);
535        case "timezone":
536            // default value "TimeZone.getDefault()"
537            return new Option<>(TimeZone.getTimeZone(value));
538        case "locale":
539            return new Option<>(Locale.forLanguageTag(value));
540        case "keyvalues":
541            return new KeyValueOption(value);
542        case "log":
543            return new LoggingOption(value);
544        case "boolean":
545            return new Option<>(value != null && Boolean.parseBoolean(value));
546        case "integer":
547            try {
548                return new Option<>(value == null ? 0 : Integer.parseInt(value));
549            } catch (final NumberFormatException nfe) {
550                throw new IllegalOptionException(t);
551            }
552        case "properties":
553            //swallow the properties and set them
554            initProps(new KeyValueOption(value));
555            return null;
556        default:
557            break;
558        }
559        throw new IllegalArgumentException(value);
560    }
561
562    private static void initProps(final KeyValueOption kv) {
563        for (final Map.Entry<String, String> entry : kv.getValues().entrySet()) {
564            System.setProperty(entry.getKey(), entry.getValue());
565        }
566    }
567
568    /**
569     * Resource name for properties file
570     */
571    private static final String MESSAGES_RESOURCE = "jdk.nashorn.internal.runtime.resources.Options";
572
573    /**
574     * Resource bundle for properties file
575     */
576    private static ResourceBundle bundle;
577
578    /**
579     * Usages per resource from properties file
580     */
581    private static HashMap<Object, Object> usage;
582
583    /**
584     * Valid options from templates in properties files
585     */
586    private static Collection<OptionTemplate> validOptions;
587
588    /**
589     * Help option
590     */
591    private static OptionTemplate helpOptionTemplate;
592
593    /**
594     * Define property option template.
595     */
596    private static OptionTemplate definePropTemplate;
597
598    /**
599     * Prefix of "define property" option.
600     */
601    private static String definePropPrefix;
602
603    static {
604        Options.bundle = ResourceBundle.getBundle(Options.MESSAGES_RESOURCE, Locale.getDefault());
605        Options.validOptions = new TreeSet<>();
606        Options.usage        = new HashMap<>();
607
608        for (final Enumeration<String> keys = Options.bundle.getKeys(); keys.hasMoreElements(); ) {
609            final String key = keys.nextElement();
610            final StringTokenizer st = new StringTokenizer(key, ".");
611            String resource = null;
612            String type = null;
613
614            if (st.countTokens() > 0) {
615                resource = st.nextToken(); // e.g. "nashorn"
616            }
617
618            if (st.countTokens() > 0) {
619                type = st.nextToken(); // e.g. "option"
620            }
621
622            if ("option".equals(type)) {
623                String helpKey = null;
624                String xhelpKey = null;
625                String definePropKey = null;
626                try {
627                    helpKey = Options.bundle.getString(resource + ".options.help.key");
628                    xhelpKey = Options.bundle.getString(resource + ".options.xhelp.key");
629                    definePropKey = Options.bundle.getString(resource + ".options.D.key");
630                } catch (final MissingResourceException e) {
631                    //ignored: no help
632                }
633                final boolean        isHelp = key.equals(helpKey);
634                final boolean        isXHelp = key.equals(xhelpKey);
635                final OptionTemplate t      = new OptionTemplate(resource, key, Options.bundle.getString(key), isHelp, isXHelp);
636
637                Options.validOptions.add(t);
638                if (isHelp) {
639                    helpOptionTemplate = t;
640                }
641
642                if (key.equals(definePropKey)) {
643                    definePropPrefix = t.getName();
644                    definePropTemplate = t;
645                }
646            } else if (resource != null && "options".equals(type)) {
647                Options.usage.put(resource, Options.bundle.getObject(key));
648            }
649        }
650    }
651
652    @SuppressWarnings("serial")
653    private static class IllegalOptionException extends IllegalArgumentException {
654        private final OptionTemplate template;
655
656        IllegalOptionException(final OptionTemplate t) {
657            super();
658            this.template = t;
659        }
660
661        OptionTemplate getTemplate() {
662            return this.template;
663        }
664    }
665
666    /**
667     * This is a resolved argument of the form key=value
668     */
669    private static class ParsedArg {
670        /** The resolved option template this argument corresponds to */
671        OptionTemplate template;
672
673        /** The value of the argument */
674        String value;
675
676        ParsedArg(final String argument) {
677            final QuotedStringTokenizer st = new QuotedStringTokenizer(argument, "=");
678            if (!st.hasMoreTokens()) {
679                throw new IllegalArgumentException();
680            }
681
682            final String token = st.nextToken();
683            this.template = Options.getOptionTemplate(token);
684            if (this.template == null) {
685                throw new IllegalArgumentException(argument);
686            }
687
688            value = "";
689            if (st.hasMoreTokens()) {
690                while (st.hasMoreTokens()) {
691                    value += st.nextToken();
692                    if (st.hasMoreTokens()) {
693                        value += ':';
694                    }
695                }
696            } else if ("boolean".equals(this.template.getType())) {
697                value = "true";
698            }
699        }
700    }
701}
702