API Docs for: 0.5.0
Show:

File: mojito_src/lib/mojito.js

/*
 * Copyright (c) 2011-2012, Yahoo! Inc.  All rights reserved.
 * Copyrights licensed under the New BSD License.
 * See the accompanying LICENSE file for terms.
 */


/*jslint anon:true, sloppy:true, nomen:true, node:true*/


//  ----------------------------------------------------------------------------
//  Prerequisites
//  ----------------------------------------------------------------------------


var express = require('express'), // TODO: [Issue 80] go back to connect?
    http = require('http'),
    store = require('./store'),
    OutputHandler = require('./output-handler.server'),
    libpath = require('path'),
    libutils = require('./management/utils'),
    YUIFactory = require('./yui-sandbox.js'),
    requestCounter = 0, // used to scope logs per request
    Mojito;

//  ----------------------------------------------------------------------------
//  Mojito Global
//  ----------------------------------------------------------------------------


/**
 * Shared global object, which isn't named 'mojito' because 'mojito' is a module
 * name defined in mojito.common.js and required via Y.use.
 */
// TODO: Merge what we put on this object with the 'mojito' module/namespace.
global._mojito = {};


//  ----------------------------------------------------------------------------
//  MojitoServer
//  ----------------------------------------------------------------------------


/**
 * The primary Mojito server type. Invoking the constructor returns an instance
 * which is not yet running. Use listen to run the server once you have made
 * any adjustments to its configuration.
 * @param {{port: number,
 *          dir: string,
 *          context: Object,
 *          appConfig: Object,
 *          verbose: boolean}} options An object containing server options. The
 *          default port is process.env.PORT or port 8666 if no port is given.
 *          Verbose is false by default. Dir is cwd() by default. Both the
 *          context and appConfig will default to empty objects.
 * @constructor
 * @return {MojitoServer}
 */
function MojitoServer(options) {

    this._options = options || {};
    this._options.port = this._options.port || process.env.PORT || 8666;
    this._options.mojitoRoot = __dirname;

    // TODO: Note we could pass some options to the express server instance.
    this._app = express.createServer();

    this._app.store = store.createStore(this._options);

    this._configureAppInstance(this._app, this._options);

    return this;
}


//  ---------
//  Constants
//  ---------

/**
 * An ordered list of the middleware module names to load for a standard Mojito
 * server instance.
 * @type {Array.<string>}
 */
MojitoServer.MOJITO_MIDDLEWARE = [
    'mojito-handler-static',
    'mojito-parser-body',
    'mojito-parser-cookies',
    'mojito-contextualizer',
    'mojito-handler-tunnel',
    'mojito-router',
    'mojito-handler-dispatcher'
];


//  ------------------
//  Private Attributes
//  ------------------


/**
 * The Express application (server) instance.
 * @type {Object}
 */
MojitoServer.prototype._app = null;


/**
 * The formatting function for the server's associated logger.
 * @type {function(string, number, string, Date, Object, number)}
 */
MojitoServer.prototype._logFormatter = null;


/**
 * The publisher function for the server's associated logger.
 * @type {function(string, number, string, Date, number)}
 */
MojitoServer.prototype._logPublisher = null;


/**
 * The write function for the server's associated logger.
 * @type {function(function(string, number, string, Date, Object, number))}
 */
MojitoServer.prototype._logWriter = null;


/**
 * The server options container. Common option keys are listed.
 * @type {{port: number,
 *          dir: string,
 *          context: Object,
 *          appConfig: Object,
 *          verbose: boolean}}
 */
MojitoServer.prototype._options = null;


/**
 * The server startup time. This value is used to both provide startup/uptime
 * information and as a signifier that the server has been configured/started.
 * @type {number}
 */
MojitoServer.prototype._startupTime = null;


//  ---------------
//  Private Methods
//  ---------------

/**
 * Adds Mojito framework components to the Express application instance.
 * @method _configureAppInstance
 * @param {Object} app The Express application instance to Mojito-enable.
 * @param {{port: number,
 *          dir: string,
 *          context: Object,
 *          appConfig: Object,
 *          verbose: boolean}} options An object containing server options.
 */
MojitoServer.prototype._configureAppInstance = function(app, options) {

    var store = app.store,
        YUI,
        Y,
        appConfig,
        yuiConfig,
        logConfig = {},
        modules = [],
        middleware,
        m,
        midName,
        midBase,
        midPath,
        midFactory,
        hasMojito,
        midConfig,
        dispatcher,
        singleton_dispatcher;

    if (!options) {
        options = {};
    }
    if (!options.dir) {
        options.dir = process.cwd();
    }
    if (!options.context) {
        options.context = {};
    }

    appConfig = store.getAppConfig(store.getStaticContext());
    yuiConfig = (appConfig.yui && appConfig.yui.config) || {};

    // redefining "combine" and/or "base" in the server side have side effects
    // and might try to load yui from CDN, so we bypass them.
    // TODO: report bug.
    delete yuiConfig.combine;
    delete yuiConfig.base;

    // in case we want to collect some performance metrics,
    // we can do that by defining the "perf" object in:
    // application.json (master)
    // You can also use the option --perf path/filename.log when
    // running mojito start to dump metrics to disk.
    if (appConfig.perf) {
        yuiConfig.perf = appConfig.perf;
        yuiConfig.perf.logFile = options.perf;
    }

    YUI = YUIFactory.getYUI(yuiConfig.filter);
    // applying the default configuration from application.json->yui-config
    Y = YUI(yuiConfig, {
        useSync: true
    });

    this._configureLogger(Y);
    this._configureYUI(Y, store, modules);

    // attaching all modules available for this application for the server side
    Y.applyConfig({ useSync: true });
    Y.use.apply(Y, modules);
    Y.applyConfig({ useSync: false });

    // computing middleware pieces
    if (appConfig.middleware && appConfig.middleware.length) {
        hasMojito = false;
        for (m = 0; m < appConfig.middleware.length; m += 1) {
            midName = appConfig.middleware[m];
            if (0 === midName.indexOf('mojito-')) {
                hasMojito = true;
                break;
            }
        }
        if (hasMojito) {
            // User has specified at least one of mojito's middleware, so
            // we assume that they have specified all that they need.
            middleware = appConfig.middleware;
        } else {
            // backwards compatibility mode:
            //  middlware = user's, then mojito's
            middleware = [];
            for (m = 0; m < appConfig.middleware.length; m += 1) {
                middleware.push(appConfig.middleware[m]);
            }
            for (m = 0; m < MojitoServer.MOJITO_MIDDLEWARE.length; m += 1) {
                middleware.push(MojitoServer.MOJITO_MIDDLEWARE[m]);
            }
        }
    } else {
        middleware = MojitoServer.MOJITO_MIDDLEWARE;
    }

    midConfig = {
        Y: Y,
        store: store,
        logger: {
            log: Y.log
        },
        context: options.context
    };

    singleton_dispatcher = Y.mojito.Dispatcher.init(
        store
    );

    dispatcher = function(req, res, next) {
        var command = req.command,
            outputHandler;

        if (!command) {
            next();
            return;
        }

        outputHandler = new OutputHandler(req, res, next);
        outputHandler.setLogger({
            log: Y.log
        });
        if (appConfig.debugMemory) {
            outputHandler.setMemoryDebugObjects({
                'YUI.Env': YUI.Env,
                'YUI.config': YUI.config,
                'YUI.process': YUI.process,
                Y: Y,
                store: store
            });
        }

        // if perf metrics are on, we should hook into
        // the mojito request to flush metrics when
        // the connection is closed.
        if (Y.mojito.perf.instrumentMojitoRequest) {
            Y.mojito.perf.instrumentMojitoRequest(req, res);
        }

        singleton_dispatcher.dispatch(command, outputHandler);
    };

    // attaching middleware pieces
    for (m = 0; m < middleware.length; m += 1) {
        midName = middleware[m];
        if (0 === midName.indexOf('mojito-')) {
            // one special one, since it might be difficult to move to a
            // separate file
            if (midName === 'mojito-handler-dispatcher') {
                //console.log("======== MIDDLEWARE mojito -- " +
                //    "builtin mojito-handler-dispatcher");
                app.use(dispatcher);
            } else {
                midPath = libpath.join(__dirname, 'app', 'middleware', midName);
                //console.log("======== MIDDLEWARE mojito " + midPath);
                midFactory = require(midPath);
                app.use(midFactory(midConfig));
            }
        } else {
            // backwards-compatibility: user-provided middleware is
            // specified by path
            midPath = libpath.join(options.dir, midName);
            //console.log("======== MIDDLEWARE user " + midPath);
            midBase = libpath.basename(midPath);
            if (0 === midBase.indexOf('mojito-')) {
                // Same as above (case of Mojito's special middlewares)
                // Gives a user-provided middleware access to the YUI
                // instance, resource store, logger, context, etc.
                midFactory = require(midPath);
                app.use(midFactory(midConfig));
            } else {
                app.use(require(midPath));
            }
        }
    }

    // TODO: [Issue 82] The last middleware in the stack should be an
    // error handler

    Y.log('Mojito HTTP Server initialized in ' +
            ((new Date().getTime()) - Mojito.MOJITO_INIT) + 'ms.');
};

/*
 * Configures YUI logger to honor the logLevel and logLevelOrder
 * TODO: this should be done at the low level in YUI.
 */
MojitoServer.prototype._configureLogger = function(Y) {
    var logLevel = (Y.config.logLevel || 'debug').toLowerCase(),
        logLevelOrder = Y.config.logLevelOrder || [],
        defaultLogLevel = logLevelOrder[0] || 'info';

    function log(c, msg, cat, src) {
        var f,
            m = (src) ? src + ': ' + msg : msg;
        if (Y.Lang.isFunction(c.logFn)) {
            c.logFn.call(Y, msg, cat, src);
        } else if (typeof console !== undefined && console.log) {
            f = (cat && console[cat]) ? cat : 'log';
            console[f](msg);
        }
    }

    // one more hack: we need to make sure that base is attached
    // to be able to listen for Y.on.
    Y.use('base');

    if (Y.config.debug) {

        logLevel = (logLevelOrder.indexOf(logLevel) >= 0 ? logLevel : logLevelOrder[0]);

        // logLevel index defines the begining of the logLevelOrder structure
        // e.g: ['foo', 'bar', 'baz'], and logLevel 'bar' should produce: ['bar', 'baz']
        logLevelOrder = (logLevel ? logLevelOrder.slice(logLevelOrder.indexOf(logLevel)) : []);

        Y.applyConfig({
            useBrowserConsole: false,
            logLevel: logLevel,
            logLevelOrder: logLevelOrder
        });

        // listening for low level log events to filter some of them.
        Y.on('yui:log', function (e) {
            var c = Y.config,
                cat = e && e.cat && e.cat.toLowerCase();

            // this covers the case Y.log(msg) without category
            // by using the low priority category from logLevelOrder.
            cat = cat || defaultLogLevel;

            // applying logLevel filters
            if (cat && ((c.logLevel === cat) || (c.logLevelOrder.indexOf(cat) >= 0))) {
                log(c, e.msg, cat, e.src);
            }
            return true;
        });
    }

};

/*
 * Configures YUI with both the Mojito framework and all the YUI modules in the
 * application.
 */
MojitoServer.prototype._configureYUI = function(Y, store, load) {
    var mojits = store.yui.getConfigAllMojits('server', {}),
        shared = store.yui.getConfigShared('server', {}, false),
        modules,
        module,
        lang;

    modules = Y.merge((mojits.modules || {}), (shared.modules || {}));

    Y.applyConfig({
        modules: modules
    });

    // pre-loading every yui module for the server runtime
    for (module in modules) {
        if (modules.hasOwnProperty(module)) {
            load.push(module);
        }
    }

    // NOTE:  Not all of these module names are guaranteed to be valid,
    // but the loader tolerates them anyways.
    for (lang in store.yui.langs) {
        if (store.yui.langs.hasOwnProperty(lang) && lang) {
            load.push('lang/datatype-date-format_' + lang);
        }
    }
};


//  --------------
//  Public Methods
//  --------------


/**
 * Closes (shuts down) the server port and stops the server.
 */
MojitoServer.prototype.close = function() {
    if (this._options.verbose) {
        libutils.warn('Closing Mojito Application');
    }

    this._app.close();
};


/**
 * Returns the instance of http.Server (or a subtype) which is the true server.
 * @return {http.Server} The node.js http.Server (or subtype) instance.
 */
MojitoServer.prototype.getHttpServer = function() {
    return this._app;
};


/**
 * Begin listening for inbound connections.
 * @param {Number} port The port number. Defaults to the server's value for
 *     options.port (which defaults to process.env.PORT followed by 8666).
 * @param {String} host The hostname or IP address in string form.
 */
MojitoServer.prototype.listen = function(port, host, cb) {

    var logger,
        app = this._app,
        p = port || this._options.port,
        h = host || this._options.host,
        callback = cb || Mojito.NOOP,
        handler = function(err) {
            callback(err, app);
        };

    // Track startup time and use it to ensure we don't try to listen() twice.
    if (this._startupTime) {
        if (this._options.verbose) {
            libutils.warn('Mojito Application Already Running');
        }
        return;
    }
    this._startupTime = new Date().getTime();

    if (this._options.verbose) {
        libutils.warn('Starting Mojito Application');
    }

    try {
        if (h) {
            app.listen(p, h, handler);
        } else {
            app.listen(p, handler);
        }
    } catch (err) {
        callback(err);
    }
};


/**
 * Invokes a callback function with the content of the requested url.
 * @param {string} url A url to fetch.
 * @param {{host: string, port: number, method: string}|function} opts A list of
 *     options, or a callback function (See @param for cb). When providing
 *     options note that the list here is not exhaustive. Any valid http.request
 *     object option may be provided. See documentation for http.request.
 * @param {function(Error, string, string)} cb A function called on request
 *     completion. Parameters are any optional Error, the original URL, and the
 *     content of that URL.
 */
MojitoServer.prototype.getWebPage = function(url, opts, cb) {
    var buffer = '',
        callback,
        options = {
            host: '127.0.0.1',
            port: this._options.port,
            path: url,
            method: 'get'
        };

    // Options block is optional, no pun intended. When it's a function we'll
    // use that as our callback function.
    if (typeof opts === 'function') {
        callback = opts;
    } else {
        // Don't assume we got a real callback function.
        callback = cb || Mojito.NOOP;

        // Map provided options into our request options object.
        Object.keys(opts).forEach(function(k) {
            if (opts.hasOwnProperty(k)) {
                options[k] = opts[k];
            }
        });
    }

    http.request(options, function(res) {
        res.setEncoding('utf8');
        res.on('data', function(chunk) {
            buffer += chunk;
        });
        res.on('end', function() {
            // TODO: 200 isn't the only success code. Support 304 etc.
            if (res.statusCode !== 200) {
                callback('Could not get web page: status code: ' +
                    res.statusCode + '\n' + buffer, url);
            } else {
                callback(null, url, buffer);
            }
        });
    }).on('error', function(err) {
        callback(err, url);
    }).end();
};


/**
 * Invokes a callback function with the content of each url requested.
 * @param {Array.<string>} urls A list of urls to fetch.
 * @param {function(Error, string, string)} cb A function called once for each
 *     url in the urls list. Parameters are any optional Error, the original URL
 *     and the URL's content.
 */
MojitoServer.prototype.getWebPages = function(urls, cb) {
    var server = this,
        callback,
        count,
        len,
        initOne;

    // If no array, or an empty array, just exit.
    if (!urls || urls.length === 0) {
        return;
    }

    // NOTE we could just say this is an error condition. No callback, what's
    // the point of doing the work?
    callback = cb || Mojito.NOOP;

    len = urls.length;
    count = 0;

    // Create a function to call getWebPage with an individual URL shifted from
    // the list. When the list is empty we can stop.
    initOne = function() {
        if (count < len) {
            server.getWebPage(urls[count], function(err, url, data) {
                count += 1;
                try {
                    callback(err, url, data);
                } finally {
                    initOne();
                }
            });
        }
    };

    // Start the ball rolling :).
    initOne();
};

//  ----------------------------------------------------------------------------
//  Mojito
//  ----------------------------------------------------------------------------

/**
 * The Mojito object is the primary server construction interface for Mojito.
 * This object is used to create new server instances but given that the raw
 * Express application object is expected/returned there's no need for a true
 * constructor since there are no true instances of a Mojito server object.
 */
// TODO: Merge what we put on this object with the 'mojito' module/namespace.
Mojito = {};


//  ---------
//  Constants
//  ---------

/**
 * The date/time the Mojito object was initialized.
 * @type {Date}
 */
Mojito.MOJITO_INIT = new Date().getTime();


/**
 * A placeholder function used to avoid overhead checking for callbacks.
 * @type {function()}
 */
Mojito.NOOP = function() {};


//  --------------
//  Public Methods
//  --------------


/**
 * Creates a properly configured MojitoServer instance and returns it.
 * @method createServer
 * @param {Object} options Options for starting the app.
 * @return {Object} Express application.
 */
Mojito.createServer = function(options) {
    // NOTE that we use the exported name here. This allows us to mock that
    // object during testing.
    return new Mojito.Server(options);
};


/**
 * Allows the bin/mojito command to leverage the current module's relative path
 * for initial startup loading.
 * @method include
 * @param {string} path The path used to locate resources.
 * @return {Object} The return value of require() for the adjusted path.
 */
Mojito.include = function(path) {
    return require('./' + path);
};


//  ----------------------------------------------------------------------------
//  EXPORT(S)
//  ----------------------------------------------------------------------------

/**
 * Export Mojito as the return value for any require() calls.
 * @type {Mojito}
 */
module.exports = Mojito;

/**
 * Export Mojito.Server to support unit testing of the server type. With this
 * approach the slot for the server can be replaced with a mock, but the actual
 * MojitoServer type remains private to the module.
 * @type {MojitoServer}
 */
module.exports.Server = MojitoServer;