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