net.orzo.scripting.JsEngineAdapter.java Source code

Java tutorial

Introduction

Here is the source code for net.orzo.scripting.JsEngineAdapter.java

Source

/*
 * Copyright (c) 2013 Tomas Machalek
 *
 * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
 *
 * 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.
 */
package net.orzo.scripting;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.function.Function;

import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;

import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
import jdk.nashorn.api.scripting.ScriptObjectMirror;
import net.orzo.IntermediateResults;
import net.orzo.SharedServices;
import net.orzo.lib.Lib;

import com.google.common.base.Joiner;

/**
 * Javascript engine wrapper to serve within the application. Please note that
 * it is not thread-safe (i.e. each worker must have its JsEngineAdapter
 * instance).
 * 
 * 
 * @author Tomas Machalek <tomas.machalek@gmail.com>
 */
@SuppressWarnings("restriction")
public class JsEngineAdapter {

    /**
     *
     */
    private Lib system;

    /**
     *
     */
    private ScriptEngine engine;

    /**
     *
     */
    private ScriptContext context;

    /**
     * Main scope. Please note that some other operations (e.g. module loader)
     * may create additional "local" scopes.
     */
    private Bindings scope;

    /**
     *
     */
    private final EnvParams envParams;

    /**
     *
     */
    private IntermediateResults intermediateResults;

    /**
     *
     */
    private final Map<String, Object> modules;

    private static final String MODULE_SEPARATOR = "/";

    private final SharedServices sharedServices;

    /**
     */
    public JsEngineAdapter(EnvParams envParams, SharedServices sharedServices,
            IntermediateResults intermediateResults) {
        this.envParams = envParams;
        this.sharedServices = sharedServices;
        this.intermediateResults = intermediateResults;
        this.modules = new HashMap<>();
        this.engine = new NashornScriptEngineFactory() // TODO configurable timezone
                .getScriptEngine(new String[] { "-timezone=" + getCurrentTimezone().getID() });
    }

    /**
     *
     */
    public JsEngineAdapter(EnvParams envParams, SharedServices sharedServices) {
        this(envParams, sharedServices, null);
    }

    /**
     *
     */
    public Function<String, Object> require = new Function<String, Object>() {

        @Override
        public Object apply(String moduleId) {
            try {
                File module = findModule(JsEngineAdapter.this.envParams.modulesPaths, moduleId);
                if (module != null) {
                    return loadModule(moduleId, SourceCode.fromFile(module));

                } else {
                    throw new ModuleException(String.format("Module <%s> not found.", moduleId));
                }

            } catch (IOException | ScriptException e) {
                throw new ModuleException(
                        String.format("Failed to load module <%s> with error %s", moduleId, e.getMessage()), e);
            }
        }
    };

    /**
     *
     * @throws ModuleException
     *             if you try to load non-sandboxed module
     */
    public static File findModule(List<String> modulePaths, String moduleId) {
        if (moduleId.startsWith(String.format(".%s", File.separator))
                || moduleId.startsWith(String.format("..%s", File.separator)) || moduleId.startsWith(File.separator)
                || moduleId.startsWith(String.format(".%s", MODULE_SEPARATOR))
                || moduleId.startsWith(String.format("..%s", MODULE_SEPARATOR))
                || moduleId.startsWith(MODULE_SEPARATOR)) {
            throw new ModuleException("Orzo.js supports only sandboxed module loading");
        }
        String[] moduleElms = moduleId.split(MODULE_SEPARATOR);
        String fsCompatibleId = Joiner.on(MODULE_SEPARATOR).join(moduleElms);

        for (String mp : modulePaths) {
            String sysDepPath = mp.replaceAll("/", File.separator);
            File f = new File(String.format("%s%s%s.js", sysDepPath, File.separator, fsCompatibleId));
            if (f.isFile()) {
                return f;
            }
        }
        return null;
    }

    /**
     *
     */
    public void beginWork() {
        beginWork(null);
    }

    private TimeZone getCurrentTimezone() {
        Calendar calendar = new GregorianCalendar();
        return calendar.getTimeZone();
    }

    /**
     *
     */
    public void beginWork(Map<String, Object> globals) {
        this.context = this.engine.getContext();
        this.scope = this.context.getBindings(ScriptContext.ENGINE_SCOPE);
        this.system = new Lib();
        if (globals != null) {
            for (String globalVar : globals.keySet()) {
                this.scope.put(globalVar, globals.get(globalVar));
            }
        }

        this.scope.put("require", this.require);

        // objects with the "_" prefix are not intended to be used directly from
        // within javascript
        this.scope.put("_lib", this.system);
        this.scope.put("_env", this.envParams);
        this.scope.put("_shared", this.sharedServices);
        if (this.intermediateResults != null) {
            this.scope.put("_result", this.intermediateResults);
        }
    }

    /**
     *
     */
    public void endWork() {
    }

    /**
     */
    public Object loadModule(String moduleId, SourceCode code) throws ScriptException, IOException {
        if (!this.modules.containsKey(code.getFullyQualifiedName())) {
            ScriptContext context = new SimpleScriptContext();
            context.setBindings(this.engine.createBindings(), ScriptContext.ENGINE_SCOPE);

            Bindings engineScope = context.getBindings(ScriptContext.ENGINE_SCOPE);
            engineScope.put("require", this.require);
            engineScope.put("doWith", this.scope.get("doWith"));
            engineScope.put("orzo", this.scope.get("orzo"));
            engineScope.put("_moduleId", moduleId);

            SourceCode modEnv = SourceCode.fromResource("net/orzo/modenv.js");
            CompiledScript moduleScript;

            // empty module
            moduleScript = modEnv.compile((Compilable) this.engine);
            moduleScript.eval(context);

            // actual module
            moduleScript = code.compile((Compilable) this.engine);
            this.engine.put(ScriptEngine.FILENAME, code.getName());
            moduleScript.eval(context);

            ScriptObjectMirror moduleObj = (ScriptObjectMirror) engineScope.get("module");
            this.modules.put(code.getFullyQualifiedName(), moduleObj.getMember("exports"));
        }
        return this.modules.get(code.getFullyQualifiedName());
    }

    /**
     * Sets object within the scripting environment
     *
     */
    public void put(String key, Object value) {
        this.scope.put(key, value);
    }

    /**
     * Retrieves a value from the scripting environment
     *
     */
    public Object get(String key) {
        return this.scope.get(key);
    }

    /**
     *
     * @param name function name
     * @param args
     *            arguments of the function
     * @return result of the called function
     */
    public Object runFunction(String name, Object... args) throws NoSuchMethodException, ScriptException {
        Invocable inv = (Invocable) this.engine;
        return inv.invokeFunction(name, args);
    }

    /**
     * Runs specified source code without any implicit imports.
     *
     * @return The return value from the last script execution
     */
    public Object runCode(SourceCode... sourceCodes) throws ScriptException {
        Object ans = null;
        if (this.engine == null || this.context == null || this.scope == null) {
            throw new ScriptConfigurationException("Context and/or scope are not initialized.");
        }
        for (SourceCode code : sourceCodes) {
            this.engine.put(ScriptEngine.FILENAME, code.getName());
            CompiledScript script = code.compile((Compilable) this.engine);
            ans = script.eval();
        }
        return ans;
    }

    public IntermediateResults getIntermediateResults() {
        return this.intermediateResults;
    }
}