Options.java revision 1291:456ffec2b5ae
133715Skato/* 216359Sasami * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. 333080Skato * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 416359Sasami * 516359Sasami * This code is free software; you can redistribute it and/or modify it 616359Sasami * under the terms of the GNU General Public License version 2 only, as 730329Skato * published by the Free Software Foundation. Oracle designates this 816359Sasami * particular file as subject to the "Classpath" exception as provided 916359Sasami * by Oracle in the LICENSE file that accompanied this code. 1016359Sasami * 1116359Sasami * This code is distributed in the hope that it will be useful, but WITHOUT 1229006Skato * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 1333711Skato * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 1429006Skato * version 2 for more details (a copy is included in the LICENSE file that 1529006Skato * accompanied this code). 1616359Sasami * 1732939Skato * You should have received a copy of the GNU General Public License version 1816359Sasami * 2 along with this work; if not, write to the Free Software Foundation, 1916359Sasami * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 2016359Sasami * 2126477Skato * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 2224657Skato * or visit www.oracle.com if you need additional information or have any 2324657Skato * questions. 2424657Skato */ 2516359Sasami 2616359Sasamipackage jdk.nashorn.internal.runtime.options; 2716359Sasami 2829631Skatoimport java.io.PrintWriter; 2932939Skatoimport java.security.AccessControlContext; 3032092Skatoimport java.security.AccessController; 3132939Skatoimport java.security.Permissions; 3227844Skatoimport java.security.PrivilegedAction; 3327844Skatoimport java.security.ProtectionDomain; 3417256Sasamiimport java.text.MessageFormat; 3525572Skatoimport java.util.ArrayList; 3625572Skatoimport java.util.Collection; 3725572Skatoimport java.util.Collections; 3825572Skatoimport java.util.Enumeration; 3925572Skatoimport java.util.HashMap; 4025195Skatoimport java.util.LinkedList; 4125195Skatoimport java.util.List; 4225195Skatoimport java.util.Locale; 4325195Skatoimport java.util.Map; 4425195Skatoimport java.util.MissingResourceException; 4518846Sasamiimport java.util.Objects; 4618846Sasamiimport java.util.PropertyPermission; 4720129Sasamiimport java.util.ResourceBundle; 4819269Sasamiimport java.util.StringTokenizer; 4918846Sasamiimport java.util.TimeZone; 5033080Skatoimport java.util.TreeMap; 5133080Skatoimport java.util.TreeSet; 5233080Skatoimport jdk.nashorn.internal.runtime.QuotedStringTokenizer; 5318208Sasami 5431556Skato/** 5527392Skato * Manages global runtime options. 5627392Skato */ 5733019Skatopublic final class Options { 5827391Skato // permission to just read nashorn.* System properties 5933019Skato private static AccessControlContext createPropertyReadAccCtxt() { 6033019Skato final Permissions perms = new Permissions(); 6133019Skato perms.add(new PropertyPermission("nashorn.*", "read")); 6233019Skato return new AccessControlContext(new ProtectionDomain[] { new ProtectionDomain(null, perms) }); 6333019Skato } 6433019Skato 6533019Skato private static final AccessControlContext READ_PROPERTY_ACC_CTXT = createPropertyReadAccCtxt(); 6633019Skato 6733019Skato /** Resource tag. */ 6833019Skato private final String resource; 6919122Sasami 7029006Skato /** Error writer. */ 7129006Skato private final PrintWriter err; 7229006Skato 7329006Skato /** File list. */ 7429006Skato private final List<String> files; 7529006Skato 7618846Sasami /** Arguments list */ 7718265Sasami private final List<String> arguments; 7818265Sasami 7927692Skato /** The options map of enabled options */ 8029137Skato private final TreeMap<String, Option<?>> options; 8133270Skato 8233270Skato /** System property that can be used to prepend options to the explicitly specified command line. */ 8318208Sasami private static final String NASHORN_ARGS_PREPEND_PROPERTY = "nashorn.args.prepend"; 8427173Skato 8527173Skato /** System property that can be used to append options to the explicitly specified command line. */ 8621773Skato private static final String NASHORN_ARGS_PROPERTY = "nashorn.args"; 8721773Skato 8821773Skato /** 8922065Skato * Constructor 9021773Skato * 9121773Skato * Options will use System.err as the output stream for any errors 9221773Skato * 9318208Sasami * @param resource resource prefix for options e.g. "nashorn" 9418208Sasami */ 9518265Sasami public Options(final String resource) { 9625268Skato this(resource, new PrintWriter(System.err, true)); 9725268Skato } 9818265Sasami 9918265Sasami /** 10018846Sasami * Constructor 10123847Skato * 10223847Skato * @param resource resource prefix for options e.g. "nashorn" 10329533Skato * @param err error stream for reporting parse errors 10429533Skato */ 10529533Skato public Options(final String resource, final PrintWriter err) { 10630553Skato this.resource = resource; 10730553Skato this.err = err; 10830553Skato this.files = new ArrayList<>(); 10930553Skato this.arguments = new ArrayList<>(); 11030553Skato this.options = new TreeMap<>(); 11130553Skato 11230553Skato // set all default values 11330553Skato for (final OptionTemplate t : Options.validOptions) { 11430553Skato if (t.getDefaultValue() != null) { 11530553Skato // populate from system properties 11630553Skato final String v = getStringProperty(t.getKey(), null); 11730553Skato if (v != null) { 11830553Skato set(t.getKey(), createOption(t, v)); 11930553Skato } else if (t.getDefaultValue() != null) { 12030553Skato set(t.getKey(), createOption(t, t.getDefaultValue())); 12133715Skato } 12233715Skato } 12333715Skato } 12433715Skato } 12533715Skato 12633715Skato /** 12733715Skato * Get the resource for this Options set, e.g. "nashorn" 12833715Skato * @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 processArgList(argList); 428 assert argList.isEmpty(); 429 Collections.addAll(argList, args); 430 processArgList(argList); 431 assert argList.isEmpty(); 432 addSystemProperties(NASHORN_ARGS_PROPERTY, argList); 433 processArgList(argList); 434 assert argList.isEmpty(); 435 } 436 437 private void processArgList(final LinkedList<String> argList) { 438 while (!argList.isEmpty()) { 439 final String arg = argList.remove(0); 440 Objects.requireNonNull(arg); 441 442 // skip empty args 443 if (arg.isEmpty()) { 444 continue; 445 } 446 447 // user arguments to the script 448 if ("--".equals(arg)) { 449 arguments.addAll(argList); 450 argList.clear(); 451 continue; 452 } 453 454 // If it doesn't start with -, it's a file. But, if it is just "-", 455 // then it is a file representing standard input. 456 if (!arg.startsWith("-") || arg.length() == 1) { 457 files.add(arg); 458 continue; 459 } 460 461 if (arg.startsWith(definePropPrefix)) { 462 final String value = arg.substring(definePropPrefix.length()); 463 final int eq = value.indexOf('='); 464 if (eq != -1) { 465 // -Dfoo=bar Set System property "foo" with value "bar" 466 System.setProperty(value.substring(0, eq), value.substring(eq + 1)); 467 } else { 468 // -Dfoo is fine. Set System property "foo" with "" as it's value 469 if (!value.isEmpty()) { 470 System.setProperty(value, ""); 471 } else { 472 // do not allow empty property name 473 throw new IllegalOptionException(definePropTemplate); 474 } 475 } 476 continue; 477 } 478 479 // it is an argument, it and assign key, value and template 480 final ParsedArg parg = new ParsedArg(arg); 481 482 // check if the value of this option is passed as next argument 483 if (parg.template.isValueNextArg()) { 484 if (argList.isEmpty()) { 485 throw new IllegalOptionException(parg.template); 486 } 487 parg.value = argList.remove(0); 488 } 489 490 // -h [args...] 491 if (parg.template.isHelp()) { 492 // check if someone wants help on an explicit arg 493 if (!argList.isEmpty()) { 494 try { 495 final OptionTemplate t = new ParsedArg(argList.get(0)).template; 496 throw new IllegalOptionException(t); 497 } catch (final IllegalArgumentException e) { 498 throw e; 499 } 500 } 501 throw new IllegalArgumentException(); // show help for 502 // everything 503 } 504 505 if (parg.template.isXHelp()) { 506 throw new IllegalOptionException(parg.template); 507 } 508 509 set(parg.template.getKey(), createOption(parg.template, parg.value)); 510 511 // Arg may have a dependency to set other args, e.g. 512 // scripting->anon.functions 513 if (parg.template.getDependency() != null) { 514 argList.addFirst(parg.template.getDependency()); 515 } 516 } 517 } 518 519 private static void addSystemProperties(final String sysPropName, final List<String> argList) { 520 final String sysArgs = getStringProperty(sysPropName, null); 521 if (sysArgs != null) { 522 final StringTokenizer st = new StringTokenizer(sysArgs); 523 while (st.hasMoreTokens()) { 524 argList.add(st.nextToken()); 525 } 526 } 527 } 528 529 private static OptionTemplate getOptionTemplate(final String key) { 530 for (final OptionTemplate t : Options.validOptions) { 531 if (t.matches(key)) { 532 return t; 533 } 534 } 535 return null; 536 } 537 538 private static Option<?> createOption(final OptionTemplate t, final String value) { 539 switch (t.getType()) { 540 case "string": 541 // default value null 542 return new Option<>(value); 543 case "timezone": 544 // default value "TimeZone.getDefault()" 545 return new Option<>(TimeZone.getTimeZone(value)); 546 case "locale": 547 return new Option<>(Locale.forLanguageTag(value)); 548 case "keyvalues": 549 return new KeyValueOption(value); 550 case "log": 551 return new LoggingOption(value); 552 case "boolean": 553 return new Option<>(value != null && Boolean.parseBoolean(value)); 554 case "integer": 555 try { 556 return new Option<>(value == null ? 0 : Integer.parseInt(value)); 557 } catch (final NumberFormatException nfe) { 558 throw new IllegalOptionException(t); 559 } 560 case "properties": 561 //swallow the properties and set them 562 initProps(new KeyValueOption(value)); 563 return null; 564 default: 565 break; 566 } 567 throw new IllegalArgumentException(value); 568 } 569 570 private static void initProps(final KeyValueOption kv) { 571 for (final Map.Entry<String, String> entry : kv.getValues().entrySet()) { 572 System.setProperty(entry.getKey(), entry.getValue()); 573 } 574 } 575 576 /** 577 * Resource name for properties file 578 */ 579 private static final String MESSAGES_RESOURCE = "jdk.nashorn.internal.runtime.resources.Options"; 580 581 /** 582 * Resource bundle for properties file 583 */ 584 private static ResourceBundle bundle; 585 586 /** 587 * Usages per resource from properties file 588 */ 589 private static HashMap<Object, Object> usage; 590 591 /** 592 * Valid options from templates in properties files 593 */ 594 private static Collection<OptionTemplate> validOptions; 595 596 /** 597 * Help option 598 */ 599 private static OptionTemplate helpOptionTemplate; 600 601 /** 602 * Define property option template. 603 */ 604 private static OptionTemplate definePropTemplate; 605 606 /** 607 * Prefix of "define property" option. 608 */ 609 private static String definePropPrefix; 610 611 static { 612 Options.bundle = ResourceBundle.getBundle(Options.MESSAGES_RESOURCE, Locale.getDefault()); 613 Options.validOptions = new TreeSet<>(); 614 Options.usage = new HashMap<>(); 615 616 for (final Enumeration<String> keys = Options.bundle.getKeys(); keys.hasMoreElements(); ) { 617 final String key = keys.nextElement(); 618 final StringTokenizer st = new StringTokenizer(key, "."); 619 String resource = null; 620 String type = null; 621 622 if (st.countTokens() > 0) { 623 resource = st.nextToken(); // e.g. "nashorn" 624 } 625 626 if (st.countTokens() > 0) { 627 type = st.nextToken(); // e.g. "option" 628 } 629 630 if ("option".equals(type)) { 631 String helpKey = null; 632 String xhelpKey = null; 633 String definePropKey = null; 634 try { 635 helpKey = Options.bundle.getString(resource + ".options.help.key"); 636 xhelpKey = Options.bundle.getString(resource + ".options.xhelp.key"); 637 definePropKey = Options.bundle.getString(resource + ".options.D.key"); 638 } catch (final MissingResourceException e) { 639 //ignored: no help 640 } 641 final boolean isHelp = key.equals(helpKey); 642 final boolean isXHelp = key.equals(xhelpKey); 643 final OptionTemplate t = new OptionTemplate(resource, key, Options.bundle.getString(key), isHelp, isXHelp); 644 645 Options.validOptions.add(t); 646 if (isHelp) { 647 helpOptionTemplate = t; 648 } 649 650 if (key.equals(definePropKey)) { 651 definePropPrefix = t.getName(); 652 definePropTemplate = t; 653 } 654 } else if (resource != null && "options".equals(type)) { 655 Options.usage.put(resource, Options.bundle.getObject(key)); 656 } 657 } 658 } 659 660 @SuppressWarnings("serial") 661 private static class IllegalOptionException extends IllegalArgumentException { 662 private final OptionTemplate template; 663 664 IllegalOptionException(final OptionTemplate t) { 665 super(); 666 this.template = t; 667 } 668 669 OptionTemplate getTemplate() { 670 return this.template; 671 } 672 } 673 674 /** 675 * This is a resolved argument of the form key=value 676 */ 677 private static class ParsedArg { 678 /** The resolved option template this argument corresponds to */ 679 OptionTemplate template; 680 681 /** The value of the argument */ 682 String value; 683 684 ParsedArg(final String argument) { 685 final QuotedStringTokenizer st = new QuotedStringTokenizer(argument, "="); 686 if (!st.hasMoreTokens()) { 687 throw new IllegalArgumentException(); 688 } 689 690 final String token = st.nextToken(); 691 this.template = Options.getOptionTemplate(token); 692 if (this.template == null) { 693 throw new IllegalArgumentException(argument); 694 } 695 696 value = ""; 697 if (st.hasMoreTokens()) { 698 while (st.hasMoreTokens()) { 699 value += st.nextToken(); 700 if (st.hasMoreTokens()) { 701 value += ':'; 702 } 703 } 704 } else if ("boolean".equals(this.template.getType())) { 705 value = "true"; 706 } 707 } 708 } 709} 710