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