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 26 package jdk.nashorn.internal.runtime.options; 27 28 import java.io.PrintWriter; 29 import java.security.AccessControlContext; 30 import java.security.AccessController; 31 import java.security.Permissions; 32 import java.security.PrivilegedAction; 33 import java.security.ProtectionDomain; 34 import java.text.MessageFormat; 35 import java.util.ArrayList; 36 import java.util.Collection; 37 import java.util.Collections; 38 import java.util.Enumeration; 39 import java.util.HashMap; 40 import java.util.LinkedList; 41 import java.util.List; 42 import java.util.Locale; 43 import java.util.Map; 44 import java.util.MissingResourceException; 45 import java.util.Objects; 46 import java.util.PropertyPermission; 47 import java.util.ResourceBundle; 48 import java.util.StringTokenizer; 49 import java.util.TimeZone; 50 import java.util.TreeMap; 51 import java.util.TreeSet; 52 import jdk.nashorn.internal.runtime.QuotedStringTokenizer; 53 54 /** 55 * Manages global runtime options. 56 */ 57 public 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 433 // skip empty args 434 if (arg.isEmpty()) { 435 continue; 436 } 437 438 // user arguments to the script 439 if ("--".equals(arg)) { 440 arguments.addAll(argList); 441 argList.clear(); 442 continue; 443 } 444 445 // If it doesn't start with -, it's a file. But, if it is just "-", 446 // then it is a file representing standard input. 447 if (!arg.startsWith("-") || arg.length() == 1) { 448 files.add(arg); 449 continue; 450 } 451 452 if (arg.startsWith(definePropPrefix)) { 453 final String value = arg.substring(definePropPrefix.length()); 454 final int eq = value.indexOf('='); 455 if (eq != -1) { 456 // -Dfoo=bar Set System property "foo" with value "bar" 457 System.setProperty(value.substring(0, eq), value.substring(eq + 1)); 458 } else { 459 // -Dfoo is fine. Set System property "foo" with "" as it's value 460 if (!value.isEmpty()) { 461 System.setProperty(value, ""); 462 } else { 463 // do not allow empty property name 464 throw new IllegalOptionException(definePropTemplate); 465 } 466 } 467 continue; 468 } 469 470 // it is an argument, it and assign key, value and template 471 final ParsedArg parg = new ParsedArg(arg); 472 473 // check if the value of this option is passed as next argument 474 if (parg.template.isValueNextArg()) { 475 if (argList.isEmpty()) { 476 throw new IllegalOptionException(parg.template); 477 } 478 parg.value = argList.remove(0); 479 } 480 481 // -h [args...] 482 if (parg.template.isHelp()) { 483 // check if someone wants help on an explicit arg 484 if (!argList.isEmpty()) { 485 try { 486 final OptionTemplate t = new ParsedArg(argList.get(0)).template; 487 throw new IllegalOptionException(t); 488 } catch (final IllegalArgumentException e) { 489 throw e; 490 } 491 } 492 throw new IllegalArgumentException(); // show help for 493 // everything 494 } 495 496 if (parg.template.isXHelp()) { 497 throw new IllegalOptionException(parg.template); 498 } 499 500 set(parg.template.getKey(), createOption(parg.template, parg.value)); 501 502 // Arg may have a dependency to set other args, e.g. 503 // scripting->anon.functions 504 if (parg.template.getDependency() != null) { 505 argList.addFirst(parg.template.getDependency()); 506 } 507 } 508 } 509 510 private static void addSystemProperties(final String sysPropName, final List<String> argList) { 511 final String sysArgs = getStringProperty(sysPropName, null); 512 if (sysArgs != null) { 513 final StringTokenizer st = new StringTokenizer(sysArgs); 514 while (st.hasMoreTokens()) { 515 argList.add(st.nextToken()); 516 } 517 } 518 } 519 520 private static OptionTemplate getOptionTemplate(final String key) { 521 for (final OptionTemplate t : Options.validOptions) { 522 if (t.matches(key)) { 523 return t; 524 } 525 } 526 return null; 527 } 528 529 private static Option<?> createOption(final OptionTemplate t, final String value) { 530 switch (t.getType()) { 531 case "string": 532 // default value null 533 return new Option<>(value); 534 case "timezone": 535 // default value "TimeZone.getDefault()" 536 return new Option<>(TimeZone.getTimeZone(value)); 537 case "locale": 538 return new Option<>(Locale.forLanguageTag(value)); 539 case "keyvalues": 540 return new KeyValueOption(value); 541 case "log": 542 return new LoggingOption(value); 543 case "boolean": 544 return new Option<>(value != null && Boolean.parseBoolean(value)); 545 case "integer": 546 try { 547 return new Option<>(value == null ? 0 : Integer.parseInt(value)); 548 } catch (final NumberFormatException nfe) { 549 throw new IllegalOptionException(t); 550 } 551 case "properties": 552 //swallow the properties and set them 553 initProps(new KeyValueOption(value)); 554 return null; 555 default: 556 break; 557 } 558 throw new IllegalArgumentException(value); 559 } 560 561 private static void initProps(final KeyValueOption kv) { 562 for (final Map.Entry<String, String> entry : kv.getValues().entrySet()) { 563 System.setProperty(entry.getKey(), entry.getValue()); 564 } 565 } 566 567 /** 568 * Resource name for properties file 569 */ 570 private static final String MESSAGES_RESOURCE = "jdk.nashorn.internal.runtime.resources.Options"; 571 572 /** 573 * Resource bundle for properties file 574 */ 575 private static ResourceBundle bundle; 576 577 /** 578 * Usages per resource from properties file 579 */ 580 private static HashMap<Object, Object> usage; 581 582 /** 583 * Valid options from templates in properties files 584 */ 585 private static Collection<OptionTemplate> validOptions; 586 587 /** 588 * Help option 589 */ 590 private static OptionTemplate helpOptionTemplate; 591 592 /** 593 * Define property option template. 594 */ 595 private static OptionTemplate definePropTemplate; 596 597 /** 598 * Prefix of "define property" option. 599 */ 600 private static String definePropPrefix; 601 602 static { 603 Options.bundle = ResourceBundle.getBundle(Options.MESSAGES_RESOURCE, Locale.getDefault()); 604 Options.validOptions = new TreeSet<>(); 605 Options.usage = new HashMap<>(); 606 607 for (final Enumeration<String> keys = Options.bundle.getKeys(); keys.hasMoreElements(); ) { 608 final String key = keys.nextElement(); 609 final StringTokenizer st = new StringTokenizer(key, "."); 610 String resource = null; 611 String type = null; 612 613 if (st.countTokens() > 0) { 614 resource = st.nextToken(); // e.g. "nashorn" 615 } 616 617 if (st.countTokens() > 0) { 618 type = st.nextToken(); // e.g. "option" 619 } 620 621 if ("option".equals(type)) { 622 String helpKey = null; 623 String xhelpKey = null; 624 String definePropKey = null; 625 try { 626 helpKey = Options.bundle.getString(resource + ".options.help.key"); 627 xhelpKey = Options.bundle.getString(resource + ".options.xhelp.key"); 628 definePropKey = Options.bundle.getString(resource + ".options.D.key"); 629 } catch (final MissingResourceException e) { 630 //ignored: no help 631 } 632 final boolean isHelp = key.equals(helpKey); 633 final boolean isXHelp = key.equals(xhelpKey); 634 final OptionTemplate t = new OptionTemplate(resource, key, Options.bundle.getString(key), isHelp, isXHelp); 635 636 Options.validOptions.add(t); 637 if (isHelp) { 638 helpOptionTemplate = t; 639 } 640 641 if (key.equals(definePropKey)) { 642 definePropPrefix = t.getName(); 643 definePropTemplate = t; 644 } 645 } else if (resource != null && "options".equals(type)) { 646 Options.usage.put(resource, Options.bundle.getObject(key)); 647 } 648 } 649 } 650 651 @SuppressWarnings("serial") 652 private static class IllegalOptionException extends IllegalArgumentException { 653 private final OptionTemplate template; 654 655 IllegalOptionException(final OptionTemplate t) { 656 super(); 657 this.template = t; 658 } 659 660 OptionTemplate getTemplate() { 661 return this.template; 662 } 663 } 664 665 /** 666 * This is a resolved argument of the form key=value 667 */ 668 private static class ParsedArg { 669 /** The resolved option template this argument corresponds to */ 670 OptionTemplate template; 671 672 /** The value of the argument */ 673 String value; 674 675 ParsedArg(final String argument) { 676 final QuotedStringTokenizer st = new QuotedStringTokenizer(argument, "="); 677 if (!st.hasMoreTokens()) { 678 throw new IllegalArgumentException(); 679 } 680 681 final String token = st.nextToken(); 682 this.template = Options.getOptionTemplate(token); 683 if (this.template == null) { 684 throw new IllegalArgumentException(argument); 685 } 686 687 value = ""; 688 if (st.hasMoreTokens()) { 689 while (st.hasMoreTokens()) { 690 value += st.nextToken(); 691 if (st.hasMoreTokens()) { 692 value += ':'; 693 } 694 } 695 } else if ("boolean".equals(this.template.getType())) { 696 value = "true"; 697 } 698 } 699 } 700 }