<commit_before>// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert');
var crypto = require('crypto');
var http = require('http');
var Stream = require('stream').Stream;
var util = require('util');

var mime = require('mime');

var errors = require('./errors');
var Request = require('./request');

///--- Globals

var RestError = errors.RestError;

var ALLOW_HEADERS = [
  'Accept',
  'Accept-Version',
  'Content-Length',
  'Content-MD5',
  'Content-Type',
  'Date',
  'X-Api-Version'
].join(', ');

var EXPOSE_HEADERS = [
  'X-Api-Version',
  'X-Request-Id',
  'X-Response-Time'
].join(', ');

var DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

var MONTHS = [
  'Jan',
  'Feb',
  'Mar',
  'Apr',
  'May',
  'Jun',
  'Jul',
  'Aug',
  'Sep',
  'Oct',
  'Nov',
  'Dec'
];

///--- Helpers

function httpDate(now) {
  if (!now)
    now = new Date();

  function pad(val) {
    if (parseInt(val, 10) < 10)
      val = '0' + val;
    return val;
  }

  return DAYS[now.getUTCDay()] + ', ' +
    pad(now.getUTCDate()) + ' ' +
    MONTHS[now.getUTCMonth()] + ' ' +
    now.getUTCFullYear() + ' ' +
    pad(now.getUTCHours()) + ':' +
    pad(now.getUTCMinutes()) + ':' +
    pad(now.getUTCSeconds()) + ' GMT';
}

function startHeaders(res) {
  assert.ok(res);

  if (!res.header('Access-Control-Allow-Origin'))
    res.header('Access-Control-Allow-Origin', '*');

  if (!res.header('Access-Control-Allow-Methods') && res.methods.length)
    res.header('Access-Control-Allow-Methods', res.methods.join());

  if (!res.header('Access-Control-Allow-Headers'))
    res.header('Access-Control-Allow-Headers', ALLOW_HEADERS);

  if (!res.header('Access-Control-Expose-Headers'))
    res.header('Access-Control-Expose-Headers', EXPOSE_HEADERS);

  if (!res.header('Server'))
    res.header('Server', 'restify');

  if (!res.header('X-Api-Version') && res.version)
    res.header('X-Api-Version', res.version);

  if (!res.header('X-Request-Id'))
    res.header('X-Request-Id', res.requestId);

  var now = new Date();
  if (!res.header('Date'))
    res.header('Date', httpDate(now));

  if (!res.header('X-Response-Time'))
    res.header('X-Response-Time', now.getTime() - res.req.time.getTime());
}

function finishHeaders(res, data) {
  assert.ok(res);

  if (!data) {
    res.contentLength = 0;
  } else {
    res.contentLength =
      Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data);
    var hash = crypto.createHash('md5');
    hash.update(data);
    res.header('Content-MD5', hash.digest('base64'));
  }

  if (!res.header('Connection') && !res.keepAlive)
    res.header('Connection', 'close');
}

function formatBinary(req, res, body) {
  if (!body)
    return null;

  if (!Buffer.isBuffer(body))
    body = new Buffer(body.toString());

  return body;
}

function formatText(req, res, body) {
  if (!body)
    return null;

  if (body instanceof Error) {
    body = body.message;
  } else if (typeof(body) === 'object') {
    body = JSON.stringify(body);
  } else {
    body = body.toString();
  }

  return body;
}

function formatJSON(req, res, body) {
  if (!body)
    return null;

  if (body instanceof Error) {
    if (body instanceof RestError) {
      body = body.body;
    } else {
      body = {
        message: body.message
      };
    }
  }

  if (Buffer.isBuffer(body))
    body = body.toString('base64');

  return JSON.stringify(body);
}

function ensureDefaultFormatters(res) {
  if (!res.formatters)
    res.formattes = res;

  if (!res.formatters['application/json'])
    res.formatters['application/json'] = formatJSON;

  if (!res.formatters['text/plain'])
    res.formatters['text/plain'] = formatText;

  if (!res.formatters['application/octet-stream'])
    res.formatters['application/octet-stream'] = formatBinary;
}

///--- API

/**
 * Response is a wrapper over node's http.OutgoingMessage object.
 *
 * Previous versions of restify just extended the prototype, but that was
 * problemtic if used in conjunction with other modules that do this as well
 * (namely express).
 *
 * This object exposes all of the properties/events/methods of the standard
 * Response object, plus some extra goodies.
 *
 * Note that at construct time, you pass in a map of mime type to formatter
 * functions. i.e:
 *
 * var res = new Response({
 *   formatters: {
 *     json: function(req, res, data, callback) {
 *       return callback(null, JSON.stringify(data));
 *     }
 *   }
 * });
 *
 * Note there is a special formatter called `error` which is invoked when
 * data is an error.
 *
 * @param {Object} options pass in log4js handle, http.OutgoingMessage and a
 *                 Request.
 * @throws {TypeError} on input error.
 */
function Response(options) {
  if (typeof(options) !== 'object')
    throw new TypeError('options (Object) required');
  if (typeof(options.log4js) !== 'object')
    throw new TypeError('options.log4js (Object) required');
  if (typeof(options.response) !== 'object')
    throw new TypeError('options.response (http.OutgoingMessage) required');
  if (!(options.request instanceof Request))
    throw new TypeError('options.request (Request) required');
  if (options.version && typeof(options.version) !== 'string')
    throw new TypeError('options.version (String) required');

  var self = this;

  Stream.call(this);

  this.expectContinue = options.expectContinue || false;
  this._continueSent = false;
  this.formatters = {};
  this.log4js = options.log4js;
  this.log = this.log4js.getLogger('Response');
  this.methods = options.methods || [];
  this.req = options.request;
  this.res = options.response;
  this.version = options.version || false;

  // Generate the formatters
  if (options.formatters) {
    Object.keys(options.formatters).forEach(function(k) {
      if (k.indexOf('/') === -1)
        k = mime.lookup(k);

      self.formatters[k] = options.formatters[k];
    });
  }
  ensureDefaultFormatters(this);

  this.__defineSetter__('code', function(c) {
    if (typeof(c) !== 'number')
      throw new TypeError('code (Number) required');
    if (c < 100 || c > 599)
      throw new TypeError('code is not a valid HTTP status code');

    self.res.statusCode = c;
  });

  this.__defineGetter__('code', function() {
    return self.res.statusCode || 200;
  });

  this.__defineSetter__('contentLength', function(len) {
    if (typeof(len) !== 'number')
      throw new TypeError('contentLength must be Number');

    self.res.setHeader('Content-Length', len);
  });

  this.__defineGetter__('contentLength', function() {
    return self.res.getHeader('Content-Length');
  });

  this.__defineSetter__('contentType', function(type) {
    if (typeof(type) !== 'string')
      throw new TypeError('contentType must be String');

    if (type.indexOf('/') === -1)
      type = mime.lookup(type);

    self.res.setHeader('Content-Type', type);
  });

  this.__defineGetter__('contentType', function() {
    return self.res.getHeader('Content-Type');
  });

  this.__defineGetter__('headers', function() {
    return self.res._headers || {};
  });

  this.__defineGetter__('id', function() {
    return self.req.requestId;
  });

  this.__defineGetter__('requestId', function() {
    return self.req.requestId;
  });

  this.__defineGetter__('statusCode', function() {
    return self.code;
  });

  this.__defineSetter__('statusCode', function(code) {
    self.code = code;
  });

  var types = Object.keys(this.formatters);
  this.__defineGetter__('types', function() {
    return types;
  });

  this.__defineGetter__('writeable', function() {
    return self.res.writable;
  });

  ///--- Events
  this.res.on('drain', function() {
    return self.emit('drain');
  });

  this.res.on('error', function(err) {
    return self.emit('error', err);
  });

  this.res.on('close', function() {
    return self.emit('close');
  });

  this.res.on('pipe', function(src) {
    return self.emit('pipe', src);
  });
}
util.inherits(Response, Stream);
module.exports = Response;

Response.ACCEPTABLE = [
  'application/json',
  'text/plain',
  'application/octet-stream'
];

///--- Writable Stream APIs

Response.prototype.write = function write() {
  return this.res.write.apply(this.res, arguments);
};

Response.prototype.end = function end() {
  return this.res.end.apply(this.res, arguments);
};

Response.prototype.destroy = function destroy() {
  return this.res.destroy.apply(this.res, arguments);
};

Response.prototype.destroySoon = function destroySoon() {
  return this.res.destroySoon.apply(this.res, arguments);
};

///--- http.OutgoingMessage APIs

Response.prototype.writeHead = function writeHead() {
  this.writeContinue();

  startHeaders(this);
  this.res.writeHead.apply(this.res, arguments);
};

Response.prototype.writeContinue = function writeContinue() {
  if (!this.expectContinue || this._continueSent)
    return false;

  this._continueSent = true;
  return this.res.writeContinue.apply(this.res, arguments);
};

Response.prototype.addTrailers = function addTrailers() {
  return this.res.addTrailers.apply(this.res, arguments);
};

Response.prototype.setHeader = function setHeader() {
  return this.res.setHeader.apply(this.res, arguments);
};

Response.prototype.getHeader = function getHeader() {
  return this.res.getHeader.apply(this.res, arguments);
};

Response.prototype.removeHeader = function removeHeader() {
  return this.res.removeHeader.apply(this.res, arguments);
};

///-- Express APIs (that we want)

Response.prototype.header = function header(name, value) {
  if (typeof(name) !== 'string')
    throw new TypeError('name (String) required');

  if (value === undefined)
    return this.getHeader(name);

  this.setHeader(name, value);
  return value;
};

Response.prototype.get = function get(name) {
  return this.getHeader(name);
};

Response.prototype.set = function set(name, val) {
  if (arguments.length === 2) {
    if (typeof(name) !== 'string')
      throw new TypeError('name (String) required');
    this.setHeader(name, val);
  } else {
    if (typeof(name) !== 'object')
      throw new TypeError('Object required for argument 1');

    var self = this;
    Object.keys(name).forEach(function(k) {
      self.setHeader(k, name[k]);
    });
  }

  return this;
};

Response.prototype.cache = function cache(type, options) {
  if (typeof(type) !== 'string') {
    options = type;
    type = 'public';
  }

  if (options && options.maxAge) {
    if (typeof(options.maxAge) !== 'number')
      throw new TypeError('options.maxAge (Number) required');
    type += ', max-age=' + (options.maxAge / 1000);
  }

  return this.setHeader('Cache-Control', type);
};

Response.prototype.status = function status(code) {
  this.code = code;
  return this;
};

Response.prototype.send = function send(body) {
  assert.ok(this.methods);

  var head = (this.req.method === 'HEAD');

  // allow status / body
  if (arguments.length === 2) {
    this.statusCode = body;
    body = arguments[1];
  }

  startHeaders(this);
  // the formatter likely tacks in Content- headers
  var data = body ? this.format(body) : null;
  finishHeaders(this, data);

  if (this.statusCode === 204 || this.statusCode === 304) {
    this.removeHeader('Content-Length');
    this.removeHeader('Content-MD5');
    this.removeHeader('Content-Type');
    data = false;
  }

  this.writeHead(this.statusCode, this.headers);
  if (!head && data)
    this.write(data);
  this.end();

  if (this.log.isTraceEnabled())
    this.log.trace('%s sent response:\n%s', this.id, this.toString());
  return this;
};

Response.prototype.json = function json(obj) {
  if (arguments.length === 2) {
    this.code = obj;
    obj = arguments[1];
  }

  if (typeof(obj) !== 'object')
    throw new TypeError('object (Object) required');

  this.contentType = 'application/json';
  return this.send(obj);
};

Response.prototype.format = function format(body) {
  var log = this.log;

  if (log.isTraceEnabled())
    log.trace('%s format entered(type=%s).', this.requestId, this.contentType);

  if (!this.contentType) {
    for (var i = 0; i < this.types.length; i++) {
      if (this.req.accepts(this.types[i])) {
        this.contentType = this.types[i];
        break;
      }
    }
  }

  if (this.types.indexOf(this.contentType) === -1) {
    this.contentType = 'application/octet-stream';

    if (log.isTraceEnabled()) {
      log.trace('%s format content-type overriden: %s',
                this.requestId, this.contentType);
    }
  }

  assert.ok(this.formatters[this.contentType]);

  if (body instanceof Error)
    this.code = (body instanceof errors.HttpError) ? body.statusCode : 500;

  var data = this.formatters[this.contentType](this.req, this, body);

  if (log.isTraceEnabled())
    log.trace('%s format returing: %s', this.requestId, data);

  return data;
};

Response.prototype.toString = function toString() {
  var headers = '';
  var self = this;
  Object.keys(this.headers).forEach(function(k) {
    headers += k + ': ' + self.headers[k] + '\n';
  });
  return 'HTTP/1.1 ' + this.code + ' ' + http.STATUS_CODES[this.code] + '\n' +
    headers;
};
<commit_msg>defaultHeader refactoring<commit_after>// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert');
var crypto = require('crypto');
var http = require('http');
var Stream = require('stream').Stream;
var util = require('util');

var mime = require('mime');

var errors = require('./errors');
var Request = require('./request');

///--- Globals

var RestError = errors.RestError;

var ALLOW_HEADERS = [
  'Accept',
  'Accept-Version',
  'Content-Length',
  'Content-MD5',
  'Content-Type',
  'Date',
  'X-Api-Version'
].join(', ');

var EXPOSE_HEADERS = [
  'X-Api-Version',
  'X-Request-Id',
  'X-Response-Time'
].join(', ');

var DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

var MONTHS = [
  'Jan',
  'Feb',
  'Mar',
  'Apr',
  'May',
  'Jun',
  'Jul',
  'Aug',
  'Sep',
  'Oct',
  'Nov',
  'Dec'
];

///--- Helpers

function httpDate(now) {
  if (!now)
    now = new Date();

  function pad(val) {
    if (parseInt(val, 10) < 10)
      val = '0' + val;
    return val;
  }

  return DAYS[now.getUTCDay()] + ', ' +
    pad(now.getUTCDate()) + ' ' +
    MONTHS[now.getUTCMonth()] + ' ' +
    now.getUTCFullYear() + ' ' +
    pad(now.getUTCHours()) + ':' +
    pad(now.getUTCMinutes()) + ':' +
    pad(now.getUTCSeconds()) + ' GMT';
}

function formatBinary(req, res, body) {
  if (!body)
    return null;

  if (!Buffer.isBuffer(body))
    body = new Buffer(body.toString());

  return body;
}

function formatText(req, res, body) {
  if (!body)
    return null;

  if (body instanceof Error) {
    body = body.message;
  } else if (typeof(body) === 'object') {
    body = JSON.stringify(body);
  } else {
    body = body.toString();
  }

  return body;
}

function formatJSON(req, res, body) {
  if (!body)
    return null;

  if (body instanceof Error) {
    if (body instanceof RestError) {
      body = body.body;
    } else {
      body = {
        message: body.message
      };
    }
  }

  if (Buffer.isBuffer(body))
    body = body.toString('base64');

  return JSON.stringify(body);
}

function ensureDefaultFormatters(res) {
  if (!res.formatters)
    res.formattes = res;

  if (!res.formatters['application/json'])
    res.formatters['application/json'] = formatJSON;

  if (!res.formatters['text/plain'])
    res.formatters['text/plain'] = formatText;

  if (!res.formatters['application/octet-stream'])
    res.formatters['application/octet-stream'] = formatBinary;
}

///--- API

/**
 * Response is a wrapper over node's http.OutgoingMessage object.
 *
 * Previous versions of restify just extended the prototype, but that was
 * problemtic if used in conjunction with other modules that do this as well
 * (namely express).
 *
 * This object exposes all of the properties/events/methods of the standard
 * Response object, plus some extra goodies.
 *
 * Note that at construct time, you pass in a map of mime type to formatter
 * functions. i.e:
 *
 * var res = new Response({
 *   formatters: {
 *     json: function(req, res, data, callback) {
 *       return callback(null, JSON.stringify(data));
 *     }
 *   }
 * });
 *
 * Note there is a special formatter called `error` which is invoked when
 * data is an error.
 *
 * @param {Object} options pass in log4js handle, http.OutgoingMessage and a
 *                 Request.
 * @throws {TypeError} on input error.
 */
function Response(options) {
  if (typeof(options) !== 'object')
    throw new TypeError('options (Object) required');
  if (typeof(options.log4js) !== 'object')
    throw new TypeError('options.log4js (Object) required');
  if (typeof(options.response) !== 'object')
    throw new TypeError('options.response (http.OutgoingMessage) required');
  if (!(options.request instanceof Request))
    throw new TypeError('options.request (Request) required');
  if (options.version && typeof(options.version) !== 'string')
    throw new TypeError('options.version (String) required');

  var self = this;

  Stream.call(this);

  this.expectContinue = options.expectContinue || false;
  this._continueSent = false;
  this.formatters = {};
  this.log4js = options.log4js;
  this.log = this.log4js.getLogger('Response');
  this.methods = options.methods || [];
  this.req = options.request;
  this.res = options.response;
  this.version = options.version || false;

  // Generate the formatters
  if (options.formatters) {
    Object.keys(options.formatters).forEach(function(k) {
      if (k.indexOf('/') === -1)
        k = mime.lookup(k);

      self.formatters[k] = options.formatters[k];
    });
  }
  ensureDefaultFormatters(this);

  this.__defineSetter__('code', function(c) {
    if (typeof(c) !== 'number')
      throw new TypeError('code (Number) required');
    if (c < 100 || c > 599)
      throw new TypeError('code is not a valid HTTP status code');

    self.res.statusCode = c;
  });

  this.__defineGetter__('code', function() {
    return self.res.statusCode || 200;
  });

  this.__defineSetter__('contentLength', function(len) {
    if (typeof(len) !== 'number')
      throw new TypeError('contentLength must be Number');

    self.res.setHeader('Content-Length', len);
  });

  this.__defineGetter__('contentLength', function() {
    return self.res.getHeader('Content-Length');
  });

  this.__defineSetter__('contentType', function(type) {
    if (typeof(type) !== 'string')
      throw new TypeError('contentType must be String');

    if (type.indexOf('/') === -1)
      type = mime.lookup(type);

    self.res.setHeader('Content-Type', type);
  });

  this.__defineGetter__('contentType', function() {
    return self.res.getHeader('Content-Type');
  });

  this.__defineGetter__('headers', function() {
    return self.res._headers || {};
  });

  this.__defineGetter__('id', function() {
    return self.req.requestId;
  });

  this.__defineGetter__('requestId', function() {
    return self.req.requestId;
  });

  this.__defineGetter__('statusCode', function() {
    return self.code;
  });

  this.__defineSetter__('statusCode', function(code) {
    self.code = code;
  });

  var types = Object.keys(this.formatters);
  this.__defineGetter__('types', function() {
    return types;
  });

  this.__defineGetter__('writeable', function() {
    return self.res.writable;
  });

  ///--- Events
  this.res.on('drain', function() {
    return self.emit('drain');
  });

  this.res.on('error', function(err) {
    return self.emit('error', err);
  });

  this.res.on('close', function() {
    return self.emit('close');
  });

  this.res.on('pipe', function(src) {
    return self.emit('pipe', src);
  });
}
util.inherits(Response, Stream);
module.exports = Response;

Response.ACCEPTABLE = [
  'application/json',
  'text/plain',
  'application/octet-stream'
];

///--- Writable Stream APIs

Response.prototype.write = function write() {
  return this.res.write.apply(this.res, arguments);
};

Response.prototype.end = function end() {
  return this.res.end.apply(this.res, arguments);
};

Response.prototype.destroy = function destroy() {
  return this.res.destroy.apply(this.res, arguments);
};

Response.prototype.destroySoon = function destroySoon() {
  return this.res.destroySoon.apply(this.res, arguments);
};

///--- http.OutgoingMessage APIs

Response.prototype.writeHead = function writeHead() {
  this.writeContinue();

  if (this.statusCode === 204 || this.statusCode === 304) {
    this.removeHeader('Content-Length');
    this.removeHeader('Content-MD5');
    this.removeHeader('Content-Type');
  }

  this.res.writeHead.apply(this.res, arguments);
};

Response.prototype.writeContinue = function writeContinue() {
  if (!this.expectContinue || this._continueSent)
    return false;

  this._continueSent = true;
  return this.res.writeContinue.apply(this.res, arguments);
};

Response.prototype.addTrailers = function addTrailers() {
  return this.res.addTrailers.apply(this.res, arguments);
};

Response.prototype.setHeader = function setHeader() {
  return this.res.setHeader.apply(this.res, arguments);
};

Response.prototype.getHeader = function getHeader() {
  return this.res.getHeader.apply(this.res, arguments);
};

Response.prototype.removeHeader = function removeHeader() {
  return this.res.removeHeader.apply(this.res, arguments);
};

///-- Express APIs (that we want)

Response.prototype.header = function header(name, value) {
  if (typeof(name) !== 'string')
    throw new TypeError('name (String) required');

  if (value === undefined)
    return this.getHeader(name);

  this.setHeader(name, value);
  return value;
};

Response.prototype.get = function get(name) {
  return this.getHeader(name);
};

Response.prototype.set = function set(name, val) {
  if (arguments.length === 2) {
    if (typeof(name) !== 'string')
      throw new TypeError('name (String) required');
    this.setHeader(name, val);
  } else {
    if (typeof(name) !== 'object')
      throw new TypeError('Object required for argument 1');

    var self = this;
    Object.keys(name).forEach(function(k) {
      self.setHeader(k, name[k]);
    });
  }

  return this;
};

Response.prototype.cache = function cache(type, options) {
  if (typeof(type) !== 'string') {
    options = type;
    type = 'public';
  }

  if (options && options.maxAge) {
    if (typeof(options.maxAge) !== 'number')
      throw new TypeError('options.maxAge (Number) required');
    type += ', max-age=' + (options.maxAge / 1000);
  }

  return this.setHeader('Cache-Control', type);
};

Response.prototype.status = function status(code) {
  this.code = code;
  return this;
};

Response.prototype.defaultHeaders = function defaultHeaders(data) {
  var now = new Date();

  if (!this.header('Access-Control-Allow-Origin'))
    this.header('Access-Control-Allow-Origin', '*');

  if (!this.header('Access-Control-Allow-Methods') && this.methods.length)
    this.header('Access-Control-Allow-Methods', this.methods.join());

  if (!this.header('Access-Control-Allow-Headers'))
    this.header('Access-Control-Allow-Headers', ALLOW_HEADERS);

  if (!this.header('Access-Control-Expose-Headers'))
    this.header('Access-Control-Expose-Headers', EXPOSE_HEADERS);

  if (!this.header('Connection') && !this.keepAlive)
    this.header('Connection', 'close');

  if (!this.header('Content-Length') && data) {
    if (Buffer.isBuffer(data)) {
      this.header('Content-Length', data.length);
    } else {
      this.header('Content-Length', Buffer.byteLength(data));
    }
  } else {
    this.header('Content-Length', 0);
  }

  if (!this.header('Content-MD5') && data) {
    var hash = crypto.createHash('md5');
    hash.update(data);
    this.header('Content-MD5', hash.digest('base64'));
  }

  if (!this.header('Date'))
    this.header('Date', httpDate(now));

  if (!this.header('Server'))
    this.header('Server', 'restify');

  if (!this.header('X-Api-Version') && this.version)
    this.header('X-Api-Version', this.version);

  if (!this.header('X-Request-Id'))
    this.header('X-Request-Id', this.id);

  if (!this.header('X-Response-Time'))
    this.header('X-Response-Time', now.getTime() - this.req.time.getTime());
}

Response.prototype.send = function send(body) {
  assert.ok(this.methods);

  var head = (this.req.method === 'HEAD');

  // allow status / body
  if (arguments.length === 2) {
    this.statusCode = body;
    body = arguments[1];
  }

  // the formatter might tack in Content- headers, so fill in headers after
  // serialization
  var data = body ? this.format(body) : null;
  this.defaultHeaders(data);

  this.writeHead(this.statusCode);
  if (data && !head && this.statusCode !== 204 && this.statusCode !== 304)
    this.write(data);
  this.end();

  if (this.log.isTraceEnabled())
    this.log.trace('%s sent response:\n%s', this.id, this.toString());
  return this;
};

Response.prototype.json = function json(obj) {
  if (arguments.length === 2) {
    this.code = obj;
    obj = arguments[1];
  }

  if (typeof(obj) !== 'object')
    throw new TypeError('object (Object) required');

  this.contentType = 'application/json';
  return this.send(obj);
};

Response.prototype.format = function format(body) {
  var log = this.log;

  if (log.isTraceEnabled())
    log.trace('%s format entered(type=%s).', this.requestId, this.contentType);

  if (!this.contentType) {
    for (var i = 0; i < this.types.length; i++) {
      if (this.req.accepts(this.types[i])) {
        this.contentType = this.types[i];
        break;
      }
    }
  }

  if (this.types.indexOf(this.contentType) === -1) {
    this.contentType = 'application/octet-stream';

    if (log.isTraceEnabled()) {
      log.trace('%s format content-type overriden: %s',
                this.requestId, this.contentType);
    }
  }

  assert.ok(this.formatters[this.contentType]);

  if (body instanceof Error)
    this.code = (body instanceof errors.HttpError) ? body.statusCode : 500;

  var data = this.formatters[this.contentType](this.req, this, body);

  if (log.isTraceEnabled())
    log.trace('%s format returing: %s', this.requestId, data);

  return data;
};

Response.prototype.toString = function toString() {
  var headers = '';
  var self = this;
  Object.keys(this.headers).forEach(function(k) {
    headers += k + ': ' + self.headers[k] + '\n';
  });
  return 'HTTP/1.1 ' + this.code + ' ' + http.STATUS_CODES[this.code] + '\n' +
    headers;
};
<|endoftext|>


<commit_before>var server = require("./server");
var tjbot = require('tjbot');
var constants = require('./config');
var AudioContext = require('web-audio-api').AudioContext
context = new AudioContext
var request = require("request");
var fs = require('fs');
var fileDir = process.cwd() + "/public/img"
var curImage = "";

// obtain our credentials from config.js
var credentials = constants.credentials;

// obtain user-specific config
var VOICE = constants.config.voice;
var WORKSPACEID = constants.config.conversationWorkspaceId;

// these are the hardware capabilities that TJ needs for this recipe
var hardware = ['microphone', 'speaker', 'led', 'servo'];

// Set up configuration paramters
var config = {
    verboseLogging: true, //enable console debugging
    servoPin: 7, // set servo pin
    cameraParams: {
        height: 720,
        width: 960,
        vflip: false,
        hflip: false
    } // setup my camera capture parameters
};

var listening = true;

// obtain our configs from config.js and merge with custom configs
config = Object.assign(constants.config, config);

// instantiate our TJBot!
var tj = new tjbot(hardware, config, credentials);

server.wss.on('connection', function connection(ws) {
    ws.on('message', function incoming(message) {
        message = JSON.parse(message)
        console.log("beee", (message));
        switch (message.event) {
            case 'wave':
                var prompt = "Waving my arm. Just for you. ";
                logSpeak("TJBot", prompt);
                wave(prompt)
                break;
            case 'dance':
                var prompt = "Sure. I'll play some music and dance.";
                logSpeak("TJBot", prompt);
                predance(prompt)
                break;
            case 'see':
                var prompt = "A moment. Let me look around.";
                logSpeak("TJBot", prompt);
                see(prompt)
                break;
            case 'seetext':
                var prompt = "Sure. Scanning for text in the image.";
                logSpeak("TJBot", prompt);
                seeText(prompt)
                break;
            case 'led':
                console.log("shinning led ", message.color)
                tj.shine(message.color)
                break;
            case 'speak':
                console.log("speaking ", message.value)
                logSpeak("you", message.value);
                converse(message.value);
                // tj.speak(message.value).then(function() {
                //     tj.shine("white");
                // });
                break;
            case 'listening':
                console.log("toggle listening", message.value)
                listening = message.value;
                break;

        }
    });

});

startListening();

function startListening() {

    tj.listen(function(msg) {
        if (listening) {
            logSpeak("you", msg);
            // send to the conversation service
            converse(msg);
        }
    });

}

function converse(msg) {
    tj.converse(WORKSPACEID, msg, function(response, responseText) {
        // speak the result
        response = response.object;
        if (response.output.text.length > 0) {
            //console.log(response)
            conversation_response = response.output.text[0];
            if (conversation_response != undefined) {
                var matchedIntent = response.intents[0].intent; // intent with the highest confidence
                var intentconfidence = response.intents[0].confidence;
                console.log("> intents : ", response.intents);

                if (intentconfidence > 0.5) {
                    tj.shine("green");
                    if (matchedIntent == "dance") {
                        predance("Sure, will play some music");
                        //dance();
                    } else if (matchedIntent == "wave") {
                        wave(conversation_response);
                    } else if (matchedIntent == "see") {
                        see(conversation_response);
                    } else if (matchedIntent == "off_topic") {
                        // do nothing
                    } else if (matchedIntent == "weather") {
                        getCordinates("Las vegas", "city", "US", "NV");
                    } else {
                        tj.speak(conversation_response).then(function() {
                            tj.shine("white");
                        });
                    }
                    if (matchedIntent != "off_topic") {
                        logSpeak("TJBot", conversation_response, matchedIntent + " (" + intentconfidence.toFixed(2) * 100 + "%)");
                    }

                } else {
                    tj.shine("red");
                    setTimeout(function() {
                        tj.shine("white");
                    }, 800);
                }

            } else {
                tj.shine("red");
                console.log("The response (output) text from your conversation is empty. Please check your conversation flow \n" + JSON.stringify(response))
            }
        } else {
            console.error("The conversation service did not return any response text.");
        }
        //console.log("conversation response", response)
    });
}

function setLED(color) {
    tj.shine(color)
}

function predance(conversation_response, intent) {
    //logSpeak("TJBot", conversation_response, intent);
    tj.speak(conversation_response).then(function() {
        dance("club.wav")
    });
}

function wave(conversation_response) {
    //logSpeak("TJBot", conversation_response);
    tj.speak(conversation_response).then(function() {
        // wave
        tj.wave();
        tj.wave();
        tj.shine("white");
    })
}

function seeText(prompt) {
    //logSpeak("TJBot", prompt);
    tj.speak(prompt).then(function() {
        curImage = Date.now() + ".jpg";
        filePath = fileDir + "/" + curImage;
        tj.captureImage(filePath).then(function(filePath) {
            logVision("tjbot", filePath)
            tj.callVisualRecognition("text", filePath).then(function(response) {
                console.log(" ... response .. ", response.description)
                if (response.description != null) {
                    logSpeak("TJBot", response.description);
                    tj.speak(response.description).then(function() {
                        tj.shine("white");
                    })
                }
            });
        })

    });
}

function see(conversation_response) {
    //logSpeak("TJBot", conversation_response);
    tj.speak(conversation_response).then(function() {
        curImage = Date.now() + ".jpg";
        filePath = fileDir + "/" + curImage;
        tj.captureImage(filePath).then(function(filePath) {
            logVision("tjbot", filePath)
            tj.callVisualRecognition("classify", filePath).then(function(response) {
                console.log(" ... response .. ", response.description)
                if (response.description != null) {
                    logSpeak("TJBot", response.description);
                    tj.speak(response.description).then(function() {
                        tj.shine("white");
                    })
                }
            });
        })

    });
}

/**
 * [dance play a soundFile and dance to its beats]
 * @param  {[type]} soundFile [soundfile to be decoded and danced to]
 * @return {[type]}           [description]
 */
function dance(soundFile) {
    // Decode the sound file to get its digital signal data
    var audioContext = new AudioContext
    fs.readFile(soundFile, function(err, buf) {
        if (err) throw err
        audioContext.decodeAudioData(buf, function(audioBuffer) {
            console.log("> finished decoding sound file ", soundFile);
            findPeaks(audioBuffer.getChannelData(0), audioBuffer.sampleRate, soundFile);
        }, function(err) {
            throw err
        })
    })

}

/**
 * [_findPeaks find peaks or high energy positions in audioBuffer data and move arm based on that to simulate dance.]
 * @param  {[type]} audioBuffer [decoded audio data]
 * @param  {[type]} sampleRate  [audio sample rate]
 * @return {[type]}             [description]
 */
function findPeaks(audioBuffer, sampleRate, soundFile) {
    var interval = 0.05 * 1000;
    var index = 0;
    var step = Math.round(sampleRate * (interval / 1000));
    var max = 0;
    var prevmax = 0;
    var prevdiffthreshold = 0.3;

    var sampleSound = setInterval(function() {
        if (index >= audioBuffer.length) {
            clearInterval(sampleSound);
            tj.shine("white");
            return;
        }
        for (var i = index; i < index + step; i++) {
            max = audioBuffer[i] > max ? audioBuffer[i].toFixed(1) : max;
        }
        // Spot a significant increase or peak? Wave Arm
        if (max - prevmax >= prevdiffthreshold) {
            // do some funky waving.
            var delay = 300;
            tj.raiseArm();
            setTimeout(function() {
                tj.lowerArm();
            }, delay);
        }
        prevmax = max;
        max = 0;
        index += step;
    }, interval);
    tj.playSound(soundFile);
}

function logVision(sender, imageurl) {
    console.log(imageurl)
    var message = {
        type: "vision",
        title: "What TJBot Sees",
        sender: sender,
        transcript: "picture taken",
        description: "",
        imageurl: "/img/" + imageurl,
        timestamp: Date.now(),
        tags: [{
            title: "visual recognition",
            url: "#"
        }, {
            title: "camera",
            url: "#"
        }],
        visiontags: [{
            title: "visual recognition",
            url: "#"
        }, {
            title: "camera",
            url: "#"
        }],
        confidence: 1
    }
    //console.log(message)
    server.sendEvent(message)
}

function logSpeak(sender, transcript, intent) {

    var message = {
        type: "speech",
        sender: sender,
        title: sender == "you" ? "What TJBot thinks you said" : "What TJBot says",
        transcript: transcript,
        intent: intent,
        description: "",
        timestamp: Date.now(),
        tags: [{
            title: "speech to text",
            url: "#"
        }, {
            title: "text to speech",
            url: "#"
        }, {
            title: "microphone",
            url: "#"
        }, {
            title: "speaker",
            url: "#"
        }],
        confidence: 1
    }
    //console.log(message)
    server.sendEvent(message)
}

function getCordinates(query, locationtype, countrycode, admindistrictcode) {
    query = query.replace(/ /g, "+")
    var url = "https://" + config.credentials.weather.username + ":" + config.credentials.weather.password + "@" + config.credentials.weather.host + ":" + config.credentials.weather.port + "/api/weather/v3/location/search?query=" + query + "&locationType=" + locationtype + "&countryCode=" + countrycode + "&adminDistrictCode=" + admindistrictcode + "&language=en-US";
    var options = {
        method: 'GET',
        url: url
    }
    request(options, function(error, response, body) {

        if (!error && response.statusCode == 200) {
            var result = JSON.parse(body)
            //console.log(result)
            var longitude = result.location.longitude[0];
            var latitude = result.location.latitude[0];
            //console.log(longitude, latitude);
            getWeather(longitude, latitude)
        } else {
            console.log("Error getting cordinates")
        }
    });

}

function getWeather(long, lat) {
    var url = "https://" + config.credentials.weather.username + ":" + config.credentials.weather.password + "@" + config.credentials.weather.host + ":" + config.credentials.weather.port + "/api/weather/v1/geocode/" + lat + "/" + long + "/observations.json?units=m&language=en-US";
    var options = {
        method: 'GET',
        url: url
    }
    request(options, function(error, response, body) {
        if (!error && response.statusCode == 200) {
            var result = JSON.parse(body)
            var ob = result.observation
            var location = ob.obs_name;
            var temp = ob.temp;
            var desc = ob.wx_phrase;
            var feelslike = ob.feels_like;
            var windspeed = ob.wspd + " km/h";
            var uv = ob.uv_desc;
            var alldesc = "The weather in " + location + " today is " + desc + " with a temperature of " + temp + " that feels more like " +
                feelslike + ". Wind speed is " + windspeed + " and UV is " + uv;
            console.log(alldesc);
            logSpeak("TJBot", alldesc);
            tj.speak(alldesc);
        } else {
            console.log("weather error")
        }
    });
}
<commit_msg>use latest version of tjbotlib<commit_after>var server = require("./server");
var tjbot = require('tjbot');
var constants = require('./config');
var AudioContext = require('web-audio-api').AudioContext
context = new AudioContext
var request = require("request");
var fs = require('fs');
var fileDir = process.cwd() + "/public/img"
var curImage = "";

// obtain our credentials from config.js
var credentials = constants.credentials;

// obtain user-specific config
var VOICE = constants.config.voice;
var WORKSPACEID = constants.config.conversationWorkspaceId;

// these are the hardware capabilities that TJ needs for this recipe
var hardware = ['microphone', 'speaker', 'led', 'servo'];

// Set up configuration paramters
var config = {
    verboseLogging: true, //enable console debugging
    servoPin: 7, // set servo pin
    cameraParams: {
        height: 720,
        width: 960,
        vflip: false,
        hflip: false
    } // setup my camera capture parameters
};

var listening = true;

// obtain our configs from config.js and merge with custom configs
config = Object.assign(constants.config, config);

// instantiate our TJBot!
var tj = new tjbot(hardware, config, credentials);

server.wss.on('connection', function connection(ws) {
    ws.on('message', function incoming(message) {
        message = JSON.parse(message)
        console.log("beee", (message));
        switch (message.event) {
            case 'wave':
                var prompt = "Waving my arm. Just for you. ";
                logSpeak("TJBot", prompt);
                wave(prompt)
                break;
            case 'dance':
                var prompt = "Sure. I'll play some music and dance.";
                logSpeak("TJBot", prompt);
                predance(prompt)
                break;
            case 'see':
                var prompt = "A moment. Let me look around.";
                logSpeak("TJBot", prompt);
                see(prompt)
                break;
            case 'seetext':
                var prompt = "Sure. Scanning for text in the image.";
                logSpeak("TJBot", prompt);
                seeText(prompt)
                break;
            case 'led':
                console.log("shinning led ", message.color)
                tj.shine(message.color)
                break;
            case 'speak':
                console.log("speaking ", message.value)
                logSpeak("you", message.value);
                converse(message.value);
                // tj.speak(message.value).then(function() {
                //     tj.shine("white");
                // });
                break;
            case 'listening':
                console.log("toggle listening", message.value)
                listening = message.value;
                break;

        }
    });

});

startListening();

function startListening() {

    tj.listen(function(msg) {
        if (listening) {
            logSpeak("you", msg);
            // send to the conversation service
            converse(msg);
        }
    });

}

function converse(msg) {
    tj.converse(WORKSPACEID, msg, function(response, responseText) {
        // speak the result
        response = response.object;
        if (response.output.text.length > 0) {
            //console.log(response)
            conversation_response = response.output.text[0];
            if (conversation_response != undefined) {
                var matchedIntent = response.intents[0].intent; // intent with the highest confidence
                var intentconfidence = response.intents[0].confidence;
                console.log("> intents : ", response.intents);

                if (intentconfidence > 0.5) {
                    tj.shine("green");
                    if (matchedIntent == "dance") {
                        predance("Sure, will play some music");
                        //dance();
                    } else if (matchedIntent == "wave") {
                        wave(conversation_response);
                    } else if (matchedIntent == "see") {
                        see(conversation_response);
                    } else if (matchedIntent == "off_topic") {
                        // do nothing
                    } else if (matchedIntent == "weather") {
                        getCordinates("Las vegas", "city", "US", "NV");
                    } else {
                        tj.speak(conversation_response).then(function() {
                            tj.shine("white");
                        });
                    }
                    if (matchedIntent != "off_topic") {
                        logSpeak("TJBot", conversation_response, matchedIntent + " (" + intentconfidence.toFixed(2) * 100 + "%)");
                    }

                } else {
                    tj.shine("red");
                    setTimeout(function() {
                        tj.shine("white");
                    }, 800);
                }

            } else {
                tj.shine("red");
                console.log("The response (output) text from your conversation is empty. Please check your conversation flow \n" + JSON.stringify(response))
            }
        } else {
            console.error("The conversation service did not return any response text.");
        }
        //console.log("conversation response", response)
    });
}

function setLED(color) {
    tj.shine(color)
}

function predance(conversation_response, intent) {
    //logSpeak("TJBot", conversation_response, intent);
    tj.speak(conversation_response).then(function() {
        dance("club.wav")
    });
}

function wave(conversation_response) {
    //logSpeak("TJBot", conversation_response);
    tj.speak(conversation_response).then(function() {
        // wave
        tj.wave();
        tj.wave();
        tj.shine("white");
    })
}

function seeText(prompt) {
    //logSpeak("TJBot", prompt);
    tj.speak(prompt).then(function() {
        curImage = Date.now() + ".jpg";
        filePath = fileDir + "/" + curImage;
        tj.captureImage(filePath).then(function(filePath) {
            logVision("tjbot", curImage)
            tj.callVisualRecognition("text", filePath).then(function(response) {
                console.log(" ... response .. ", response.description)
                if (response.description != null) {
                    logSpeak("TJBot", response.description);
                    tj.speak(response.description).then(function() {
                        tj.shine("white");
                    })
                }
            });
        })

    });
}

function see(conversation_response) {
    //logSpeak("TJBot", conversation_response);
    tj.speak(conversation_response).then(function() {
        curImage = Date.now() + ".jpg";
        filePath = fileDir + "/" + curImage;
        tj.captureImage(filePath).then(function(filePath) {
            logVision("tjbot", curImage)
            tj.callVisualRecognition("classify", filePath).then(function(response) {
                console.log(" ... response .. ", response.description)
                if (response.description != null) {
                    logSpeak("TJBot", response.description);
                    tj.speak(response.description).then(function() {
                        tj.shine("white");
                    })
                }
            });
        })

    });
}

/**
 * [dance play a soundFile and dance to its beats]
 * @param  {[type]} soundFile [soundfile to be decoded and danced to]
 * @return {[type]}           [description]
 */
function dance(soundFile) {
    // Decode the sound file to get its digital signal data
    var audioContext = new AudioContext
    fs.readFile(soundFile, function(err, buf) {
        if (err) throw err
        audioContext.decodeAudioData(buf, function(audioBuffer) {
            console.log("> finished decoding sound file ", soundFile);
            findPeaks(audioBuffer.getChannelData(0), audioBuffer.sampleRate, soundFile);
        }, function(err) {
            throw err
        })
    })

}

/**
 * [_findPeaks find peaks or high energy positions in audioBuffer data and move arm based on that to simulate dance.]
 * @param  {[type]} audioBuffer [decoded audio data]
 * @param  {[type]} sampleRate  [audio sample rate]
 * @return {[type]}             [description]
 */
function findPeaks(audioBuffer, sampleRate, soundFile) {
    var interval = 0.05 * 1000;
    var index = 0;
    var step = Math.round(sampleRate * (interval / 1000));
    var max = 0;
    var prevmax = 0;
    var prevdiffthreshold = 0.3;

    var sampleSound = setInterval(function() {
        if (index >= audioBuffer.length) {
            clearInterval(sampleSound);
            tj.shine("white");
            return;
        }
        for (var i = index; i < index + step; i++) {
            max = audioBuffer[i] > max ? audioBuffer[i].toFixed(1) : max;
        }
        // Spot a significant increase or peak? Wave Arm
        if (max - prevmax >= prevdiffthreshold) {
            // do some funky waving.
            var delay = 300;
            tj.raiseArm();
            setTimeout(function() {
                tj.lowerArm();
            }, delay);
        }
        prevmax = max;
        max = 0;
        index += step;
    }, interval);
    tj.playSound(soundFile);
}

function logVision(sender, imageurl) {
    console.log(imageurl)
    var message = {
        type: "vision",
        title: "What TJBot Sees",
        sender: sender,
        transcript: "picture taken",
        description: "",
        imageurl: "/img/" + imageurl,
        timestamp: Date.now(),
        tags: [{
            title: "visual recognition",
            url: "#"
        }, {
            title: "camera",
            url: "#"
        }],
        visiontags: [{
            title: "visual recognition",
            url: "#"
        }, {
            title: "camera",
            url: "#"
        }],
        confidence: 1
    }
    //console.log(message)
    server.sendEvent(message)
}

function logSpeak(sender, transcript, intent) {

    var message = {
        type: "speech",
        sender: sender,
        title: sender == "you" ? "What TJBot thinks you said" : "What TJBot says",
        transcript: transcript,
        intent: intent,
        description: "",
        timestamp: Date.now(),
        tags: [{
            title: "speech to text",
            url: "#"
        }, {
            title: "text to speech",
            url: "#"
        }, {
            title: "microphone",
            url: "#"
        }, {
            title: "speaker",
            url: "#"
        }],
        confidence: 1
    }
    //console.log(message)
    server.sendEvent(message)
}

function getCordinates(query, locationtype, countrycode, admindistrictcode) {
    query = query.replace(/ /g, "+")
    var url = "https://" + config.credentials.weather.username + ":" + config.credentials.weather.password + "@" + config.credentials.weather.host + ":" + config.credentials.weather.port + "/api/weather/v3/location/search?query=" + query + "&locationType=" + locationtype + "&countryCode=" + countrycode + "&adminDistrictCode=" + admindistrictcode + "&language=en-US";
    var options = {
        method: 'GET',
        url: url
    }
    request(options, function(error, response, body) {

        if (!error && response.statusCode == 200) {
            var result = JSON.parse(body)
            //console.log(result)
            var longitude = result.location.longitude[0];
            var latitude = result.location.latitude[0];
            //console.log(longitude, latitude);
            getWeather(longitude, latitude)
        } else {
            console.log("Error getting cordinates")
        }
    });

}

function getWeather(long, lat) {
    var url = "https://" + config.credentials.weather.username + ":" + config.credentials.weather.password + "@" + config.credentials.weather.host + ":" + config.credentials.weather.port + "/api/weather/v1/geocode/" + lat + "/" + long + "/observations.json?units=m&language=en-US";
    var options = {
        method: 'GET',
        url: url
    }
    request(options, function(error, response, body) {
        if (!error && response.statusCode == 200) {
            var result = JSON.parse(body)
            var ob = result.observation
            var location = ob.obs_name;
            var temp = ob.temp;
            var desc = ob.wx_phrase;
            var feelslike = ob.feels_like;
            var windspeed = ob.wspd + " km/h";
            var uv = ob.uv_desc;
            var alldesc = "The weather in " + location + " today is " + desc + " with a temperature of " + temp + " that feels more like " +
                feelslike + ". Wind speed is " + windspeed + " and UV is " + uv;
            console.log(alldesc);
            logSpeak("TJBot", alldesc);
            tj.speak(alldesc);
        } else {
            console.log("weather error")
        }
    });
}
<|endoftext|>


<commit_before>
'use strict';

/**
 * Module dependencies.
 */

const contentDisposition = require('content-disposition');
const ensureErrorHandler = require('error-inject');
const getType = require('cache-content-type');
const onFinish = require('on-finished');
const isJSON = require('koa-is-json');
const escape = require('escape-html');
const typeis = require('type-is').is;
const statuses = require('statuses');
const destroy = require('destroy');
const assert = require('assert');
const extname = require('path').extname;
const vary = require('vary');
const only = require('only');
const util = require('util');
const encodeUrl = require('encodeurl');

/**
 * Prototype.
 */

module.exports = {

  /**
   * Return the request socket.
   *
   * @return {Connection}
   * @api public
   */

  get socket() {
    return this.res.socket;
  },

  /**
   * Return response header.
   *
   * @return {Object}
   * @api public
   */

  get header() {
    const { res } = this;
    return typeof res.getHeaders === 'function'
      ? res.getHeaders()
      : res._headers || {}; // Node < 7.7
  },

  /**
   * Return response header, alias as response.header
   *
   * @return {Object}
   * @api public
   */

  get headers() {
    return this.header;
  },

  /**
   * Get response status code.
   *
   * @return {Number}
   * @api public
   */

  get status() {
    return this.res.statusCode;
  },

  /**
   * Set response status code.
   *
   * @param {Number} code
   * @api public
   */

  set status(code) {
    if (this.headerSent) return;

    assert(Number.isInteger(code), 'status code must be a number');
    assert(code >= 100 && code <= 999, `invalid status code: ${code}`);
    this._explicitStatus = true;
    this.res.statusCode = code;
    if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code];
    if (this.body && statuses.empty[code]) this.body = null;
  },

  /**
   * Get response status message
   *
   * @return {String}
   * @api public
   */

  get message() {
    return this.res.statusMessage || statuses[this.status];
  },

  /**
   * Set response status message
   *
   * @param {String} msg
   * @api public
   */

  set message(msg) {
    this.res.statusMessage = msg;
  },

  /**
   * Get response body.
   *
   * @return {Mixed}
   * @api public
   */

  get body() {
    return this._body;
  },

  /**
   * Set response body.
   *
   * @param {String|Buffer|Object|Stream} val
   * @api public
   */

  set body(val) {
    const original = this._body;
    this._body = val;

    // no content
    if (null == val) {
      if (!statuses.empty[this.status]) this.status = 204;
      this.remove('Content-Type');
      this.remove('Content-Length');
      this.remove('Transfer-Encoding');
      return;
    }

    // set the status
    if (!this._explicitStatus) this.status = 200;

    // set the content-type only if not yet set
    const setType = !this.has('Content-Type');

    // string
    if ('string' == typeof val) {
      if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text';
      this.length = Buffer.byteLength(val);
      return;
    }

    // buffer
    if (Buffer.isBuffer(val)) {
      if (setType) this.type = 'bin';
      this.length = val.length;
      return;
    }

    // stream
    if ('function' == typeof val.pipe) {
      onFinish(this.res, destroy.bind(null, val));
      ensureErrorHandler(val, err => this.ctx.onerror(err));

      // overwriting
      if (null != original && original != val) this.remove('Content-Length');

      if (setType) this.type = 'bin';
      return;
    }

    // json
    this.remove('Content-Length');
    this.type = 'json';
  },

  /**
   * Set Content-Length field to `n`.
   *
   * @param {Number} n
   * @api public
   */

  set length(n) {
    this.set('Content-Length', n);
  },

  /**
   * Return parsed response Content-Length when present.
   *
   * @return {Number}
   * @api public
   */

  get length() {
    const len = this.header['content-length'];
    const body = this.body;

    if (null == len) {
      if (!body) return;
      if ('string' == typeof body) return Buffer.byteLength(body);
      if (Buffer.isBuffer(body)) return body.length;
      if (isJSON(body)) return Buffer.byteLength(JSON.stringify(body));
      return;
    }

    return Math.trunc(len) || 0;
  },

  /**
   * Check if a header has been written to the socket.
   *
   * @return {Boolean}
   * @api public
   */

  get headerSent() {
    return this.res.headersSent;
  },

  /**
   * Vary on `field`.
   *
   * @param {String} field
   * @api public
   */

  vary(field) {
    if (this.headerSent) return;

    vary(this.res, field);
  },

  /**
   * Perform a 302 redirect to `url`.
   *
   * The string "back" is special-cased
   * to provide Referrer support, when Referrer
   * is not present `alt` or "/" is used.
   *
   * Examples:
   *
   *    this.redirect('back');
   *    this.redirect('back', '/index.html');
   *    this.redirect('/login');
   *    this.redirect('http://google.com');
   *
   * @param {String} url
   * @param {String} [alt]
   * @api public
   */

  redirect(url, alt) {
    // location
    if ('back' == url) url = this.ctx.get('Referrer') || alt || '/';
    this.set('Location', encodeUrl(url));

    // status
    if (!statuses.redirect[this.status]) this.status = 302;

    // html
    if (this.ctx.accepts('html')) {
      url = escape(url);
      this.type = 'text/html; charset=utf-8';
      this.body = `Redirecting to <a href="${url}">${url}</a>.`;
      return;
    }

    // text
    this.type = 'text/plain; charset=utf-8';
    this.body = `Redirecting to ${url}.`;
  },

  /**
   * Set Content-Disposition header to "attachment" with optional `filename`.
   *
   * @param {String} filename
   * @api public
   */

  attachment(filename, options) {
    if (filename) this.type = extname(filename);
    this.set('Content-Disposition', contentDisposition(filename, options));
  },

  /**
   * Set Content-Type response header with `type` through `mime.lookup()`
   * when it does not contain a charset.
   *
   * Examples:
   *
   *     this.type = '.html';
   *     this.type = 'html';
   *     this.type = 'json';
   *     this.type = 'application/json';
   *     this.type = 'png';
   *
   * @param {String} type
   * @api public
   */

  set type(type) {
    type = getType(type);
    if (type) {
      this.set('Content-Type', type);
    } else {
      this.remove('Content-Type');
    }
  },

  /**
   * Set the Last-Modified date using a string or a Date.
   *
   *     this.response.lastModified = new Date();
   *     this.response.lastModified = '2013-09-13';
   *
   * @param {String|Date} type
   * @api public
   */

  set lastModified(val) {
    if ('string' == typeof val) val = new Date(val);
    this.set('Last-Modified', val.toUTCString());
  },

  /**
   * Get the Last-Modified date in Date form, if it exists.
   *
   * @return {Date}
   * @api public
   */

  get lastModified() {
    const date = this.get('last-modified');
    if (date) return new Date(date);
  },

  /**
   * Set the ETag of a response.
   * This will normalize the quotes if necessary.
   *
   *     this.response.etag = 'md5hashsum';
   *     this.response.etag = '"md5hashsum"';
   *     this.response.etag = 'W/"123456789"';
   *
   * @param {String} etag
   * @api public
   */

  set etag(val) {
    if (!/^(W\/)?"/.test(val)) val = `"${val}"`;
    this.set('ETag', val);
  },

  /**
   * Get the ETag of a response.
   *
   * @return {String}
   * @api public
   */

  get etag() {
    return this.get('ETag');
  },

  /**
   * Return the response mime type void of
   * parameters such as "charset".
   *
   * @return {String}
   * @api public
   */

  get type() {
    const type = this.get('Content-Type');
    if (!type) return '';
    return type.split(';', 1)[0];
  },

  /**
   * Check whether the response is one of the listed types.
   * Pretty much the same as `this.request.is()`.
   *
   * @param {String|String[]} [type]
   * @param {String[]} [types]
   * @return {String|false}
   * @api public
   */

  is(type, ...types) {
    return typeis(this.type, type, ...types);
  },

  /**
   * Return response header.
   *
   * Examples:
   *
   *     this.get('Content-Type');
   *     // => "text/plain"
   *
   *     this.get('content-type');
   *     // => "text/plain"
   *
   * @param {String} field
   * @return {String}
   * @api public
   */

  get(field) {
    return this.header[field.toLowerCase()] || '';
  },

  /**
   * Returns true if the header identified by name is currently set in the outgoing headers.
   * The header name matching is case-insensitive.
   *
   * Examples:
   *
   *     this.has('Content-Type');
   *     // => true
   *
   *     this.get('content-type');
   *     // => true
   *
   * @param {String} field
   * @return {boolean}
   * @api public
   */
  has(field) {
    return typeof this.res.hasHeader === 'function'
      ? this.res.hasHeader(field)
      // Node < 7.7
      : field.toLowerCase() in this.headers;
  },

  /**
   * Set header `field` to `val`, or pass
   * an object of header fields.
   *
   * Examples:
   *
   *    this.set('Foo', ['bar', 'baz']);
   *    this.set('Accept', 'application/json');
   *    this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
   *
   * @param {String|Object|Array} field
   * @param {String} val
   * @api public
   */

  set(field, val) {
    if (this.headerSent) return;

    if (2 == arguments.length) {
      if (Array.isArray(val)) val = val.map(v => typeof v === 'string' ? v : String(v));
      else if (typeof val !== 'string') val = String(val);
      this.res.setHeader(field, val);
    } else {
      for (const key in field) {
        this.set(key, field[key]);
      }
    }
  },

  /**
   * Append additional header `field` with value `val`.
   *
   * Examples:
   *
   * ```
   * this.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
   * this.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
   * this.append('Warning', '199 Miscellaneous warning');
   * ```
   *
   * @param {String} field
   * @param {String|Array} val
   * @api public
   */

  append(field, val) {
    const prev = this.get(field);

    if (prev) {
      val = Array.isArray(prev)
        ? prev.concat(val)
        : [prev].concat(val);
    }

    return this.set(field, val);
  },

  /**
   * Remove header `field`.
   *
   * @param {String} name
   * @api public
   */

  remove(field) {
    if (this.headerSent) return;

    this.res.removeHeader(field);
  },

  /**
   * Checks if the request is writable.
   * Tests for the existence of the socket
   * as node sometimes does not set it.
   *
   * @return {Boolean}
   * @api private
   */

  get writable() {
    // can't write any more after response finished
    if (this.res.finished) return false;

    const socket = this.res.socket;
    // There are already pending outgoing res, but still writable
    // https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486
    if (!socket) return true;
    return socket.writable;
  },

  /**
   * Inspect implementation.
   *
   * @return {Object}
   * @api public
   */

  inspect() {
    if (!this.res) return;
    const o = this.toJSON();
    o.body = this.body;
    return o;
  },

  /**
   * Return JSON representation.
   *
   * @return {Object}
   * @api public
   */

  toJSON() {
    return only(this, [
      'status',
      'message',
      'header'
    ]);
  },

  /**
   * Flush any set headers, and begin the body
   */
  flushHeaders() {
    this.res.flushHeaders();
  }
};

/**
 * Custom inspection implementation for newer Node.js versions.
 *
 * @return {Object}
 * @api public
 */
if (util.inspect.custom) {
  module.exports[util.inspect.custom] = module.exports.inspect;
}
<commit_msg>chore: remove isJSON in res.length (#1399)<commit_after>
'use strict';

/**
 * Module dependencies.
 */

const contentDisposition = require('content-disposition');
const ensureErrorHandler = require('error-inject');
const getType = require('cache-content-type');
const onFinish = require('on-finished');
const escape = require('escape-html');
const typeis = require('type-is').is;
const statuses = require('statuses');
const destroy = require('destroy');
const assert = require('assert');
const extname = require('path').extname;
const vary = require('vary');
const only = require('only');
const util = require('util');
const encodeUrl = require('encodeurl');
const Stream = require('stream');

/**
 * Prototype.
 */

module.exports = {

  /**
   * Return the request socket.
   *
   * @return {Connection}
   * @api public
   */

  get socket() {
    return this.res.socket;
  },

  /**
   * Return response header.
   *
   * @return {Object}
   * @api public
   */

  get header() {
    const { res } = this;
    return typeof res.getHeaders === 'function'
      ? res.getHeaders()
      : res._headers || {}; // Node < 7.7
  },

  /**
   * Return response header, alias as response.header
   *
   * @return {Object}
   * @api public
   */

  get headers() {
    return this.header;
  },

  /**
   * Get response status code.
   *
   * @return {Number}
   * @api public
   */

  get status() {
    return this.res.statusCode;
  },

  /**
   * Set response status code.
   *
   * @param {Number} code
   * @api public
   */

  set status(code) {
    if (this.headerSent) return;

    assert(Number.isInteger(code), 'status code must be a number');
    assert(code >= 100 && code <= 999, `invalid status code: ${code}`);
    this._explicitStatus = true;
    this.res.statusCode = code;
    if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code];
    if (this.body && statuses.empty[code]) this.body = null;
  },

  /**
   * Get response status message
   *
   * @return {String}
   * @api public
   */

  get message() {
    return this.res.statusMessage || statuses[this.status];
  },

  /**
   * Set response status message
   *
   * @param {String} msg
   * @api public
   */

  set message(msg) {
    this.res.statusMessage = msg;
  },

  /**
   * Get response body.
   *
   * @return {Mixed}
   * @api public
   */

  get body() {
    return this._body;
  },

  /**
   * Set response body.
   *
   * @param {String|Buffer|Object|Stream} val
   * @api public
   */

  set body(val) {
    const original = this._body;
    this._body = val;

    // no content
    if (null == val) {
      if (!statuses.empty[this.status]) this.status = 204;
      this.remove('Content-Type');
      this.remove('Content-Length');
      this.remove('Transfer-Encoding');
      return;
    }

    // set the status
    if (!this._explicitStatus) this.status = 200;

    // set the content-type only if not yet set
    const setType = !this.has('Content-Type');

    // string
    if ('string' == typeof val) {
      if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text';
      this.length = Buffer.byteLength(val);
      return;
    }

    // buffer
    if (Buffer.isBuffer(val)) {
      if (setType) this.type = 'bin';
      this.length = val.length;
      return;
    }

    // stream
    if ('function' == typeof val.pipe) {
      onFinish(this.res, destroy.bind(null, val));
      ensureErrorHandler(val, err => this.ctx.onerror(err));

      // overwriting
      if (null != original && original != val) this.remove('Content-Length');

      if (setType) this.type = 'bin';
      return;
    }

    // json
    this.remove('Content-Length');
    this.type = 'json';
  },

  /**
   * Set Content-Length field to `n`.
   *
   * @param {Number} n
   * @api public
   */

  set length(n) {
    this.set('Content-Length', n);
  },

  /**
   * Return parsed response Content-Length when present.
   *
   * @return {Number}
   * @api public
   */

  get length() {
    if (this.has('Content-Length')) {
      return parseInt(this.get('Content-Length'), 10) || 0;
    }

    const { body } = this;
    if (!body || body instanceof Stream) return undefined;
    if ('string' === typeof body) return Buffer.byteLength(body);
    if (Buffer.isBuffer(body)) return body.length;
    return Buffer.byteLength(JSON.stringify(body));
  },

  /**
   * Check if a header has been written to the socket.
   *
   * @return {Boolean}
   * @api public
   */

  get headerSent() {
    return this.res.headersSent;
  },

  /**
   * Vary on `field`.
   *
   * @param {String} field
   * @api public
   */

  vary(field) {
    if (this.headerSent) return;

    vary(this.res, field);
  },

  /**
   * Perform a 302 redirect to `url`.
   *
   * The string "back" is special-cased
   * to provide Referrer support, when Referrer
   * is not present `alt` or "/" is used.
   *
   * Examples:
   *
   *    this.redirect('back');
   *    this.redirect('back', '/index.html');
   *    this.redirect('/login');
   *    this.redirect('http://google.com');
   *
   * @param {String} url
   * @param {String} [alt]
   * @api public
   */

  redirect(url, alt) {
    // location
    if ('back' == url) url = this.ctx.get('Referrer') || alt || '/';
    this.set('Location', encodeUrl(url));

    // status
    if (!statuses.redirect[this.status]) this.status = 302;

    // html
    if (this.ctx.accepts('html')) {
      url = escape(url);
      this.type = 'text/html; charset=utf-8';
      this.body = `Redirecting to <a href="${url}">${url}</a>.`;
      return;
    }

    // text
    this.type = 'text/plain; charset=utf-8';
    this.body = `Redirecting to ${url}.`;
  },

  /**
   * Set Content-Disposition header to "attachment" with optional `filename`.
   *
   * @param {String} filename
   * @api public
   */

  attachment(filename, options) {
    if (filename) this.type = extname(filename);
    this.set('Content-Disposition', contentDisposition(filename, options));
  },

  /**
   * Set Content-Type response header with `type` through `mime.lookup()`
   * when it does not contain a charset.
   *
   * Examples:
   *
   *     this.type = '.html';
   *     this.type = 'html';
   *     this.type = 'json';
   *     this.type = 'application/json';
   *     this.type = 'png';
   *
   * @param {String} type
   * @api public
   */

  set type(type) {
    type = getType(type);
    if (type) {
      this.set('Content-Type', type);
    } else {
      this.remove('Content-Type');
    }
  },

  /**
   * Set the Last-Modified date using a string or a Date.
   *
   *     this.response.lastModified = new Date();
   *     this.response.lastModified = '2013-09-13';
   *
   * @param {String|Date} type
   * @api public
   */

  set lastModified(val) {
    if ('string' == typeof val) val = new Date(val);
    this.set('Last-Modified', val.toUTCString());
  },

  /**
   * Get the Last-Modified date in Date form, if it exists.
   *
   * @return {Date}
   * @api public
   */

  get lastModified() {
    const date = this.get('last-modified');
    if (date) return new Date(date);
  },

  /**
   * Set the ETag of a response.
   * This will normalize the quotes if necessary.
   *
   *     this.response.etag = 'md5hashsum';
   *     this.response.etag = '"md5hashsum"';
   *     this.response.etag = 'W/"123456789"';
   *
   * @param {String} etag
   * @api public
   */

  set etag(val) {
    if (!/^(W\/)?"/.test(val)) val = `"${val}"`;
    this.set('ETag', val);
  },

  /**
   * Get the ETag of a response.
   *
   * @return {String}
   * @api public
   */

  get etag() {
    return this.get('ETag');
  },

  /**
   * Return the response mime type void of
   * parameters such as "charset".
   *
   * @return {String}
   * @api public
   */

  get type() {
    const type = this.get('Content-Type');
    if (!type) return '';
    return type.split(';', 1)[0];
  },

  /**
   * Check whether the response is one of the listed types.
   * Pretty much the same as `this.request.is()`.
   *
   * @param {String|String[]} [type]
   * @param {String[]} [types]
   * @return {String|false}
   * @api public
   */

  is(type, ...types) {
    return typeis(this.type, type, ...types);
  },

  /**
   * Return response header.
   *
   * Examples:
   *
   *     this.get('Content-Type');
   *     // => "text/plain"
   *
   *     this.get('content-type');
   *     // => "text/plain"
   *
   * @param {String} field
   * @return {String}
   * @api public
   */

  get(field) {
    return this.header[field.toLowerCase()] || '';
  },

  /**
   * Returns true if the header identified by name is currently set in the outgoing headers.
   * The header name matching is case-insensitive.
   *
   * Examples:
   *
   *     this.has('Content-Type');
   *     // => true
   *
   *     this.get('content-type');
   *     // => true
   *
   * @param {String} field
   * @return {boolean}
   * @api public
   */
  has(field) {
    return typeof this.res.hasHeader === 'function'
      ? this.res.hasHeader(field)
      // Node < 7.7
      : field.toLowerCase() in this.headers;
  },

  /**
   * Set header `field` to `val`, or pass
   * an object of header fields.
   *
   * Examples:
   *
   *    this.set('Foo', ['bar', 'baz']);
   *    this.set('Accept', 'application/json');
   *    this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
   *
   * @param {String|Object|Array} field
   * @param {String} val
   * @api public
   */

  set(field, val) {
    if (this.headerSent) return;

    if (2 == arguments.length) {
      if (Array.isArray(val)) val = val.map(v => typeof v === 'string' ? v : String(v));
      else if (typeof val !== 'string') val = String(val);
      this.res.setHeader(field, val);
    } else {
      for (const key in field) {
        this.set(key, field[key]);
      }
    }
  },

  /**
   * Append additional header `field` with value `val`.
   *
   * Examples:
   *
   * ```
   * this.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
   * this.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
   * this.append('Warning', '199 Miscellaneous warning');
   * ```
   *
   * @param {String} field
   * @param {String|Array} val
   * @api public
   */

  append(field, val) {
    const prev = this.get(field);

    if (prev) {
      val = Array.isArray(prev)
        ? prev.concat(val)
        : [prev].concat(val);
    }

    return this.set(field, val);
  },

  /**
   * Remove header `field`.
   *
   * @param {String} name
   * @api public
   */

  remove(field) {
    if (this.headerSent) return;

    this.res.removeHeader(field);
  },

  /**
   * Checks if the request is writable.
   * Tests for the existence of the socket
   * as node sometimes does not set it.
   *
   * @return {Boolean}
   * @api private
   */

  get writable() {
    // can't write any more after response finished
    if (this.res.finished) return false;

    const socket = this.res.socket;
    // There are already pending outgoing res, but still writable
    // https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486
    if (!socket) return true;
    return socket.writable;
  },

  /**
   * Inspect implementation.
   *
   * @return {Object}
   * @api public
   */

  inspect() {
    if (!this.res) return;
    const o = this.toJSON();
    o.body = this.body;
    return o;
  },

  /**
   * Return JSON representation.
   *
   * @return {Object}
   * @api public
   */

  toJSON() {
    return only(this, [
      'status',
      'message',
      'header'
    ]);
  },

  /**
   * Flush any set headers, and begin the body
   */
  flushHeaders() {
    this.res.flushHeaders();
  }
};

/**
 * Custom inspection implementation for newer Node.js versions.
 *
 * @return {Object}
 * @api public
 */
if (util.inspect.custom) {
  module.exports[util.inspect.custom] = module.exports.inspect;
}
<|endoftext|>