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