1/*
2 * Copyright (c) 2005, 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
26package com.sun.tools.script.shell;
27
28import java.io.*;
29import java.net.*;
30import java.text.*;
31import java.util.*;
32import javax.script.*;
33
34/**
35 * This is the main class for Java script shell.
36 */
37public class Main {
38    /**
39     * main entry point to the command line tool
40     * @param args command line argument array
41     */
42    public static void main(String[] args) {
43        // parse command line options
44        String[] scriptArgs = processOptions(args);
45
46        // process each script command
47        for (Command cmd : scripts) {
48            cmd.run(scriptArgs);
49        }
50
51        System.exit(EXIT_SUCCESS);
52    }
53
54    // Each -e or -f or interactive mode is represented
55    // by an instance of Command.
56    private static interface Command {
57        public void run(String[] arguments);
58    }
59
60    /**
61     * Parses and processes command line options.
62     * @param args command line argument array
63     */
64    private static String[] processOptions(String[] args) {
65        // current scripting language selected
66        String currentLanguage = DEFAULT_LANGUAGE;
67        // current script file encoding selected
68        String currentEncoding = null;
69
70        // check for -classpath or -cp first
71        checkClassPath(args);
72
73        // have we seen -e or -f ?
74        boolean seenScript = false;
75        // have we seen -f - already?
76        boolean seenStdin = false;
77        for (int i=0; i < args.length; i++) {
78            String arg = args[i];
79            if (arg.equals("-classpath") ||
80                    arg.equals("-cp")) {
81                // handled already, just continue
82                i++;
83                continue;
84            }
85
86            // collect non-option arguments and pass these as script arguments
87            if (!arg.startsWith("-")) {
88                int numScriptArgs;
89                int startScriptArg;
90                if (seenScript) {
91                    // if we have seen -e or -f already all non-option arguments
92                    // are passed as script arguments
93                    numScriptArgs = args.length - i;
94                    startScriptArg = i;
95                } else {
96                    // if we have not seen -e or -f, first non-option argument
97                    // is treated as script file name and rest of the non-option
98                    // arguments are passed to script as script arguments
99                    numScriptArgs = args.length - i - 1;
100                    startScriptArg = i + 1;
101                    ScriptEngine se = getScriptEngine(currentLanguage);
102                    addFileSource(se, args[i], currentEncoding);
103                }
104                // collect script arguments and return to main
105                String[] result = new String[numScriptArgs];
106                System.arraycopy(args, startScriptArg, result, 0, numScriptArgs);
107                return result;
108            }
109
110            if (arg.startsWith("-D")) {
111                String value = arg.substring(2);
112                int eq = value.indexOf('=');
113                if (eq != -1) {
114                    System.setProperty(value.substring(0, eq),
115                            value.substring(eq + 1));
116                } else {
117                    if (!value.equals("")) {
118                        System.setProperty(value, "");
119                    } else {
120                        // do not allow empty property name
121                        usage(EXIT_CMD_NO_PROPNAME);
122                    }
123                }
124                continue;
125            } else if (arg.equals("-?") || arg.equals("-help")) {
126                usage(EXIT_SUCCESS);
127            } else if (arg.equals("-e")) {
128                seenScript = true;
129                if (++i == args.length)
130                    usage(EXIT_CMD_NO_SCRIPT);
131
132                ScriptEngine se = getScriptEngine(currentLanguage);
133                addStringSource(se, args[i]);
134                continue;
135            } else if (arg.equals("-encoding")) {
136                if (++i == args.length)
137                    usage(EXIT_CMD_NO_ENCODING);
138                currentEncoding = args[i];
139                continue;
140            } else if (arg.equals("-f")) {
141                seenScript = true;
142                if (++i == args.length)
143                    usage(EXIT_CMD_NO_FILE);
144                ScriptEngine se = getScriptEngine(currentLanguage);
145                if (args[i].equals("-")) {
146                    if (seenStdin) {
147                        usage(EXIT_MULTIPLE_STDIN);
148                    } else {
149                        seenStdin = true;
150                    }
151                    addInteractiveMode(se);
152                } else {
153                    addFileSource(se, args[i], currentEncoding);
154                }
155                continue;
156            } else if (arg.equals("-l")) {
157                if (++i == args.length)
158                    usage(EXIT_CMD_NO_LANG);
159                currentLanguage = args[i];
160                continue;
161            } else if (arg.equals("-q")) {
162                listScriptEngines();
163            }
164            // some unknown option...
165            usage(EXIT_UNKNOWN_OPTION);
166        }
167
168        if (! seenScript) {
169            ScriptEngine se = getScriptEngine(currentLanguage);
170            addInteractiveMode(se);
171        }
172        return new String[0];
173    }
174
175    /**
176     * Adds interactive mode Command
177     * @param se ScriptEngine to use in interactive mode.
178     */
179    private static void addInteractiveMode(final ScriptEngine se) {
180        scripts.add(new Command() {
181            public void run(String[] args) {
182                setScriptArguments(se, args);
183                processSource(se, "-", null);
184            }
185        });
186    }
187
188    /**
189     * Adds script source file Command
190     * @param se ScriptEngine used to evaluate the script file
191     * @param fileName script file name
192     * @param encoding script file encoding
193     */
194    private static void addFileSource(final ScriptEngine se,
195            final String fileName,
196            final String encoding) {
197        scripts.add(new Command() {
198            public void run(String[] args) {
199                setScriptArguments(se, args);
200                processSource(se, fileName, encoding);
201            }
202        });
203    }
204
205    /**
206     * Adds script string source Command
207     * @param se ScriptEngine to be used to evaluate the script string
208     * @param source Script source string
209     */
210    private static void addStringSource(final ScriptEngine se,
211            final String source) {
212        scripts.add(new Command() {
213            public void run(String[] args) {
214                setScriptArguments(se, args);
215                String oldFile = setScriptFilename(se, "<string>");
216                try {
217                    evaluateString(se, source);
218                } finally {
219                    setScriptFilename(se, oldFile);
220                }
221            }
222        });
223    }
224
225    /**
226     * Prints list of script engines available and exits.
227     */
228    private static void listScriptEngines() {
229        List<ScriptEngineFactory> factories = engineManager.getEngineFactories();
230        for (ScriptEngineFactory factory: factories) {
231            getError().println(getMessage("engine.info",
232                    new Object[] { factory.getLanguageName(),
233                            factory.getLanguageVersion(),
234                            factory.getEngineName(),
235                            factory.getEngineVersion()
236            }));
237        }
238        System.exit(EXIT_SUCCESS);
239    }
240
241    /**
242     * Processes a given source file or standard input.
243     * @param se ScriptEngine to be used to evaluate
244     * @param filename file name, can be null
245     * @param encoding script file encoding, can be null
246     */
247    private static void processSource(ScriptEngine se, String filename,
248            String encoding) {
249        if (filename.equals("-")) {
250            BufferedReader in = new BufferedReader
251                    (new InputStreamReader(getIn()));
252            boolean hitEOF = false;
253            String prompt = getPrompt(se);
254            se.put(ScriptEngine.FILENAME, "<STDIN>");
255            while (!hitEOF) {
256                getError().print(prompt);
257                String source = "";
258                try {
259                    source = in.readLine();
260                } catch (IOException ioe) {
261                    getError().println(ioe.toString());
262                }
263                if (source == null) {
264                    hitEOF = true;
265                    break;
266                }
267                Object res = evaluateString(se, source, false);
268                if (res != null) {
269                    res = res.toString();
270                    if (res == null) {
271                        res = "null";
272                    }
273                    getError().println(res);
274                }
275            }
276        } else {
277            FileInputStream fis = null;
278            try {
279                fis = new FileInputStream(filename);
280            } catch (FileNotFoundException fnfe) {
281                getError().println(getMessage("file.not.found",
282                        new Object[] { filename }));
283                        System.exit(EXIT_FILE_NOT_FOUND);
284            }
285            evaluateStream(se, fis, filename, encoding);
286        }
287    }
288
289    /**
290     * Evaluates given script source
291     * @param se ScriptEngine to evaluate the string
292     * @param script Script source string
293     * @param exitOnError whether to exit the process on script error
294     */
295    private static Object evaluateString(ScriptEngine se,
296            String script, boolean exitOnError) {
297        try {
298            return se.eval(script);
299        } catch (ScriptException sexp) {
300            getError().println(getMessage("string.script.error",
301                    new Object[] { sexp.getMessage() }));
302                    if (exitOnError)
303                        System.exit(EXIT_SCRIPT_ERROR);
304        } catch (Exception exp) {
305            exp.printStackTrace(getError());
306            if (exitOnError)
307                System.exit(EXIT_SCRIPT_ERROR);
308        }
309
310        return null;
311    }
312
313    /**
314     * Evaluate script string source and exit on script error
315     * @param se ScriptEngine to evaluate the string
316     * @param script Script source string
317     */
318    private static void evaluateString(ScriptEngine se, String script) {
319        evaluateString(se, script, true);
320    }
321
322    /**
323     * Evaluates script from given reader
324     * @param se ScriptEngine to evaluate the string
325     * @param reader Reader from which is script is read
326     * @param name file name to report in error.
327     */
328    private static Object evaluateReader(ScriptEngine se,
329            Reader reader, String name) {
330        String oldFilename = setScriptFilename(se, name);
331        try {
332            return se.eval(reader);
333        } catch (ScriptException sexp) {
334            getError().println(getMessage("file.script.error",
335                    new Object[] { name, sexp.getMessage() }));
336                    System.exit(EXIT_SCRIPT_ERROR);
337        } catch (Exception exp) {
338            exp.printStackTrace(getError());
339            System.exit(EXIT_SCRIPT_ERROR);
340        } finally {
341            setScriptFilename(se, oldFilename);
342        }
343        return null;
344    }
345
346    /**
347     * Evaluates given input stream
348     * @param se ScriptEngine to evaluate the string
349     * @param is InputStream from which script is read
350     * @param name file name to report in error
351     */
352    private static Object evaluateStream(ScriptEngine se,
353            InputStream is, String name,
354            String encoding) {
355        BufferedReader reader = null;
356        if (encoding != null) {
357            try {
358                reader = new BufferedReader(new InputStreamReader(is,
359                        encoding));
360            } catch (UnsupportedEncodingException uee) {
361                getError().println(getMessage("encoding.unsupported",
362                        new Object[] { encoding }));
363                        System.exit(EXIT_NO_ENCODING_FOUND);
364            }
365        } else {
366            reader = new BufferedReader(new InputStreamReader(is));
367        }
368        return evaluateReader(se, reader, name);
369    }
370
371    /**
372     * Prints usage message and exits
373     * @param exitCode process exit code
374     */
375    private static void usage(int exitCode) {
376        getError().println(getMessage("main.usage",
377                new Object[] { PROGRAM_NAME }));
378                System.exit(exitCode);
379    }
380
381    /**
382     * Gets prompt for interactive mode
383     * @return prompt string to use
384     */
385    private static String getPrompt(ScriptEngine se) {
386        List<String> names = se.getFactory().getNames();
387        return names.get(0) + "> ";
388    }
389
390    /**
391     * Get formatted, localized error message
392     */
393    private static String getMessage(String key, Object[] params) {
394        return MessageFormat.format(msgRes.getString(key), params);
395    }
396
397    // input stream from where we will read
398    private static InputStream getIn() {
399        return System.in;
400    }
401
402    // stream to print error messages
403    private static PrintStream getError() {
404        return System.err;
405    }
406
407    // get current script engine
408    private static ScriptEngine getScriptEngine(String lang) {
409        ScriptEngine se = engines.get(lang);
410        if (se == null) {
411            se = engineManager.getEngineByName(lang);
412            if (se == null) {
413                getError().println(getMessage("engine.not.found",
414                        new Object[] { lang }));
415                        System.exit(EXIT_ENGINE_NOT_FOUND);
416            }
417
418            // initialize the engine
419            initScriptEngine(se);
420            // to avoid re-initialization of engine, store it in a map
421            engines.put(lang, se);
422        }
423        return se;
424    }
425
426    // initialize a given script engine
427    private static void initScriptEngine(ScriptEngine se) {
428        // put engine global variable
429        se.put("engine", se);
430
431        // load init.<ext> file from resource
432        List<String> exts = se.getFactory().getExtensions();
433        InputStream sysIn = null;
434        ClassLoader cl = Thread.currentThread().getContextClassLoader();
435        for (String ext : exts) {
436            try {
437                sysIn = Main.class.getModule().getResourceAsStream("com/sun/tools/script/shell/init." + ext);
438            } catch (IOException ioe) {
439                throw new RuntimeException(ioe);
440            }
441            if (sysIn != null) break;
442        }
443        if (sysIn != null) {
444            evaluateStream(se, sysIn, "<system-init>", null);
445        }
446    }
447
448    /**
449     * Checks for -classpath, -cp in command line args. Creates a ClassLoader
450     * and sets it as Thread context loader for current thread.
451     *
452     * @param args command line argument array
453     */
454    private static void checkClassPath(String[] args) {
455        String classPath = null;
456        for (int i = 0; i < args.length; i++) {
457            if (args[i].equals("-classpath") ||
458                    args[i].equals("-cp")) {
459                if (++i == args.length) {
460                    // just -classpath or -cp with no value
461                    usage(EXIT_CMD_NO_CLASSPATH);
462                } else {
463                    classPath = args[i];
464                }
465            }
466        }
467
468        if (classPath != null) {
469            /* We create a class loader, configure it with specified
470             * classpath values and set the same as context loader.
471             * Note that ScriptEngineManager uses context loader to
472             * load script engines. So, this ensures that user defined
473             * script engines will be loaded. For classes referred
474             * from scripts, Rhino engine uses thread context loader
475             * but this is script engine dependent. We don't have
476             * script engine independent solution anyway. Unless we
477             * know the class loader used by a specific engine, we
478             * can't configure correct loader.
479             */
480            URL[] urls = pathToURLs(classPath);
481            URLClassLoader loader = new URLClassLoader(urls);
482            Thread.currentThread().setContextClassLoader(loader);
483        }
484
485        // now initialize script engine manager. Note that this has to
486        // be done after setting the context loader so that manager
487        // will see script engines from user specified classpath
488        engineManager = new ScriptEngineManager();
489    }
490
491    /**
492     * Utility method for converting a search path string to an array
493     * of directory and JAR file URLs.
494     *
495     * @param path the search path string
496     * @return the resulting array of directory and JAR file URLs
497     */
498    private static URL[] pathToURLs(String path) {
499        String[] components = path.split(File.pathSeparator);
500        URL[] urls = new URL[components.length];
501        int count = 0;
502        while(count < components.length) {
503            URL url = fileToURL(new File(components[count]));
504            if (url != null) {
505                urls[count++] = url;
506            }
507        }
508        if (urls.length != count) {
509            URL[] tmp = new URL[count];
510            System.arraycopy(urls, 0, tmp, 0, count);
511            urls = tmp;
512        }
513        return urls;
514    }
515
516    /**
517     * Returns the directory or JAR file URL corresponding to the specified
518     * local file name.
519     *
520     * @param file the File object
521     * @return the resulting directory or JAR file URL, or null if unknown
522     */
523    private static URL fileToURL(File file) {
524        String name;
525        try {
526            name = file.getCanonicalPath();
527        } catch (IOException e) {
528            name = file.getAbsolutePath();
529        }
530        name = name.replace(File.separatorChar, '/');
531        if (!name.startsWith("/")) {
532            name = "/" + name;
533        }
534        // If the file does not exist, then assume that it's a directory
535        if (!file.isFile()) {
536            name = name + "/";
537        }
538        try {
539            return new URL("file", "", name);
540        } catch (MalformedURLException e) {
541            throw new IllegalArgumentException("file");
542        }
543    }
544
545    private static void setScriptArguments(ScriptEngine se, String[] args) {
546        se.put("arguments", args);
547        se.put(ScriptEngine.ARGV, args);
548    }
549
550    private static String setScriptFilename(ScriptEngine se, String name) {
551        String oldName = (String) se.get(ScriptEngine.FILENAME);
552        se.put(ScriptEngine.FILENAME, name);
553        return oldName;
554    }
555
556    // exit codes
557    private static final int EXIT_SUCCESS            = 0;
558    private static final int EXIT_CMD_NO_CLASSPATH   = 1;
559    private static final int EXIT_CMD_NO_FILE        = 2;
560    private static final int EXIT_CMD_NO_SCRIPT      = 3;
561    private static final int EXIT_CMD_NO_LANG        = 4;
562    private static final int EXIT_CMD_NO_ENCODING    = 5;
563    private static final int EXIT_CMD_NO_PROPNAME    = 6;
564    private static final int EXIT_UNKNOWN_OPTION     = 7;
565    private static final int EXIT_ENGINE_NOT_FOUND   = 8;
566    private static final int EXIT_NO_ENCODING_FOUND  = 9;
567    private static final int EXIT_SCRIPT_ERROR       = 10;
568    private static final int EXIT_FILE_NOT_FOUND     = 11;
569    private static final int EXIT_MULTIPLE_STDIN     = 12;
570
571    // default scripting language
572    private static final String DEFAULT_LANGUAGE = "js";
573    // list of scripts to process
574    private static List<Command> scripts;
575    // the script engine manager
576    private static ScriptEngineManager engineManager;
577    // map of engines we loaded
578    private static Map<String, ScriptEngine> engines;
579    // error messages resource
580    private static ResourceBundle msgRes;
581    private static String BUNDLE_NAME = "com.sun.tools.script.shell.messages";
582    private static String PROGRAM_NAME = "jrunscript";
583
584    static {
585        scripts = new ArrayList<Command>();
586        engines = new HashMap<String, ScriptEngine>();
587        msgRes = ResourceBundle.getBundle(BUNDLE_NAME, Locale.getDefault());
588    }
589}
590