Source: ptolemy/actor/lib/jjs/nashornHost.js

// JavaScript functions for a Ptolemy II (Nashorn) accessor host.
//
// Copyright (c) 2016-2018 The Regents of the University of California.
// All rights reserved.
//
// Permission is hereby granted, without written agreement and without
// license or royalty fees, to use, copy, modify, and distribute this
// software and its documentation for any purpose, provided that the above
// copyright notice and the following two paragraphs appear in all copies
// of this software.
//
// IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
//
// THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
// PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
// CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
// ENHANCEMENTS, OR MODIFICATIONS.
//

/** JavaScript functions for the Nashorn host, which uses
 *  Java's Nashorn JavaScript engine.
 *  This host supports version 1 accessors.
 *
 *  This host is almost entirely independent of Ptolemy II,
 *  except for some utility functions in
 *  FileUtilities (which could easily be factored out).
 *  Also, modules that are required by accessors are loaded
 *  from $PTII/ptolemy/actor/lib/jjs, and some of those modules
 *  may have dependencies on Ptolemy II.
 *
 *  To invoke this, the accessors repository has a script in
 *  accessors/web/hosts/nashorn called nashornAccessorHost.
 *  Execute that script with command-line arguments (e.g. a composite
 *  accessor to instantiate and initialize).
 *
 *  @module nashornHost
 *  @author Edward A. Lee, Contributor: Christopher Brooks
 *  @version $$Id$$
 *  @since Ptolemy II 11.0
 */

// Stop extra messages from jslint.  Note that there should be no
// space between the / and the * and global.
/*globals Java, actor, getAccessorCode, load, process, print */
/*jshint globalstrict: true */
/*jslint nomen: true */
"use strict";

////////////////////////////////////////////////////////////////////////////
//// Java dependencies.

// Java classes that define some static functions to call from JS.
var FileUtilities = Java.type('ptolemy.util.FileUtilities');
var NashornAccessorHostApplication = Java.type('ptolemy.actor.lib.jjs.NashornAccessorHostApplication');
var System = Java.type('java.lang.System');

////////////////////////////////////////////////////////////////////////////
//// Global variables.

// Flag that will cause debug output to the console if set to true.
var debug = false;

////////////////////////////////////////////////////////////////////////////
//// Variables supporting module loading and accessor loading.

var __moduleFile = FileUtilities.nameToFile(
    '$CLASSPATH/ptolemy/actor/lib/jjs/',
    null
);

/** A string giving the full path to the root directory for installed modules. */
var _moduleRoot = __moduleFile.getAbsolutePath();

// Check to see if _moduleFile is a Jar URL like.  Windows: check !\\.
if (_moduleRoot.indexOf("!/") !== -1 || _moduleRoot.indexOf("!\\") !== -1) {
    _moduleRoot = "jar:" + __moduleFile.toString();
}

var __accessorFile = FileUtilities.nameToFile(
    '$CLASSPATH/org/terraswarm/accessor/accessors/web/',
    null
);

var _accessorRoot = __accessorFile.getAbsolutePath();

// Check to see if _accessorRoot is a Jar URL like. Windows: check !\\.
if (_accessorRoot.indexOf("!/") !== -1 || _accessorRoot.indexOf("!\\") !== -1) {
    _accessorRoot = "jar:" + __accessorFile.toString();
}

/** An array that gives the search path for modules to be required. */
var _modulePath = [_moduleRoot + '/',
    _moduleRoot + '/modules/',
    _moduleRoot + '/node/',
    _moduleRoot + '/node_modules/',
    _accessorRoot + '/hosts/',
    _accessorRoot + '/',
    _accessorRoot + '/hosts/common/modules/'
];

/** An array that gives the search path for modules to be required relative to the classpath. */
var _moduleClasspath = ['$CLASSPATH/ptolemy/actor/lib/jjs/',
    '$CLASSPATH/ptolemy/actor/lib/jjs/modules/',
    '$CLASSPATH/ptolemy/actor/lib/jjs/node/',
    '$CLASSPATH/ptolemy/actor/lib/jjs/node_modules/',
    '$CLASSPATH/org/terraswarm/accessor/accessors/web/hosts/',
    '$CLASSPATH/org/terraswarm/accessor/accessors/web/'
];

/** A string giving the full path to the root directory for installed accessors. */
var _accessorRoot = FileUtilities.nameToFile(
    '$CLASSPATH/org/terraswarm/accessor/accessors/web/',
    null
).getAbsolutePath();

/** A string giving the full path to the root directory for test accessors. */
var _testAccessors = FileUtilities.nameToFile(
    '$CLASSPATH/org/terraswarm/accessor/test/auto/accessors/',
    null
).getAbsolutePath();


/** An array that gives the search path for accessors to be extended. */
var _accessorPath = [_accessorRoot + '/', _testAccessors + '/'].concat(_modulePath);

/** An array that gives the search path for accessors to be extended. */
var _accessorClasspath = ['$CLASSPATH/org/terraswarm/accessor/test/auto/accessors/'].concat(_moduleClasspath);

////////////////////////////////////////////////////////////////////////////
//// Function definitions.

/** Print a message to the console.
 *  NOTE: This function is not required by the accessor specification, so accessors
 *  should not rely on it being present.
 *  @param message The message
 */
function alert(message) {
    console.log(message);
}

/** Get a resource, which may be a relative file name or a URL, and return the
 *  value of the resource as a string.
 *
 *  Implementations of this function may restrict the locations from which
 *  resources can be retrieved. This implementation restricts relative file
 *  names to be in the same directory where the swarmlet model is located or
 *  in a subdirectory, or if the resource begins with "$CLASSPATH/", to the
 *  classpath of the current Java process.
 *
 *  If the accessor is not restricted, the $KEYSTORE is resolved to
 *  $HOME/.ptKeystore.
 *
 *  The options parameter may have the following values:
 *  * If the type of the options parameter is a Number, then it is assumed
 *    to be the timeout in milliseconds.
 *  * If the type of the options parameter is a String, then it is assumed
 *    to be the encoding, for example "UTF-8".  If the value is "Raw" or "raw"
 *    then the data is returned as an unsigned array of bytes.
 *    The default encoding is the default encoding of the system.
 *    In CapeCode, the default encoding is returned by Charset.defaultCharset().
 *  * If the type of the options parameter is an Object, then it may
 *    have the following fields:
 *  ** encoding {string} The encoding of the file, see above for values.
 *  ** timeout {number} The timeout in milliseconds.
 *
 *  If the callback parameter is not present, then getResource() will
 *  be synchronous read like Node.js's
 *  {@link https://nodejs.org/api/fs.html#fs_fs_readfilesync_path_options|fs.readFileSync()}.
 *  If the callback argument is present, then getResource() will be asynchronous like
 *  {@link https://nodejs.org/api/fs.html#fs_fs_readfile_path_options_callback|fs.readFile()}.
 *
 *  @param path {string} The URI or path to the resource
 *  @param options The options for reading the resource
 *  @param callback The callback function.  The first argument is the error,
 *  if any, the second argument is the data, if any.
 */
function getResource(path, options, callback) {
    return actor.getResource(path, options, callback);
}

/** Clear an interval timer with the specified handle.
 *  @param handle The handle.
 *  @see setInterval().
 */
function clearInterval(handle) {
    if (typeof actor === 'undefined') {
        throw new Error('clearInterval(): No actor variable defined.');
    }
    actor.clearTimeout(handle);
}

/** Clear a timeout with the specified handle.
 *  @param handle The handle.
 *  @see setTimeout().
 */
function clearTimeout(handle) {
    if (typeof actor === 'undefined') {
        throw new Error('clearTimeout(): No actor variable defined.');
    }
    actor.clearTimeout(handle);
}

/** Return the current time as a number (in seconds).
 *  @return The current time.
 */
function currentTime() {
    if (typeof actor === 'undefined') {
        throw new Error('currentTime(): No actor variable defined.');
    }
    return actor.currentTime();
}

/** Report an error by printing using console.error().
 *  @param message The message for the exception.
 */
function error(message) {
    // Print a stack trace to the console.
    /*
    console.error(message);
    console.error('------------------------- error stack trace:');
    var e = new Error('dummy');
    var stack = e.stack.replace(/^[^\(]+?[\n$]/gm, '')
            .replace(/^\s+at\s+/gm, '')
            .replace(/^Object.<anonymous>\s*\(/gm, '{anonymous}()@')
            .split('\n');
    console.error(stack);
    console.error('-------------------------');
    */

    console.error(message);
}

/**
 * Require the named module. This function imports modules formatted
 * according to the CommonJS standard.
 *
 * <p>If the name begins with './' or '/', then it is assumed to
 * specify a file or directory on the local disk. If it is a file, the
 * '.js' suffix may be optionally omitted. If it is a directory, then
 * this function will look for a package.json file in that directory
 * and load the file specified by the 'main' property the JSON object
 * defined in that file. If there is no package.json file, then it
 * will load an 'index.js' file, if there is one.</p>
 *
 * <p>If the name does not begin with './' or '/', then it is assumed
 * to specify a module installed in this accessor host.</p>
 *
 * <p>In both cases, this function returns an object that includes as
 * properties any properties that have been added to the 'exports'
 * property. For example, to export a function, the module JavaScript
 * file could define the function as follows:</p>
 *
 * <pre>
 *   exports.myFunction = function() {...};
 * </pre>
 *
 * <p>Alternatively, the module JavaScript file can explicitly define
 * the exports object as follows:<p>
 *
 * <pre>
 *   var myFunction = function() {...};
 *   module.exports = {
 *       myFunction : myFunction
 *   };
 * </pre>
 *
 * <p>This implementation uses the requires() function implemented by Walter Higgins,
 * found here: <a href="https://github.com/walterhiggins/commonjs-modules-javax-script.">https://github.com/walterhiggins/commonjs-modules-javax-script</a>.</p>
 *
 * @see http://nodejs.org/api/modules.html#modules_the_module_object
 * @see also: http://wiki.commonjs.org/wiki/Modules
 */
var require = null;
try {
    require = load(_moduleRoot + '/external/require.js')(
        // Invoke the function returned by 'load' immediately with the following arguments.
        //    - a root directory in which to look for modules.
        //    - an array of paths in which to look for modules.
        //    - an optional hook object that includes two callback functions for notification.
        _moduleRoot,
        _modulePath
    );
} catch (err) {
    // We could be under Windows, try using Nashorn's load() "classpath:" extension.
    // See http://stackoverflow.com/questions/28221006/is-it-possible-to-have-nashorn-load-scripts-from-classpath,
    // See https://wiki.openjdk.java.net/display/Nashorn/Nashorn+extensions
    require = load("classpath:ptolemy/actor/lib/jjs/external/require.js")(_moduleRoot, _modulePath);
}

/**
 * Require the named accessor. This is a version of require() that looks
 * in a different place for accessors.
 * @see #require()
 */
/* FIXME: requireAccessor is not used anywhere.
var requireAccessor = load(_moduleRoot + '/external/require.js')(
    // Invoke the function returned by 'load' immediately with the following arguments.
    //    - a root directory in which to look for accessors.
    //    - an array of paths in which to look for accessors.
    //    - an optional hook object that includes two callback functions for notification.
    _accessorRoot,
    _accessorPath
);
*/

////////////////////
// Pull in the util and console modules, now that require is defined.
// NOTE: If require() is defined using "function require" rather than
// "var require = function", then this could be done at the top of the file,
// because the function keyword binds function definitions in this script
// before evaluating the script. However, then loading a subsequent script
// like capeCodeHost.js will not override the definition of require in any
// of its uses here. So we stick with this style.

var util = require('util');
var console = require('console');

// Locally defined modules.
var commonHost = require('commonHost.js');

// This Nashorn allows trusted accessors, which means that any
// accessor whose class name begins with 'trusted/' can invoke the
// function getTopLevelAccessors().
commonHost.allowTrustedAccessors(true);
////////////////////

/** Return the source code for an accessor from its fully qualified name.
 *  This will throw an exception if there is no such accessor on the accessor
 *  search path.
 *  @param name Fully qualified accessor name, e.g. 'net/REST'.
 */
function getAccessorCode(name) {
    var code,
        i,
        location;
    // Append a '.js' to the name, if needed.
    if (name.indexOf('.js') !== name.length - 3) {
        name += '.js';
    }

    // Handle absolute pathnames.
    if (name[0] === '/' || name[0] === '\\') {
        code = FileUtilities.getFileAsString(name);
        return code;
    } else {
        try {
            // Handle URLs and pathnames relative the current model directory.
            code = getResource(name);
            return code;
        } catch(e) {
            // console.log(e.toString());
            // Ignore and continue.
        }
    }

    // _accessorPath is defined in basicFunctions.js.
    for (i = 0; i < _accessorPath.length; i++) {
        location = _accessorPath[i].concat(name);
        try {
            code = FileUtilities.getFileAsString(location);
            break;
        } catch (err) {
            continue;
        }
    }
    if (!code) {
        for (i = 0; i < _accessorClasspath.length; i++) {
            location = _accessorClasspath[i].concat(name);
            try {
                code = FileUtilities.getFileAsString(location);
                break;
            } catch (err) {
                continue;
            }
        }
    }
    if (!code) {
        throw ('Accessor ' + name + ' not found on path: ' + _accessorPath + ' or relative path: ' + _accessorClasspath);
    }
    return code;
}

/** Return the name of this host.
 *
 *  Return the string "Nashorn".
 *
 *  @return In nashornHost.js, return "Nashorn".
 */ 
function getHostName() {
    return "Nashorn";
};

/** Instantiate and return an accessor. If there is no 'actor' variable in scope,
 *  then this method assumes there is nothing in charge of execution of this accessor
 *  and therefore creates an orchestrator for it and starts an event loop.
 *  This will throw an exception if there is no such accessor class on the accessor
 *  search path.
 *  @param accessorName The name to give to the instance.
 *  @param accessorClass Fully qualified accessor class name, e.g. 'net/REST'.
 */
function instantiate(accessorName, accessorClass) {

    // NOTE: The definition of the require var in this file may be overridden if
    // capeCodeHost.js is evaluated after this file is evaluated.
    var bindings = {
        'require': require,
    };

    // If the variable actor does not exist, then create an orchestrator
    // to provide an event loop for executing this accessor.
    var orchestrator = null;
    if (typeof actor === 'undefined') {
        orchestrator = NashornAccessorHostApplication.createOrchestrator(accessorName);
        bindings.actor = orchestrator;
    }

    var instance = new commonHost.instantiateAccessor(
        accessorName, accessorClass, getAccessorCode, bindings);
    console.log('Instantiated accessor ' + accessorName + ' with class ' + accessorClass);

    if (orchestrator) {
        console.log('Starting event loop for ' + accessorName);
        // Make it so that 'this.actor' refers to the orchestrator.
        instance.actor = orchestrator;
        // The following will start a thread to handle the event loop for this accessor.
        orchestrator.setTopLevelAccessor(instance);
    }

    return instance;
}

/** Instantiate and return a top-level accessor.
 *  For now, this is the same as instantiate().
 *  @param accessorName The name to give to the instance.
 *  @param accessorClass Fully qualified accessor class name, e.g. 'net/REST'.
 */
function instantiateTopLevel(accessorName, accessorClass) {
    return instantiate(accessorName, accessorClass);
}

/** Evaluate command-line arguments by first converting the arguments
 *  from a Java array to a JavaScript array, and then invoking main()
 *  in commonHost.js.
 *  @param argv Command-line arguments.
 *  @return True if any standalone accessors with active event loops
 *   were instantiated.
 */
function processCommandLineArguments(argv) {
    // nodeHost has a similar method.

    var result = commonHost.processCommandLineArguments(
        // Command-line arguments.
        // Java.from is Nashorn-specific
        Java.from(argv),
        // Function to read a file and return a string.
        FileUtilities.getFileAsString,
        // Function to instantiate accessors with their own event loop.
        instantiateTopLevel,
        // Function terminate to call upon termination.
        function () {
            // Do let failure to stop accessors block exiting.
            try {
                commonHost.stopAllAccessors();
            } catch (e) {
                console.error("Failed to stop accessors: " + e);
            }
            // Ptolemy defines a process module that defines exit()
            // that invokes ptolemy.util.StringUtilities.exit(), which
            // checks environment variables before possibly exiting.
            var process = require('process');
            process.exit(0);
        }
    );
    if (!result) {
        // No accessors were initialized and the keepalive argument
        // was not given, so there is presumably no more to do.
        print('No standalone accessors were instantiated');
        //process.exit(0);
    }
}

/**
 * Set a timeout to call the specified function after the specified
 * time and repeatedly at multiples of that time.
 *
 * <p> Return a handle to use in clearInterval(). If there are
 * additional arguments beyond the first two, then those arguments
 * will be passed to the function when it is invoked. This
 * implementation uses fireAt() of the director in charge of the host
 * JavaScript actor in Ptolemy II. Hence, actors that use this should
 * be used with a director that respects fireAt(), such as DE.  If the
 * director has synchronizeToRealTime set to true, then it will
 * approximate real-time behavior reasonably closely. Otherwise, the
 * timeout will only be simulated. Either way, the timing is much more
 * precise and well-defined than usual for JavaScript environments. If
 * two actors specify the same timeout time in, say, their
 * initialize() function, then they will be invoked at the same model
 * time, and their outputs will be simultaneous. Any downstream actor
 * will see them simultaneously.</p>
 *
 * <p>Note with this implementation, it is not necessary to
 * call clearInterval() in the actor's wrapup() function.
 * Nevertheless, it is a good idea to do that in an accessor
 * since other accessor hosts may not work the same way.
 *
 * @param func The callback function.
 * @param milliseconds The interval in milliseconds.
 */
function setInterval(func, milliseconds) {
    if (typeof actor === 'undefined') {
        throw new Error('setInterval(): No actor variable defined.');
    }
    var callback = func,
        // If there are arguments to the callback, create a new function.
        // Get an array of arguments excluding the first two.
        tail = Array.prototype.slice.call(arguments, 2),
        id;
    if (tail.length !== 0) {
        callback = function () {
            func.apply(this, tail);
        };
    }
    id = actor.setInterval(callback, milliseconds);
    return id;
}

/**
 * Set a timeout to call the specified function after the specified time.
 * Return a handle to use in clearTimeout(). If there are
 * additional arguments beyond the first two, then those arguments
 * will be passed to the function when it is invoked. This
 * implementation uses fireAt() of the director in charge of the host
 * JavaScript actor in Ptolemy II. Hence, actors that use this should
 * be used with a director that respects fireAt(), such as DE.  If the
 * director has synchronizeToRealTime set to true, then it will
 * approximate real-time behavior reasonably closely. Otherwise, the
 * timeout will only be simulated. Either way, the timing is much more
 * precise and well-defined than usual for JavaScript environments. If
 * two actors specify the same timeout time in, say, their
 * initialize() function, then they will be invoked at the same model
 * time, and their outputs will be simultaneous. Any downstream actor
 * will see them simultaneously.
 *
 * Note with this implementation, it is not necessary to
 * call clearTimeout() in the actor's wrapup() function.
 * @param func The callback function.
 * @param milliseconds The interval in milliseconds.
 */
function setTimeout(func, milliseconds) {
    // console.log("+++++ after " + milliseconds + " invoke " + func);
    if (typeof actor === 'undefined') {
        throw new Error('setTimeout(): No actor variable defined.');
    }
    var callback = func,
        // If there are arguments to the callback, create a new function.
        // Get an array of arguments excluding the first two.
        tail = Array.prototype.slice.call(arguments, 2),
        id;
    if (tail.length !== 0) {
        callback = function () {
            func.apply(this, tail);
        };
    }

    id = actor.setTimeout(callback, milliseconds);
    return id;
}

///////////////////////////////////////////////////////////////////////
// Make commonHost functions visible when this file is evaluated directly.

var Accessor = commonHost.Accessor;
var getTopLevelAccessors = commonHost.getTopLevelAccessors;
var stopAllAccessors = commonHost.stopAllAccessors;
var uniqueName = commonHost.uniqueName;
var isReifiableBy = commonHost.isReifiableBy;

// FIXME: Handle exit calls like how we do in nodeHost?