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