Options.java revision 1291:456ffec2b5ae
133715Skato/*
216359Sasami * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
333080Skato * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
416359Sasami *
516359Sasami * This code is free software; you can redistribute it and/or modify it
616359Sasami * under the terms of the GNU General Public License version 2 only, as
730329Skato * published by the Free Software Foundation.  Oracle designates this
816359Sasami * particular file as subject to the "Classpath" exception as provided
916359Sasami * by Oracle in the LICENSE file that accompanied this code.
1016359Sasami *
1116359Sasami * This code is distributed in the hope that it will be useful, but WITHOUT
1229006Skato * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
1333711Skato * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
1429006Skato * version 2 for more details (a copy is included in the LICENSE file that
1529006Skato * accompanied this code).
1616359Sasami *
1732939Skato * You should have received a copy of the GNU General Public License version
1816359Sasami * 2 along with this work; if not, write to the Free Software Foundation,
1916359Sasami * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
2016359Sasami *
2126477Skato * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
2224657Skato * or visit www.oracle.com if you need additional information or have any
2324657Skato * questions.
2424657Skato */
2516359Sasami
2616359Sasamipackage jdk.nashorn.internal.runtime.options;
2716359Sasami
2829631Skatoimport java.io.PrintWriter;
2932939Skatoimport java.security.AccessControlContext;
3032092Skatoimport java.security.AccessController;
3132939Skatoimport java.security.Permissions;
3227844Skatoimport java.security.PrivilegedAction;
3327844Skatoimport java.security.ProtectionDomain;
3417256Sasamiimport java.text.MessageFormat;
3525572Skatoimport java.util.ArrayList;
3625572Skatoimport java.util.Collection;
3725572Skatoimport java.util.Collections;
3825572Skatoimport java.util.Enumeration;
3925572Skatoimport java.util.HashMap;
4025195Skatoimport java.util.LinkedList;
4125195Skatoimport java.util.List;
4225195Skatoimport java.util.Locale;
4325195Skatoimport java.util.Map;
4425195Skatoimport java.util.MissingResourceException;
4518846Sasamiimport java.util.Objects;
4618846Sasamiimport java.util.PropertyPermission;
4720129Sasamiimport java.util.ResourceBundle;
4819269Sasamiimport java.util.StringTokenizer;
4918846Sasamiimport java.util.TimeZone;
5033080Skatoimport java.util.TreeMap;
5133080Skatoimport java.util.TreeSet;
5233080Skatoimport jdk.nashorn.internal.runtime.QuotedStringTokenizer;
5318208Sasami
5431556Skato/**
5527392Skato * Manages global runtime options.
5627392Skato */
5733019Skatopublic final class Options {
5827391Skato    // permission to just read nashorn.* System properties
5933019Skato    private static AccessControlContext createPropertyReadAccCtxt() {
6033019Skato        final Permissions perms = new Permissions();
6133019Skato        perms.add(new PropertyPermission("nashorn.*", "read"));
6233019Skato        return new AccessControlContext(new ProtectionDomain[] { new ProtectionDomain(null, perms) });
6333019Skato    }
6433019Skato
6533019Skato    private static final AccessControlContext READ_PROPERTY_ACC_CTXT = createPropertyReadAccCtxt();
6633019Skato
6733019Skato    /** Resource tag. */
6833019Skato    private final String resource;
6919122Sasami
7029006Skato    /** Error writer. */
7129006Skato    private final PrintWriter err;
7229006Skato
7329006Skato    /** File list. */
7429006Skato    private final List<String> files;
7529006Skato
7618846Sasami    /** Arguments list */
7718265Sasami    private final List<String> arguments;
7818265Sasami
7927692Skato    /** The options map of enabled options */
8029137Skato    private final TreeMap<String, Option<?>> options;
8133270Skato
8233270Skato    /** System property that can be used to prepend options to the explicitly specified command line. */
8318208Sasami    private static final String NASHORN_ARGS_PREPEND_PROPERTY = "nashorn.args.prepend";
8427173Skato
8527173Skato    /** System property that can be used to append options to the explicitly specified command line. */
8621773Skato    private static final String NASHORN_ARGS_PROPERTY = "nashorn.args";
8721773Skato
8821773Skato    /**
8922065Skato     * Constructor
9021773Skato     *
9121773Skato     * Options will use System.err as the output stream for any errors
9221773Skato     *
9318208Sasami     * @param resource resource prefix for options e.g. "nashorn"
9418208Sasami     */
9518265Sasami    public Options(final String resource) {
9625268Skato        this(resource, new PrintWriter(System.err, true));
9725268Skato    }
9818265Sasami
9918265Sasami    /**
10018846Sasami     * Constructor
10123847Skato     *
10223847Skato     * @param resource resource prefix for options e.g. "nashorn"
10329533Skato     * @param err      error stream for reporting parse errors
10429533Skato     */
10529533Skato    public Options(final String resource, final PrintWriter err) {
10630553Skato        this.resource  = resource;
10730553Skato        this.err       = err;
10830553Skato        this.files     = new ArrayList<>();
10930553Skato        this.arguments = new ArrayList<>();
11030553Skato        this.options   = new TreeMap<>();
11130553Skato
11230553Skato        // set all default values
11330553Skato        for (final OptionTemplate t : Options.validOptions) {
11430553Skato            if (t.getDefaultValue() != null) {
11530553Skato                // populate from system properties
11630553Skato                final String v = getStringProperty(t.getKey(), null);
11730553Skato                if (v != null) {
11830553Skato                    set(t.getKey(), createOption(t, v));
11930553Skato                } else if (t.getDefaultValue() != null) {
12030553Skato                    set(t.getKey(), createOption(t, t.getDefaultValue()));
12133715Skato                 }
12233715Skato            }
12333715Skato        }
12433715Skato    }
12533715Skato
12633715Skato    /**
12733715Skato     * Get the resource for this Options set, e.g. "nashorn"
12833715Skato     * @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        processArgList(argList);
428        assert argList.isEmpty();
429        Collections.addAll(argList, args);
430        processArgList(argList);
431        assert argList.isEmpty();
432        addSystemProperties(NASHORN_ARGS_PROPERTY, argList);
433        processArgList(argList);
434        assert argList.isEmpty();
435    }
436
437    private void processArgList(final LinkedList<String> argList) {
438        while (!argList.isEmpty()) {
439            final String arg = argList.remove(0);
440            Objects.requireNonNull(arg);
441
442            // skip empty args
443            if (arg.isEmpty()) {
444                continue;
445            }
446
447            // user arguments to the script
448            if ("--".equals(arg)) {
449                arguments.addAll(argList);
450                argList.clear();
451                continue;
452            }
453
454            // If it doesn't start with -, it's a file. But, if it is just "-",
455            // then it is a file representing standard input.
456            if (!arg.startsWith("-") || arg.length() == 1) {
457                files.add(arg);
458                continue;
459            }
460
461            if (arg.startsWith(definePropPrefix)) {
462                final String value = arg.substring(definePropPrefix.length());
463                final int eq = value.indexOf('=');
464                if (eq != -1) {
465                    // -Dfoo=bar Set System property "foo" with value "bar"
466                    System.setProperty(value.substring(0, eq), value.substring(eq + 1));
467                } else {
468                    // -Dfoo is fine. Set System property "foo" with "" as it's value
469                    if (!value.isEmpty()) {
470                        System.setProperty(value, "");
471                    } else {
472                        // do not allow empty property name
473                        throw new IllegalOptionException(definePropTemplate);
474                    }
475                }
476                continue;
477            }
478
479            // it is an argument,  it and assign key, value and template
480            final ParsedArg parg = new ParsedArg(arg);
481
482            // check if the value of this option is passed as next argument
483            if (parg.template.isValueNextArg()) {
484                if (argList.isEmpty()) {
485                    throw new IllegalOptionException(parg.template);
486                }
487                parg.value = argList.remove(0);
488            }
489
490            // -h [args...]
491            if (parg.template.isHelp()) {
492                // check if someone wants help on an explicit arg
493                if (!argList.isEmpty()) {
494                    try {
495                        final OptionTemplate t = new ParsedArg(argList.get(0)).template;
496                        throw new IllegalOptionException(t);
497                    } catch (final IllegalArgumentException e) {
498                        throw e;
499                    }
500                }
501                throw new IllegalArgumentException(); // show help for
502                // everything
503            }
504
505            if (parg.template.isXHelp()) {
506                throw new IllegalOptionException(parg.template);
507            }
508
509            set(parg.template.getKey(), createOption(parg.template, parg.value));
510
511            // Arg may have a dependency to set other args, e.g.
512            // scripting->anon.functions
513            if (parg.template.getDependency() != null) {
514                argList.addFirst(parg.template.getDependency());
515            }
516        }
517    }
518
519    private static void addSystemProperties(final String sysPropName, final List<String> argList) {
520        final String sysArgs = getStringProperty(sysPropName, null);
521        if (sysArgs != null) {
522            final StringTokenizer st = new StringTokenizer(sysArgs);
523            while (st.hasMoreTokens()) {
524                argList.add(st.nextToken());
525            }
526        }
527    }
528
529    private static OptionTemplate getOptionTemplate(final String key) {
530        for (final OptionTemplate t : Options.validOptions) {
531            if (t.matches(key)) {
532                return t;
533            }
534        }
535        return null;
536    }
537
538    private static Option<?> createOption(final OptionTemplate t, final String value) {
539        switch (t.getType()) {
540        case "string":
541            // default value null
542            return new Option<>(value);
543        case "timezone":
544            // default value "TimeZone.getDefault()"
545            return new Option<>(TimeZone.getTimeZone(value));
546        case "locale":
547            return new Option<>(Locale.forLanguageTag(value));
548        case "keyvalues":
549            return new KeyValueOption(value);
550        case "log":
551            return new LoggingOption(value);
552        case "boolean":
553            return new Option<>(value != null && Boolean.parseBoolean(value));
554        case "integer":
555            try {
556                return new Option<>(value == null ? 0 : Integer.parseInt(value));
557            } catch (final NumberFormatException nfe) {
558                throw new IllegalOptionException(t);
559            }
560        case "properties":
561            //swallow the properties and set them
562            initProps(new KeyValueOption(value));
563            return null;
564        default:
565            break;
566        }
567        throw new IllegalArgumentException(value);
568    }
569
570    private static void initProps(final KeyValueOption kv) {
571        for (final Map.Entry<String, String> entry : kv.getValues().entrySet()) {
572            System.setProperty(entry.getKey(), entry.getValue());
573        }
574    }
575
576    /**
577     * Resource name for properties file
578     */
579    private static final String MESSAGES_RESOURCE = "jdk.nashorn.internal.runtime.resources.Options";
580
581    /**
582     * Resource bundle for properties file
583     */
584    private static ResourceBundle bundle;
585
586    /**
587     * Usages per resource from properties file
588     */
589    private static HashMap<Object, Object> usage;
590
591    /**
592     * Valid options from templates in properties files
593     */
594    private static Collection<OptionTemplate> validOptions;
595
596    /**
597     * Help option
598     */
599    private static OptionTemplate helpOptionTemplate;
600
601    /**
602     * Define property option template.
603     */
604    private static OptionTemplate definePropTemplate;
605
606    /**
607     * Prefix of "define property" option.
608     */
609    private static String definePropPrefix;
610
611    static {
612        Options.bundle = ResourceBundle.getBundle(Options.MESSAGES_RESOURCE, Locale.getDefault());
613        Options.validOptions = new TreeSet<>();
614        Options.usage        = new HashMap<>();
615
616        for (final Enumeration<String> keys = Options.bundle.getKeys(); keys.hasMoreElements(); ) {
617            final String key = keys.nextElement();
618            final StringTokenizer st = new StringTokenizer(key, ".");
619            String resource = null;
620            String type = null;
621
622            if (st.countTokens() > 0) {
623                resource = st.nextToken(); // e.g. "nashorn"
624            }
625
626            if (st.countTokens() > 0) {
627                type = st.nextToken(); // e.g. "option"
628            }
629
630            if ("option".equals(type)) {
631                String helpKey = null;
632                String xhelpKey = null;
633                String definePropKey = null;
634                try {
635                    helpKey = Options.bundle.getString(resource + ".options.help.key");
636                    xhelpKey = Options.bundle.getString(resource + ".options.xhelp.key");
637                    definePropKey = Options.bundle.getString(resource + ".options.D.key");
638                } catch (final MissingResourceException e) {
639                    //ignored: no help
640                }
641                final boolean        isHelp = key.equals(helpKey);
642                final boolean        isXHelp = key.equals(xhelpKey);
643                final OptionTemplate t      = new OptionTemplate(resource, key, Options.bundle.getString(key), isHelp, isXHelp);
644
645                Options.validOptions.add(t);
646                if (isHelp) {
647                    helpOptionTemplate = t;
648                }
649
650                if (key.equals(definePropKey)) {
651                    definePropPrefix = t.getName();
652                    definePropTemplate = t;
653                }
654            } else if (resource != null && "options".equals(type)) {
655                Options.usage.put(resource, Options.bundle.getObject(key));
656            }
657        }
658    }
659
660    @SuppressWarnings("serial")
661    private static class IllegalOptionException extends IllegalArgumentException {
662        private final OptionTemplate template;
663
664        IllegalOptionException(final OptionTemplate t) {
665            super();
666            this.template = t;
667        }
668
669        OptionTemplate getTemplate() {
670            return this.template;
671        }
672    }
673
674    /**
675     * This is a resolved argument of the form key=value
676     */
677    private static class ParsedArg {
678        /** The resolved option template this argument corresponds to */
679        OptionTemplate template;
680
681        /** The value of the argument */
682        String value;
683
684        ParsedArg(final String argument) {
685            final QuotedStringTokenizer st = new QuotedStringTokenizer(argument, "=");
686            if (!st.hasMoreTokens()) {
687                throw new IllegalArgumentException();
688            }
689
690            final String token = st.nextToken();
691            this.template = Options.getOptionTemplate(token);
692            if (this.template == null) {
693                throw new IllegalArgumentException(argument);
694            }
695
696            value = "";
697            if (st.hasMoreTokens()) {
698                while (st.hasMoreTokens()) {
699                    value += st.nextToken();
700                    if (st.hasMoreTokens()) {
701                        value += ':';
702                    }
703                }
704            } else if ("boolean".equals(this.template.getType())) {
705                value = "true";
706            }
707        }
708    }
709}
710