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 }