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