Source: org/terraswarm/accessor/accessors/web/hosts/cordova/cordovaFiles/cordovaHost.js

// Cordova swarmlet host.
//
// Copyright (c) 2015-2017 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.
//

/** Cordova host implementation.
 *
 *  @module cordovaHost
 *  @author Chadlia Jerad, Edward A. Lee, Marten Lohstroh, Elizabeth Osyk, Victor Nouvellet, Matt Weber
 *  @version $$Id: cordovaHost.js 1365 2017-04-17 00:44:19Z chadlia.jerad $$
 */

//////////////////////////////////////////////////////////////////////////
// Module variables.

/** Full path to the root (www). */
var _rootStr = document.location + "";
var _root = _rootStr.substring(0, (_rootStr.lastIndexOf("/")+1) );
// var _root = document.location + '/..';
//var _root = document.location + '/..';

/** Relative path (from www) to the include directory. */
var _includePath = 'js/include/';

/** Relative path (from www) to the accessors directory. */
var _accessorPath = _includePath + 'accessors/';

/** Relative path (from www) to the common modules directory. */
var _modulesCommonPath = _includePath + 'common/modules/';

/** Relative path (from www) to the local modules directory. */
var _modulesLocalPath = _includePath + 'cordova/modules/';

/** Cached modules. */
var _loadedModules = {};

/** Cached accessors. */
var _loadedAccessors = {};

//////////////////////////////////////////////////////////////////////////

var common = require('common/commonHost');

/**
 * Return true if the module was loaded successfully, false otherwise.
 * If the code was loaded before, do nothing, otherwise, retrieve it.
 * @param {string} id Identifier for the module.
 * @returns {boolean} Whether the module was loaded successfully.
 */
function loadModule(id) {
    if (!_loadedModules.hasOwnProperty(id)) {
        try {
            _loadedModules[id] = getJavaScript(id);
        } catch (e) {
            return false;
        }
    } 
    return true;
}

/**
 * This require function is similar to Node's implementation (module.js).
 * If the given identifier specifies a full path then try to use that path.
 * Otherwise, attempt to load a local module. If that fails, attempt to 
 * load a global module. If that also fails, use the native require function
 * implemented by Cordova.
 * 
 * @param {string} id Identifier for the module.
 * @returns {object} The modules exports.
 * @see https://github.com/nodejs/node/blob/master/lib/module.js
 */
function require(id) {
    var path;
    // Create the exports object to be populated.
    var module = {};
    module.exports = {};
    var pathArray = id.split("/");

    // Change the id of the module if it starts with @accessors-modules
    if (id.indexOf("@accessors-modules/") == 0) {
        // If "@accessors-modules" is followed by the module name only
        // then replicate it as the directory + / + module name
        if (pathArray.length == 2) {
            id = pathArray[1] + '/' + pathArray[1];
        } else {
            // If followed by an entire path, then just keep it
            id = id.substr(19);
        }
    }
    // Change the root of relative paths that start with './modules'
    if (id.indexOf("./modules") == 0) {
        if(loadModule(_modulesCommonPath + pathArray[pathArray.length - 1]) === true) {
            id = pathArray[pathArray.length - 1];
        } else {
            if (pathArray.length == 2) {
                id = pathArray[1] + '/' + pathArray[1];
            } else {
                // If followed by an entire path, then just keep it
                // number of characters in "./modules/" = 10
                id = id.substr(10);
            }
        }
    } 

    // Follow the path if it is given.
    if (id.includes("/") && loadModule(_includePath + id) === true) {
        path = _includePath + id;
    } else {
        // Check if there is a locally-defined module with given id.
        if (loadModule(_modulesLocalPath + id) === true) {
            path = _modulesLocalPath + id;
        }
        // If not, check whether this this is a common module.
        else if (loadModule(_modulesCommonPath + id) === true) {
            path = _modulesCommonPath + id;
        }
        // Else, use the native require.
        else {
            return cordova.require(id); 
        }
    }
    // If the module was loaded successfully, evaluate its code.
    if (path) {
        var wrapper = eval('(function (module, exports) {' + _loadedModules[path] + '})');
        wrapper(module, module.exports);
    } 

    return module.exports;
};
 
/** 
 * Return the source of an accessor definition from the accessor library on the host.
 * The ".js" extension of the accessor class is optional. This is a blocking call.
 * @param {string} path The path on the server for the JavaScript code, e.g. 'net/REST'.
 * @returns {string} The source code of the accessor.
 */
function getAccessorCode(id) {
    if (!_loadedAccessors.hasOwnProperty(id)) {
        _loadedAccessors[id] = getJavaScript(_accessorPath + id);
    }
    return _loadedAccessors[id];
}

/** 
 * Return the source code of a JavaScript file. 
 * Append the string '.js' to the specified path (if it is not already there) 
 * and issue an HTTP GET request with the specified relative path. If no 
 * callback function is given, then this is a blocking request. If a callback 
 * is given, then the response will be passed as the second argument of the
 * callback. The first argument is used to pass an error message in case the
 * operation was unsuccessful (and will be null otherwise).
 * @param {string} path The relative path on the server.
 * @param {function} callback The callback function.
 */
function getJavaScript(path, callback) { // FIXME: try to merge with browser version.
                                         // FIXME: works with browser and android, test iOS.
    var request = new XMLHttpRequest();
    request.overrideMimeType("application/javascript");

    // Append '.js' if necessary.
    if (path.lastIndexOf('.js') != path.length - 3) {
        path = path + '.js';
    }
    // Use absolute path (necessary for Android.)
    path = _root + path;
    
    if (!callback) {
        // Synchronous version.
        request.open('GET', path, false); // Pass false for synchronous
        request.send(); // Send the request now
        // Throw an error if the request was not 200 OK or not 0 (local read success on iOS)
        if (request.status === 0) {
            if (request.responseText == "") {
                throw 'Failed to get ' + path + ': ' + request.statusText;
            } else {
                return request.responseText;
            }
        } else if (request.status !== 200) {
            throw 'Failed to get ' + path + ': ' + request.statusText;
        }
        return request.responseText;
    } else {
        // Asynchronous version.
        request.open('GET', path, true); // Pass true for asynchronous
        request.onreadystatechange = function () {
            // If the request is complete (state is 4)
            if (request.readyState === 4) {
                // If the request was successful.
                if (request.status === 200 && request.status !== 0) {
                    callback(null, request.responseText);
                } else {
                    callback('Failed to get ' +
                        path +
                        ': ' + request.statusText,
                        null);
                }
            }
        };
        request.send();
    }
}

/** Get a resource using XMLHttpRequest for remote files and window.resolveLocalFileSystemURL
 *  for local files.
 *  
 *  The reason for using a different function for local files is subtle, and has to do with
 *  how mobile apps are sandboxed differently than web apps.
 *  XMLHttpRequest will load local files, but is restricted by the domain name and top level domain name
 *  of the current URI, which can change from the app's www directory if the user navigates to another
 *  page of the app and interfere with file loading. Not only does 
 *  window.resolveLocalFileSystemURL not have this issue
 *  it provides access to other files in the app sandbox which may not be in the
 *  app's www directory. For example both Android and iOS provide persistant and private data
 *  storage (see https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-file/) outside
 *  of the app's www directory.
 *
 *  Unlike other hosts, this implementation is exclusively asynchronous.
 *  Although getJavaScript above shows synchronous loading, this is to be avoided because 
 *  a synchronous call to getResource during execution would make the swarmlet freeze.
 *  Therefore a null callback argument will generate an error.
 *
 *   *  $KEYSTORE is replaced with _root + "keystore/"
 *  This means each cordova swarmlet on a phone is assumed to have
 *  its own app-specific read-only keystore.
 * 
 *  FIXME: complete this implementation for more resource types such as binary files
 *  (see https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-file/index.html#store-an-existing-binary-file-)
 *  WARNING: the encoding parameter in options is currently ignored
 *  FIXME: print file errors with more informative messages than "[object]". 
 * 
 *
 *  Below are the types of resources that are handled
 *  all other resources will cause an error.
 * 
 *  - Text (UTF-8)
 *
 *  @param uri {string} A specification for the resource. If the uri starts with http or https,
 *    this function will attempt to load the resource from the web. If the uri is an
 *    absolute path on mobile (starts with file:///) it will attempt to load it locally.
 *    If the uri is a relative path, this function will attempt to load it relative to this app's www
 *    directory (located at _root).
 *  @param options FXIME: Until more than UTF-8 resources are supported, encoding inputs are ignored
 *    is ignored. The below documentation is copied from the GetResource accessor.
 *    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. Timeout defaults to 5000ms.
 *  * 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.
 *  * 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.
 *  @param callback A callback function which will be called with two arguments:
 *     1) The first argument is null if the resource was successfully
 *     acquired and a non-null error code if wasn't. The error message will be
 *     an http status message https://www.w3schools.com/tags/ref_httpmessages.asp
 *     if a web resource was sought, and a cordova-plugin-file error code
 *     (see https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-file/#list-of-error-codes-and-meanings)
 *     if a local resource was sought.
 *     2) The second argument is the desired resource, if successfully acquired
 *     and null otherwise.
 */
function getResource(uri, options, callback) {
    var timeout;

    if(typeof options == "number"){
        timeout = options;
    }
    if(typeof options == "object" && typeof options.timeout == "number"){
        timeout = options.timeout;
    }

    if (!timeout && timeout !== 0) {
        timeout = 5000;
    }
    if(callback == null){
        console.log("Error: cordovaHost's getResource implementation requires a non-null callback");
        return;         
    }

    var path = "";
    if(uri.startsWith("http://") || uri.startsWith("https://")){
        path = uri;
        var request = new XMLHttpRequest();
        var complete = false;

        request.onreadystatechange = function(){
            // readyState === 4 is the same as readyState === request.DONE.
            if (this.readyState === this.DONE) {
                complete = true;
                if (this.status == 200) {
                    callback(null, this.responseText);
                } else {
                    callback(this.status, null);
                    console.log("cordovaHost getResource failed with code " + this.status + " at URL: " + uri);
                }
            }
        };
        // The third argument specifies an asynchronous read.
        // A synchronous read will block this host during the read, so 
        request.open("GET", uri, true);
        request.send();
        
        var timeoutHandler = setTimeout(handleTimeout, timeout);

        function handleTimeout() {
            if(!complete){
                console.log("cordovaHost getResource timed out at URI: " + uri);
                request.abort();
                //No need to call the callback here because abort will trigger onreadystatechange
                //with readyState DONE
                //callback("timeout", null);                
            }
        }
    } else {
        if(uri.startsWith("file:///")){
            //Absolute path. Starting with "/" is not a valid path on mobile.
            path = uri;

        } else if(uri.startsWith("$KEYSTORE")){
            //On other hosts this is a hidden directory, but Cordova doesn't seem to load hidden files.
            // path = _root + "keystore/" + uri.substring(10);
            path = _root + "keystore/" + uri.substring(10);

        } else {
            //Relative path
            path = _root + uri;
        }

        function resolveFileFail(e) {
            callback(e, null);
            console.log("cordovaHost getResource failed resolving local file with error: " + e);
        }

        function onErrorReadFile(){
            console.log("cordovaHost getResource failed reading file.");
            callback( "FileReadError", null);
        }

        function resolveFileSuccess(fileEntry){
            fileEntry.file(function (file) {
                var reader = new FileReader();
                var complete = false;

                reader.onloadend = function() {
                    //It seems reader.abort doesn't prevent this function from being called,
                    //although abort does appear to make this execute immediately without the
                    //result. 
                    if(!complete){
                        callback(null, this.result);
                        complete = true;
                    }

                };
                reader.readAsText(file);

                var timeoutHandler = setTimeout(handleTimeout, timeout);

                function handleTimeout() {
                    if(!complete){
                        complete = true;
                        reader.abort(); //I think this function might indirectly call onloadend
                        console.log("cordovaHost getResource timed out at URI: " + uri);
                        callback("timeout", null);
                    }
                }
            }, onErrorReadFile);
        }
        window.resolveLocalFileSystemURL(path, resolveFileSuccess, resolveFileFail);
    }
    
};

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

/** Instantiate and return an accessor.
 *  This will throw an exception if there is no such accessor class on the accessor
 *  search path.
 *  @param {string} accessorName The name to give to the instance.
 *  @param {string} accessorClass Fully qualified accessor class name, e.g. 'net/REST'.
 *  @returns {object} An accessor instance.
 */
function instantiate(accessorName, accessorClass) {
    var bindings = { 
        'require': require,
        'getResource': getResource // FIXME: add more bindings
    };
    var instance = instantiateAccessor(
        accessorName, accessorClass, getAccessorCode, bindings);

    return instance;
}

// ============================================================================

var instantiateAccessor = common.instantiateAccessor;
var Accessor = common.Accessor;
var isReifiableBy = common.isReifiableBy;
Accessor.prototype.require = require;

// Override the default log function.
// FIXME: create UI module with a UI.log() instead.
console.log = function(txt) {
    var x = document.getElementById("log");
    if (txt) {
        x.innerHTML += "<br/>" + "\> "  + txt;
    }
};