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     private static void checkPropertyName(final String name) {
 140         if (! Objects.requireNonNull(name).startsWith("nashorn.")) {
 141             throw new IllegalArgumentException(name);
 142         }
 143     }
 144 
 145     /**
 146      * Convenience function for getting system properties in a safe way
 147 
 148      * @param name of boolean property
 149      * @param defValue default value of boolean property
 150      * @return true if set to true, default value if unset or set to false
 151      */
 152     public static boolean getBooleanProperty(final String name, final Boolean defValue) {
 153         checkPropertyName(name);
 154         return AccessController.doPrivileged(
 155                 new PrivilegedAction<Boolean>() {
 156                     @Override
 157                     public Boolean run() {
 158                         try {
 159                             final String property = System.getProperty(name);
 160                             if (property == null && defValue != null) {
 161                                 return defValue;
 162                             }
 163                             return property != null && !"false".equalsIgnoreCase(property);
 164                         } catch (final SecurityException e) {
 165                             // if no permission to read, assume false
 166                             return false;
 167                         }
 168                     }
 169                 }, READ_PROPERTY_ACC_CTXT);
 170     }
 171 
 172     /**
 173      * Convenience function for getting system properties in a safe way
 174 
 175      * @param name of boolean property
 176      * @return true if set to true, false if unset or set to false
 177      */
 178     public static boolean getBooleanProperty(final String name) {
 179         return getBooleanProperty(name, null);
 180     }
 181 
 182     /**
 183      * Convenience function for getting system properties in a safe way
 184      *
 185      * @param name of string property
 186      * @param defValue the default value if unset
 187      * @return string property if set or default value
 188      */
 189     public static String getStringProperty(final String name, final String defValue) {
 190         checkPropertyName(name);
 191         return AccessController.doPrivileged(
 192                 new PrivilegedAction<String>() {
 193                     @Override
 194                     public String run() {
 195                         try {
 196                             return System.getProperty(name, defValue);
 197                         } catch (final SecurityException e) {
 198                             // if no permission to read, assume the default value
 199                             return defValue;
 200                         }
 201                     }
 202                 }, READ_PROPERTY_ACC_CTXT);
 203     }
 204 
 205     /**
 206      * Convenience function for getting system properties in a safe way
 207      *
 208      * @param name of integer property
 209      * @param defValue the default value if unset
 210      * @return integer property if set or default value
 211      */
 212     public static int getIntProperty(final String name, final int defValue) {
 213         checkPropertyName(name);
 214         return AccessController.doPrivileged(
 215                 new PrivilegedAction<Integer>() {
 216                     @Override
 217                     public Integer run() {
 218                         try {
 219                             return Integer.getInteger(name, defValue);
 220                         } catch (final SecurityException e) {
 221                             // if no permission to read, assume the default value
 222                             return defValue;
 223                         }
 224                     }
 225                 }, READ_PROPERTY_ACC_CTXT);
 226     }
 227 
 228     /**
 229      * Return an option given its resource key. If the key doesn't begin with
 230      * {@literal <resource>}.option it will be completed using the resource from this
 231      * instance
 232      *
 233      * @param key key for option
 234      * @return an option value
 235      */
 236     public Option<?> get(final String key) {
 237         return options.get(key(key));
 238     }
 239 
 240     /**
 241      * Return an option as a boolean
 242      *
 243      * @param key key for option
 244      * @return an option value
 245      */
 246     public boolean getBoolean(final String key) {
 247         final Option<?> option = get(key);
 248         return option != null ? (Boolean)option.getValue() : false;
 249     }
 250 
 251     /**
 252      * Return an option as a integer
 253      *
 254      * @param key key for option
 255      * @return an option value
 256      */
 257     public int getInteger(final String key) {
 258         final Option<?> option = get(key);
 259         return option != null ? (Integer)option.getValue() : 0;
 260     }
 261 
 262     /**
 263      * Return an option as a String
 264      *
 265      * @param key key for option
 266      * @return an option value
 267      */
 268     public String getString(final String key) {
 269         final Option<?> option = get(key);
 270         if (option != null) {
 271             final String value = (String)option.getValue();
 272             if(value != null) {
 273                 return value.intern();
 274             }
 275         }
 276         return null;
 277     }
 278 
 279     /**
 280      * Set an option, overwriting an existing state if one exists
 281      *
 282      * @param key    option key
 283      * @param option option
 284      */
 285     public void set(final String key, final Option<?> option) {
 286         options.put(key(key), option);
 287     }
 288 
 289     /**
 290      * Set an option as a boolean value, overwriting an existing state if one exists
 291      *
 292      * @param key    option key
 293      * @param option option
 294      */
 295     public void set(final String key, final boolean option) {
 296         set(key, new Option<>(option));
 297     }
 298 
 299     /**
 300      * Set an option as a String value, overwriting an existing state if one exists
 301      *
 302      * @param key    option key
 303      * @param option option
 304      */
 305     public void set(final String key, final String option) {
 306         set(key, new Option<>(option));
 307     }
 308 
 309     /**
 310      * Return the user arguments to the program, i.e. those trailing "--" after
 311      * the filename
 312      *
 313      * @return a list of user arguments
 314      */
 315     public List<String> getArguments() {
 316         return Collections.unmodifiableList(this.arguments);
 317     }
 318 
 319     /**
 320      * Return the JavaScript files passed to the program
 321      *
 322      * @return a list of files
 323      */
 324     public List<String> getFiles() {
 325         return Collections.unmodifiableList(files);
 326     }
 327 
 328     /**
 329      * Return the option templates for all the valid option supported.
 330      *
 331      * @return a collection of OptionTemplate objects.
 332      */
 333     public static Collection<OptionTemplate> getValidOptions() {
 334         return Collections.unmodifiableCollection(validOptions);
 335     }
 336 
 337     /**
 338      * Make sure a key is fully qualified for table lookups
 339      *
 340      * @param shortKey key for option
 341      * @return fully qualified key
 342      */
 343     private String key(final String shortKey) {
 344         String key = shortKey;
 345         while (key.startsWith("-")) {
 346             key = key.substring(1, key.length());
 347         }
 348         key = key.replace("-", ".");
 349         final String keyPrefix = this.resource + ".option.";
 350         if (key.startsWith(keyPrefix)) {
 351             return key;
 352         }
 353         return keyPrefix + key;
 354     }
 355 
 356     static String getMsg(final String msgId, final String... args) {
 357         try {
 358             final String msg = Options.bundle.getString(msgId);
 359             if (args.length == 0) {
 360                 return msg;
 361             }
 362             return new MessageFormat(msg).format(args);
 363         } catch (final MissingResourceException e) {
 364             throw new IllegalArgumentException(e);
 365         }
 366     }
 367 
 368     /**
 369      * Display context sensitive help
 370      *
 371      * @param e  exception that caused a parse error
 372      */
 373     public void displayHelp(final IllegalArgumentException e) {
 374         if (e instanceof IllegalOptionException) {
 375             final OptionTemplate template = ((IllegalOptionException)e).getTemplate();
 376             if (template.isXHelp()) {
 377                 // display extended help information
 378                 displayHelp(true);
 379             } else {
 380                 err.println(((IllegalOptionException)e).getTemplate());
 381             }
 382             return;
 383         }
 384 
 385         if (e != null && e.getMessage() != null) {
 386             err.println(getMsg("option.error.invalid.option",
 387                     e.getMessage(),
 388                     helpOptionTemplate.getShortName(),
 389                     helpOptionTemplate.getName()));
 390             err.println();
 391             return;
 392         }
 393 
 394         displayHelp(false);
 395     }
 396 
 397     /**
 398      * Display full help
 399      *
 400      * @param extended show the extended help for all options, including undocumented ones
 401      */
 402     public void displayHelp(final boolean extended) {
 403         for (final OptionTemplate t : Options.validOptions) {
 404             if ((extended || !t.isUndocumented()) && t.getResource().equals(resource)) {
 405                 err.println(t);
 406                 err.println();
 407             }
 408         }
 409     }
 410 
 411     /**
 412      * Processes the arguments and stores their information. Throws
 413      * IllegalArgumentException on error. The message can be analyzed by the
 414      * displayHelp function to become more context sensitive
 415      *
 416      * @param args arguments from command line
 417      */
 418     public void process(final String[] args) {
 419         final LinkedList<String> argList = new LinkedList<>();
 420         addSystemProperties(NASHORN_ARGS_PREPEND_PROPERTY, argList);
 421         processArgList(argList);
 422         assert argList.isEmpty();
 423         Collections.addAll(argList, args);
 424         processArgList(argList);
 425         assert argList.isEmpty();
 426         addSystemProperties(NASHORN_ARGS_PROPERTY, argList);
 427         processArgList(argList);
 428         assert argList.isEmpty();
 429     }
 430 
 431     private void processArgList(final LinkedList<String> argList) {
 432         while (!argList.isEmpty()) {
 433             final String arg = argList.remove(0);
 434             Objects.requireNonNull(arg);
 435 
 436             // skip empty args
 437             if (arg.isEmpty()) {
 438                 continue;
 439             }
 440 
 441             // user arguments to the script
 442             if ("--".equals(arg)) {
 443                 arguments.addAll(argList);
 444                 argList.clear();
 445                 continue;
 446             }
 447 
 448             // If it doesn't start with -, it's a file. But, if it is just "-",
 449             // then it is a file representing standard input.
 450             if (!arg.startsWith("-") || arg.length() == 1) {
 451                 files.add(arg);
 452                 continue;
 453             }
 454 
 455             if (arg.startsWith(definePropPrefix)) {
 456                 final String value = arg.substring(definePropPrefix.length());
 457                 final int eq = value.indexOf('=');
 458                 if (eq != -1) {
 459                     // -Dfoo=bar Set System property "foo" with value "bar"
 460                     System.setProperty(value.substring(0, eq), value.substring(eq + 1));
 461                 } else {
 462                     // -Dfoo is fine. Set System property "foo" with "" as it's value
 463                     if (!value.isEmpty()) {
 464                         System.setProperty(value, "");
 465                     } else {
 466                         // do not allow empty property name
 467                         throw new IllegalOptionException(definePropTemplate);
 468                     }
 469                 }
 470                 continue;
 471             }
 472 
 473             // it is an argument,  it and assign key, value and template
 474             final ParsedArg parg = new ParsedArg(arg);
 475 
 476             // check if the value of this option is passed as next argument
 477             if (parg.template.isValueNextArg()) {
 478                 if (argList.isEmpty()) {
 479                     throw new IllegalOptionException(parg.template);
 480                 }
 481                 parg.value = argList.remove(0);
 482             }
 483 
 484             // -h [args...]
 485             if (parg.template.isHelp()) {
 486                 // check if someone wants help on an explicit arg
 487                 if (!argList.isEmpty()) {
 488                     try {
 489                         final OptionTemplate t = new ParsedArg(argList.get(0)).template;
 490                         throw new IllegalOptionException(t);
 491                     } catch (final IllegalArgumentException e) {
 492                         throw e;
 493                     }
 494                 }
 495                 throw new IllegalArgumentException(); // show help for
 496                 // everything
 497             }
 498 
 499             if (parg.template.isXHelp()) {
 500                 throw new IllegalOptionException(parg.template);
 501             }
 502 
 503             if (parg.template.isRepeated()) {
 504                 assert parg.template.getType().equals("string");
 505 
 506                 final String key = key(parg.template.getKey());
 507                 final String value = options.containsKey(key)?
 508                     (options.get(key).getValue() + "," + parg.value) : Objects.toString(parg.value);
 509                 options.put(key, new Option<>(value));
 510             } else {
 511                 set(parg.template.getKey(), createOption(parg.template, parg.value));
 512             }
 513 
 514             // Arg may have a dependency to set other args, e.g.
 515             // scripting->anon.functions
 516             if (parg.template.getDependency() != null) {
 517                 argList.addFirst(parg.template.getDependency());
 518             }
 519         }
 520     }
 521 
 522     private static void addSystemProperties(final String sysPropName, final List<String> argList) {
 523         final String sysArgs = getStringProperty(sysPropName, null);
 524         if (sysArgs != null) {
 525             final StringTokenizer st = new StringTokenizer(sysArgs);
 526             while (st.hasMoreTokens()) {
 527                 argList.add(st.nextToken());
 528             }
 529         }
 530     }
 531 
 532     /**
 533      * Retrieves an option template identified by key.
 534      * @param shortKey the short (that is without the e.g. "nashorn.option." part) key
 535      * @return the option template identified by the key
 536      * @throws IllegalArgumentException if the key doesn't specify an existing template
 537      */
 538     public OptionTemplate getOptionTemplateByKey(final String shortKey) {
 539         final String fullKey = key(shortKey);
 540         for(final OptionTemplate t: validOptions) {
 541             if(t.getKey().equals(fullKey)) {
 542                 return t;
 543             }
 544         }
 545         throw new IllegalArgumentException(shortKey);
 546     }
 547 
 548     private static OptionTemplate getOptionTemplateByName(final String name) {
 549         for (final OptionTemplate t : Options.validOptions) {
 550             if (t.nameMatches(name)) {
 551                 return t;
 552             }
 553         }
 554         return null;
 555     }
 556 
 557     private static Option<?> createOption(final OptionTemplate t, final String value) {
 558         switch (t.getType()) {
 559         case "string":
 560             // default value null
 561             return new Option<>(value);
 562         case "timezone":
 563             // default value "TimeZone.getDefault()"
 564             return new Option<>(TimeZone.getTimeZone(value));
 565         case "locale":
 566             return new Option<>(Locale.forLanguageTag(value));
 567         case "keyvalues":
 568             return new KeyValueOption(value);
 569         case "log":
 570             return new LoggingOption(value);
 571         case "boolean":
 572             return new Option<>(value != null && Boolean.parseBoolean(value));
 573         case "integer":
 574             try {
 575                 return new Option<>(value == null ? 0 : Integer.parseInt(value));
 576             } catch (final NumberFormatException nfe) {
 577                 throw new IllegalOptionException(t);
 578             }
 579         case "properties":
 580             //swallow the properties and set them
 581             initProps(new KeyValueOption(value));
 582             return null;
 583         default:
 584             break;
 585         }
 586         throw new IllegalArgumentException(value);
 587     }
 588 
 589     private static void initProps(final KeyValueOption kv) {
 590         for (final Map.Entry<String, String> entry : kv.getValues().entrySet()) {
 591             System.setProperty(entry.getKey(), entry.getValue());
 592         }
 593     }
 594 
 595     /**
 596      * Resource name for properties file
 597      */
 598     private static final String MESSAGES_RESOURCE = "jdk.nashorn.internal.runtime.resources.Options";
 599 
 600     /**
 601      * Resource bundle for properties file
 602      */
 603     private static ResourceBundle bundle;
 604 
 605     /**
 606      * Usages per resource from properties file
 607      */
 608     private static HashMap<Object, Object> usage;
 609 
 610     /**
 611      * Valid options from templates in properties files
 612      */
 613     private static Collection<OptionTemplate> validOptions;
 614 
 615     /**
 616      * Help option
 617      */
 618     private static OptionTemplate helpOptionTemplate;
 619 
 620     /**
 621      * Define property option template.
 622      */
 623     private static OptionTemplate definePropTemplate;
 624 
 625     /**
 626      * Prefix of "define property" option.
 627      */
 628     private static String definePropPrefix;
 629 
 630     static {
 631         Options.bundle = ResourceBundle.getBundle(Options.MESSAGES_RESOURCE, Locale.getDefault());
 632         Options.validOptions = new TreeSet<>();
 633         Options.usage        = new HashMap<>();
 634 
 635         for (final Enumeration<String> keys = Options.bundle.getKeys(); keys.hasMoreElements(); ) {
 636             final String key = keys.nextElement();
 637             final StringTokenizer st = new StringTokenizer(key, ".");
 638             String resource = null;
 639             String type = null;
 640 
 641             if (st.countTokens() > 0) {
 642                 resource = st.nextToken(); // e.g. "nashorn"
 643             }
 644 
 645             if (st.countTokens() > 0) {
 646                 type = st.nextToken(); // e.g. "option"
 647             }
 648 
 649             if ("option".equals(type)) {
 650                 String helpKey = null;
 651                 String xhelpKey = null;
 652                 String definePropKey = null;
 653                 try {
 654                     helpKey = Options.bundle.getString(resource + ".options.help.key");
 655                     xhelpKey = Options.bundle.getString(resource + ".options.xhelp.key");
 656                     definePropKey = Options.bundle.getString(resource + ".options.D.key");
 657                 } catch (final MissingResourceException e) {
 658                     //ignored: no help
 659                 }
 660                 final boolean        isHelp = key.equals(helpKey);
 661                 final boolean        isXHelp = key.equals(xhelpKey);
 662                 final OptionTemplate t      = new OptionTemplate(resource, key, Options.bundle.getString(key), isHelp, isXHelp);
 663 
 664                 Options.validOptions.add(t);
 665                 if (isHelp) {
 666                     helpOptionTemplate = t;
 667                 }
 668 
 669                 if (key.equals(definePropKey)) {
 670                     definePropPrefix = t.getName();
 671                     definePropTemplate = t;
 672                 }
 673             } else if (resource != null && "options".equals(type)) {
 674                 Options.usage.put(resource, Options.bundle.getObject(key));
 675             }
 676         }
 677     }
 678 
 679     @SuppressWarnings("serial")
 680     private static class IllegalOptionException extends IllegalArgumentException {
 681         private final OptionTemplate template;
 682 
 683         IllegalOptionException(final OptionTemplate t) {
 684             super();
 685             this.template = t;
 686         }
 687 
 688         OptionTemplate getTemplate() {
 689             return this.template;
 690         }
 691     }
 692 
 693     /**
 694      * This is a resolved argument of the form key=value
 695      */
 696     private static class ParsedArg {
 697         /** The resolved option template this argument corresponds to */
 698         OptionTemplate template;
 699 
 700         /** The value of the argument */
 701         String value;
 702 
 703         ParsedArg(final String argument) {
 704             final QuotedStringTokenizer st = new QuotedStringTokenizer(argument, "=");
 705             if (!st.hasMoreTokens()) {
 706                 throw new IllegalArgumentException();
 707             }
 708 
 709             final String token = st.nextToken();
 710             this.template = getOptionTemplateByName(token);
 711             if (this.template == null) {
 712                 throw new IllegalArgumentException(argument);
 713             }
 714 
 715             value = "";
 716             if (st.hasMoreTokens()) {
 717                 while (st.hasMoreTokens()) {
 718                     value += st.nextToken();
 719                     if (st.hasMoreTokens()) {
 720                         value += ':';
 721                     }
 722                 }
 723             } else if ("boolean".equals(this.template.getType())) {
 724                 value = "true";
 725             }
 726         }
 727     }
 728 }