/* * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package jdk.nashorn.internal.runtime.options; import java.io.PrintWriter; import java.security.AccessControlContext; import java.security.AccessController; import java.security.Permissions; import java.security.PrivilegedAction; import java.security.ProtectionDomain; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.Objects; import java.util.PropertyPermission; import java.util.ResourceBundle; import java.util.StringTokenizer; import java.util.TimeZone; import java.util.TreeMap; import java.util.TreeSet; import jdk.nashorn.internal.runtime.QuotedStringTokenizer; /** * Manages global runtime options. */ public final class Options { // permission to just read nashorn.* System properties private static AccessControlContext createPropertyReadAccCtxt() { final Permissions perms = new Permissions(); perms.add(new PropertyPermission("nashorn.*", "read")); return new AccessControlContext(new ProtectionDomain[] { new ProtectionDomain(null, perms) }); } private static final AccessControlContext READ_PROPERTY_ACC_CTXT = createPropertyReadAccCtxt(); /** Resource tag. */ private final String resource; /** Error writer. */ private final PrintWriter err; /** File list. */ private final List files; /** Arguments list */ private final List arguments; /** The options map of enabled options */ private final TreeMap> options; /** System property that can be used to prepend options to the explicitly specified command line. */ private static final String NASHORN_ARGS_PREPEND_PROPERTY = "nashorn.args.prepend"; /** System property that can be used to append options to the explicitly specified command line. */ private static final String NASHORN_ARGS_PROPERTY = "nashorn.args"; /** * Constructor * * Options will use System.err as the output stream for any errors * * @param resource resource prefix for options e.g. "nashorn" */ public Options(final String resource) { this(resource, new PrintWriter(System.err, true)); } /** * Constructor * * @param resource resource prefix for options e.g. "nashorn" * @param err error stream for reporting parse errors */ public Options(final String resource, final PrintWriter err) { this.resource = resource; this.err = err; this.files = new ArrayList<>(); this.arguments = new ArrayList<>(); this.options = new TreeMap<>(); // set all default values for (final OptionTemplate t : Options.validOptions) { if (t.getDefaultValue() != null) { // populate from system properties final String v = getStringProperty(t.getKey(), null); if (v != null) { set(t.getKey(), createOption(t, v)); } else if (t.getDefaultValue() != null) { set(t.getKey(), createOption(t, t.getDefaultValue())); } } } } /** * Get the resource for this Options set, e.g. "nashorn" * @return the resource */ public String getResource() { return resource; } @Override public String toString() { return options.toString(); } /** * Convenience function for getting system properties in a safe way * @param name of boolean property * @param defValue default value of boolean property * @return true if set to true, default value if unset or set to false */ public static boolean getBooleanProperty(final String name, final Boolean defValue) { Objects.requireNonNull(name); if (!name.startsWith("nashorn.")) { throw new IllegalArgumentException(name); } return AccessController.doPrivileged( new PrivilegedAction() { @Override public Boolean run() { try { final String property = System.getProperty(name); if (property == null && defValue != null) { return defValue; } return property != null && !"false".equalsIgnoreCase(property); } catch (final SecurityException e) { // if no permission to read, assume false return false; } } }, READ_PROPERTY_ACC_CTXT); } /** * Convenience function for getting system properties in a safe way * @param name of boolean property * @return true if set to true, false if unset or set to false */ public static boolean getBooleanProperty(final String name) { return getBooleanProperty(name, null); } /** * Convenience function for getting system properties in a safe way * * @param name of string property * @param defValue the default value if unset * @return string property if set or default value */ public static String getStringProperty(final String name, final String defValue) { Objects.requireNonNull(name); if (! name.startsWith("nashorn.")) { throw new IllegalArgumentException(name); } return AccessController.doPrivileged( new PrivilegedAction() { @Override public String run() { try { return System.getProperty(name, defValue); } catch (final SecurityException e) { // if no permission to read, assume the default value return defValue; } } }, READ_PROPERTY_ACC_CTXT); } /** * Convenience function for getting system properties in a safe way * * @param name of integer property * @param defValue the default value if unset * @return integer property if set or default value */ public static int getIntProperty(final String name, final int defValue) { Objects.requireNonNull(name); if (! name.startsWith("nashorn.")) { throw new IllegalArgumentException(name); } return AccessController.doPrivileged( new PrivilegedAction() { @Override public Integer run() { try { return Integer.getInteger(name, defValue); } catch (final SecurityException e) { // if no permission to read, assume the default value return defValue; } } }, READ_PROPERTY_ACC_CTXT); } /** * Return an option given its resource key. If the key doesn't begin with * {@literal }.option it will be completed using the resource from this * instance * * @param key key for option * @return an option value */ public Option get(final String key) { return options.get(key(key)); } /** * Return an option as a boolean * * @param key key for option * @return an option value */ public boolean getBoolean(final String key) { final Option option = get(key); return option != null ? (Boolean)option.getValue() : false; } /** * Return an option as a integer * * @param key key for option * @return an option value */ public int getInteger(final String key) { final Option option = get(key); return option != null ? (Integer)option.getValue() : 0; } /** * Return an option as a String * * @param key key for option * @return an option value */ public String getString(final String key) { final Option option = get(key); if (option != null) { final String value = (String)option.getValue(); if(value != null) { return value.intern(); } } return null; } /** * Set an option, overwriting an existing state if one exists * * @param key option key * @param option option */ public void set(final String key, final Option option) { options.put(key(key), option); } /** * Set an option as a boolean value, overwriting an existing state if one exists * * @param key option key * @param option option */ public void set(final String key, final boolean option) { set(key, new Option<>(option)); } /** * Set an option as a String value, overwriting an existing state if one exists * * @param key option key * @param option option */ public void set(final String key, final String option) { set(key, new Option<>(option)); } /** * Return the user arguments to the program, i.e. those trailing "--" after * the filename * * @return a list of user arguments */ public List getArguments() { return Collections.unmodifiableList(this.arguments); } /** * Return the JavaScript files passed to the program * * @return a list of files */ public List getFiles() { return Collections.unmodifiableList(files); } /** * Return the option templates for all the valid option supported. * * @return a collection of OptionTemplate objects. */ public static Collection getValidOptions() { return Collections.unmodifiableCollection(validOptions); } /** * Make sure a key is fully qualified for table lookups * * @param shortKey key for option * @return fully qualified key */ private String key(final String shortKey) { String key = shortKey; while (key.startsWith("-")) { key = key.substring(1, key.length()); } key = key.replace("-", "."); final String keyPrefix = this.resource + ".option."; if (key.startsWith(keyPrefix)) { return key; } return keyPrefix + key; } static String getMsg(final String msgId, final String... args) { try { final String msg = Options.bundle.getString(msgId); if (args.length == 0) { return msg; } return new MessageFormat(msg).format(args); } catch (final MissingResourceException e) { throw new IllegalArgumentException(e); } } /** * Display context sensitive help * * @param e exception that caused a parse error */ public void displayHelp(final IllegalArgumentException e) { if (e instanceof IllegalOptionException) { final OptionTemplate template = ((IllegalOptionException)e).getTemplate(); if (template.isXHelp()) { // display extended help information displayHelp(true); } else { err.println(((IllegalOptionException)e).getTemplate()); } return; } if (e != null && e.getMessage() != null) { err.println(getMsg("option.error.invalid.option", e.getMessage(), helpOptionTemplate.getShortName(), helpOptionTemplate.getName())); err.println(); return; } displayHelp(false); } /** * Display full help * * @param extended show the extended help for all options, including undocumented ones */ public void displayHelp(final boolean extended) { for (final OptionTemplate t : Options.validOptions) { if ((extended || !t.isUndocumented()) && t.getResource().equals(resource)) { err.println(t); err.println(); } } } /** * Processes the arguments and stores their information. Throws * IllegalArgumentException on error. The message can be analyzed by the * displayHelp function to become more context sensitive * * @param args arguments from command line */ public void process(final String[] args) { final LinkedList argList = new LinkedList<>(); addSystemProperties(NASHORN_ARGS_PREPEND_PROPERTY, argList); Collections.addAll(argList, args); addSystemProperties(NASHORN_ARGS_PROPERTY, argList); while (!argList.isEmpty()) { final String arg = argList.remove(0); Objects.requireNonNull(arg); // skip empty args if (arg.isEmpty()) { continue; } // user arguments to the script if ("--".equals(arg)) { arguments.addAll(argList); argList.clear(); continue; } // If it doesn't start with -, it's a file. But, if it is just "-", // then it is a file representing standard input. if (!arg.startsWith("-") || arg.length() == 1) { files.add(arg); continue; } if (arg.startsWith(definePropPrefix)) { final String value = arg.substring(definePropPrefix.length()); final int eq = value.indexOf('='); if (eq != -1) { // -Dfoo=bar Set System property "foo" with value "bar" System.setProperty(value.substring(0, eq), value.substring(eq + 1)); } else { // -Dfoo is fine. Set System property "foo" with "" as it's value if (!value.isEmpty()) { System.setProperty(value, ""); } else { // do not allow empty property name throw new IllegalOptionException(definePropTemplate); } } continue; } // it is an argument, it and assign key, value and template final ParsedArg parg = new ParsedArg(arg); // check if the value of this option is passed as next argument if (parg.template.isValueNextArg()) { if (argList.isEmpty()) { throw new IllegalOptionException(parg.template); } parg.value = argList.remove(0); } // -h [args...] if (parg.template.isHelp()) { // check if someone wants help on an explicit arg if (!argList.isEmpty()) { try { final OptionTemplate t = new ParsedArg(argList.get(0)).template; throw new IllegalOptionException(t); } catch (final IllegalArgumentException e) { throw e; } } throw new IllegalArgumentException(); // show help for // everything } if (parg.template.isXHelp()) { throw new IllegalOptionException(parg.template); } set(parg.template.getKey(), createOption(parg.template, parg.value)); // Arg may have a dependency to set other args, e.g. // scripting->anon.functions if (parg.template.getDependency() != null) { argList.addFirst(parg.template.getDependency()); } } } private static void addSystemProperties(final String sysPropName, final List argList) { final String sysArgs = getStringProperty(sysPropName, null); if (sysArgs != null) { final StringTokenizer st = new StringTokenizer(sysArgs); while (st.hasMoreTokens()) { argList.add(st.nextToken()); } } } private static OptionTemplate getOptionTemplate(final String key) { for (final OptionTemplate t : Options.validOptions) { if (t.matches(key)) { return t; } } return null; } private static Option createOption(final OptionTemplate t, final String value) { switch (t.getType()) { case "string": // default value null return new Option<>(value); case "timezone": // default value "TimeZone.getDefault()" return new Option<>(TimeZone.getTimeZone(value)); case "locale": return new Option<>(Locale.forLanguageTag(value)); case "keyvalues": return new KeyValueOption(value); case "log": return new LoggingOption(value); case "boolean": return new Option<>(value != null && Boolean.parseBoolean(value)); case "integer": try { return new Option<>(value == null ? 0 : Integer.parseInt(value)); } catch (final NumberFormatException nfe) { throw new IllegalOptionException(t); } case "properties": //swallow the properties and set them initProps(new KeyValueOption(value)); return null; default: break; } throw new IllegalArgumentException(value); } private static void initProps(final KeyValueOption kv) { for (final Map.Entry entry : kv.getValues().entrySet()) { System.setProperty(entry.getKey(), entry.getValue()); } } /** * Resource name for properties file */ private static final String MESSAGES_RESOURCE = "jdk.nashorn.internal.runtime.resources.Options"; /** * Resource bundle for properties file */ private static ResourceBundle bundle; /** * Usages per resource from properties file */ private static HashMap usage; /** * Valid options from templates in properties files */ private static Collection validOptions; /** * Help option */ private static OptionTemplate helpOptionTemplate; /** * Define property option template. */ private static OptionTemplate definePropTemplate; /** * Prefix of "define property" option. */ private static String definePropPrefix; static { Options.bundle = ResourceBundle.getBundle(Options.MESSAGES_RESOURCE, Locale.getDefault()); Options.validOptions = new TreeSet<>(); Options.usage = new HashMap<>(); for (final Enumeration keys = Options.bundle.getKeys(); keys.hasMoreElements(); ) { final String key = keys.nextElement(); final StringTokenizer st = new StringTokenizer(key, "."); String resource = null; String type = null; if (st.countTokens() > 0) { resource = st.nextToken(); // e.g. "nashorn" } if (st.countTokens() > 0) { type = st.nextToken(); // e.g. "option" } if ("option".equals(type)) { String helpKey = null; String xhelpKey = null; String definePropKey = null; try { helpKey = Options.bundle.getString(resource + ".options.help.key"); xhelpKey = Options.bundle.getString(resource + ".options.xhelp.key"); definePropKey = Options.bundle.getString(resource + ".options.D.key"); } catch (final MissingResourceException e) { //ignored: no help } final boolean isHelp = key.equals(helpKey); final boolean isXHelp = key.equals(xhelpKey); final OptionTemplate t = new OptionTemplate(resource, key, Options.bundle.getString(key), isHelp, isXHelp); Options.validOptions.add(t); if (isHelp) { helpOptionTemplate = t; } if (key.equals(definePropKey)) { definePropPrefix = t.getName(); definePropTemplate = t; } } else if (resource != null && "options".equals(type)) { Options.usage.put(resource, Options.bundle.getObject(key)); } } } @SuppressWarnings("serial") private static class IllegalOptionException extends IllegalArgumentException { private final OptionTemplate template; IllegalOptionException(final OptionTemplate t) { super(); this.template = t; } OptionTemplate getTemplate() { return this.template; } } /** * This is a resolved argument of the form key=value */ private static class ParsedArg { /** The resolved option template this argument corresponds to */ OptionTemplate template; /** The value of the argument */ String value; ParsedArg(final String argument) { final QuotedStringTokenizer st = new QuotedStringTokenizer(argument, "="); if (!st.hasMoreTokens()) { throw new IllegalArgumentException(); } final String token = st.nextToken(); this.template = Options.getOptionTemplate(token); if (this.template == null) { throw new IllegalArgumentException(argument); } value = ""; if (st.hasMoreTokens()) { while (st.hasMoreTokens()) { value += st.nextToken(); if (st.hasMoreTokens()) { value += ':'; } } } else if ("boolean".equals(this.template.getType())) { value = "true"; } } } }