/** * Copyright Alex Objelean */ package ro.isdc.wro.extensions.script; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.Validate; import org.mozilla.javascript.Context; import org.mozilla.javascript.RhinoException; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.tools.ToolErrorReporter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ro.isdc.wro.WroRuntimeException; import ro.isdc.wro.extensions.locator.WebjarUriLocator; /** * Used to evaluate javascript on the serverside using rhino javascript engine. Encapsulate and hides all implementation * details used by rhino to evaluate javascript on the serverside. * * @author Alex Objelean */ public final class RhinoScriptBuilder { private static final Logger LOG = LoggerFactory.getLogger(RhinoScriptBuilder.class); private static final String SCRIPT_ENV = "env.rhino.min.js"; private static final String SCRIPT_JSON = "json2.min.js"; private static final String SCRIPT_CYCLE = "cycle.js"; private final ScriptableObject scope; private RhinoScriptBuilder() { this(null); } private RhinoScriptBuilder(final ScriptableObject scope) { this.scope = createContext(scope); } private Context getContext() { initContext(); return Context.getCurrentContext(); } /** * @return the context */ public ScriptableObject getScope() { return this.scope; } /** * Initialize the context. */ private ScriptableObject createContext(final ScriptableObject initialScope) { final Context context = getContext(); context.setOptimizationLevel(-1); // TODO redirect errors from System.err to LOG.error() context.setErrorReporter(new ToolErrorReporter(false)); context.setLanguageVersion(Context.VERSION_1_8); InputStream script = null; final ScriptableObject scriptCommon = (ScriptableObject) context.initStandardObjects(initialScope); try { script = getClass().getResourceAsStream("commons.js"); context.evaluateReader(scriptCommon, new InputStreamReader(script), "commons.js", 1, null); } catch (final IOException e) { throw new RuntimeException("Problem while evaluationg commons script.", e); } finally { IOUtils.closeQuietly(script); } return scriptCommon; } /** * Add a client side environment to the script context (client-side aware). * * @return {@link RhinoScriptBuilder} used to chain evaluation of the scripts. * @throws IOException */ public RhinoScriptBuilder addClientSideEnvironment() { try { final InputStream scriptEnv = getClass().getResourceAsStream(SCRIPT_ENV); evaluateChain(scriptEnv, SCRIPT_ENV); return this; } catch (final IOException e) { throw new RuntimeException("Couldn't initialize env.rhino script", e); } } /** * This method will load JSON utility and aslo a Douglas Crockford's <a * href="https://github.com/douglascrockford/JSON-js/blob/master/cycle.js">utility</a> required for decycling objects * which would fail otherwise when using JSON.stringify. */ public RhinoScriptBuilder addJSON() { try { final InputStream script = new WebjarUriLocator().locate(WebjarUriLocator.createUri("20110223/json2.js")); final InputStream scriptCycle = getClass().getResourceAsStream(SCRIPT_CYCLE); evaluateChain(script, SCRIPT_JSON); evaluateChain(scriptCycle, SCRIPT_CYCLE); return this; } catch (final IOException e) { throw new RuntimeException("Couldn't initialize json2.min.js script", e); } } /** * Evaluates a script and return {@link RhinoScriptBuilder} for a chained script evaluation. * * @param stream {@link InputStream} of the script to evaluate. * @param sourceName the name of the evaluated script. * @return {@link RhinoScriptBuilder} chain with required script evaluated. * @throws IOException if the script couldn't be retrieved. */ public RhinoScriptBuilder evaluateChain(final InputStream stream, final String sourceName) throws IOException { Validate.notNull(stream); try { getContext().evaluateReader(scope, new InputStreamReader(stream), sourceName, 1, null); return this; } catch(final RhinoException e) { if (e instanceof RhinoException) { LOG.error("RhinoException: {}", RhinoUtils.createExceptionMessage(e)); } throw e; } catch (final RuntimeException e) { LOG.error("Exception caught", e); throw e; } finally { stream.close(); } } /** * Makes sure the context is properly initialized. */ private void initContext() { if (Context.getCurrentContext() == null) { Context.enter(); } } /** * Evaluates a script and return {@link RhinoScriptBuilder} for a chained script evaluation. * * @param script the string representation of the script to evaluate. * @param sourceName the name of the evaluated script. * @return evaluated object. * @throws IOException if the script couldn't be retrieved. */ public RhinoScriptBuilder evaluateChain(final String script, final String sourceName) { Validate.notNull(script); getContext().evaluateString(scope, script, sourceName, 1, null); return this; } /** * Evaluates a script from a reader. * * @param reader {@link Reader} of the script to evaluate. * @param sourceName the name of the evaluated script. * @return evaluated object. * @throws IOException if the script couldn't be retrieved. */ public Object evaluate(final Reader reader, final String sourceName) throws IOException { Validate.notNull(reader); try { return evaluate(IOUtils.toString(reader), sourceName); } finally { reader.close(); } } /** * Evaluates a script. * * @param script string representation of the script to evaluate. * @param sourceName the name of the evaluated script. * @return evaluated object. * @throws IOException if the script couldn't be retrieved. */ public Object evaluate(final String script, final String sourceName) { Validate.notNull(script); // make sure we have a context associated with current thread try { return getContext().evaluateString(scope, script, sourceName, 1, null); } catch (final RhinoException e) { final String message = RhinoUtils.createExceptionMessage(e); LOG.error("JavaScriptException occured: {}", message); throw new WroRuntimeException(message); } finally { // Rhino throws an exception when trying to exit twice. Make sure we don't get any exception if (Context.getCurrentContext() != null) { Context.exit(); } } } /** * @return default {@link RhinoScriptBuilder} for script evaluation chaining. */ public static RhinoScriptBuilder newChain() { return new RhinoScriptBuilder(); } public static RhinoScriptBuilder newChain(final ScriptableObject scope) { return new RhinoScriptBuilder(scope); } /** * @return default {@link RhinoScriptBuilder} for script evaluation chaining. */ public static RhinoScriptBuilder newClientSideAwareChain() { return new RhinoScriptBuilder().addClientSideEnvironment(); } }