//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;
};