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