UNPKG

bbop-rest-manager

Version:

The is the 'new' version of the manager event model for BBOP systems.

883 lines (770 loc) 26.2 kB
/** * Generic BBOP manager for dealing with basic generic REST calls. * This specific one is designed to be overridden by its subclasses. * This one pretty much just uses its incoming resource string as the data. * Mostly for testing purposes. * * Both a <bbop-rest-response> (or clean error data) and the manager * itself (this as anchor) should be passed to the callbacks. * * @module bbop-rest-manager */ // For base. var us = require('underscore'); var each = us.each; var bbop = require('bbop-core'); var registry = require('bbop-registry'); // For engines. var Q = require('q'); var querystring = require('querystring'); var jQuery = require('jquery'); var sync_request = require('sync-request'); /// /// Base class. /// /** * Contructor for the REST manager. * * See also: module:bbop-registry * * @constructor * @param {Object} response_parser - the response handler class to use for each call * @returns {Object} rest manager object */ function manager_base(response_handler){ registry.call(this, ['success', 'error']); this._is_a = 'bbop-rest-manager.base'; // Get a good self-reference point. var anchor = this; // Per-manager logger. this._logger = new bbop.logger(this._is_a); //this._logger.DEBUG = true; this._logger.DEBUG = false; function ll(str){ anchor._logger.kvetch(str); } // Handler instance. this._response_handler = response_handler; // The URL to query. this._qurl = null; // The argument payload to deliver to the URL. this._qpayload = {}; // The way to do the above. this._qmethod = 'GET'; this._headers = []; // Whether or not to prevent ajax events from going. // This may not be usable, or applicable, to all backends. this._safety = false; /** * Turn on or off the verbose messages. Uses <bbop.logger>, so * they should come out everywhere. * * @param {Boolean} [p] - true or false for debugging * @returns {Boolean} the current state of debugging */ this.debug = function(p){ if( p === true || p === false ){ this._logger.DEBUG = p; // TODO: add debug parameter a la include_highlighting } return this._logger.DEBUG; }; // The main callback function called after a successful AJAX call in // the update function. this._run_success_callbacks = function(in_data){ ll('run success callbacks...'); //var response = anchor.(in_data); var response = new anchor._response_handler(in_data); anchor.apply_callbacks('success', [response, anchor]); }; // This set is called when we run into a problem. this._run_error_callbacks = function(in_data){ ll('run error callbacks...'); var response = new anchor._response_handler(in_data); anchor.apply_callbacks('error', [response, anchor]); }; // Ensure the necessary this._ensure_arguments = function (url, payload, method, headers){ ll('ensure arguments...'); // Allow default settings to be set at the moment. if( typeof(url) !== 'undefined' ){ this.resource(url); } if( typeof(payload) !== 'undefined' ){ this.payload(payload); } if( typeof(method) !== 'undefined' ){ this.method(method); } if( typeof(headers) !== 'undefined' ){ this.headers(headers); } // Bail if no good resource to try. if( ! this.resource() ){ throw new Error('must have resource defined'); } }; // Apply the callbacks by the status of the response. this._apply_callbacks_by_response = function (response){ ll('apply callbacks by response...'); if( response && response.okay() ){ anchor.apply_callbacks('success', [response, anchor]); }else{ anchor.apply_callbacks('error', [response, anchor]); } }; /** * The base target URL for our operations. * * @param {String} [in_url] - update resource target with string * @returns {String|null} the url as string (or null) */ this.resource = function(in_url){ ll('resource called with: ' + in_url); if( typeof(in_url) !== 'undefined' && bbop.what_is(in_url) === 'string' ){ anchor._qurl = in_url; } return anchor._qurl; }; /** * The information to deliver to the resource. * * @param {Object} [payload] - update payload information * @returns {Object|null} a copy of the current payload */ this.payload = function(payload){ ll('payload called with: ' + payload); if( bbop.is_defined(payload) && bbop.what_is(payload) === 'object' ){ anchor._qpayload = payload; } return bbop.clone(anchor._qpayload); }; /** * The method to use to get the resource, as a string. * * @param {String} [method] - update aquisition method with string * @returns {String|null} the string or null */ this.method = function(method){ ll('method called with: ' + method); if( bbop.is_defined(method) && bbop.what_is(method) === 'string' ){ anchor._qmethod = method; } return anchor._qmethod; }; /** * The headers to use to get the resource, as a list of pairs. * * @param {Array} [headers] - update headers to send with form of: [['x-content', 'x/blarg'], ...] * @returns {Array} the current headers list */ this.headers = function(in_headers){ ll('headers called with: ' + in_headers); if( in_headers && us.isArray(in_headers) ){ anchor._headers = in_headers; } return anchor._headers; }; /** * Coordinate an arbitary number of promise generating functions * serially. * * @param {Array} [promise_function_stack] - An ordered list of functions that produce a Q promise in the order that they should be run. * @param {Function} [accumulator_function] - The function to run on every successful return; it takes a single <bbop-response-golr> and this manager as arguments. * @param {Function} [final_function] - The function to run after all queries have completed; it takes this manager as an argument. * @param {Function} [error_function] - The function to run on an error; it takes the Q error and this manager as arguments. * @returns {Number} the number of functions that will run */ anchor.run_promise_functions = function(promise_function_stack, accumulator_function, final_function, error_function){ if( ! us.isEmpty(promise_function_stack) ){ var promise_runner = promise_function_stack.shift(); promise_runner().then(function(resp){ accumulator_function(resp, anchor); anchor.run_promise_functions(promise_function_stack, accumulator_function, final_function, error_function); }).fail(function(err){ if(err){ error_function(err, anchor); } }).done(); }else{ final_function(anchor); } // Return the number of functions that will be run. return promise_function_stack.length || 0; }; } bbop.extend(manager_base, registry); /// /// Overridables. /// /** * It should combine the URL, payload, and method in the ways * appropriate to the subclass engine. * * This model class always returns true, with set messages; the * "payload" is fed as the argument into the response handler. * * What we're aiming for is a system that: * - runs callbacks (in order: success, error, return) * - return response * * @param {String} [url] - update resource target with string * @param {Object} [payload] - object to represent arguments * @param {String} [method] - GET, POST, etc. * @param {Array} [headers] - List of header pairs. * @returns {Object} response (given the incoming payload) */ manager_base.prototype.fetch = function(url, payload, method, headers){ var anchor = this; anchor._logger.kvetch('called fetch'); this._ensure_arguments(url, payload, method, headers); // This is an empty "sync" example, so just return the empty and // see. var response = new this._response_handler(this.payload()); response.okay(true); response.message('empty'); response.message_type('success'); // Run through the callbacks--naturally always "success" in our // case. this._apply_callbacks_by_response(response); return response; }; /** * It should combine the URL, payload, and method in the ways * appropriate to the subclass engine. * * This model class always returns true, with set messages; the * "payload" is fed as the argument into the response handler. * * What we're aiming for is a system that: * - runs callbacks (in order: success, error, return) * - return promise (delivering response) * * @param {String} [url] - update resource target with string * @param {Object} [payload] - object to represent arguments * @param {String} [method] - GET, POST, etc. * @param {Array} [headers] - List of header pairs. * @returns {Object} promise for the processed response subclass */ manager_base.prototype.start = function(url, payload, method, headers){ var anchor = this; this._ensure_arguments(url, payload, method, headers); // No actual async here, but do anyways. var deferred = Q.defer(); // This is an empty "sync" example, so just return the empty and // see. var response = new this._response_handler(this.payload()); response.okay(true); response.message('empty'); response.message_type('success'); // Run through the callbacks--naturally always "success" in our // case. this._apply_callbacks_by_response(response); deferred.resolve(response); return deferred.promise; }; /// /// Node async engine. /// /** * Contructor for the REST query manager; NodeJS-style. * * This is an asynchronous engine, so while both fetch and start will * run the callbacks, fetch will return null while start returns a * promise for the eventual result. Using the promise is entirely * optional--the main method is still considered to be the callbacks. * * NodeJS BBOP manager for dealing with remote calls. Remember, * this is actually a "subclass" of <bbop.rest.manager>. * * See also: {module:bbop-rest-manager#manager} * * @constructor * @param {Object} response_handler * @returns {manager_node} */ var manager_node = function(response_handler){ manager_base.call(this, response_handler); this._is_a = 'bbop-rest-manager.node'; // Grab an http(s) client. this._http_client = require('http'); this._https_client = require('https'); this._url_parser = require('url'); }; bbop.extend(manager_node, manager_base); /** * It should combine the URL, payload, and method in the ways * appropriate to the subclass engine. * * Runs callbacks, returns null. * * @param {String} [url] - update resource target with string * @param {Object} [payload] - object to represent arguments * @param {String} [method] - GET, POST, etc. * @param {Array} [headers] - List of header pairs. * @returns {null} returns null */ manager_node.prototype.fetch = function(url, payload, method, headers){ var anchor = this; anchor._logger.kvetch('called fetch'); // As we /only/ have async here, pass off to start(). this.start(url, payload, method, headers); return null; }; /** * It should combine the URL, payload, and method in the ways * appropriate to the subclass engine. * * What we're aiming for is a system that: * - runs callbacks (in order: success, error, return) * - return promise (delivering response) * * WARNING/NOTE: This method automatically injects the headers: * 'Content-Type': 'application/x-www-form-urlencoded' and * 'Content-Length': <calculated> during a POST. * * @param {String} [url] - update resource target with string * @param {Object} [payload] - object to represent arguments * @param {String} [method] - GET, POST, etc. * @param {Array} [headers] - List of header pairs. * @returns {Object} promise for the processed response subclass */ manager_node.prototype.start = function(url, payload, method, headers){ var anchor = this; this._ensure_arguments(url, payload, method, headers); // Our eventual promise. var deferred = Q.defer(); // What to do if an error is triggered. function on_error(e) { console.log('problem with request: ' + e.message); var response = new anchor._response_handler(null); response.okay(false); response.message(e.message); response.message_type('error'); anchor.apply_callbacks('error', [response, anchor]); deferred.resolve(response); } // Two things to do here: 1) collect data and 2) what to do with // it when we're done (create response). function on_connect(res){ //console.log('STATUS: ' + res.statusCode); //console.log('HEADERS: ' + JSON.stringify(res.headers)); res.setEncoding('utf8'); var raw_data = ''; res.on('data', function (chunk) { //console.log('BODY: ' + chunk); raw_data = raw_data + chunk; }); // Throw to . res.on('end', function () { //console.log('END with: ' + raw_data); var response = new anchor._response_handler(raw_data); if( response && response.okay() && res.statusCode < 400){ anchor.apply_callbacks('success', [response, anchor]); deferred.resolve(response); }else{ // Make sure that there is something there to // hold on to. if( ! response ){ response = new anchor._response_handler(null); response.okay(false); response.message_type('error'); response.message('null response'); }else{ response.okay(false); response.message_type('error'); response.message('bad response'); } anchor.apply_callbacks('error', [response, anchor]); deferred.resolve(response); } }); } // http://nodejs.org/api/url.html var purl = anchor._url_parser.parse(anchor.resource()); var protocol = purl['protocol']; var req_opts = { //'hostname': anchor.resource(), //'path': '/amigo/term/GO:0022008/json', //'port': 80, 'method': anchor.method(), 'headers': us.object(anchor.headers()) }; // Tranfer the interesting bit over. each(['protocol', 'hostname', 'port', 'path'], function(purl_prop){ if( purl[purl_prop] ){ req_opts[purl_prop] = purl[purl_prop]; } }); // Add any payload if it exists. On an empty payload, post_data // will still be '', so no real harm done. var post_data = ''; if( ! us.isEmpty(anchor.payload()) ){ if( anchor.method() === 'POST' ){ post_data = querystring.stringify(anchor.payload()); // WARNING: Injecting headers for some reason. req_opts['headers']['Content-Type'] = 'application/x-www-form-urlencoded'; req_opts['headers']['Content-Length'] = post_data.length; }else{ var qs = querystring.stringify(anchor.payload()); if( qs ){ req_opts['path'] = req_opts['path'] + '?' + qs; }else{ req_opts['path'] = req_opts['path']; } } } //console.log('req_opts', req_opts); // Ready either an HTTP or HTTPS run. var req = null; if( protocol && protocol === 'https:' ){ req = anchor._https_client.request(req_opts, on_connect); }else{ req = anchor._http_client.request(req_opts, on_connect); } //console.log('protocol', protocol); //console.log('protocol', protocol); // Oh yeah, add the error responder. req.on('error', on_error); // Write data to request body. if( anchor.method() === 'POST' ){ req.write(post_data); } req.end(); return deferred.promise; }; /// /// Node sync engine. /// /** * Contructor for the REST query manager--synchronous in node. * * This is an synchronous engine, so while both fetch and start will * run the callbacks, fetch will return a response while start returns * an instantly resolvable promise. Using the response results is * entirely optional--the main method is still considered to be the * callbacks. * * See also: <bbop.rest.manager> * * @constructor * @param {Object} response_handler * @returns {manager_sync_request} */ var manager_sync_request = function(response_handler){ manager_base.call(this, response_handler); this._is_a = 'bbop-rest-manager.sync_request'; }; bbop.extend(manager_sync_request, manager_base); /** * It should combine the URL, payload, and method in the ways * appropriate to the subclass engine. * * WARNING/NOTE: This method automatically injects the headers: * 'Content-Type': 'application/x-www-form-urlencoded' and * 'Content-Length': <calculated> during a POST. * * @param {String} [url] - update resource target with string * @param {Object} [payload] - object to represent arguments * @param {String} [method] - GET, POST, etc. * @param {Array} [headers] - List of header pairs. * @returns {Object} returns response */ manager_sync_request.prototype.fetch = function(url, payload, method, headers){ var anchor = this; this._ensure_arguments(url, payload, method, headers); // The wrapped version of the post request. function _post_request(){ var res = null; // try { var use_headers = us.object(anchor.headers()); // Need special handling if we are posting something to // the server. if( ! us.isEmpty(anchor.payload()) ){ var qs = querystring.stringify(anchor.payload()); //console.log('qs',qs); // WARNING: Injecting headers. use_headers['Content-Type'] = 'application/x-www-form-urlencoded'; use_headers['Content-Length'] = qs.length; res = sync_request('POST', anchor.resource(), { 'headers': use_headers, 'body': qs }); }else{ res = sync_request('POST', anchor.resource(), { 'headers': use_headers }); //console.log('res', res); } } catch(e){ console.log('ERROR in sync_request call, will try to recover'); } return res; } function _get_request(){ var res = null; // try { var use_headers = us.object(anchor.headers()); // Need special handling if we are sending something to // the server. if( ! us.isEmpty(anchor.payload()) ){ var qs = querystring.stringify(anchor.payload()); //console.log('qs',qs); if( qs ){ res = sync_request('GET', anchor.resource() + '?' + qs, { 'headers': use_headers }); }else{ res = sync_request('GET', anchor.resource(), { 'headers': use_headers }); } }else{ res = sync_request('GET', anchor.resource(), { 'headers': use_headers }); //console.log('res', res); } } catch(e){ console.log('ERROR in sync_request call, will try to recover'); } return res; } // Minimal processing and then grab the data from the server. var res = null; if( anchor.method() === 'POST' ){ res = _post_request(); }else{ res = _get_request(); } // var raw_str = null; if( res && res.statusCode < 400 ){ raw_str = res.getBody().toString(); }else if( res && res.body ){ raw_str = res.body.toString(); }else{ // } // Process and pick the right callback group accordingly. var response = new anchor._response_handler(raw_str); if( raw_str && raw_str !== '' && res.statusCode < 400 ){ this.apply_callbacks('success', [response, anchor]); }else{ this.apply_callbacks('error', [response, anchor]); //throw new Error('explody'); } return response; }; /** * This is the synchronous data getter for Node (and technically the * browser, but never never do that)--probably your best bet right now * for scripting. * * Works as fetch, except returns an (already resolved) promise. * * @param {String} [url] - update resource target with string * @param {Object} [payload] - object to represent arguments * @param {String} [method] - GET, POST, etc. * @param {Array} [headers] - List of header pairs. * @returns {Object} returns promise */ manager_sync_request.prototype.start = function(url, payload, method, headers){ var anchor = this; var response = anchor.fetch(url, payload, method, headers); // . var deferred = Q.defer(); deferred.resolve(response); return deferred.promise; }; /// /// jQuery engine. /// /** * Contructor for the jQuery REST manager * * jQuery BBOP manager for dealing with actual ajax calls. Remember, * this is actually a "subclass" of {bbop-rest-manager}. * * Use <use_jsonp> is you are working against a JSONP service instead * of a non-cross-site JSON service. * * See also: * <bbop.rest.manager> * * @constructor * @param {Object} response_handler * @returns {manager_sync_request} */ var manager_jquery = function(response_handler){ manager_base.call(this, response_handler); this._is_a = 'bbop-rest-manager.jquery'; this._use_jsonp = false; this._jsonp_callback = 'json.wrf'; // Track down and try jQuery. var anchor = this; //anchor.JQ = new bbop.rest.manager.jquery_faux_ajax(); try{ // some interpreters might not like this kind of probing if( typeof(jQuery) !== 'undefined' ){ anchor.JQ = jQuery; //anchor.JQ = jQuery.noConflict(); } }catch (x){ throw new Error('unable to find "jQuery" in the environment'); } }; bbop.extend(manager_jquery, manager_base); /** * Set the jQuery engine to use JSONP handling instead of the default * JSON. If set, the callback function to use will be given my the * argument "json.wrf" (like Solr), so consider that special. * * @param {Boolean} [use_p] - external setter for * @returns {Boolean} boolean */ manager_jquery.prototype.use_jsonp = function(use_p){ var anchor = this; if( typeof(use_p) !== 'undefined' ){ if( use_p === true || use_p === false ){ anchor._use_jsonp = use_p; } } return anchor._use_jsonp; }; /** * Get/set the jQuery jsonp callback string to something other than * "json.wrf". * * @param {String} [cstring] - setter string * @returns {String} string */ manager_jquery.prototype.jsonp_callback = function(cstring){ var anchor = this; if( typeof(cstring) !== 'undefined' ){ anchor._jsonp_callback = cstring; } return anchor._jsonp_callback; }; /** * It should combine the URL, payload, and method in the ways * appropriate to the subclass engine. * * Runs callbacks, returns null. * * @param {String} [url] - update resource target with string * @param {Object} [payload] - object to represent arguments * @param {String} [method] - GET, POST, etc. * @param {Array} [headers] - List of header pairs. * @returns {null} returns null */ manager_jquery.prototype.fetch = function(url, payload, method, headers){ var anchor = this; anchor._logger.kvetch('called fetch'); // Pass off. anchor.start(url, payload, method, headers); return null; }; /** * See the documentation in <manager.js> on update to get more * of the story. This override function adds functionality for * jQuery. * * @param {String} [url] - update resource target with string * @param {Object} [payload] - object to represent arguments * @param {String} [method] - GET, POST, etc. * @param {Array} [headers] - List of header pairs. * @returns {Object} promise for the processed response subclass */ manager_jquery.prototype.start = function(url, payload, method, headers){ var anchor = this; this._ensure_arguments(url, payload, method, headers); // Our eventual promise. var deferred = Q.defer(); // URL and payload (jQuery will just append as arg for GETs). //var qurl = anchor.resource(); //var pl = anchor.payload(); // The base jQuery Ajax args we need with the setup we have. var jq_vars = {}; if( anchor.method() === 'GET' ){ jq_vars['type'] = 'GET'; var qs = querystring.stringify(anchor.payload()); if( qs ){ jq_vars['url'] = anchor.resource() + '?' + qs; }else{ jq_vars['url'] = anchor.resource(); } }else{ // POST jq_vars['type'] = 'POST'; jq_vars['url'] = anchor.resource(); jq_vars['data'] = anchor.payload(); jq_vars['dataType'] = 'json'; // headers: { // "Content-Type": "application/javascript", // "Accept": "application/javascript" // }, } // If we're going to use JSONP instead of the defaults, set that now. if( anchor.use_jsonp() ){ jq_vars['dataType'] = 'jsonp'; jq_vars['jsonp'] = anchor._jsonp_callback; } // if( ! us.isEmpty(anchor.headers()) ){ jq_vars['headers'] = us.object(anchor.headers()); } // What to do if an error is triggered. // Remember that with jQuery, when using JSONP, there is no error. function on_error(xhr, status, error) { var response = new anchor._response_handler(null); response.okay(false); response.message(error); response.message_type(status); anchor.apply_callbacks('error', [response, anchor]); deferred.resolve(response); } function on_success(raw_data, status, xhr){ var response = new anchor._response_handler(raw_data); if( response && response.okay() ){ anchor.apply_callbacks('success', [response, anchor]); deferred.resolve(response); }else{ // Make sure that there is something there to // hold on to. if( ! response ){ response = new anchor._response_handler(null); response.okay(false); response.message_type(status); response.message('null response'); }else{ response.message_type(status); response.message('bad response'); } //anchor.apply_callbacks('error', [response, anchor]); //anchor.apply_callbacks('error', [raw_data, anchor]); anchor.apply_callbacks('error', [response, anchor]); deferred.resolve(response); } } // Setup JSONP for Solr and jQuery ajax-specific parameters. jq_vars['success'] = on_success; jq_vars['error'] = on_error; //done: _callback_type_decider, // decide & run search or reset //fail: _run_error_callbacks, // run error callbacks //always: function(){} // do I need this? anchor.JQ.ajax(jq_vars); return deferred.promise; }; /// /// Exportable body. /// module.exports = { "base" : manager_base, "node" : manager_node, "sync_request" : manager_sync_request, "jquery" : manager_jquery };