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