Options.java revision 1790:785843878cf7
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 if (parg.template.isRepeated()) { 504 assert parg.template.getType().equals("string"); 505 506 final String key = key(parg.template.getKey()); 507 final String value = options.containsKey(key)? 508 (options.get(key).getValue() + "," + parg.value) : Objects.toString(parg.value); 509 options.put(key, new Option<>(value)); 510 } else { 511 set(parg.template.getKey(), createOption(parg.template, parg.value)); 512 } 513 514 // Arg may have a dependency to set other args, e.g. 515 // scripting->anon.functions 516 if (parg.template.getDependency() != null) { 517 argList.addFirst(parg.template.getDependency()); 518 } 519 } 520 } 521 522 private static void addSystemProperties(final String sysPropName, final List<String> argList) { 523 final String sysArgs = getStringProperty(sysPropName, null); 524 if (sysArgs != null) { 525 final StringTokenizer st = new StringTokenizer(sysArgs); 526 while (st.hasMoreTokens()) { 527 argList.add(st.nextToken()); 528 } 529 } 530 } 531 532 /** 533 * Retrieves an option template identified by key. 534 * @param shortKey the short (that is without the e.g. "nashorn.option." part) key 535 * @return the option template identified by the key 536 * @throws IllegalArgumentException if the key doesn't specify an existing template 537 */ 538 public OptionTemplate getOptionTemplateByKey(final String shortKey) { 539 final String fullKey = key(shortKey); 540 for(final OptionTemplate t: validOptions) { 541 if(t.getKey().equals(fullKey)) { 542 return t; 543 } 544 } 545 throw new IllegalArgumentException(shortKey); 546 } 547 548 private static OptionTemplate getOptionTemplateByName(final String name) { 549 for (final OptionTemplate t : Options.validOptions) { 550 if (t.nameMatches(name)) { 551 return t; 552 } 553 } 554 return null; 555 } 556 557 private static Option<?> createOption(final OptionTemplate t, final String value) { 558 switch (t.getType()) { 559 case "string": 560 // default value null 561 return new Option<>(value); 562 case "timezone": 563 // default value "TimeZone.getDefault()" 564 return new Option<>(TimeZone.getTimeZone(value)); 565 case "locale": 566 return new Option<>(Locale.forLanguageTag(value)); 567 case "keyvalues": 568 return new KeyValueOption(value); 569 case "log": 570 return new LoggingOption(value); 571 case "boolean": 572 return new Option<>(value != null && Boolean.parseBoolean(value)); 573 case "integer": 574 try { 575 return new Option<>(value == null ? 0 : Integer.parseInt(value)); 576 } catch (final NumberFormatException nfe) { 577 throw new IllegalOptionException(t); 578 } 579 case "properties": 580 //swallow the properties and set them 581 initProps(new KeyValueOption(value)); 582 return null; 583 default: 584 break; 585 } 586 throw new IllegalArgumentException(value); 587 } 588 589 private static void initProps(final KeyValueOption kv) { 590 for (final Map.Entry<String, String> entry : kv.getValues().entrySet()) { 591 System.setProperty(entry.getKey(), entry.getValue()); 592 } 593 } 594 595 /** 596 * Resource name for properties file 597 */ 598 private static final String MESSAGES_RESOURCE = "jdk.nashorn.internal.runtime.resources.Options"; 599 600 /** 601 * Resource bundle for properties file 602 */ 603 private static ResourceBundle bundle; 604 605 /** 606 * Usages per resource from properties file 607 */ 608 private static HashMap<Object, Object> usage; 609 610 /** 611 * Valid options from templates in properties files 612 */ 613 private static Collection<OptionTemplate> validOptions; 614 615 /** 616 * Help option 617 */ 618 private static OptionTemplate helpOptionTemplate; 619 620 /** 621 * Define property option template. 622 */ 623 private static OptionTemplate definePropTemplate; 624 625 /** 626 * Prefix of "define property" option. 627 */ 628 private static String definePropPrefix; 629 630 static { 631 Options.bundle = ResourceBundle.getBundle(Options.MESSAGES_RESOURCE, Locale.getDefault()); 632 Options.validOptions = new TreeSet<>(); 633 Options.usage = new HashMap<>(); 634 635 for (final Enumeration<String> keys = Options.bundle.getKeys(); keys.hasMoreElements(); ) { 636 final String key = keys.nextElement(); 637 final StringTokenizer st = new StringTokenizer(key, "."); 638 String resource = null; 639 String type = null; 640 641 if (st.countTokens() > 0) { 642 resource = st.nextToken(); // e.g. "nashorn" 643 } 644 645 if (st.countTokens() > 0) { 646 type = st.nextToken(); // e.g. "option" 647 } 648 649 if ("option".equals(type)) { 650 String helpKey = null; 651 String xhelpKey = null; 652 String definePropKey = null; 653 try { 654 helpKey = Options.bundle.getString(resource + ".options.help.key"); 655 xhelpKey = Options.bundle.getString(resource + ".options.xhelp.key"); 656 definePropKey = Options.bundle.getString(resource + ".options.D.key"); 657 } catch (final MissingResourceException e) { 658 //ignored: no help 659 } 660 final boolean isHelp = key.equals(helpKey); 661 final boolean isXHelp = key.equals(xhelpKey); 662 final OptionTemplate t = new OptionTemplate(resource, key, Options.bundle.getString(key), isHelp, isXHelp); 663 664 Options.validOptions.add(t); 665 if (isHelp) { 666 helpOptionTemplate = t; 667 } 668 669 if (key.equals(definePropKey)) { 670 definePropPrefix = t.getName(); 671 definePropTemplate = t; 672 } 673 } else if (resource != null && "options".equals(type)) { 674 Options.usage.put(resource, Options.bundle.getObject(key)); 675 } 676 } 677 } 678 679 @SuppressWarnings("serial") 680 private static class IllegalOptionException extends IllegalArgumentException { 681 private final OptionTemplate template; 682 683 IllegalOptionException(final OptionTemplate t) { 684 super(); 685 this.template = t; 686 } 687 688 OptionTemplate getTemplate() { 689 return this.template; 690 } 691 } 692 693 /** 694 * This is a resolved argument of the form key=value 695 */ 696 private static class ParsedArg { 697 /** The resolved option template this argument corresponds to */ 698 OptionTemplate template; 699 700 /** The value of the argument */ 701 String value; 702 703 ParsedArg(final String argument) { 704 final QuotedStringTokenizer st = new QuotedStringTokenizer(argument, "="); 705 if (!st.hasMoreTokens()) { 706 throw new IllegalArgumentException(); 707 } 708 709 final String token = st.nextToken(); 710 this.template = getOptionTemplateByName(token); 711 if (this.template == null) { 712 throw new IllegalArgumentException(argument); 713 } 714 715 value = ""; 716 if (st.hasMoreTokens()) { 717 while (st.hasMoreTokens()) { 718 value += st.nextToken(); 719 if (st.hasMoreTokens()) { 720 value += ':'; 721 } 722 } 723 } else if ("boolean".equals(this.template.getType())) { 724 value = "true"; 725 } 726 } 727 } 728} 729