Options.java revision 1204:9597425b6b38
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 /** 140 * Convenience function for getting system properties in a safe way 141 142 * @param name of boolean property 143 * @param defValue default value of boolean property 144 * @return true if set to true, default value if unset or set to false 145 */ 146 public static boolean getBooleanProperty(final String name, final Boolean defValue) { 147 Objects.requireNonNull(name); 148 if (!name.startsWith("nashorn.")) { 149 throw new IllegalArgumentException(name); 150 } 151 152 return AccessController.doPrivileged( 153 new PrivilegedAction<Boolean>() { 154 @Override 155 public Boolean run() { 156 try { 157 final String property = System.getProperty(name); 158 if (property == null && defValue != null) { 159 return defValue; 160 } 161 return property != null && !"false".equalsIgnoreCase(property); 162 } catch (final SecurityException e) { 163 // if no permission to read, assume false 164 return false; 165 } 166 } 167 }, READ_PROPERTY_ACC_CTXT); 168 } 169 170 /** 171 * Convenience function for getting system properties in a safe way 172 173 * @param name of boolean property 174 * @return true if set to true, false if unset or set to false 175 */ 176 public static boolean getBooleanProperty(final String name) { 177 return getBooleanProperty(name, null); 178 } 179 180 /** 181 * Convenience function for getting system properties in a safe way 182 * 183 * @param name of string property 184 * @param defValue the default value if unset 185 * @return string property if set or default value 186 */ 187 public static String getStringProperty(final String name, final String defValue) { 188 Objects.requireNonNull(name); 189 if (! name.startsWith("nashorn.")) { 190 throw new IllegalArgumentException(name); 191 } 192 193 return AccessController.doPrivileged( 194 new PrivilegedAction<String>() { 195 @Override 196 public String run() { 197 try { 198 return System.getProperty(name, defValue); 199 } catch (final SecurityException e) { 200 // if no permission to read, assume the default value 201 return defValue; 202 } 203 } 204 }, READ_PROPERTY_ACC_CTXT); 205 } 206 207 /** 208 * Convenience function for getting system properties in a safe way 209 * 210 * @param name of integer property 211 * @param defValue the default value if unset 212 * @return integer property if set or default value 213 */ 214 public static int getIntProperty(final String name, final int defValue) { 215 Objects.requireNonNull(name); 216 if (! name.startsWith("nashorn.")) { 217 throw new IllegalArgumentException(name); 218 } 219 220 return AccessController.doPrivileged( 221 new PrivilegedAction<Integer>() { 222 @Override 223 public Integer run() { 224 try { 225 return Integer.getInteger(name, defValue); 226 } catch (final SecurityException e) { 227 // if no permission to read, assume the default value 228 return defValue; 229 } 230 } 231 }, READ_PROPERTY_ACC_CTXT); 232 } 233 234 /** 235 * Return an option given its resource key. If the key doesn't begin with 236 * {@literal <resource>}.option it will be completed using the resource from this 237 * instance 238 * 239 * @param key key for option 240 * @return an option value 241 */ 242 public Option<?> get(final String key) { 243 return options.get(key(key)); 244 } 245 246 /** 247 * Return an option as a boolean 248 * 249 * @param key key for option 250 * @return an option value 251 */ 252 public boolean getBoolean(final String key) { 253 final Option<?> option = get(key); 254 return option != null ? (Boolean)option.getValue() : false; 255 } 256 257 /** 258 * Return an option as a integer 259 * 260 * @param key key for option 261 * @return an option value 262 */ 263 public int getInteger(final String key) { 264 final Option<?> option = get(key); 265 return option != null ? (Integer)option.getValue() : 0; 266 } 267 268 /** 269 * Return an option as a String 270 * 271 * @param key key for option 272 * @return an option value 273 */ 274 public String getString(final String key) { 275 final Option<?> option = get(key); 276 if (option != null) { 277 final String value = (String)option.getValue(); 278 if(value != null) { 279 return value.intern(); 280 } 281 } 282 return null; 283 } 284 285 /** 286 * Set an option, overwriting an existing state if one exists 287 * 288 * @param key option key 289 * @param option option 290 */ 291 public void set(final String key, final Option<?> option) { 292 options.put(key(key), option); 293 } 294 295 /** 296 * Set an option as a boolean value, overwriting an existing state if one exists 297 * 298 * @param key option key 299 * @param option option 300 */ 301 public void set(final String key, final boolean option) { 302 set(key, new Option<>(option)); 303 } 304 305 /** 306 * Set an option as a String value, overwriting an existing state if one exists 307 * 308 * @param key option key 309 * @param option option 310 */ 311 public void set(final String key, final String option) { 312 set(key, new Option<>(option)); 313 } 314 315 /** 316 * Return the user arguments to the program, i.e. those trailing "--" after 317 * the filename 318 * 319 * @return a list of user arguments 320 */ 321 public List<String> getArguments() { 322 return Collections.unmodifiableList(this.arguments); 323 } 324 325 /** 326 * Return the JavaScript files passed to the program 327 * 328 * @return a list of files 329 */ 330 public List<String> getFiles() { 331 return Collections.unmodifiableList(files); 332 } 333 334 /** 335 * Return the option templates for all the valid option supported. 336 * 337 * @return a collection of OptionTemplate objects. 338 */ 339 public static Collection<OptionTemplate> getValidOptions() { 340 return Collections.unmodifiableCollection(validOptions); 341 } 342 343 /** 344 * Make sure a key is fully qualified for table lookups 345 * 346 * @param shortKey key for option 347 * @return fully qualified key 348 */ 349 private String key(final String shortKey) { 350 String key = shortKey; 351 while (key.startsWith("-")) { 352 key = key.substring(1, key.length()); 353 } 354 key = key.replace("-", "."); 355 final String keyPrefix = this.resource + ".option."; 356 if (key.startsWith(keyPrefix)) { 357 return key; 358 } 359 return keyPrefix + key; 360 } 361 362 static String getMsg(final String msgId, final String... args) { 363 try { 364 final String msg = Options.bundle.getString(msgId); 365 if (args.length == 0) { 366 return msg; 367 } 368 return new MessageFormat(msg).format(args); 369 } catch (final MissingResourceException e) { 370 throw new IllegalArgumentException(e); 371 } 372 } 373 374 /** 375 * Display context sensitive help 376 * 377 * @param e exception that caused a parse error 378 */ 379 public void displayHelp(final IllegalArgumentException e) { 380 if (e instanceof IllegalOptionException) { 381 final OptionTemplate template = ((IllegalOptionException)e).getTemplate(); 382 if (template.isXHelp()) { 383 // display extended help information 384 displayHelp(true); 385 } else { 386 err.println(((IllegalOptionException)e).getTemplate()); 387 } 388 return; 389 } 390 391 if (e != null && e.getMessage() != null) { 392 err.println(getMsg("option.error.invalid.option", 393 e.getMessage(), 394 helpOptionTemplate.getShortName(), 395 helpOptionTemplate.getName())); 396 err.println(); 397 return; 398 } 399 400 displayHelp(false); 401 } 402 403 /** 404 * Display full help 405 * 406 * @param extended show the extended help for all options, including undocumented ones 407 */ 408 public void displayHelp(final boolean extended) { 409 for (final OptionTemplate t : Options.validOptions) { 410 if ((extended || !t.isUndocumented()) && t.getResource().equals(resource)) { 411 err.println(t); 412 err.println(); 413 } 414 } 415 } 416 417 /** 418 * Processes the arguments and stores their information. Throws 419 * IllegalArgumentException on error. The message can be analyzed by the 420 * displayHelp function to become more context sensitive 421 * 422 * @param args arguments from command line 423 */ 424 public void process(final String[] args) { 425 final LinkedList<String> argList = new LinkedList<>(); 426 addSystemProperties(NASHORN_ARGS_PREPEND_PROPERTY, argList); 427 Collections.addAll(argList, args); 428 addSystemProperties(NASHORN_ARGS_PROPERTY, argList); 429 430 while (!argList.isEmpty()) { 431 final String arg = argList.remove(0); 432 Objects.requireNonNull(arg); 433 434 // skip empty args 435 if (arg.isEmpty()) { 436 continue; 437 } 438 439 // user arguments to the script 440 if ("--".equals(arg)) { 441 arguments.addAll(argList); 442 argList.clear(); 443 continue; 444 } 445 446 // If it doesn't start with -, it's a file. But, if it is just "-", 447 // then it is a file representing standard input. 448 if (!arg.startsWith("-") || arg.length() == 1) { 449 files.add(arg); 450 continue; 451 } 452 453 if (arg.startsWith(definePropPrefix)) { 454 final String value = arg.substring(definePropPrefix.length()); 455 final int eq = value.indexOf('='); 456 if (eq != -1) { 457 // -Dfoo=bar Set System property "foo" with value "bar" 458 System.setProperty(value.substring(0, eq), value.substring(eq + 1)); 459 } else { 460 // -Dfoo is fine. Set System property "foo" with "" as it's value 461 if (!value.isEmpty()) { 462 System.setProperty(value, ""); 463 } else { 464 // do not allow empty property name 465 throw new IllegalOptionException(definePropTemplate); 466 } 467 } 468 continue; 469 } 470 471 // it is an argument, it and assign key, value and template 472 final ParsedArg parg = new ParsedArg(arg); 473 474 // check if the value of this option is passed as next argument 475 if (parg.template.isValueNextArg()) { 476 if (argList.isEmpty()) { 477 throw new IllegalOptionException(parg.template); 478 } 479 parg.value = argList.remove(0); 480 } 481 482 // -h [args...] 483 if (parg.template.isHelp()) { 484 // check if someone wants help on an explicit arg 485 if (!argList.isEmpty()) { 486 try { 487 final OptionTemplate t = new ParsedArg(argList.get(0)).template; 488 throw new IllegalOptionException(t); 489 } catch (final IllegalArgumentException e) { 490 throw e; 491 } 492 } 493 throw new IllegalArgumentException(); // show help for 494 // everything 495 } 496 497 if (parg.template.isXHelp()) { 498 throw new IllegalOptionException(parg.template); 499 } 500 501 set(parg.template.getKey(), createOption(parg.template, parg.value)); 502 503 // Arg may have a dependency to set other args, e.g. 504 // scripting->anon.functions 505 if (parg.template.getDependency() != null) { 506 argList.addFirst(parg.template.getDependency()); 507 } 508 } 509 } 510 511 private static void addSystemProperties(final String sysPropName, final List<String> argList) { 512 final String sysArgs = getStringProperty(sysPropName, null); 513 if (sysArgs != null) { 514 final StringTokenizer st = new StringTokenizer(sysArgs); 515 while (st.hasMoreTokens()) { 516 argList.add(st.nextToken()); 517 } 518 } 519 } 520 521 private static OptionTemplate getOptionTemplate(final String key) { 522 for (final OptionTemplate t : Options.validOptions) { 523 if (t.matches(key)) { 524 return t; 525 } 526 } 527 return null; 528 } 529 530 private static Option<?> createOption(final OptionTemplate t, final String value) { 531 switch (t.getType()) { 532 case "string": 533 // default value null 534 return new Option<>(value); 535 case "timezone": 536 // default value "TimeZone.getDefault()" 537 return new Option<>(TimeZone.getTimeZone(value)); 538 case "locale": 539 return new Option<>(Locale.forLanguageTag(value)); 540 case "keyvalues": 541 return new KeyValueOption(value); 542 case "log": 543 return new LoggingOption(value); 544 case "boolean": 545 return new Option<>(value != null && Boolean.parseBoolean(value)); 546 case "integer": 547 try { 548 return new Option<>(value == null ? 0 : Integer.parseInt(value)); 549 } catch (final NumberFormatException nfe) { 550 throw new IllegalOptionException(t); 551 } 552 case "properties": 553 //swallow the properties and set them 554 initProps(new KeyValueOption(value)); 555 return null; 556 default: 557 break; 558 } 559 throw new IllegalArgumentException(value); 560 } 561 562 private static void initProps(final KeyValueOption kv) { 563 for (final Map.Entry<String, String> entry : kv.getValues().entrySet()) { 564 System.setProperty(entry.getKey(), entry.getValue()); 565 } 566 } 567 568 /** 569 * Resource name for properties file 570 */ 571 private static final String MESSAGES_RESOURCE = "jdk.nashorn.internal.runtime.resources.Options"; 572 573 /** 574 * Resource bundle for properties file 575 */ 576 private static ResourceBundle bundle; 577 578 /** 579 * Usages per resource from properties file 580 */ 581 private static HashMap<Object, Object> usage; 582 583 /** 584 * Valid options from templates in properties files 585 */ 586 private static Collection<OptionTemplate> validOptions; 587 588 /** 589 * Help option 590 */ 591 private static OptionTemplate helpOptionTemplate; 592 593 /** 594 * Define property option template. 595 */ 596 private static OptionTemplate definePropTemplate; 597 598 /** 599 * Prefix of "define property" option. 600 */ 601 private static String definePropPrefix; 602 603 static { 604 Options.bundle = ResourceBundle.getBundle(Options.MESSAGES_RESOURCE, Locale.getDefault()); 605 Options.validOptions = new TreeSet<>(); 606 Options.usage = new HashMap<>(); 607 608 for (final Enumeration<String> keys = Options.bundle.getKeys(); keys.hasMoreElements(); ) { 609 final String key = keys.nextElement(); 610 final StringTokenizer st = new StringTokenizer(key, "."); 611 String resource = null; 612 String type = null; 613 614 if (st.countTokens() > 0) { 615 resource = st.nextToken(); // e.g. "nashorn" 616 } 617 618 if (st.countTokens() > 0) { 619 type = st.nextToken(); // e.g. "option" 620 } 621 622 if ("option".equals(type)) { 623 String helpKey = null; 624 String xhelpKey = null; 625 String definePropKey = null; 626 try { 627 helpKey = Options.bundle.getString(resource + ".options.help.key"); 628 xhelpKey = Options.bundle.getString(resource + ".options.xhelp.key"); 629 definePropKey = Options.bundle.getString(resource + ".options.D.key"); 630 } catch (final MissingResourceException e) { 631 //ignored: no help 632 } 633 final boolean isHelp = key.equals(helpKey); 634 final boolean isXHelp = key.equals(xhelpKey); 635 final OptionTemplate t = new OptionTemplate(resource, key, Options.bundle.getString(key), isHelp, isXHelp); 636 637 Options.validOptions.add(t); 638 if (isHelp) { 639 helpOptionTemplate = t; 640 } 641 642 if (key.equals(definePropKey)) { 643 definePropPrefix = t.getName(); 644 definePropTemplate = t; 645 } 646 } else if (resource != null && "options".equals(type)) { 647 Options.usage.put(resource, Options.bundle.getObject(key)); 648 } 649 } 650 } 651 652 @SuppressWarnings("serial") 653 private static class IllegalOptionException extends IllegalArgumentException { 654 private final OptionTemplate template; 655 656 IllegalOptionException(final OptionTemplate t) { 657 super(); 658 this.template = t; 659 } 660 661 OptionTemplate getTemplate() { 662 return this.template; 663 } 664 } 665 666 /** 667 * This is a resolved argument of the form key=value 668 */ 669 private static class ParsedArg { 670 /** The resolved option template this argument corresponds to */ 671 OptionTemplate template; 672 673 /** The value of the argument */ 674 String value; 675 676 ParsedArg(final String argument) { 677 final QuotedStringTokenizer st = new QuotedStringTokenizer(argument, "="); 678 if (!st.hasMoreTokens()) { 679 throw new IllegalArgumentException(); 680 } 681 682 final String token = st.nextToken(); 683 this.template = Options.getOptionTemplate(token); 684 if (this.template == null) { 685 throw new IllegalArgumentException(argument); 686 } 687 688 value = ""; 689 if (st.hasMoreTokens()) { 690 while (st.hasMoreTokens()) { 691 value += st.nextToken(); 692 if (st.hasMoreTokens()) { 693 value += ':'; 694 } 695 } 696 } else if ("boolean".equals(this.template.getType())) { 697 value = "true"; 698 } 699 } 700 } 701} 702