Options.java revision 1387:864aaf4e6441
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            set(parg.template.getKey(), createOption(parg.template, parg.value));
504
505            // Arg may have a dependency to set other args, e.g.
506            // scripting->anon.functions
507            if (parg.template.getDependency() != null) {
508                argList.addFirst(parg.template.getDependency());
509            }
510        }
511    }
512
513    private static void addSystemProperties(final String sysPropName, final List<String> argList) {
514        final String sysArgs = getStringProperty(sysPropName, null);
515        if (sysArgs != null) {
516            final StringTokenizer st = new StringTokenizer(sysArgs);
517            while (st.hasMoreTokens()) {
518                argList.add(st.nextToken());
519            }
520        }
521    }
522
523    /**
524     * Retrieves an option template identified by key.
525     * @param shortKey the short (that is without the e.g. "nashorn.option." part) key
526     * @return the option template identified by the key
527     * @throws IllegalArgumentException if the key doesn't specify an existing template
528     */
529    public OptionTemplate getOptionTemplateByKey(final String shortKey) {
530        final String fullKey = key(shortKey);
531        for(final OptionTemplate t: validOptions) {
532            if(t.getKey().equals(fullKey)) {
533                return t;
534            }
535        }
536        throw new IllegalArgumentException(shortKey);
537    }
538
539    private static OptionTemplate getOptionTemplateByName(final String name) {
540        for (final OptionTemplate t : Options.validOptions) {
541            if (t.nameMatches(name)) {
542                return t;
543            }
544        }
545        return null;
546    }
547
548    private static Option<?> createOption(final OptionTemplate t, final String value) {
549        switch (t.getType()) {
550        case "string":
551            // default value null
552            return new Option<>(value);
553        case "timezone":
554            // default value "TimeZone.getDefault()"
555            return new Option<>(TimeZone.getTimeZone(value));
556        case "locale":
557            return new Option<>(Locale.forLanguageTag(value));
558        case "keyvalues":
559            return new KeyValueOption(value);
560        case "log":
561            return new LoggingOption(value);
562        case "boolean":
563            return new Option<>(value != null && Boolean.parseBoolean(value));
564        case "integer":
565            try {
566                return new Option<>(value == null ? 0 : Integer.parseInt(value));
567            } catch (final NumberFormatException nfe) {
568                throw new IllegalOptionException(t);
569            }
570        case "properties":
571            //swallow the properties and set them
572            initProps(new KeyValueOption(value));
573            return null;
574        default:
575            break;
576        }
577        throw new IllegalArgumentException(value);
578    }
579
580    private static void initProps(final KeyValueOption kv) {
581        for (final Map.Entry<String, String> entry : kv.getValues().entrySet()) {
582            System.setProperty(entry.getKey(), entry.getValue());
583        }
584    }
585
586    /**
587     * Resource name for properties file
588     */
589    private static final String MESSAGES_RESOURCE = "jdk.nashorn.internal.runtime.resources.Options";
590
591    /**
592     * Resource bundle for properties file
593     */
594    private static ResourceBundle bundle;
595
596    /**
597     * Usages per resource from properties file
598     */
599    private static HashMap<Object, Object> usage;
600
601    /**
602     * Valid options from templates in properties files
603     */
604    private static Collection<OptionTemplate> validOptions;
605
606    /**
607     * Help option
608     */
609    private static OptionTemplate helpOptionTemplate;
610
611    /**
612     * Define property option template.
613     */
614    private static OptionTemplate definePropTemplate;
615
616    /**
617     * Prefix of "define property" option.
618     */
619    private static String definePropPrefix;
620
621    static {
622        Options.bundle = ResourceBundle.getBundle(Options.MESSAGES_RESOURCE, Locale.getDefault());
623        Options.validOptions = new TreeSet<>();
624        Options.usage        = new HashMap<>();
625
626        for (final Enumeration<String> keys = Options.bundle.getKeys(); keys.hasMoreElements(); ) {
627            final String key = keys.nextElement();
628            final StringTokenizer st = new StringTokenizer(key, ".");
629            String resource = null;
630            String type = null;
631
632            if (st.countTokens() > 0) {
633                resource = st.nextToken(); // e.g. "nashorn"
634            }
635
636            if (st.countTokens() > 0) {
637                type = st.nextToken(); // e.g. "option"
638            }
639
640            if ("option".equals(type)) {
641                String helpKey = null;
642                String xhelpKey = null;
643                String definePropKey = null;
644                try {
645                    helpKey = Options.bundle.getString(resource + ".options.help.key");
646                    xhelpKey = Options.bundle.getString(resource + ".options.xhelp.key");
647                    definePropKey = Options.bundle.getString(resource + ".options.D.key");
648                } catch (final MissingResourceException e) {
649                    //ignored: no help
650                }
651                final boolean        isHelp = key.equals(helpKey);
652                final boolean        isXHelp = key.equals(xhelpKey);
653                final OptionTemplate t      = new OptionTemplate(resource, key, Options.bundle.getString(key), isHelp, isXHelp);
654
655                Options.validOptions.add(t);
656                if (isHelp) {
657                    helpOptionTemplate = t;
658                }
659
660                if (key.equals(definePropKey)) {
661                    definePropPrefix = t.getName();
662                    definePropTemplate = t;
663                }
664            } else if (resource != null && "options".equals(type)) {
665                Options.usage.put(resource, Options.bundle.getObject(key));
666            }
667        }
668    }
669
670    @SuppressWarnings("serial")
671    private static class IllegalOptionException extends IllegalArgumentException {
672        private final OptionTemplate template;
673
674        IllegalOptionException(final OptionTemplate t) {
675            super();
676            this.template = t;
677        }
678
679        OptionTemplate getTemplate() {
680            return this.template;
681        }
682    }
683
684    /**
685     * This is a resolved argument of the form key=value
686     */
687    private static class ParsedArg {
688        /** The resolved option template this argument corresponds to */
689        OptionTemplate template;
690
691        /** The value of the argument */
692        String value;
693
694        ParsedArg(final String argument) {
695            final QuotedStringTokenizer st = new QuotedStringTokenizer(argument, "=");
696            if (!st.hasMoreTokens()) {
697                throw new IllegalArgumentException();
698            }
699
700            final String token = st.nextToken();
701            this.template = getOptionTemplateByName(token);
702            if (this.template == null) {
703                throw new IllegalArgumentException(argument);
704            }
705
706            value = "";
707            if (st.hasMoreTokens()) {
708                while (st.hasMoreTokens()) {
709                    value += st.nextToken();
710                    if (st.hasMoreTokens()) {
711                        value += ':';
712                    }
713                }
714            } else if ("boolean".equals(this.template.getType())) {
715                value = "true";
716            }
717        }
718    }
719}
720