// Copyright (c) 2014 Tom Zhou<iwebpp@gmail.com> package com.iwebpp.node.js.rhino; import org.mozilla.javascript.Context; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import com.iwebpp.SimpleDebug; import com.iwebpp.node.NodeContext; import com.iwebpp.node.js.JS; import com.iwebpp.nodeandroid.Toaster; /* * @description * NodeJS host env implementation with Rhino, * https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino/Embedding_tutorial * Notes: * the internal nodejs module has been imported in JS standard jsscope, * just use it, like http, httpp, TCP, UDT, Dns, Url, Readable2, Writable2, etc * * */ public abstract class Host extends SimpleDebug implements JS { private final static String TAG = "RhinoHost"; private final NodeContext nodectx; // node.js native context private Context jsctx; // js context private ScriptableObject jsscope; private boolean onceRun; // have-only run once public Host() { nodectx = new NodeContext(); } @Override public NodeContext getNodeContext() { return nodectx; } @Override /* * @description * NodeJS like require * */ public Object require(String module) throws Exception { Object ret = Scriptable.NOT_FOUND; info(TAG, "require path: "+module); // Retrieve script source locally by file path, or remotely by URL // TBD... String modulesrc = "exports.modulepath=" + "'" + module + "';"; debug(TAG, "module source: "+modulesrc); // Entering Module Context Context subctx = Context.enter(); // Turn off optimization to make Rhino Android compatible // not use dex, just java bytecode subctx.setOptimizationLevel(-1); // Initializing standard objects ScriptableObject subscope = subctx.initStandardObjects(); try { // Expose host env in js as NodeHostEnv alias as NHE ScriptableObject.putProperty(subscope, "NodeHostEnv", Context.javaToJS(this, subscope)); // Expose node-android context in js as NodeCurrentContext alias as NCC ScriptableObject.putProperty(subscope, "NodeCurrentContext", Context.javaToJS(nodectx, subscope)); // Create exports variable String exports = "var exports = {};"; subctx.evaluateString(subscope, exports, "ExportsVariable", 1, null); // Create require function // - return built-in module directly with NodeCurrentContext injection // - ask framework for local/remote Js module, TBD ... security/permission check String require = "var require = function(module){" + " if (module.toLowerCase() === 'http') return {" + " createServer: function(requestListener) {" + " return http.createServer(NCC, requestListener);" + " }," + "" + " request: function(options, responseListener) {" + " // parse ReqOptions" + " ReqOptions reqopt = new ReqOptions();" + " return http.request(NCC, reqopt, responseListener);" + " }," + "" + " request: function(url, responseListener) {" + " return http.request(NCC, url, responseListener);" + " }," + "" + " get: function(options, responseListener) {" + " // parse ReqOptions" + " ReqOptions reqopt = new ReqOptions();" + " return http.get(NCC, reqopt, responseListener);" + " }," + "" + " get: function(url, responseListener) {" + " return http.get(NCC, url, responseListener);" + " }," + " };" + "" + " if (module.toLowerCase() === 'httpp') return {" + " createServer: function(requestListener) {" + " return httpp.createServer(NCC, requestListener);" + " }," + "" + " request: function(options, responseListener) {" + " // parse ReqOptions" + " ReqOptions reqopt = new ReqOptions();" + " return httpp.request(NCC, reqopt, responseListener);" + " }," + "" + " request: function(url, responseListener) {" + " return httpp.request(NCC, url, responseListener);" + " }," + "" + " get: function(options, responseListener) {" + " // parse ReqOptions" + " ReqOptions reqopt = new ReqOptions();" + " return http.get(NCC, reqopt, responseListener);" + " }," + "" + " get: function(url, responseListener) {" + " return httpp.get(NCC, url, responseListener);" + " }," + " };" + "" + " if (module.toLowerCase() === 'websocket') return Websocket;" + " if (module.toLowerCase() === 'websocketserver') return WebSocketServer;" + "" + " if (module.toLowerCase() === 'net') return TCP;" + " if (module.toLowerCase() === 'tcp') return TCP;" + " if (module.toLowerCase() === 'udt') return UDT;" + "" + " if (module.toLowerCase() === 'readable') return Readable2;" + " if (module.toLowerCase() === 'writable') return Writable2;" + " if (module.toLowerCase() === 'duplex') return Duplex;" + " if (module.toLowerCase() === 'transform') return Transform;" + " if (module.toLowerCase() === 'passthrough') return PassThrough;" + "" + " if (module.toLowerCase() === 'dns') return Dns;" + " if (module.toLowerCase() === 'url') return Url;" + "" + " return NodeHostEnv.require(module);" + "};"; subctx.evaluateString(subscope, require, "RequireFunction", 1, null); // Expose node-android API in js String nodejs = "var NodeJS = new JavaImporter(" + "com.iwebpp.node.EventEmitter2," + "com.iwebpp.node.Dns," + "com.iwebpp.node.Url," + "com.iwebpp.node.http," + "com.iwebpp.node.net," + "com.iwebpp.node.stream," + "com.iwebpp.wspp.WebSocket," + "com.iwebpp.wspp.WebSocketServer," + "android.util.Log" + ");"; subctx.evaluateString(subscope, nodejs, "NodeJSAPI", 1, null); // Evaluating module script source in one line String modulescript = ("with(NodeJS){(function(){var NCC=NodeCurrentContext;" + modulesrc + "})();}").replace("[\r\n]+", ""); ///DebugLevel lvl = getDebugLevel(); ///setDebugLevel(DebugLevel.INFO); info(TAG, "module script: \n\n"+modulescript+"\n\n"); ///setDebugLevel(lvl); subctx.evaluateString(subscope, modulescript, "ModuleContent", 1, null); // Retrieve exports variable ret = subscope.get("exports", subscope); } catch (Throwable e) { ///e.printStackTrace(); error(TAG, e.toString()); throw new Exception("Rhino require exception: "+e.toString()); } finally { Context.exit(); } return ret; } @Override /* * @description * NodeJS like require, TBD... * */ public void require(String module, RequireCallback cb) throws Exception { Object ret = Scriptable.NOT_FOUND; info(TAG, "require path: "+module); cb.onResponse(ret); } @Override /* * @description * refer to https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino/Embedding_tutorial#runScript * */ public void execute() throws Exception { // Check if run once if (onceRun) return; else onceRun = true; // Entering a Context jsctx = Context.enter(); // Turn off optimization to make Rhino Android compatible jsctx.setOptimizationLevel(-1); try { // Initializing standard objects jsscope = jsctx.initStandardObjects(); // Add Toaster jsscope.defineProperty("toast", Toaster.getInstance(), ScriptableObject.DONTENUM); // Expose host env in js as NodeHostEnv alias as NHE ScriptableObject.putProperty(jsscope, "NodeHostEnv", Context.javaToJS(this, jsscope)); // Expose node-android context in js as NodeCurrentContext alias as NCC ScriptableObject.putProperty(jsscope, "NodeCurrentContext", Context.javaToJS(nodectx, jsscope)); // Create exports variable String exports = "var exports = {};"; jsctx.evaluateString(jsscope, exports, "ExportsVariable", 1, null); // Create require function String require = "var require = function(module){" + " if (module.toLowerCase() === 'http') return http;" + " if (module.toLowerCase() === 'httpp') return httpp;" + "" + " if (module.toLowerCase() === 'websocket') return Websocket;" + " if (module.toLowerCase() === 'websocketserver') return WebSocketServer;" + "" + " if (module.toLowerCase() === 'net') return TCP;" + " if (module.toLowerCase() === 'tcp') return TCP;" + " if (module.toLowerCase() === 'udt') return UDT;" + "" + " if (module.toLowerCase() === 'readable') return Readable2;" + " if (module.toLowerCase() === 'writable') return Writable2;" + " if (module.toLowerCase() === 'duplex') return Duplex;" + " if (module.toLowerCase() === 'transform') return Transform;" + " if (module.toLowerCase() === 'passthrough') return PassThrough;" + "" + " if (module.toLowerCase() === 'dns') return Dns;" + " if (module.toLowerCase() === 'url') return Url;" + "" + " return NodeHostEnv.require(module);" + "};"; jsctx.evaluateString(jsscope, require, "RequireFunction", 1, null); // Expose node-android API in js String nodejs = "var NodeJS = new JavaImporter(" + "com.iwebpp.node.EventEmitter2," + "com.iwebpp.node.Dns," + "com.iwebpp.node.Url," + "com.iwebpp.node.http," + "com.iwebpp.node.net," + "com.iwebpp.node.stream," + "com.iwebpp.wspp.WebSocket," + "com.iwebpp.wspp.WebSocketServer," + "android.util.Log" + ");"; jsctx.evaluateString(jsscope, nodejs, "NodeJSAPI", 1, null); // Evaluating user authored script in one line String userscript = ("with(NodeJS){(function(){var NCC=NodeCurrentContext;" + content() + "})();}").replace("[\r\n]+", ""); ///DebugLevel lvl = getDebugLevel(); ///setDebugLevel(DebugLevel.INFO); info(TAG, "user script: \n\n"+userscript+"\n\n"); ///setDebugLevel(lvl); jsctx.evaluateString(jsscope, userscript, "UserContent", 1, null); // Run node-android loop nodectx.execute(); } catch (Throwable e) { ///e.printStackTrace(); error(TAG, e.toString()); throw new Exception("Rhino runtime exception: "+e.toString()); } finally { Context.exit(); } } }