BasicRhinoShell.java

/*
 * YuiCompressor Maven plugin
 *
 * Copyright 2012-2025 Hazendaz.
 *
 * Licensed under the GNU Lesser General Public License (LGPL),
 * version 2.1 or later (the "License").
 * You may not use this file except in compliance with the License.
 * You may read the licence in the 'lgpl.txt' file in the root folder of
 * project or obtain a copy at
 *
 *     https://www.gnu.org/licenses/lgpl-2.1.html
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Rhino code, released
 * May 6, 1998.
 *
 * The Initial Developer of the Original Code is
 * Netscape Communications Corporation.
 * Portions created by the Initial Developer are Copyright (C) 1997-1999
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * the GNU General Public License Version 2 or later (the "GPL"), in which
 * case the provisions of the GPL are applicable instead of those above. If
 * you wish to allow use of your version of this file only under the terms of
 * the GPL and not to allow others to use your version of this file under the
 * MPL, indicate your decision by deleting the provisions above and replacing
 * them with the notice and other provisions required by the GPL. If you do
 * not delete the provisions above, a recipient may use your version of this
 * file under either the MPL or the GPL.
 *
 * ***** END LICENSE BLOCK ***** */

package net.alchim31.maven.yuicompressor;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.ErrorReporter;
import org.mozilla.javascript.EvaluatorException;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.JavaScriptException;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.WrappedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The BasicRhinoShell program.
 * <p>
 * Can execute scripts interactively or in batch mode at the command line. An example of controlling the JavaScript
 * engine.
 * <p>
 * Based on Rhino.
 *
 * @see <a href="http://lxr.mozilla.org/mozilla/source/js/rhino/examples/BasicRhinoShell.java">Basic Rhino Shell</a>
 */
public class BasicRhinoShell extends ScriptableObject {

    /** The Constant serial version uid. */
    private static final long serialVersionUID = 1L;

    /** The Constant logger. */
    private static final Logger logger = LoggerFactory.getLogger(BasicRhinoShell.class);

    /** The quitting. */
    private boolean quitting;

    @Override
    public String getClassName() {
        return "global";
    }

    /**
     * Main entry point.
     * <p>
     * Process arguments as would a normal Java program. Also create a new Context and associate it with the current
     * thread. Then set up the execution environment and begin to execute scripts.
     *
     * @param args
     *            the args
     * @param reporter
     *            the reporter
     */
    public static void exec(String[] args, ErrorReporter reporter) {
        // Associate a new Context with this thread
        Context cx = Context.enter();
        cx.setErrorReporter(reporter);
        try {
            // Initialize the standard objects (Object, Function, etc.)
            // This must be done before scripts can be executed.
            BasicRhinoShell basicRhinoShell = new BasicRhinoShell();
            cx.initStandardObjects(basicRhinoShell);

            // Define some global functions particular to the BasicRhinoShell.
            // Note
            // that these functions are not part of ECMA.
            String[] names = { "print", "quit", "version", "load", "help", "readFile", "warn" };
            basicRhinoShell.defineFunctionProperties(names, BasicRhinoShell.class, ScriptableObject.DONTENUM);

            args = processOptions(cx, args);

            // Set up "arguments" in the global scope to contain the command
            // line arguments after the name of the script to execute
            Object[] array;
            if (args.length == 0) {
                array = new Object[0];
            } else {
                int length = args.length - 1;
                array = new Object[length];
                System.arraycopy(args, 1, array, 0, length);
            }
            Scriptable argsObj = cx.newArray(basicRhinoShell, array);
            basicRhinoShell.defineProperty("arguments", argsObj, ScriptableObject.DONTENUM);

            basicRhinoShell.processSource(cx, args.length == 0 ? null : args[0]);
        } finally {
            Context.exit();
        }
    }

    /**
     * Parse arguments.
     *
     * @param cx
     *            the cx
     * @param args
     *            the args
     *
     * @return the string[]
     */
    public static String[] processOptions(Context cx, String[] args) {
        for (int i = 0; i < args.length; i++) {
            String arg = args[i];
            if (!arg.startsWith("-")) {
                String[] result = new String[args.length - i];
                for (int j = i; j < args.length; j++) {
                    result[j - i] = args[j];
                }
                return result;
            }
            if (arg.equals("-version")) {
                i++;
                if (i == args.length) {
                    usage(arg);
                }
                double d = Context.toNumber(args[i]);
                if (Double.isNaN(d)) {
                    usage(arg);
                }
                cx.setLanguageVersion((int) d);
                continue;
            }
            usage(arg);
        }
        return new String[0];
    }

    /**
     * Print a usage message.
     *
     * @param s
     *            the s
     */
    private static void usage(String s) {
        p("Didn't understand \"" + s + "\".");
        p("Valid arguments are:");
        p("-version 100|110|120|130|140|150|160|170");
        System.exit(1);
    }

    /**
     * Print a help message.
     * <p>
     * This method is defined as a JavaScript function.
     */
    public void help() {
        p("");
        p("Command                Description");
        p("=======                ===========");
        p("help()                 Display usage and help messages. ");
        p("defineClass(className) Define an extension using the Java class");
        p("                       named with the string argument. ");
        p("                       Uses ScriptableObject.defineClass(). ");
        p("load(['foo.js', ...])  Load JavaScript source files named by ");
        p("                       string arguments. ");
        p("loadClass(className)   Load a class named by a string argument.");
        p("                       The class must be a script compiled to a");
        p("                       class file. ");
        p("print([expr ...])      Evaluate and print expressions. ");
        p("quit()                 Quit the BasicRhinoShell. ");
        p("version([number])      Get or set the JavaScript version number.");
        p("");
    }

    /**
     * Print the string values of its arguments.
     * <p>
     * This method is defined as a JavaScript function. Note that its arguments are of the "varargs" form, which allows
     * it to handle an arbitrary number of arguments supplied to the JavaScript function.
     *
     * @param cx
     *            the cx
     * @param thisObj
     *            the this obj
     * @param args
     *            the args
     * @param funObj
     *            the fun obj
     */
    public static void print(Context cx, Scriptable thisObj, Object[] args, Function funObj) {
        for (int i = 0; i < args.length; i++) {
            if (i > 0) {
                logger.info("");
            }

            // Convert the arbitrary JavaScript value into a string form.
            String s = Context.toString(args[i]);

            logger.info(s);
        }
        logger.info("");
    }

    /**
     * Quit the BasicRhinoShell.
     * <p>
     * This only affects the interactive mode.
     * <p>
     * This method is defined as a JavaScript function.
     */
    public void quit() {
        quitting = true;
    }

    /**
     * Warn.
     *
     * @param cx
     *            the cx
     * @param thisObj
     *            the this obj
     * @param args
     *            the args
     * @param funObj
     *            the fun obj
     */
    public static void warn(Context cx, Scriptable thisObj, Object[] args, Function funObj) {
        String message = Context.toString(args[0]);
        int line = (int) Context.toNumber(args[1]);
        String source = Context.toString(args[2]);
        int column = (int) Context.toNumber(args[3]);
        cx.getErrorReporter().warning(message, null, line, source, column);
    }

    /**
     * This method is defined as a JavaScript function.
     *
     * @param path
     *            the path
     *
     * @return the string
     */
    public String readFile(String path) {
        try {
            return new String(Files.readAllBytes(Path.of(path)), StandardCharsets.UTF_8);
        } catch (RuntimeException exc) {
            throw exc;
        } catch (IOException exc) {
            throw new RuntimeException("wrap: " + exc.getMessage(), exc);
        }
    }

    /**
     * Get and set the language version.
     * <p>
     * This method is defined as a JavaScript function.
     *
     * @param cx
     *            the cx
     * @param thisObj
     *            the this obj
     * @param args
     *            the args
     * @param funObj
     *            the fun obj
     *
     * @return the double
     */
    public static double version(Context cx, Scriptable thisObj, Object[] args, Function funObj) {
        double result = cx.getLanguageVersion();
        if (args.length > 0) {
            double d = Context.toNumber(args[0]);
            cx.setLanguageVersion((int) d);
        }
        return result;
    }

    /**
     * Load and execute a set of JavaScript source files.
     * <p>
     * This method is defined as a JavaScript function.
     *
     * @param cx
     *            the cx
     * @param thisObj
     *            the this obj
     * @param args
     *            the args
     * @param funObj
     *            the fun obj
     */
    public static void load(Context cx, Scriptable thisObj, Object[] args, Function funObj) {
        BasicRhinoShell basicRhinoShell = (BasicRhinoShell) getTopLevelScope(thisObj);
        for (Object element : args) {
            basicRhinoShell.processSource(cx, Context.toString(element));
        }
    }

    /**
     * Evaluate JavaScript source.
     *
     * @param cx
     *            the current context
     * @param filename
     *            the name of the file to compile, or null for interactive mode.
     */
    private void processSource(Context cx, String filename) {
        if (filename == null) {
            BufferedReader in = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
            String sourceName = "<stdin>";
            int lineno = 1;
            boolean hitEOF = false;
            do {
                int startline = lineno;
                logger.info("js> ");
                try {
                    StringBuilder source = new StringBuilder();
                    // Collect lines of source to compile.
                    while (true) {
                        String newline = in.readLine();
                        if (newline == null) {
                            hitEOF = true;
                            break;
                        }
                        source.append(newline).append("\n");
                        lineno++;
                        // Continue collecting as long as more lines are needed to complete the current statement.
                        // stringIsCompilableUnit is also true if the source statement will result in any error other
                        // than one that might be resolved by appending more source.
                        if (cx.stringIsCompilableUnit(source.toString())) {
                            break;
                        }
                    }
                    Object result = cx.evaluateString(this, source.toString(), sourceName, startline, null);
                    if (result != Context.getUndefinedValue() && logger.isInfoEnabled()) {
                        logger.info("{}", Context.toString(result));
                    }
                } catch (WrappedException e) {
                    // Some form of exception was caught by JavaScript and propagated up.
                    logger.info(e.getWrappedException().toString());
                    logger.error("", e);
                } catch (EvaluatorException | JavaScriptException e) {
                    // Some form of JavaScript error.
                    logger.info("js: {}", e.getMessage());
                } catch (IOException e) {
                    logger.info(e.toString());
                }
                if (quitting) {
                    // The user executed the quit() function.
                    break;
                }
            } while (!hitEOF);
            logger.info("");
        } else {
            try (BufferedReader in = Files.newBufferedReader(Path.of(filename), StandardCharsets.UTF_8)) {
                // Here we evaluate the entire contents of the file as a script. Text is printed only if the
                // print() function is called.
                cx.evaluateReader(this, in, filename, 1, null);
            } catch (WrappedException e) {
                logger.info(e.getWrappedException().toString());
                logger.error("", e);
            } catch (EvaluatorException | JavaScriptException e) {
                logger.info("js: {}", e.getMessage());
            } catch (IOException e) {
                logger.error("", e);
            }
        }
    }

    /**
     * P.
     *
     * @param s
     *            the s
     */
    private static void p(String s) {
        logger.info(s);
    }

}