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