Source: http-api.js

'use strict';

var inherits = require('inherits'),
    events = require('events'),
    _ = require('lodash/core'),
    Promise = require('./promise');

/**
 * HTTP based API class with authorization hook
 *
 * @constructor
 * @extends events.EventEmitter
 * @param {Connection} conn - Connection object
 * @param {Object} [options] - Http API Options
 * @param {String} [options.responseType] - Overriding content mime-type in response
 * @param {Transport} [options.transport] - Transport for http api
 * @param {Object} [options.noContentResponse] - Alternative response when no content returned in response (= HTTP 204)
 */
var HttpApi = function(conn, options) {
  options = options || {};
  this._conn = conn;
  this.on('resume', function(err) { conn.emit('resume', err); });
  this._responseType = options.responseType;
  this._transport = options.transport || conn._transport;
  this._noContentResponse = options.noContentResponse;
};

inherits(HttpApi, events.EventEmitter);

/**
 * Callout to API endpoint using http
 *
 * @param {Object} request - Http Request object
 * @param {String} request.url - Endpoint URL to request
 * @param {String} request.method - Http method for request
 * @param {Object} [request.headers] - Http request headers in hash object
 * @param {Callback.<Object>} callback - Callback function
 * @returns {Promise.<Object>} -
 */
HttpApi.prototype.request = function(request, callback) {
  var self = this;
  var conn = this._conn;
  var logger = conn._logger;
  var refreshDelegate = this.getRefreshDelegate();
  // remember previous instance url in case it changes after a refresh
  var lastInstanceUrl = conn.instanceUrl;

  var deferred = Promise.defer();

  var onResume = function(err) {
    if (err) {
      deferred.reject(err);
      return;
    }
    // check to see if the token refresh has changed the instance url
    if(lastInstanceUrl !== conn.instanceUrl){
      // if the instance url has changed
      // then replace the current request urls instance url fragment
      // with the updated instance url
      request.url = request.url.replace(lastInstanceUrl,conn.instanceUrl);
    }

    self.request(request).then(function(response) {
      deferred.resolve(response);
    }, function(err) {
      deferred.reject(err);
    });
  };

  if (refreshDelegate && refreshDelegate._refreshing) {
    refreshDelegate.once('resume', onResume);
    return deferred.promise.thenCall(callback);
  }

  // hook before sending
  self.beforeSend(request);

  self.emit('request', request);
  logger.debug("<request> method=" + request.method + ", url=" + request.url);
  var requestTime = Date.now();

  return this._transport.httpRequest(request).then(function(response) {
    var responseTime = Date.now();
    logger.debug("elappsed time : " + (responseTime - requestTime) + "msec");
    logger.debug("<response> status=" + response.statusCode + ", url=" + request.url);

    self.emit('response', response);
    // Refresh token if session has been expired and requires authentication
    // when session refresh delegate is available
    if (self.isSessionExpired(response) && refreshDelegate) {
      refreshDelegate.refresh(requestTime, onResume);
      return deferred.promise;
    }
    if (self.isErrorResponse(response)) {
      var err = self.getError(response);
      throw err;
    }
    return self.getResponseBody(response);
  }, function(err) {
    var responseTime = Date.now();
    logger.debug("elappsed time : " + (responseTime - requestTime) + "msec");
    logger.error(err);
    throw err;
  })
  .thenCall(callback);
};

/**
 * @protected
 */
HttpApi.prototype.getRefreshDelegate = function() {
  return this._conn._refreshDelegate;
};

/**
 *
 * @protected
 */
HttpApi.prototype.beforeSend = function(request) {
  request.headers = request.headers || {};
  if (this._conn.accessToken) {
    request.headers.Authorization = "Bearer " + this._conn.accessToken;
  }
  if (this._conn.callOptions) {
    var callOptions = [];
    for (var name in this._conn.callOptions) {
      callOptions.push(name + "=" + this._conn.callOptions[name]);
    }
    request.headers["Sforce-Call-Options"] = callOptions.join(', ');
  }
};

/**
 * Detect response content mime-type
 * @protected
 */
HttpApi.prototype.getResponseContentType = function(response) {
  return this._responseType || response.headers && response.headers["content-type"];
};

/**
 *
 */
HttpApi.prototype.parseResponseBody = function(response) {
  var contentType = this.getResponseContentType(response);
  var parseBody = /^(text|application)\/xml(;|$)/.test(contentType) ? parseXML :
         /^application\/json(;|$)/.test(contentType) ? parseJSON :
         /^text\/csv(;|$)/.test(contentType) ? parseCSV :
         parseText;
  try {
    return parseBody(response.body);
  } catch(e) {
    return response.body;
  }
};

/**
 * Get response body
 * @protected
 */
HttpApi.prototype.getResponseBody = function(response) {
  if (response.statusCode === 204) { // No Content
    return this._noContentResponse;
  }
  var body = this.parseResponseBody(response);
  var err;
  if (this.hasErrorInResponseBody(body)) {
    err = this.getError(response, body);
    throw err;
  }
  if (response.statusCode === 300) { // Multiple Choices
    err = new Error('Multiple records found');
    err.name = "MULTIPLE_CHOICES";
    err.content = body;
    throw err;
  }
  return body;
};

/** @private */
function parseJSON(str) {
  return JSON.parse(str);
}

/** @private */
function parseXML(str) {
  var ret = {};
  require('xml2js').parseString(str, { explicitArray: false }, function(err, result) {
    ret = { error: err, result : result };
  });
  if (ret.error) { throw ret.error; }
  return ret.result;
}

/** @private */
function parseCSV(str) {
  return require('./csv').parseCSV(str);
}

/** @private */
function parseText(str) { return str; }


/**
 * Detect session expiry
 * @protected
 */
HttpApi.prototype.isSessionExpired = function(response) {
  return response.statusCode === 401;
};

/**
 * Detect error response
 * @protected
 */
HttpApi.prototype.isErrorResponse = function(response) {
  return response.statusCode >= 400;
};

/**
 * Detect error in response body
 * @protected
 */
HttpApi.prototype.hasErrorInResponseBody = function(body) {
  return false;
};

/**
 * Parsing error message in response
 * @protected
 */
HttpApi.prototype.parseError = function(body) {
  var errors = body;
  return _.isArray(errors) ? errors[0] : errors;
};

/**
 * Get error message in response
 * @protected
 */
HttpApi.prototype.getError = function(response, body) {
  var error;
  try {
    error = this.parseError(body || this.parseResponseBody(response));
  } catch(e) {}
  error = _.isObject(error) && _.isString(error.message) ? error : {
    errorCode: 'ERROR_HTTP_' + response.statusCode,
    message : response.body
  };
  var err = new Error(error.message);
  err.name = error.errorCode;
  for (var key in error) { err[key] = error[key]; }
  return err;
};

/*-------------------------------------------------------------------------*/

/**
 * @protected
 */
var SessionRefreshDelegate = function(conn, refreshFn) {
  this._conn = conn;
  this._refreshFn = refreshFn;
  this._refreshing = false;
};

inherits(SessionRefreshDelegate, events.EventEmitter);

/**
 * Refresh access token
 * @private
 */
SessionRefreshDelegate.prototype.refresh = function(since, callback) {
  // Callback immediately When refreshed after designated time
  if (this._lastRefreshedAt > since) { return callback(); }
  var self = this;
  var conn = this._conn;
  var logger = conn._logger;
  self.once('resume', callback);
  if (self._refreshing) { return; }
  logger.debug("<refresh token>");
  self._refreshing = true;
  return self._refreshFn(conn, function(err, accessToken, res) {
    if (!err) {
      logger.debug("Connection refresh completed. Refreshed access token = " + accessToken);
      conn.accessToken = accessToken;
      conn.emit("refresh", accessToken, res);
    }
    self._lastRefreshedAt = Date.now();
    self._refreshing = false;
    self.emit('resume', err);
  });
};


/**
 *
 */
HttpApi.SessionRefreshDelegate = SessionRefreshDelegate;
module.exports = HttpApi;