/*
* 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;
};