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