src/services/$Response.js
/**
* @module $Response.js
* @author Joe Groseclose <@benderTheCrime>
* @date 8/16/2015
*/
// System Modules
import util from 'util';
import {blue} from 'chalk';
import {
default as $Injector,
$injectionBinder
} from 'angie-injector';
import $LogProvider from 'angie-log';
// Angie Modules
import {config} from '../Config';
import app from '../Angie';
import $CacheFactory from '../factories/$CacheFactory';
import {
$templateCache,
$$templateLoader,
$resourceLoader
} from '../factories/$TemplateCache';
import $compile from '../factories/$Compile';
import {default as $MimeType} from '../util/$MimeTypeProvider';
import {$FileUtil} from '../util/Util';
const RESPONSE_HEADER_MESSAGES = $Injector.get('RESPONSE_HEADER_MESSAGES');
/**
* @desc The $Response class controls all of the content contained in the
* response from the Angie application. This is an extended NodeJS http/https
* createServer response and is responsible for storing this response and the
* content associated with the response. It can be required using a module
* import, but probably should not be unless it is being subclassed for a
* dependency package. It can also be used as an injected provider using
* `$request`.
* @since 0.4.0
* @access public
* @example $Injector.get('$response');
*/
class $Response {
constructor(response) {
// Define $Response based instance of createServer.prototype.response
this.response = response;
// Define the Angie content string
this.response.content = '';
}
}
/**
* @desc BaseResponse defines the default Angie response. It is responsible for
* serving the default response and setting up the headers associated with the
* default response.
* @todo Move Content-Type resolution to $Response constructor
* @since 0.4.0
* @access private
*/
class BaseResponse {
constructor() {
let request,
contentType;
[ request, this.response ] = $Injector.get('$request', '$response');
// Set the route and otherwise
[
this.path,
this.route,
this.otherwise
] = [
request.path,
request.route,
request.otherwise
];
// Parse out the response content type
contentType = request.headers ? request.headers.accept : null;
if (contentType && contentType.indexOf(',') > -1) {
contentType = contentType.split(',')[0];
} else {
contentType = $MimeType.fromPath(request.path);
}
// Set the response headers
this.response.$headers = { 'Content-Type': contentType };
}
/**
* @desc Sets up the headers associated with the Asset Response
* @since 0.4.0
* @access private
*/
head(code = 200) {
this.response.statusCode = code;
for (let header in this.response.$headers) {
this.response.setHeader(header, this.response.$headers[ header ]);
}
return this;
}
/**
* @desc Loads the default Angie template html file, `index.html`, and
* writes the file to the response.
* @since 0.4.0
* @access private
*/
write() {
let me = this;
return new Promise(function(resolve) {
me.writeSync();
resolve();
});
}
/**
* @desc Loads the default Angie template html file, `index.html`, and
* writes the file to the response synchronously
* @since 0.4.0
* @access private
*/
writeSync() {
this.response.write($$templateLoader('html/index.html'));
}
}
/**
* @desc AssetResponse defines any Angie response that has a path which can be
* mapped to a path in the Angie `staticDir`s which could not be routed via a
* controller. It is responsible for serving the asset response and setting up
* the headers associated with the served asset.
* @since 0.4.0
* @access private
* @extends {BaseResponse}
*/
class AssetResponse extends BaseResponse {
constructor() {
super();
// Set the content type based on the asset path
this.path = $Injector.get('$request').path;
}
/**
* @desc Sets up the headers associated with the AssetResponse
* @since 0.4.0
* @access private
*/
head() {
return super.head();
}
/**
* @desc Finds the asset and writes it to the response.
* @since 0.4.0
* @access private
*/
write() {
let assetCache = new $CacheFactory('staticAssets'),
asset = this.response.content =
assetCache.get(this.path) ||
$$templateLoader(this.path, 'static') || undefined,
me = this;
return new Promise(function(resolve) {
if (asset) {
if (
config.hasOwnProperty('cacheStaticAssets') &&
config.cacheStaticAssets === true
) {
assetCache.put(me.path, asset);
}
me.response.write(asset);
} else {
return new UnknownResponse().head().write();
}
resolve();
});
}
/**
* @desc Determines whether or not the response has an asset to which it can
* be associated.
* @param {string} path The relative url of the asset path from the
* AngieFile.json staticDirs
* @returns {boolean} Does the relative staticDirs path exist
* @since 0.4.0
* @access private
*/
static $isRoutedAssetResourceResponse(path) {
return config.staticDirs.some(
(v) => !!$FileUtil.find(v, path)
);
}
}
/**
* @desc ControllerResponse defines any Angie response that has a path which is
* associated with a template or template path. It is responsible for calling
* the controller and any post-processed templating.
* @since 0.4.0
* @access private
* @extends {BaseResponse}
*/
class ControllerResponse extends BaseResponse {
constructor() {
super();
}
/**
* @desc Sets up the headers associated with the ControllerResponse
* @since 0.4.0
* @access private
*/
head() {
return super.head();
}
/**
* @desc Performs the Controller and calls any templating in the response
* @since 0.4.0
* @access private
*/
write() {
this.$scope = $Injector.get('$scope');
let me = this;
return new Promise(function(resolve) {
let controller = me.route.Controller || me.route.controller;
// Assign a function that can be called to resolve async
// behavior in Controllers
app.services.$response.Controller = {
done: resolve
};
// Get controller and compile scope
if (typeof controller === 'function') {
controller = controller;
} else if (typeof controller === 'string') {
if (app.Controllers[ controller ]) {
controller = app.Controllers[ controller ];
} else {
throw new $$ControllerNotFoundError(controller);
}
} else {
return resolve();
}
// Call the bound controller function
let controllerResponse = new $injectionBinder(
controller,
'controller'
).call(me.$scope, resolve);
// Resolve the Promise if the controller does not return a
// function
if (
!controllerResponse ||
!controllerResponse.constructor ||
controllerResponse.constructor.name !== 'Promise'
) {
resolve(controller);
}
});
}
}
/**
* @desc ControllerTemplateResponse defines any Angie response that has a path
* which is associated with a template. It is responsible for calling the
* controller and any post-processed templating.
* @since 0.4.0
* @access private
* @extends {ControllerResponse}
*/
class ControllerTemplateResponse extends ControllerResponse {
constructor() {
super();
}
/**
* @desc Sets up the headers associated with the ControllerTemplateResponse
* @since 0.4.0
* @access private
*/
head() {
return super.head();
}
/**
* @desc Performs the Controller templating
* @since 0.4.0
* @access private
*/
write() {
let me = this;
return super.write().then(function() {
me.template = me.route.template;
}).then(
controllerTemplateRouteResponse.bind(this)
);
}
}
/**
* @desc ControllerTemplatePathResponse defines any Angie response that has a
* path which is associated with a template path. It is responsible for calling
* the controller and any post-processed templating.
* @since 0.4.0
* @access private
* @extends {ControllerResponse}
*/
class ControllerTemplatePathResponse extends ControllerResponse {
constructor() {
super();
}
/**
* @desc Sets up the headers associated with the
* ControllerTemplatePathResponse
* @since 0.4.0
* @access private
*/
head() {
return super.head();
}
/**
* @desc Performs the Controller path templating
* @since 0.4.0
* @access private
*/
write() {
let me = this;
return super.write().then(function() {
let template = $templateCache.get(me.route.templatePath);
// Check to see if we can associate the template path with a
// mime type
me.response.$headers[ 'Content-Type' ] =
$MimeType.fromPath(me.route.templatePath);
me.template = template;
}).then(
controllerTemplateRouteResponse.bind(this)
);
}
}
/**
* @desc RedirectResponse is either forced as a byproduct of the controller or
* when no other route can be matched and an "otherwise" route is defined. It
* is responsible for serving an empty response and setting up the headers
* associated with a 302 response.
* @since 0.4.0
* @access private
* @extends {BaseResponse}
*/
class RedirectResponse extends BaseResponse {
/**
* @desc Loads a redirect path and the response via BaseResponse
* @since 0.4.0
* @access private
*/
constructor(path) {
super();
this.path = path || this.otherwise;
}
/**
* @desc Sets up the headers associated with the RedirectResponse
* @since 0.4.0
* @access private
*/
head() {
this.response.setHeader('Location', this.path);
return super.head(302);
}
/**
* @desc Placeholder method
* @since 0.4.0
* @access private
*/
write() {
// There is no content in this method
return new Promise((r) => r());
}
/**
* @desc Ends the redirect response (synchronously).
* @since 0.4.0
* @access private
*/
writeSync() {
this.response.end();
}
}
/**
* @desc UnknownResponse writes any Angie response that has a path which cannot
* be mapped to a route or a static asset. It is responsible for serving an
* unknown response and setting up the headers associated with a 404 response.
* @since 0.4.0
* @access private
* @extends {BaseResponse}
*/
class UnknownResponse extends BaseResponse {
/**
* @desc Loads the 404.html and the response via BaseResponse
* @since 0.4.0
* @access private
*/
constructor() {
super();
this.html = $$templateLoader('html/404.html');
}
/**
* @desc Sets up the headers associated with the UnknownResponse
* @since 0.4.0
* @access private
*/
head() {
return super.head(404);
}
/**
* @desc Writes the 404 html to the response.
* @since 0.4.0
* @access private
*/
write() {
let me = this;
return new Promise(function(resolve) {
me.response.write(me.html);
resolve();
});
}
}
/**
* @desc ErrorResponse defines a generic error response from Angie. It is called
* in the event that no routes or static assets are found, there is an issue
* with the 404 path, or a generic error occurs. It is responsible for serving an
* error response and setting up the headers associated with a 500 response.
* @since 0.4.0
* @access private
* @extends {BaseResponse}
*/
class ErrorResponse extends BaseResponse {
/**
* @desc Loads the error response message and the response via BaseResponse
* @since 0.4.0
* @access private
*/
constructor(e) {
super();
let html = '<h1>';
if (e && config.development === true) {
html += `${e}</h1><p>${e.stack || 'No Traceback'}</p>`;
} else {
// Call the response header constants to write the html
html += `${RESPONSE_HEADER_MESSAGES[ '500' ]}</h1>`;
}
this.html = html;
}
/**
* @desc Sets up the headers associated with the ErrorResponse
* @since 0.4.0
* @access private
*/
head() {
return super.head(500);
}
/**
* @desc Writes the 500 html to the response.
* @since 0.4.0
* @access private
*/
write() {
let me = this;
return new Promise(function(resolve) {
me.writeSync();
resolve();
});
}
/**
* @desc Writes the 500 html to the response synchronously.
* @since 0.4.0
* @access private
*/
writeSync() {
this.response.write(this.html);
}
}
/**
* @desc $CustomResponse is an exposed custom response method which can be used
* to defined any response outside of the pre-canned response classes. It is,
* for example, used by the Angie server to return a Gateway Timeout (504) in
* the event that a request is not resolved within the timeframe defined by the
* AngieFile.json `responseErrorTimeout`.
* @since 0.4.0
* @access private
* @extends {BaseResponse}
*/
class $CustomResponse extends BaseResponse {
constructor() {
super();
}
/**
* @desc Sets up the headers associated with the CustomResponse
* @since 0.4.0
* @access private
*/
head(code = 200, headers = {}) {
this.response.$headers = util._extend(this.response.$headers, headers);
return super.head(code);
}
/**
* @desc Writes the custom data to the response.
* @since 0.4.0
* @access private
*/
write(data) {
let me = this;
return new Promise(function(resolve) {
me.writeSync(data);
resolve();
});
}
/**
* @desc Writes the custom data to the response synchronously.
* @since 0.4.0
* @access private
*/
writeSync(data) {
this.response.write(data);
}
}
/**
* @desc Resolves any situation in which a Controller is referenced where it
* does not exist
* @since 0.4.0
* @access private
* @extends {Reference}
*/
class $$ControllerNotFoundError extends ReferenceError {
/**
* @param {string} name Controller Name
* @since 0.4.0
* @access private
*/
constructor(name) {
$LogProvider.error(`Unknown Controller ${blue(name)}`);
super();
}
}
// Performs the templating inside of Controller Classes
function controllerTemplateRouteResponse() {
if (this.template) {
let match = this.template.toString().match(/!doctype ([a-z]+)/i),
mime;
// In the context where MIME type is not set, but we have a
// DOCTYPE tag, we can force set the MIME
// We want this here instead of the explicit template definition
// in case the MIME failed earlier
if (match && !this.response.$headers.hasOwnProperty('Content-Type')) {
mime = this.response.$headers[ 'Content-Type' ] =
$MimeType.$$(match[1].toLowerCase());
}
// Check to see if this is an HTML template and has a DOCTYPE
// and that the proper configuration options are set
if (
mime === 'text/html' &&
config.loadDefaultScriptFile &&
(
this.route.hasOwnProperty('useDefaultScriptFile') ||
this.route.useDefaultScriptFile !== false
)
) {
// Check that option is not true
let scriptFile = config.loadDefaultScriptFile === true ?
'application.js' : config.loadDefaultScriptFile;
$resourceLoader(scriptFile);
}
// Pull the response back in from wherever it was before
this.content = this.response.content;
// Render the template into the resoponse
let me = this;
return new Promise(function(resolve) {
// $Compile to parse template strings and app.directives
return $compile(me.template)(
// In the context of the scope
me.$scope
).then(function(template) {
resolve(template);
});
}).then(function(template) {
me.response.content = me.content += template;
me.response.write(me.content);
});
}
}
export default $Response;
export {
BaseResponse,
AssetResponse,
ControllerTemplateResponse,
ControllerTemplatePathResponse,
RedirectResponse,
UnknownResponse,
ErrorResponse,
$CustomResponse
};