Options.java revision 1790:785843878cf7
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    private static void checkPropertyName(final String name) {
140        if (! Objects.requireNonNull(name).startsWith("nashorn.")) {
141            throw new IllegalArgumentException(name);
142        }
143    }
144
145    /**
146     * Convenience function for getting system properties in a safe way
147
148     * @param name of boolean property
149     * @param defValue default value of boolean property
150     * @return true if set to true, default value if unset or set to false
151     */
152    public static boolean getBooleanProperty(final String name, final Boolean defValue) {
153        checkPropertyName(name);
154        return AccessController.doPrivileged(
155                new PrivilegedAction<Boolean>() {
156                    @Override
157                    public Boolean run() {
158                        try {
159                            final String property = System.getProperty(name);
160                            if (property == null && defValue != null) {
161                                return defValue;
162                            }
163                            return property != null && !"false".equalsIgnoreCase(property);
164                        } catch (final SecurityException e) {
165                            // if no permission to read, assume false
166                            return false;
167                        }
168                    }
169                }, READ_PROPERTY_ACC_CTXT);
170    }
171
172    /**
173     * Convenience function for getting system properties in a safe way
174
175     * @param name of boolean property
176     * @return true if set to true, false if unset or set to false
177     */
178    public static boolean getBooleanProperty(final String name) {
179        return getBooleanProperty(name, null);
180    }
181
182    /**
183     * Convenience function for getting system properties in a safe way
184     *
185     * @param name of string property
186     * @param defValue the default value if unset
187     * @return string property if set or default value
188     */
189    public static String getStringProperty(final String name, final String defValue) {
190        checkPropertyName(name);
191        return AccessController.doPrivileged(
192                new PrivilegedAction<String>() {
193                    @Override
194                    public String run() {
195                        try {
196                            return System.getProperty(name, defValue);
197                        } catch (final SecurityException e) {
198                            // if no permission to read, assume the default value
199                            return defValue;
200                        }
201                    }
202                }, READ_PROPERTY_ACC_CTXT);
203    }
204
205    /**
206     * Convenience function for getting system properties in a safe way
207     *
208     * @param name of integer property
209     * @param defValue the default value if unset
210     * @return integer property if set or default value
211     */
212    public static int getIntProperty(final String name, final int defValue) {
213        checkPropertyName(name);
214        return AccessController.doPrivileged(
215                new PrivilegedAction<Integer>() {
216                    @Override
217                    public Integer run() {
218                        try {
219                            return Integer.getInteger(name, defValue);
220                        } catch (final SecurityException e) {
221                            // if no permission to read, assume the default value
222                            return defValue;
223                        }
224                    }
225                }, READ_PROPERTY_ACC_CTXT);
226    }
227
228    /**
229     * Return an option given its resource key. If the key doesn't begin with
230     * {@literal <resource>}.option it will be completed using the resource from this
231     * instance
232     *
233     * @param key key for option
234     * @return an option value
235     */
236    public Option<?> get(final String key) {
237        return options.get(key(key));
238    }
239
240    /**
241     * Return an option as a boolean
242     *
243     * @param key key for option
244     * @return an option value
245     */
246    public boolean getBoolean(final String key) {
247        final Option<?> option = get(key);
248        return option != null ? (Boolean)option.getValue() : false;
249    }
250
251    /**
252     * Return an option as a integer
253     *
254     * @param key key for option
255     * @return an option value
256     */
257    public int getInteger(final String key) {
258        final Option<?> option = get(key);
259        return option != null ? (Integer)option.getValue() : 0;
260    }
261
262    /**
263     * Return an option as a String
264     *
265     * @param key key for option
266     * @return an option value
267     */
268    public String getString(final String key) {
269        final Option<?> option = get(key);
270        if (option != null) {
271            final String value = (String)option.getValue();
272            if(value != null) {
273                return value.intern();
274            }
275        }
276        return null;
277    }
278
279    /**
280     * Set an option, overwriting an existing state if one exists
281     *
282     * @param key    option key
283     * @param option option
284     */
285    public void set(final String key, final Option<?> option) {
286        options.put(key(key), option);
287    }
288
289    /**
290     * Set an option as a boolean value, overwriting an existing state if one exists
291     *
292     * @param key    option key
293     * @param option option
294     */
295    public void set(final String key, final boolean option) {
296        set(key, new Option<>(option));
297    }
298
299    /**
300     * Set an option as a String value, overwriting an existing state if one exists
301     *
302     * @param key    option key
303     * @param option option
304     */
305    public void set(final String key, final String option) {
306        set(key, new Option<>(option));
307    }
308
309    /**
310     * Return the user arguments to the program, i.e. those trailing "--" after
311     * the filename
312     *
313     * @return a list of user arguments
314     */
315    public List<String> getArguments() {
316        return Collections.unmodifiableList(this.arguments);
317    }
318
319    /**
320     * Return the JavaScript files passed to the program
321     *
322     * @return a list of files
323     */
324    public List<String> getFiles() {
325        return Collections.unmodifiableList(files);
326    }
327
328    /**
329     * Return the option templates for all the valid option supported.
330     *
331     * @return a collection of OptionTemplate objects.
332     */
333    public static Collection<OptionTemplate> getValidOptions() {
334        return Collections.unmodifiableCollection(validOptions);
335    }
336
337    /**
338     * Make sure a key is fully qualified for table lookups
339     *
340     * @param shortKey key for option
341     * @return fully qualified key
342     */
343    private String key(final String shortKey) {
344        String key = shortKey;
345        while (key.startsWith("-")) {
346            key = key.substring(1, key.length());
347        }
348        key = key.replace("-", ".");
349        final String keyPrefix = this.resource + ".option.";
350        if (key.startsWith(keyPrefix)) {
351            return key;
352        }
353        return keyPrefix + key;
354    }
355
356    static String getMsg(final String msgId, final String... args) {
357        try {
358            final String msg = Options.bundle.getString(msgId);
359            if (args.length == 0) {
360                return msg;
361            }
362            return new MessageFormat(msg).format(args);
363        } catch (final MissingResourceException e) {
364            throw new IllegalArgumentException(e);
365        }
366    }
367
368    /**
369     * Display context sensitive help
370     *
371     * @param e  exception that caused a parse error
372     */
373    public void displayHelp(final IllegalArgumentException e) {
374        if (e instanceof IllegalOptionException) {
375            final OptionTemplate template = ((IllegalOptionException)e).getTemplate();
376            if (template.isXHelp()) {
377                // display extended help information
378                displayHelp(true);
379            } else {
380                err.println(((IllegalOptionException)e).getTemplate());
381            }
382            return;
383        }
384
385        if (e != null && e.getMessage() != null) {
386            err.println(getMsg("option.error.invalid.option",
387                    e.getMessage(),
388                    helpOptionTemplate.getShortName(),
389                    helpOptionTemplate.getName()));
390            err.println();
391            return;
392        }
393
394        displayHelp(false);
395    }
396
397    /**
398     * Display full help
399     *
400     * @param extended show the extended help for all options, including undocumented ones
401     */
402    public void displayHelp(final boolean extended) {
403        for (final OptionTemplate t : Options.validOptions) {
404            if ((extended || !t.isUndocumented()) && t.getResource().equals(resource)) {
405                err.println(t);
406                err.println();
407            }
408        }
409    }
410
411    /**
412     * Processes the arguments and stores their information. Throws
413     * IllegalArgumentException on error. The message can be analyzed by the
414     * displayHelp function to become more context sensitive
415     *
416     * @param args arguments from command line
417     */
418    public void process(final String[] args) {
419        final LinkedList<String> argList = new LinkedList<>();
420        addSystemProperties(NASHORN_ARGS_PREPEND_PROPERTY, argList);
421        processArgList(argList);
422        assert argList.isEmpty();
423        Collections.addAll(argList, args);
424        processArgList(argList);
425        assert argList.isEmpty();
426        addSystemProperties(NASHORN_ARGS_PROPERTY, argList);
427        processArgList(argList);
428        assert argList.isEmpty();
429    }
430
431    private void processArgList(final LinkedList<String> argList) {
432        while (!argList.isEmpty()) {
433            final String arg = argList.remove(0);
434            Objects.requireNonNull(arg);
435
436            // skip empty args
437            if (arg.isEmpty()) {
438                continue;
439            }
440
441            // user arguments to the script
442            if ("--".equals(arg)) {
443                arguments.addAll(argList);
444                argList.clear();
445                continue;
446            }
447
448            // If it doesn't start with -, it's a file. But, if it is just "-",
449            // then it is a file representing standard input.
450            if (!arg.startsWith("-") || arg.length() == 1) {
451                files.add(arg);
452                continue;
453            }
454
455            if (arg.startsWith(definePropPrefix)) {
456                final String value = arg.substring(definePropPrefix.length());
457                final int eq = value.indexOf('=');
458                if (eq != -1) {
459                    // -Dfoo=bar Set System property "foo" with value "bar"
460                    System.setProperty(value.substring(0, eq), value.substring(eq + 1));
461                } else {
462                    // -Dfoo is fine. Set System property "foo" with "" as it's value
463                    if (!value.isEmpty()) {
464                        System.setProperty(value, "");
465                    } else {
466                        // do not allow empty property name
467                        throw new IllegalOptionException(definePropTemplate);
468                    }
469                }
470                continue;
471            }
472
473            // it is an argument,  it and assign key, value and template
474            final ParsedArg parg = new ParsedArg(arg);
475
476            // check if the value of this option is passed as next argument
477            if (parg.template.isValueNextArg()) {
478                if (argList.isEmpty()) {
479                    throw new IllegalOptionException(parg.template);
480                }
481                parg.value = argList.remove(0);
482            }
483
484            // -h [args...]
485            if (parg.template.isHelp()) {
486                // check if someone wants help on an explicit arg
487                if (!argList.isEmpty()) {
488                    try {
489                        final OptionTemplate t = new ParsedArg(argList.get(0)).template;
490                        throw new IllegalOptionException(t);
491                    } catch (final IllegalArgumentException e) {
492                        throw e;
493                    }
494                }
495                throw new IllegalArgumentException(); // show help for
496                // everything
497            }
498
499            if (parg.template.isXHelp()) {
500                throw new IllegalOptionException(parg.template);
501            }
502
503            if (parg.template.isRepeated()) {
504                assert parg.template.getType().equals("string");
505
506                final String key = key(parg.template.getKey());
507                final String value = options.containsKey(key)?
508                    (options.get(key).getValue() + "," + parg.value) : Objects.toString(parg.value);
509                options.put(key, new Option<>(value));
510            } else {
511                set(parg.template.getKey(), createOption(parg.template, parg.value));
512            }
513
514            // Arg may have a dependency to set other args, e.g.
515            // scripting->anon.functions
516            if (parg.template.getDependency() != null) {
517                argList.addFirst(parg.template.getDependency());
518            }
519        }
520    }
521
522    private static void addSystemProperties(final String sysPropName, final List<String> argList) {
523        final String sysArgs = getStringProperty(sysPropName, null);
524        if (sysArgs != null) {
525            final StringTokenizer st = new StringTokenizer(sysArgs);
526            while (st.hasMoreTokens()) {
527                argList.add(st.nextToken());
528            }
529        }
530    }
531
532    /**
533     * Retrieves an option template identified by key.
534     * @param shortKey the short (that is without the e.g. "nashorn.option." part) key
535     * @return the option template identified by the key
536     * @throws IllegalArgumentException if the key doesn't specify an existing template
537     */
538    public OptionTemplate getOptionTemplateByKey(final String shortKey) {
539        final String fullKey = key(shortKey);
540        for(final OptionTemplate t: validOptions) {
541            if(t.getKey().equals(fullKey)) {
542                return t;
543            }
544        }
545        throw new IllegalArgumentException(shortKey);
546    }
547
548    private static OptionTemplate getOptionTemplateByName(final String name) {
549        for (final OptionTemplate t : Options.validOptions) {
550            if (t.nameMatches(name)) {
551                return t;
552            }
553        }
554        return null;
555    }
556
557    private static Option<?> createOption(final OptionTemplate t, final String value) {
558        switch (t.getType()) {
559        case "string":
560            // default value null
561            return new Option<>(value);
562        case "timezone":
563            // default value "TimeZone.getDefault()"
564            return new Option<>(TimeZone.getTimeZone(value));
565        case "locale":
566            return new Option<>(Locale.forLanguageTag(value));
567        case "keyvalues":
568            return new KeyValueOption(value);
569        case "log":
570            return new LoggingOption(value);
571        case "boolean":
572            return new Option<>(value != null && Boolean.parseBoolean(value));
573        case "integer":
574            try {
575                return new Option<>(value == null ? 0 : Integer.parseInt(value));
576            } catch (final NumberFormatException nfe) {
577                throw new IllegalOptionException(t);
578            }
579        case "properties":
580            //swallow the properties and set them
581            initProps(new KeyValueOption(value));
582            return null;
583        default:
584            break;
585        }
586        throw new IllegalArgumentException(value);
587    }
588
589    private static void initProps(final KeyValueOption kv) {
590        for (final Map.Entry<String, String> entry : kv.getValues().entrySet()) {
591            System.setProperty(entry.getKey(), entry.getValue());
592        }
593    }
594
595    /**
596     * Resource name for properties file
597     */
598    private static final String MESSAGES_RESOURCE = "jdk.nashorn.internal.runtime.resources.Options";
599
600    /**
601     * Resource bundle for properties file
602     */
603    private static ResourceBundle bundle;
604
605    /**
606     * Usages per resource from properties file
607     */
608    private static HashMap<Object, Object> usage;
609
610    /**
611     * Valid options from templates in properties files
612     */
613    private static Collection<OptionTemplate> validOptions;
614
615    /**
616     * Help option
617     */
618    private static OptionTemplate helpOptionTemplate;
619
620    /**
621     * Define property option template.
622     */
623    private static OptionTemplate definePropTemplate;
624
625    /**
626     * Prefix of "define property" option.
627     */
628    private static String definePropPrefix;
629
630    static {
631        Options.bundle = ResourceBundle.getBundle(Options.MESSAGES_RESOURCE, Locale.getDefault());
632        Options.validOptions = new TreeSet<>();
633        Options.usage        = new HashMap<>();
634
635        for (final Enumeration<String> keys = Options.bundle.getKeys(); keys.hasMoreElements(); ) {
636            final String key = keys.nextElement();
637            final StringTokenizer st = new StringTokenizer(key, ".");
638            String resource = null;
639            String type = null;
640
641            if (st.countTokens() > 0) {
642                resource = st.nextToken(); // e.g. "nashorn"
643            }
644
645            if (st.countTokens() > 0) {
646                type = st.nextToken(); // e.g. "option"
647            }
648
649            if ("option".equals(type)) {
650                String helpKey = null;
651                String xhelpKey = null;
652                String definePropKey = null;
653                try {
654                    helpKey = Options.bundle.getString(resource + ".options.help.key");
655                    xhelpKey = Options.bundle.getString(resource + ".options.xhelp.key");
656                    definePropKey = Options.bundle.getString(resource + ".options.D.key");
657                } catch (final MissingResourceException e) {
658                    //ignored: no help
659                }
660                final boolean        isHelp = key.equals(helpKey);
661                final boolean        isXHelp = key.equals(xhelpKey);
662                final OptionTemplate t      = new OptionTemplate(resource, key, Options.bundle.getString(key), isHelp, isXHelp);
663
664                Options.validOptions.add(t);
665                if (isHelp) {
666                    helpOptionTemplate = t;
667                }
668
669                if (key.equals(definePropKey)) {
670                    definePropPrefix = t.getName();
671                    definePropTemplate = t;
672                }
673            } else if (resource != null && "options".equals(type)) {
674                Options.usage.put(resource, Options.bundle.getObject(key));
675            }
676        }
677    }
678
679    @SuppressWarnings("serial")
680    private static class IllegalOptionException extends IllegalArgumentException {
681        private final OptionTemplate template;
682
683        IllegalOptionException(final OptionTemplate t) {
684            super();
685            this.template = t;
686        }
687
688        OptionTemplate getTemplate() {
689            return this.template;
690        }
691    }
692
693    /**
694     * This is a resolved argument of the form key=value
695     */
696    private static class ParsedArg {
697        /** The resolved option template this argument corresponds to */
698        OptionTemplate template;
699
700        /** The value of the argument */
701        String value;
702
703        ParsedArg(final String argument) {
704            final QuotedStringTokenizer st = new QuotedStringTokenizer(argument, "=");
705            if (!st.hasMoreTokens()) {
706                throw new IllegalArgumentException();
707            }
708
709            final String token = st.nextToken();
710            this.template = getOptionTemplateByName(token);
711            if (this.template == null) {
712                throw new IllegalArgumentException(argument);
713            }
714
715            value = "";
716            if (st.hasMoreTokens()) {
717                while (st.hasMoreTokens()) {
718                    value += st.nextToken();
719                    if (st.hasMoreTokens()) {
720                        value += ':';
721                    }
722                }
723            } else if ("boolean".equals(this.template.getType())) {
724                value = "true";
725            }
726        }
727    }
728}
729