View Javadoc
1   /*
2    * SPDX-License-Identifier: LGPL-2.1-or-later
3    * See LICENSE file for details.
4    *
5    * Copyright 2012-2026 Hazendaz.
6    */
7   /* ***** BEGIN LICENSE BLOCK *****
8    * Version: MPL 1.1/GPL 2.0
9    *
10   * The contents of this file are subject to the Mozilla Public License Version
11   * 1.1 (the "License"); you may not use this file except in compliance with
12   * the License. You may obtain a copy of the License at
13   * http://www.mozilla.org/MPL/
14   *
15   * Software distributed under the License is distributed on an "AS IS" basis,
16   * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
17   * for the specific language governing rights and limitations under the
18   * License.
19   *
20   * The Original Code is Rhino code, released
21   * May 6, 1998.
22   *
23   * The Initial Developer of the Original Code is
24   * Netscape Communications Corporation.
25   * Portions created by the Initial Developer are Copyright (C) 1997-1999
26   * the Initial Developer. All Rights Reserved.
27   *
28   * Contributor(s):
29   *
30   * Alternatively, the contents of this file may be used under the terms of
31   * the GNU General Public License Version 2 or later (the "GPL"), in which
32   * case the provisions of the GPL are applicable instead of those above. If
33   * you wish to allow use of your version of this file only under the terms of
34   * the GPL and not to allow others to use your version of this file under the
35   * MPL, indicate your decision by deleting the provisions above and replacing
36   * them with the notice and other provisions required by the GPL. If you do
37   * not delete the provisions above, a recipient may use your version of this
38   * file under either the MPL or the GPL.
39   *
40   * ***** END LICENSE BLOCK ***** */
41  
42  package net.alchim31.maven.yuicompressor;
43  
44  import java.io.BufferedReader;
45  import java.io.IOException;
46  import java.io.InputStreamReader;
47  import java.nio.charset.StandardCharsets;
48  import java.nio.file.Files;
49  import java.nio.file.Path;
50  
51  import org.mozilla.javascript.Context;
52  import org.mozilla.javascript.ErrorReporter;
53  import org.mozilla.javascript.EvaluatorException;
54  import org.mozilla.javascript.Function;
55  import org.mozilla.javascript.JavaScriptException;
56  import org.mozilla.javascript.Scriptable;
57  import org.mozilla.javascript.ScriptableObject;
58  import org.mozilla.javascript.WrappedException;
59  import org.slf4j.Logger;
60  import org.slf4j.LoggerFactory;
61  
62  /**
63   * The BasicRhinoShell program.
64   * <p>
65   * Can execute scripts interactively or in batch mode at the command line. An example of controlling the JavaScript
66   * engine.
67   * <p>
68   * Based on Rhino.
69   *
70   * @see <a href="http://lxr.mozilla.org/mozilla/source/js/rhino/examples/BasicRhinoShell.java">Basic Rhino Shell</a>
71   */
72  public class BasicRhinoShell extends ScriptableObject {
73  
74      /** The Constant serial version uid. */
75      private static final long serialVersionUID = 1L;
76  
77      /** The Constant logger. */
78      private static final Logger logger = LoggerFactory.getLogger(BasicRhinoShell.class);
79  
80      /** The quitting. */
81      private boolean quitting;
82  
83      @Override
84      public String getClassName() {
85          return "global";
86      }
87  
88      /**
89       * Main entry point.
90       * <p>
91       * Process arguments as would a normal Java program. Also create a new Context and associate it with the current
92       * thread. Then set up the execution environment and begin to execute scripts.
93       *
94       * @param args
95       *            the args
96       * @param reporter
97       *            the reporter
98       */
99      public static void exec(String[] args, ErrorReporter reporter) {
100         // Associate a new Context with this thread
101         Context cx = Context.enter();
102         cx.setErrorReporter(reporter);
103         try {
104             // Initialize the standard objects (Object, Function, etc.)
105             // This must be done before scripts can be executed.
106             BasicRhinoShell basicRhinoShell = new BasicRhinoShell();
107             cx.initStandardObjects(basicRhinoShell);
108 
109             // Define some global functions particular to the BasicRhinoShell.
110             // Note
111             // that these functions are not part of ECMA.
112             String[] names = { "print", "quit", "version", "load", "help", "readFile", "warn" };
113             basicRhinoShell.defineFunctionProperties(names, BasicRhinoShell.class, ScriptableObject.DONTENUM);
114 
115             args = processOptions(cx, args);
116 
117             // Set up "arguments" in the global scope to contain the command
118             // line arguments after the name of the script to execute
119             Object[] array;
120             if (args.length == 0) {
121                 array = new Object[0];
122             } else {
123                 int length = args.length - 1;
124                 array = new Object[length];
125                 System.arraycopy(args, 1, array, 0, length);
126             }
127             Scriptable argsObj = cx.newArray(basicRhinoShell, array);
128             basicRhinoShell.defineProperty("arguments", argsObj, ScriptableObject.DONTENUM);
129 
130             basicRhinoShell.processSource(cx, args.length == 0 ? null : args[0]);
131         } finally {
132             Context.exit();
133         }
134     }
135 
136     /**
137      * Parse arguments.
138      *
139      * @param cx
140      *            the cx
141      * @param args
142      *            the args
143      *
144      * @return the string[]
145      */
146     public static String[] processOptions(Context cx, String[] args) {
147         for (int i = 0; i < args.length; i++) {
148             String arg = args[i];
149             if (!arg.startsWith("-")) {
150                 String[] result = new String[args.length - i];
151                 for (int j = i; j < args.length; j++) {
152                     result[j - i] = args[j];
153                 }
154                 return result;
155             }
156             if (arg.equals("-version")) {
157                 i++;
158                 if (i == args.length) {
159                     usage(arg);
160                 }
161                 double d = Context.toNumber(args[i]);
162                 if (Double.isNaN(d)) {
163                     usage(arg);
164                 }
165                 cx.setLanguageVersion((int) d);
166                 continue;
167             }
168             usage(arg);
169         }
170         return new String[0];
171     }
172 
173     /**
174      * Print a usage message.
175      *
176      * @param s
177      *            the s
178      */
179     private static void usage(String s) {
180         p("Didn't understand \"" + s + "\".");
181         p("Valid arguments are:");
182         p("-version 100|110|120|130|140|150|160|170");
183         System.exit(1);
184     }
185 
186     /**
187      * Print a help message.
188      * <p>
189      * This method is defined as a JavaScript function.
190      */
191     public void help() {
192         p("");
193         p("Command                Description");
194         p("=======                ===========");
195         p("help()                 Display usage and help messages. ");
196         p("defineClass(className) Define an extension using the Java class");
197         p("                       named with the string argument. ");
198         p("                       Uses ScriptableObject.defineClass(). ");
199         p("load(['foo.js', ...])  Load JavaScript source files named by ");
200         p("                       string arguments. ");
201         p("loadClass(className)   Load a class named by a string argument.");
202         p("                       The class must be a script compiled to a");
203         p("                       class file. ");
204         p("print([expr ...])      Evaluate and print expressions. ");
205         p("quit()                 Quit the BasicRhinoShell. ");
206         p("version([number])      Get or set the JavaScript version number.");
207         p("");
208     }
209 
210     /**
211      * Print the string values of its arguments.
212      * <p>
213      * This method is defined as a JavaScript function. Note that its arguments are of the "varargs" form, which allows
214      * it to handle an arbitrary number of arguments supplied to the JavaScript function.
215      *
216      * @param cx
217      *            the cx
218      * @param thisObj
219      *            the this obj
220      * @param args
221      *            the args
222      * @param funObj
223      *            the fun obj
224      */
225     public static void print(Context cx, Scriptable thisObj, Object[] args, Function funObj) {
226         for (int i = 0; i < args.length; i++) {
227             if (i > 0) {
228                 logger.info("");
229             }
230 
231             // Convert the arbitrary JavaScript value into a string form.
232             String s = Context.toString(args[i]);
233 
234             logger.info(s);
235         }
236         logger.info("");
237     }
238 
239     /**
240      * Quit the BasicRhinoShell.
241      * <p>
242      * This only affects the interactive mode.
243      * <p>
244      * This method is defined as a JavaScript function.
245      */
246     public void quit() {
247         quitting = true;
248     }
249 
250     /**
251      * Warn.
252      *
253      * @param cx
254      *            the cx
255      * @param thisObj
256      *            the this obj
257      * @param args
258      *            the args
259      * @param funObj
260      *            the fun obj
261      */
262     public static void warn(Context cx, Scriptable thisObj, Object[] args, Function funObj) {
263         String message = Context.toString(args[0]);
264         int line = (int) Context.toNumber(args[1]);
265         String source = Context.toString(args[2]);
266         int column = (int) Context.toNumber(args[3]);
267         cx.getErrorReporter().warning(message, null, line, source, column);
268     }
269 
270     /**
271      * This method is defined as a JavaScript function.
272      *
273      * @param path
274      *            the path
275      *
276      * @return the string
277      */
278     public String readFile(String path) {
279         try {
280             return new String(Files.readAllBytes(Path.of(path)), StandardCharsets.UTF_8);
281         } catch (RuntimeException exc) {
282             throw exc;
283         } catch (IOException exc) {
284             throw new RuntimeException("wrap: " + exc.getMessage(), exc);
285         }
286     }
287 
288     /**
289      * Get and set the language version.
290      * <p>
291      * This method is defined as a JavaScript function.
292      *
293      * @param cx
294      *            the cx
295      * @param thisObj
296      *            the this obj
297      * @param args
298      *            the args
299      * @param funObj
300      *            the fun obj
301      *
302      * @return the double
303      */
304     public static double version(Context cx, Scriptable thisObj, Object[] args, Function funObj) {
305         double result = cx.getLanguageVersion();
306         if (args.length > 0) {
307             double d = Context.toNumber(args[0]);
308             cx.setLanguageVersion((int) d);
309         }
310         return result;
311     }
312 
313     /**
314      * Load and execute a set of JavaScript source files.
315      * <p>
316      * This method is defined as a JavaScript function.
317      *
318      * @param cx
319      *            the cx
320      * @param thisObj
321      *            the this obj
322      * @param args
323      *            the args
324      * @param funObj
325      *            the fun obj
326      */
327     public static void load(Context cx, Scriptable thisObj, Object[] args, Function funObj) {
328         BasicRhinoShell basicRhinoShell = (BasicRhinoShell) getTopLevelScope(thisObj);
329         for (Object element : args) {
330             basicRhinoShell.processSource(cx, Context.toString(element));
331         }
332     }
333 
334     /**
335      * Evaluate JavaScript source.
336      *
337      * @param cx
338      *            the current context
339      * @param filename
340      *            the name of the file to compile, or null for interactive mode.
341      */
342     private void processSource(Context cx, String filename) {
343         if (filename == null) {
344             BufferedReader in = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
345             String sourceName = "<stdin>";
346             int lineno = 1;
347             boolean hitEOF = false;
348             do {
349                 int startline = lineno;
350                 logger.info("js> ");
351                 try {
352                     StringBuilder source = new StringBuilder();
353                     // Collect lines of source to compile.
354                     while (true) {
355                         String newline = in.readLine();
356                         if (newline == null) {
357                             hitEOF = true;
358                             break;
359                         }
360                         source.append(newline).append("\n");
361                         lineno++;
362                         // Continue collecting as long as more lines are needed to complete the current statement.
363                         // stringIsCompilableUnit is also true if the source statement will result in any error other
364                         // than one that might be resolved by appending more source.
365                         if (cx.stringIsCompilableUnit(source.toString())) {
366                             break;
367                         }
368                     }
369                     Object result = cx.evaluateString(this, source.toString(), sourceName, startline, null);
370                     if (result != Context.getUndefinedValue() && logger.isInfoEnabled()) {
371                         logger.info("{}", Context.toString(result));
372                     }
373                 } catch (WrappedException e) {
374                     // Some form of exception was caught by JavaScript and propagated up.
375                     logger.info(e.getWrappedException().toString());
376                     logger.error("", e);
377                 } catch (EvaluatorException | JavaScriptException e) {
378                     // Some form of JavaScript error.
379                     logger.info("js: {}", e.getMessage());
380                 } catch (IOException e) {
381                     logger.info(e.toString());
382                 }
383                 if (quitting) {
384                     // The user executed the quit() function.
385                     break;
386                 }
387             } while (!hitEOF);
388             logger.info("");
389         } else {
390             try (BufferedReader in = Files.newBufferedReader(Path.of(filename), StandardCharsets.UTF_8)) {
391                 // Here we evaluate the entire contents of the file as a script. Text is printed only if the
392                 // print() function is called.
393                 cx.evaluateReader(this, in, filename, 1, null);
394             } catch (WrappedException e) {
395                 logger.info(e.getWrappedException().toString());
396                 logger.error("", e);
397             } catch (EvaluatorException | JavaScriptException e) {
398                 logger.info("js: {}", e.getMessage());
399             } catch (IOException e) {
400                 logger.error("", e);
401             }
402         }
403     }
404 
405     /**
406      * P.
407      *
408      * @param s
409      *            the s
410      */
411     private static void p(String s) {
412         logger.info(s);
413     }
414 
415 }