Show:

File: platform/classes/Q/Dispatcher.js

/**
 * @module Q
 */
var Q = require('../Q');
var events = require('events');
var express = require('express');

/**
 * Used to dispatch request to the appropriate handler
 * @class Dispatcher
 * @namespace Q
 * @constructor
 * @param {object} server The server object
 * @param {object} [options={}] Options
 */
function Dispatcher (server, options) {
	var dispatcher = this;
	dispatcher.route = Dispatcher.route;
	var app = server.attached.express;
	app.use(express.methodOverride('Q.method'));
	app.use(function Q_Dispatcher_request_handler (req, res, next) {
		var handler;
		var uri = Dispatcher.route(req.url);
		if (!uri) {
			return next();
		}
		var info = {request: req, uri: uri, response: res};

		req.query.callback = req.query["Q.callback"] || req.query.callback;

		/**
		 * Http request validation
		 * @event validate
		 * @param {object} info
		 *	The object containing request, response and url
		 */
		dispatcher.emit('validate', info);
		handler = Q.getObject([uri.module, uri.action, 'validate'], Q.handlers);
		if (!Q.isEmpty(handler)) {
			Q.handle(handler, this, [info, afterValidate]);
		} else {
			afterValidate();
		}

		function afterValidate(errors) {
			if (errors && errors.length) {
				res.send(JSON.stringify({"errors": errors}));
				return;
			}
			var methods = Q.Config.get(['Q', 'methods'], ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']);
			var method = Q.Request.method(req);
			if (!methods.indexOf(method)) {
				res.send(JSON.stringify({"errors": method + " method not supported"}));
				return;
			}
			var m = method.toLowerCase();
			/**
			 * Http POST request
			 * @event post
			 * @param {object} info
			 *	The object containing request, response and url
			 */
			/**
			 * Http PUT request
			 * @event put
			 * @param {object} info
			 *	The object containing request, response and url
			 */
			/**
			 * Http DELETE request
			 * @event delete
			 * @param {object} info
			 *	The object containing request, response and url
			 */
			/**
			 * Http OPTIONS request
			 * @event options
			 * @param {object} info
			 *	The object containing request, response and url
			 */

			dispatcher.emit(m, info);
			handler = Q.getObject([uri.module, uri.action, m], Q.handlers);
			if (!Q.isEmpty(handler)) {
				Q.handle(handler, this, [info, afterMethod]);
			} else {
				afterMethod();
			}
		}

		function afterMethod(errors) {
			if (errors && errors.length) {
				res.send(JSON.stringify({"errors": errors}));
				return;
			}
			/**
			 * Http response
			 * @event response
			 * @param {object} info
			 *	The object containing request, response and url
			 */
			dispatcher.emit('response', info);

			var slotNames = Q.Request.slotNames(req);
			if (!slotNames) {
				result = {"errors": ["missing slot names"]};
				res.contentType('application/json');
				var content = req.query.callback
					? req.query.callback + "(" + JSON.stringify(result) + ")"
					: JSON.stringify(result);
				res.send(content);
				return;
			}
			var p = new Q.Pipe(slotNames, function (params) {
				// this happens only after all the slots are filled
				// so make sure those handlers call the callbacks, or no response will be written
				var slots = {}, errors = [];
				for (var i=0; i<slotNames.length; ++i) {
					var slotName = slotNames[i];
					var err = params[slotName][0];
					if (err) {
						errors.push(err);
					}
					slots[slotName] = params[slotName][1];
				}
				var result = {"slots": slots};
				if (errors.length) {
					result.errors = errors;
				}
				result.timestamp = Date.now();
				res.contentType('application/json');
				var content = req.query.callback
					? req.query.callback + "(" + JSON.stringify(result) + ")"
					: JSON.stringify(result);
				res.send(content);
			});
			for (var i=0; i<slotNames.length; ++i) {
				var slotName = slotNames[i];
				var handler = Q.getObject([uri.module, uri.action, 'response', slotName], Q.handlers);
				if (!Q.isEmpty(handler)) {
					Q.handle(handler, this, [info, p.fill(slotName)]);
				} else {
					p.fill(slotName)(uri.module+"/"+uri.action+" does not define slot "+slotName);
				}
			}
		}
	});
}

/**
 * Start external server
 * @method listen
 * @param {object} [options={}]
 * @return {object} The server object
 * @throws {Q.Exception} if Q/node/port or Q/node/host config fields are missing
 */
Dispatcher.listen = function (options) {
	options = options || {};
	var port = Q.Config.get(['Q', 'node', 'port'], null);
	var host = Q.Config.get(['Q', 'node', 'host'], null);
	if (port === null)
		throw new Q.Exception("Q: Missing config field: Q/node/port");
	if (host === null)
		throw new Q.Exception("Q: Missing config field: Q/node/host");
	var server = Q.listen({
		port: options.port || port,
		host: options.host || host
	});
	if (!server.attached.dispatcher) {
		server.attached.dispatcher = new Dispatcher(server, options);
	}
	return server;
};

/**
 * Route the request
 * @method route
 * @param {string} url The URL to route
 * @return {object} Object containint the requested "module" and "action"
 */
Dispatcher.route = function (url) {
	var index = url.indexOf('/action.php/'), tailStart;
	if (index === -1) {
		var appRootUrl = Q.Config.get('Q', 'web', 'appRootUrl', false);
		if (!appRootUrl) {
			return false;
		}
		var parsed = require('url').parse(appRootUrl);
		tailStart = parsed.pathname.length;
	} else {
		tailStart = index+'/action.php/'.length;
	}
	var parts1 = url.substr(tailStart).split('?');
	var parts2 = parts1[0].split('/');
	if (!parts2[1]) {
		return false;
	}
	return {module: parts2[0], action: parts2[1]};
};

Q.makeEventEmitter(Dispatcher, true);
module.exports = Dispatcher;