Source: org/terraswarm/accessor/accessors/web/hosts/node/node_modules/@accessors-modules/http-client/http-client.js

//Copyright (c) 2015 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.

/**
 * Module for HTTP clients.
 * A simple use of this module is to request a web page and print its contents, as
 * illustrated by the following example:
 * <pre>
 *    var httpClient = require('@accessors-modules/http-client');
 *    httpClient.get('http://accessors.org', function(message) {
 *        print(message.body);
 *    });
 * </pre>
 * Both http and https are supported.
 *
 * @module @accessors-modules/http-client
 * @author Marten Lohstroh, Edward A. Lee, Elizabeth Osyk
 * @version $$Id$$
 */

//Stop extra messages from jslint.  Note that there should be no
//space between the / and the * and global.
/*globals Java, actor, error, exports, IncomingMessage, require, util */

//FIXME: Setting "use strict" causes a warning about the IncomingMessage function declaration being Read Only
//and then opening the camera library fails.  The error is:
//Error: Error executing module net/REST line #237 : Error executing module httpClient line #356 : "IncomingMessage" is not defined
//In file: /Users/cxh/ptII/ptolemy/actor/lib/jjs/modules/httpClient/httpClient.js
//"use strict";

var http = require('http');
var https = require('https');
var url = require('url');

// TODO:  POST and PUT implementation.
/*  FIXME:  Ordering is not implemented.
 *  This implementation ensures that for any accessor that calls this function,
 *  the callback functions are called in the same order as
 *  invocations of this request() function that triggered the request.
 *  If you call this function from the same accessor before the previous
 *  request has been completed (the callback function has been called or it has
 *  timed out), then the request will be queued to be issued only after the previous
 *  request has been satisfied.
 */

/** Convenience method to issue an HTTP GET.  This just calls request() and then
 *  calls end() on the object returned by request(). It returns the object returned
 *  by request() (an instance of ClientRequest). See request() for documentation of
 *  the arguments.
 *  
 *  @param options The options.
 *  @param responseCallback The callback function to call with an instance of IncomingMessage,
 *   or with a null argument to signal an error.
 */

exports.get = function (options, responseCallback) {
    var request = exports.request(options, responseCallback);
    request.end();
    return request;
};

/** Convenience method to issue an HTTP POST.  This just calls request() and then
 *  calls end() on the object returned by request(). It returns the object returned
 *  by request() (an instance of ClientRequest). See request() for documentation of
 *  the arguments.
 *  
 *  @param options The options.
 *  @param responseCallback The callback function to call with an instance of IncomingMessage,
 *   or with a null argument to signal an error.
 */
exports.post = function (options, responseCallback) {
        options.method = "POST";
    var request = exports.request(options, responseCallback);
    request.end();
    return request;
};

/** Convenience method to issue an HTTP PUT.  This just calls request() and then
 *  calls end() on the object returned by request(). It returns the object returned
 *  by request() (an instance of ClientRequest). See request() for documentation of
 *  the arguments.
 *  
 *  @param options The options.
 *  @param responseCallback The callback function to call with an instance of IncomingMessage,
 *   or with a null argument to signal an error.
 */
exports.put = function (options, responseCallback) {
        options.method = "PUT";
    var request = exports.request(options, responseCallback);
    request.end();
    return request;
};

/** Issue an HTTP request and provide a callback function for responses.
 *  The callback is a function that is passed an instance of IncomingMessage,
 *  defined here. This function returns an instance of ClientRequest, also 
 *  defined here.  The HTTP request will not actually be issued until you call 
 *  the end() function on the returned ClientRequest.
 *
 *  The options argument is an object with the following fields.  The REST 
 *  accessor takes care of creating this options object for cases where only 
 *  a string URL is provided.  The url field is required.  Defaults are provided
 *  for other fields if not present.
 *  <ul>
 *  <li> body: The request body, if any.  This supports at least strings and image data.
 *  <li> headers: An object containing request headers. By default this
 *       is an empty object. Items may have a value that is an array of values,
 *       for headers with more than one value.
 *  <li> keepAlive: A boolean that specified whether to keep sockets around
 *       in a pool to be used by other requests in the future. This defaults to false.
 *  <li> method: A string specifying the HTTP request method.
 *       This defaults to 'GET', but can also be 'PUT', 'POST', 'DELETE', etc.
 *  <li> outputCompleteResponseOnly: If false, then the multiple invocations of the
 *       callback may be invoked for each request. This defaults to true, in which case
 *       there will be only one invocation of the callback.
 *  <li> timeout: The amount of time (in milliseconds) to wait for a response
 *       before triggering a null response and an error. This defaults to 5000.
 *  <li> url: A string that can be parsed as a URL, or an object containing
 *       the following fields:
 *       <ul>
 *       <li> host: A string giving the domain name or IP address of
 *            the server to issue the request to. This defaults to 'localhost'.
 *       <li> path: Request path as a string. This defaults to '/'. This can
 *            include a query string, e.g. '/index.html?page=12', or the query
 *            string can be specified as a separate field (see below). 
 *            An exception is thrown if the request path contains illegal characters.
 *       <li> protocol: The protocol. This is a string that defaults to 'http'.
 *       <li> port: Port of remote server. This defaults to 80. 
 *       <li> query: A query string to be appended to the path, such as '?page=12'.
 *       </ul>
 *  </ul>
 *  @param options The options or URL.
 *  @param responseCallback The callback function to call with an instance of IncomingMessage,
 *   or with a null argument to signal an error.
 *  @return An instance of ClientRequest.
 */

// // TODO:  Handle images, multi-part messages.
// // FIXME:  Does this approach work for streaming?  Is 'end' event received?

exports.request = function(options, callback) {
    // TODO:  Timeout.

    var nodeOptions = {
                    'headers' : {},
                    'host' : 'localhost',
                    'method' : 'GET',
                    'outputCompleteResponseOnly' : true,
                    'path' : '/',
                    'port' : 80,
                    'protocol' : 'http:',
                    'timeout': 5000
    };
    
    // The REST accessor converts any string URLs to objects before calling 
    // exports.request.
        
    var urlObject;
    // Options is an object.
    if (options.hasOwnProperty('keepAlive') && options.keepAlive) {
            nodeOptions.agent = new http.Agent({keepAlive: true});
    }
    
    if (options.hasOwnProperty('headers')) {
    	nodeOptions.headers = options.headers;
    }
    
    if (options.hasOwnProperty('method')) {
        nodeOptions.method = options.method;
    } 
    
    if (options.hasOwnProperty('outputCompleteResponseOnly')) {
    	nodeOptions.outputCompleteResponseOnly = options.outputCompleteResponseOnly;
    }
    
    if (options.hasOwnProperty('timeout')) {
    	nodeOptions.timeout = options.timeout;
    }
    
    // options.url should always exist, but, check just in case.
    if (options.hasOwnProperty('url')) {
            
            // options.url may be a string or an object.
            if (typeof options.url === "string") {
                    // See https://nodejs.org/docs/latest/api/url.html
                    urlObject = url.parse(options.url);
                    
                    nodeOptions.host = urlObject.hostname; // hostname does not include port.  (host does).
                    nodeOptions.protocol = urlObject.protocol;
                    nodeOptions.port = urlObject.port;
                    nodeOptions.path = urlObject.path; // path does not include query string.  (pathname does).
                    
                    // Remove any trailing /
                    if (nodeOptions.path[nodeOptions.path.length -1] === "/") {
                            nodeOptions.path = nodeOptions.path.substring(0, nodeOptions.path.length - 1);
                    }
            } else {
                    if (options.url.hasOwnProperty('host')) {
                            nodeOptions.host = options.url.host;
                    } 
                    
                    // node path includes querystring, if any.
                    if (options.url.hasOwnProperty('path')) {
                            var nodePath = options.url.path;
                            if (options.url.hasOwnProperty('querystring')) {
                                    nodePath = nodePath + options.url.querystring;
                            }
                            nodeOptions.path = nodePath;
                    }
                    if (options.url.hasOwnProperty('port')) {
                            nodeOptions.port = options.url.port;
                    }
                    if (options.url.hasOwnProperty('protocol')) {
                            nodeOptions.protocol = options.url.protocol;
                    }
            }
    	}
        
        // Do not pass the callback directly.  Instead, call it upon 'end' event,
        // after all body data has been received.
        // FIXME:  Does this approach work for streaming?  Is 'end' event received?
        var req = null;
        
        // If a request body, set the Content-Length header.
        if (options.hasOwnProperty('body') && options.body !== null) {
                nodeOptions.headers["Content-Length"] = Buffer.byteLength(options.body);
        }
        
        // Node requires the trailing : therefore, add it if not present.
        if (nodeOptions.protocol === "http") {
                nodeOptions.protocol = "http:";
        }
        if (nodeOptions.protocol === "https") {
                nodeOptions.protocol = "https:";
        }
        
        if (nodeOptions.protocol === "http:"){
                req = http.request(nodeOptions);
        } else if (nodeOptions.protocol === "https:") {
                req = https.request(nodeOptions);
        } else {
                // Have http emit an error. 
                console.log('Unsupported protocol: ' + nodeOptions.protocol + '.  Trying HTTP protocol.');
                req = http.request(nodeOptions);
        }
        
        // Add a response handler.  In node, to get the body, a 'data' event handler
        // must be added to the 'response' event handler.
        
        // message is an http.IncomingMessage.
        // TODO:  Handle images, multi-part requests.
        if (req !== null) {
        	if (nodeOptions.outputCompleteResponseOnly) {
                req.on('response', function(message) {
                    var data = "";
                    
                    // TODO:  Handle images, multi-part messages.
                    message.on('data', function(chunk) {
                            data += chunk;        // Should append here for multi-part?
                    });
                    
                    message.on('end', function() {
                            // Do not parse any JSON data.  Instead, output stringified JSON
                            // as CapeCode version does.  See HttpClientHelper line 343.
                            message.body = data;
                            // Call the callback specified by the accessor.
                            callback(message);
                    });
                });
        	} else {
        		
                req.on('response', function(message) {
                    var data = "";
                    
                    // TODO:  Handle images, multi-part messages.
                    message.on('data', function(chunk) {
                    	if (typeof chunk === 'object') {
                			message.body = chunk.toString();
                    	} else {
                    		message.body = chunk;
                    	}
                    	
                    	callback(message);
                    });
                });
        	}
                
            // Write any request body.
            // Presumes that any JSON is already stringified.
            // TODO:  Does this work for images?
            if (options.hasOwnProperty('body') && options.body !== null) {
                    req.write(options.body);
            }
        }
        
        // REST accessor uses stop() instead of abort()
        req.stop = req.abort;
        
        return req;
};