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