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