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