blob: ed39fa95481c70f64e147f459df1c4533b16ef30 [file] [log] [blame]
/*
* 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.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<String> files;
/** Arguments list */
private final List<String> arguments;
/** The options map of enabled options */
private final TreeMap<String, Option<?>> 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) {
name.getClass(); // null check
if (!name.startsWith("nashorn.")) {
throw new IllegalArgumentException(name);
}
return AccessController.doPrivileged(
new PrivilegedAction<Boolean>() {
@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) {
name.getClass(); // null check
if (! name.startsWith("nashorn.")) {
throw new IllegalArgumentException(name);
}
return AccessController.doPrivileged(
new PrivilegedAction<String>() {
@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) {
name.getClass(); // null check
if (! name.startsWith("nashorn.")) {
throw new IllegalArgumentException(name);
}
return AccessController.doPrivileged(
new PrivilegedAction<Integer>() {
@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 <resource>}.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<String> getArguments() {
return Collections.unmodifiableList(this.arguments);
}
/**
* Return the JavaScript files passed to the program
*
* @return a list of files
*/
public List<String> getFiles() {
return Collections.unmodifiableList(files);
}
/**
* Return the option templates for all the valid option supported.
*
* @return a collection of OptionTemplate objects.
*/
public static Collection<OptionTemplate> 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<String> 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);
// 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<String> 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<String, String> 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<Object, Object> usage;
/**
* Valid options from templates in properties files
*/
private static Collection<OptionTemplate> 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<String> 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";
}
}
}
}