/* * Copyright 2013 Jive Software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @module service */ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Private var fs = require('fs'); var path = require('path'); var express = require('express'); var q = require('q'); var bootstrap = require('./bootstrap'); var definitionConfigurator = require('./definitionSetup'); var osAppConfigurator = require('./appSetup'); var serviceConfigurator = require('./serviceSetup'); var jive = require('../api'); var log4js = require('log4js'); var mustache = require('mustache'); var url = require('url'); var app; var rootDir = process.cwd(); var tilesDir = rootDir + '/tiles'; var osAppsDir = rootDir + '/apps'; var cartridgesDir = rootDir + '/cartridges'; var storagesDir = rootDir + '/storages'; var security = require('./security'); var serviceState = 'stopped'; var _dir = function(theDir, defaultDir ) { theDir = theDir || defaultDir; if ( theDir.indexOf('/') == 0 ) { return rootDir + theDir; } else { return process.cwd() + '/' + theDir; } }; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Public /** * Service configuration options. */ exports.options = {}; /** * @deprecated This is just a symlink to jive.community (use that instead). To be removed in next major version. * @type {module:community} */ exports.community = jive.community; var persistence; /** * Retrieves or sets current persistence strategy, defaults to file. * @param {Object} persistenceStrategy If set, the service will be configured to use the provided strategy. * @param {function} persistenceStrategy.find * @param {function} persistenceStrategy.findByID * @param {function} persistenceStrategy.remove * @param {function} persistenceStrategy.save * @returns {Object} */ exports.persistence = function(persistenceStrategy) { if ( persistenceStrategy ) { // set persistence if ( !persistenceStrategy['find'] || !persistenceStrategy['findByID'] || !persistenceStrategy['remove'] || !persistenceStrategy['save'] ) { throw 'Unsupported persistence strategy - must implement find, findByID, remove, save methods.'; } persistence = persistenceStrategy; jive.context['persistence'] = persistence; } // retrieve persistence return { save: function() { return persistence ? persistence.save( arguments.length > 0 ? arguments[0] : undefined, arguments.length > 1 ? arguments[1] : undefined, arguments.length > 2 ? arguments[2] : undefined, arguments.length > 3 ? arguments[3] : undefined, arguments.length > 4 ? arguments[4] : undefined, arguments.length > 5 ? arguments[5] : undefined ) : function() { return q.reject( new Error("persistence not defined") ); } }, find: function() { return persistence ? persistence.find( arguments.length > 0 ? arguments[0] : undefined, arguments.length > 1 ? arguments[1] : undefined, arguments.length > 2 ? arguments[2] : undefined, arguments.length > 3 ? arguments[3] : undefined, arguments.length > 4 ? arguments[4] : undefined, arguments.length > 5 ? arguments[5] : undefined ) : function() { return q.reject( new Error("persistence not defined") ); } }, findByID: function() { return persistence ? persistence.findByID( arguments.length > 0 ? arguments[0] : undefined, arguments.length > 1 ? arguments[1] : undefined, arguments.length > 2 ? arguments[2] : undefined, arguments.length > 3 ? arguments[3] : undefined, arguments.length > 4 ? arguments[4] : undefined, arguments.length > 5 ? arguments[5] : undefined ) : function() { return q.reject( new Error("persistence not defined") ); } }, remove: function() { return persistence ? persistence.remove( arguments.length > 0 ? arguments[0] : undefined, arguments.length > 1 ? arguments[1] : undefined, arguments.length > 2 ? arguments[2] : undefined, arguments.length > 3 ? arguments[3] : undefined, arguments.length > 4 ? arguments[4] : undefined, arguments.length > 5 ? arguments[5] : undefined ) : function() { return q.reject( new Error("persistence not defined") ); } }, close: function() { return persistence ? persistence.close() : function() { return q.reject( new Error("persistence not defined") ); } }, sync: function() { if ( !persistence ) { return q.reject( new Error("persistence not defined") ); } if ( !persistence['sync'] ) { return q.resolve(); } return persistence.sync( arguments.length > 0 ? arguments[0] : undefined, arguments.length > 1 ? arguments[1] : undefined, arguments.length > 2 ? arguments[2] : undefined, arguments.length > 3 ? arguments[3] : undefined, arguments.length > 4 ? arguments[4] : undefined, arguments.length > 5 ? arguments[5] : undefined ); } } }; var scheduler; /** * Retrieves or sets the scheduling strategy. Defaults to memory (single node). todo * @param {Object} schedulerStrategy If set, the service will be configured to use the provided strategy. * @param {function} schedulerStrategy.schedule * @param {function} schedulerStrategy.unschedule * @param {function} schedulerStrategy.getTasks */ exports.scheduler = function( schedulerStrategy ) { if ( schedulerStrategy ) { if ( !schedulerStrategy['schedule'] || !schedulerStrategy['unschedule'] || !schedulerStrategy['getTasks'] ) { throw 'Unsupported scheduler strategy - must implement schedule, unschedule, isScheduled, getTasks.'; } scheduler = schedulerStrategy; if ( !scheduler ) { scheduler = new jive.scheduler.memory(); } jive.context.scheduler = scheduler; } scheduler = jive.context.scheduler; return scheduler; }; /** * Initializes the service based on the passed-in express app, and any configuration options. * If an express app isn't provided, one is created. * <ul> * <li>Applies globally applicable middleware to the express app</li> * <li>Configures service role based on options</li> * <li>Sets up service logging based on options</li> * <li>Sets up service persistence based on options</li> * <li>Sets up service scheduler based on options</li> * </ul> * * Environment variable overrides: * <ul> * <li>jive_sdk_service_role: optional, one of http, worker, pusher.</li> * <li>jive_sdk_config_file: optional, if set, JSON options will be loaded from this path.</li> * </ul> * @param expressApp * @param {Object} options JSON, or path to the options file. */ exports.init = function(expressApp, options ) { if (expressApp) { app = expressApp; } else { app = express(); } rootDir = (options && options['svcRootDir']) || rootDir || process.cwd(); tilesDir = rootDir + '/tiles'; // for some reason this needs to be configured earlier than later app.use(express.bodyParser()); if ( options && !options['suppressHttpLogging'] ) { app.use(express.logger('dev')); } app.use(express.methodOverride()); app.use(app.router); app.use(express.errorHandler()); var applyDefaults = function(config) { if ( !config['persistence'] ) { config['persistence'] = 'file'; } if ( !config['scheduler'] ) { config['scheduler'] = 'memory'; } }; var applyOverrides = function(config) { // override role var roleFromEnv = process.env['jive_sdk_service_role']; if ( roleFromEnv ) { config['role'] = roleFromEnv; } if (process && process.env && process.env.PORT) { config['port'] = process.env.PORT; } }; var initialPromise; if ( typeof options === 'object' ) { applyDefaults(options); applyOverrides(options); exports.options = options; jive.context.config = exports.options; initialPromise = q.fcall( function() { return options; }); } else { // if no options are provided, then try getting them from // cmd line arguments, or from environemnt if ( !options ) { var configFileFromEnv = process.env['jive_sdk_config_file']; var configFilePathFromArgs = undefined; process.argv.forEach(function (val, index, array) { if ( val.indexOf('=') > -1 ) { var arg = val.split(/=/); if ( arg[0] == 'configFile' ) { jive.logger.debug("Command line argument:" + arg[0] + "=" + arg[1]); configFilePathFromArgs = arg[1]; } } }); options = configFilePathFromArgs || configFileFromEnv; if( !options ) { // assume its a file in the rootdir options = rootDir + "/jiveclientconfiguration.json"; } } initialPromise = q.nfcall( fs.readFile, options, 'utf8').then( function (data) { var jiveConfig = JSON.parse(data); applyDefaults(jiveConfig); applyOverrides(jiveConfig); exports.options = jiveConfig; jive.context.config = exports.options; jive.logger.debug('Startup configuration from', options); jive.logger.debug(jiveConfig); return jiveConfig; }); } return initialPromise .then(initLogger) .then(initPersistence) .then(initScheduler); }; function initLogger(options) { var logfile = options['logFile'] || options['logfile']; var logLevel = process.env['jive_logging_level'] || options['logLevel'] || options['loglevel'] || 'INFO'; logLevel = logLevel.toUpperCase(); if (logfile) { if (logfile.indexOf('logs/') === 0) { if (!fs.existsSync('logs')) { jive.logger.warn('logs subdirectory does not exist. Creating directory now.'); fs.mkdirSync('logs'); } } log4js.loadAppender('file'); log4js.addAppender(log4js.appenders.file(logfile), 'jive-sdk'); } jive.logger.setLevel(logLevel); return options; } function initPersistence(options) { /** * Offered to the persistence strategy; may or may not be used. */ var defaultSchema = { 'tileDefinition' : { 'sampleData' : { type: "text", required: false }, 'displayName': { type: "text", required: false }, 'name': { type: "text", required: true, index: true }, 'description': { type: "text", required: false }, 'style': { type: "text", required: false }, 'icons': { type: "text", required: false }, 'action': { type: "text", required: false }, 'id': { type: "text", required: true }, 'definitionDirName': { type: "text", required: false } }, 'extstreamsDefinition': { 'displayName': { type: "text", required: false }, 'name': { type: "text", required: true, index: true }, 'description': { type: "text", required: false }, 'style': { type: "text", required: false }, 'icons': { type: "text", required: false }, 'action': { type: "text", required: false }, 'id': { type: "text", required: true }, 'definitionDirName': { type: "text", required: false } }, 'jiveExtension' : { 'id': { type: "text", required: false }, 'uuid': { type: "text", required: false }, 'name': { type: "text", required: false }, 'systemAdmin': { type: "text", required: false }, 'jiveServiceSignature': { type: "text", required: false } }, 'tileInstance' : { "url": { type: "text", required: false }, "config": { type: "text", required: false, expandable: true }, "name": { type: "text", required: false, index: true }, "accessToken": { type: "text", required: false }, "expiresIn": { type: "text", required: false }, "refreshToken": { type: "text", required: false }, "scope": { type: "text", required: false }, "guid": { type: "text", required: false }, "jiveCommunity": { type: "text", required: false }, "id": { type: "text", required: true } }, 'community': { "id": { type: "text", required: false }, "jiveUrl": { type: "text", required: false, index: true }, "version":{ type: "text", required: false }, "tenantId": { type: "text", required: false, index: true }, "clientId": { type: "text", required: false }, "clientSecret": { type: "text", required: false }, "jiveCommunity": { type: "text", required: false }, "oauth": { type: "text", required: false } } }; var persistence = options['persistence']; if ( typeof persistence === 'object' ) { // set persistence if the object is provided exports.persistence( options['persistence'] ); } else if ( typeof persistence === 'string' ) { //If a string is provided, and it's a valid type exported in jive.persistence, then use that. options['schema'] = defaultSchema; if (jive.persistence[persistence]) { exports.persistence(new jive.persistence[persistence](options)); //pass options, such as location of DB for mongoDB. } else { // finally try to require the persistence strategy try { var persistenceStrategy = require(process.cwd() + '/node_modules/' + persistence); exports.persistence(new persistenceStrategy(options)); } catch ( e ) { jive.logger.error(e); jive.logger.warn('Invalid persistence option given "' + persistence + '". Must be one of ' + Object.keys(jive.persistence)); process.exit(-1); } } } return options; } function initScheduler(options) { var scheduler = options['scheduler']; if ( typeof scheduler === 'object' ) { // set scheduler if the object is provided exports.scheduler(scheduler); } else if ( typeof scheduler === 'string' ) { //If a string is provided, and it's a valid type exported in jive.scheduler, then use that. if (jive.scheduler[scheduler]) { exports.scheduler(new jive.scheduler[scheduler](options)); //pass options, such as location of redis for Kue. } else { // finally try to require the scheduler strategy try { var schedulerStrategy = require(process.cwd() + '/node_modules/' + scheduler); exports.scheduler(new schedulerStrategy(options)); } catch ( e ) { jive.logger.error(e); jive.logger.warn('Invalid scheduler option given "' + scheduler + '". Must be one of ' + Object.keys(jive.scheduler)); process.exit(-1); } } } return options; } /** * Autowires the entire service. Service autowiring will setup: * <ul> * <li>Public web assets such as html, javascript, and images available from the /public directory</li> * <li>Public endpoints ('routes') shared by all components</li> * <li>Public endpoints ('routes') for each service subcomponent</li> * <li>Event listeners</li> * <li>Recurrent tasks</li> * <li>Execute serivce component bootstrap code</li> * </ul> * @param {Object} options Autowiring options. If not present, all service components (tiles, apps, services, storage frameworks, etc.) will * be autowired. * @param {String} options.componentsToWire Polymorphic, optional field. If present and value is 'all', then all service components will be autowired. * Otherwise, if present and value is list of one or more of <b>tiles</b>, <b>apps</b>, <b>services</b>, and <b>storages</b>, the specified components are autowired. * @returns {Promise} Promise */ exports.autowire = function(options) { var doTiles = true; var doApps = true; var doServices = true; var doStorages = true; if ( options ) { var componentsToWire = options['componentsToWire']; if ( componentsToWire ) { if ( componentsToWire !== 'all' && componentsToWire['indexOf'] ) { if ( !componentsToWire.indexOf['tiles'] ) { doTiles = false; } if ( !componentsToWire.indexOf['apps'] ) { doApps = false; } if ( !componentsToWire.indexOf['services'] ) { doServices = false; } if ( !componentsToWire.indexOf['storages'] ) { doStorages = false; } } } } return ( doTiles ? definitionConfigurator.setupAllDefinitions(app, _dir( '/tiles', '/tiles')) : q.resolve() ) .then( function () { return ( doApps ? osAppConfigurator.setupAllApps( app, _dir( '/apps', '/apps')) : q.resolve() ) }) .then( function () { return ( doServices ? serviceConfigurator.setupAllServices( app, _dir( '/services', '/services')) : q.resolve() ) }) .then( function () { return ( doStorages ? serviceConfigurator.setupAllServices( app, _dir( '/storages', '/storages')) : q.resolve ); }); }; /** * Return promise - fail or succeed * @returns {Promise} promise */ exports.start = function() { serviceState = 'starting'; return bootstrap.start( app, exports.options, rootDir, tilesDir, osAppsDir, cartridgesDir, storagesDir).then( function() { if (app['settings'] && app['settings']['env']) { jive.logger.info("Service started in " + app['settings']['env'] + " mode"); serviceState = 'started'; } return q.resolve(); }); }; /** * Halt the service. * @returns {Promise} promise */ exports.stop = function() { return bootstrap.teardown().then( function() { jive.logger.info("Service stopped."); serviceState = 'stopped'; }); }; /** * Computes the full service URL for this service, taking into account * jiveclientconfiguration.json (clientUrl & port) */ exports.serviceURL = function() { var conf = jive.service.options; var clientUrlExcludesPort = conf['clientUrlExcludesPort']; var clientUrl = conf['clientUrl']; var port = conf['port']; if ( !clientUrlExcludesPort && port && port != 443 && port != 80 ) { var urlParts = url.parse(clientUrl); urlParts['port'] = port; urlParts['host'] = null; clientUrl = url.format(urlParts); if ( clientUrl.lastIndexOf( '/') == clientUrl.length-1 ) { clientUrl = clientUrl.substring( 0, clientUrl.length - 1); } } return clientUrl; }; /** * Public service routes * @property {Object} tiles * @property {Object} jive * @property {Object} dev * @property {Object} oauth */ exports.routes = { 'tiles' : require('../routes/tiles'), 'jive' : require('../routes/jive'), 'dev' : require('../routes/dev'), 'oauth' : require('../routes/oauth') }; /** * Interrogate the role of this node */ exports.role = { 'isWorker' : function() { return !exports.options['role'] || exports.options['role'] === jive.constants.roles.WORKER; }, 'isPusher' : function() { return !exports.options['role'] || exports.options['role'] === jive.constants.roles.PUSHER; }, 'isHttp' : function() { return !exports.options['role'] || exports.options['role'] === jive.constants.roles.HTTP_HANDLER; } }; /** * API for managing add-on extensions. * @returns {module:extensions} */ exports.extensions = function() { return require('./extension/extension'); }; /** * @private * @param all * @returns {Array} */ exports.getExpandedTileDefinitions = function(all) { var conf = exports.options; var host = exports.serviceURL(); var processed = []; all.forEach( function( tile ) { if ( !tile ) { return; } var name = tile.name; var stringified = JSON.stringify(tile); stringified = mustache.render(stringified, { host: host, tile_public: host + '/' + name, tile_route: host + '/' + name, clientId: conf.clientId }); var processedTile = JSON.parse(stringified); // defaults if ( !processedTile['published'] ) { processedTile['published'] = "2013-02-28T15:12:16.768-0800"; } if ( !processedTile['updated'] ) { processedTile['updated'] = "2013-02-28T15:12:16.768-0800"; } if ( processedTile['action'] ) { if ( processedTile['action'].indexOf('http') != 0 ) { // assume its relative to host then processedTile['action'] = host + ( processedTile['action'].indexOf('/') == 0 ? "" : "/" ) + processedTile['action']; } } if ( !processedTile['config'] ) { processedTile['config'] = host + '/' + processedTile['definitionDirName'] + '/configure'; } else { if ( processedTile['config'].indexOf('http') != 0 ) { // assume its relative to host then processedTile['config'] = host + ( processedTile['config'].indexOf('/') == 0 ? "" : "/" ) + processedTile['config']; } } if ( !processedTile['unregister']) { processedTile['unregister'] = host + '/unregister'; } else { if ( processedTile['unregister'].indexOf('http') != 0 ) { // assume its relative to host then processedTile['unregister'] = host + ( processedTile['unregister'].indexOf('/') == 0 ? "" : "/" ) + processedTile['unregister']; } } if ( !processedTile['register'] ) { processedTile['register'] = host + '/registration'; } else { if ( processedTile['register'].indexOf('http') != 0 ) { // assume its relative to host then processedTile['register'] = host + ( processedTile['register'].indexOf('/') == 0 ? "" : "/" ) + processedTile['register']; } } if ( processedTile['unregister'] && processedTile['unregister'].indexOf('http') != 0 ) { // assume its relative to host then processedTile['unregister'] = host + ( processedTile['unregister'].indexOf('/') == 0 ? "" : "/" ) + processedTile['unregister']; } if ( !processedTile['client_id'] ) { processedTile['client_id'] = conf.clientId; } if ( !processedTile['id'] ) { processedTile['id'] = '{{{definition_id}}}'; } if (conf.clientId) { processedTile.description += ' for ' + conf.clientId; } processed.push( processedTile ); }); return processed; }; /** * API for managing service security * @returns {module:security} */ exports.security = function() { return security; }; /** * @returns {boolean} Returns true if service configuration options has 'development' : true set. */ exports.isDevelopment = function() { return exports.options['development'] === true; }; /** * @returns {string} */ exports.serviceStatus = function() { return serviceState; };