arrowjs/ArrowjsCore

View on GitHub
libs/ArrowApplication.js

Summary

Maintainability
F
1 wk
Test Coverage
'use strict';

/**
 * Module dependencies.
 */
const fs = require('fs'),
    arrowStack = require('./ArrStack'),
    path = require('path'),
    express = require('express'),
    _ = require('lodash'),
    Promise = require('bluebird'),
    RedisCache = require("./RedisCache"),
    installLog = require("./logger").init,
    logger = require('./logger'),
    __ = require("./global_function"),
    EventEmitter = require('events').EventEmitter,
    r = require('nunjucks').runtime,
    DefaultManager = require("../manager/DefaultManager"),
    ConfigManager = require("../manager/ConfigManager"),
    buildStructure = require("./buildStructure"),
    socket_io = require('socket.io'),
    http = require('http'),
    socketRedisAdapter = require('socket.io-redis'),
    ViewEngine = require("../libs/ViewEngine"),
    fsExtra = require('fs-extra'),
    loadingLanguage = require("./i18n").loadLanguage;
let eventEmitter;
/**
 * ArrowApplication is a singleton object. It is heart of Arrowjs.io web app. it wraps Express and adds following functions:
 * support Redis, multi-languages, passport, check permission and socket.io / websocket
 */

class ArrowApplication {
    /**
     *
     * @param {object} setting - {passport: bool ,role : bool, order :bool}
     * <ul>
     *     <li>passport: true, turn on authentication using Passport module</li>
     *     <li>role: true, turn on permission checking, RBAC</li>
     *     <li>order: true, order router priority rule.</li>
     * </ul>
     */
    constructor(setting) {
        //if NODE_ENV does not exist, use development by default
        /* istanbul ignore next */
        process.env.NODE_ENV = process.env.NODE_ENV || 'development';

        this.beforeAuth = [];  //add middle-wares before user authenticates
        this.afterAuth = [];   //add middle-ware after user authenticates
        this.plugins = [];   //add middle-ware after user authenticates
        let app = express();

        global.Arrow = {};

        this.arrowErrorLink = {};
        //Move all functions of express to ArrowApplication
        //So we can call ArrowApplication.listen(port)
        _.assign(this, app);
        this.expressApp = function (req, res, next) {
            this.handle(req, res, next);
        }.bind(this);

        // 0 : location of this file
        // 1 : location of index.js (module file)
        // 2 : location of server.js file
        let requester = arrowStack(2);
        this.arrFolder = path.dirname(requester) + '/';

        //assign current Arrowjs application folder to global variable
        global.__base = this.arrFolder;

        //Read config/config.js into this._config
        this._config = __.getRawConfig();

        //Read and parse config/structure.js
        /* buildStructure() takes input as an object -> returns an array
         * __.getStructure() reads file config/structure.js and returns the struc obj
         */
        this.structure = buildStructure(__.getStructure());

        //display longer stack trace in console
        if (this._config.long_stack) {
            require('longjohn')
        }

        installLog(this);
        this.logger = logger;

        this.middleware = Object.create(null);
        //Add passport and its authentication strategies
        this.middleware.passport = require("../config/middleware/loadPassport").bind(this);

        //Display flash message when user reloads view
        this.middleware.flashMessage = require("../config/middleware/flashMessage").bind(this);

        //Use middleware express-session to store user session
        this.middleware.session = require("../config/middleware/useSession").bind(this);

        //Serve static resources when not using Nginx.
        //See config/view.js section resource
        this.middleware.serveStatic = require("../config/middleware/staticResource").bind(this);

        this.middleware.helmet = require("helmet");
        this.middleware.bodyParser = require("body-parser");
        this.middleware.cookieParser = require("cookie-parser");
        this.middleware.morgan = require("morgan");
        this.middleware.methodOverride = require('method-override');


        //Load available languages. See libs/i18n.js and folder /lang
        loadingLanguage(this._config);

        //Bind all global functions to ArrowApplication object
        loadingGlobalFunction(this);


        //Declare _arrRoutes to store all routes of features
        this._arrRoutes = Object.create(null);

        this.utils = Object.create(null);
        this.utils.dotChain = getDataByDotNotation; // not found
        this.utils.fs = fsExtra;
        this.utils.loadAndCreate = loadSetting.bind(this); // not found
        this.utils.markSafe = r.markSafe;

        this.arrowSettings = Object.create(null);
    }

    /**
     * When user requests an URL, execute this function before server checks in session if user has authenticates
     * If web app does not require authentication, beforeAuthenticate and afterAuthenticate are same. <br>
     * beforeAuthenticate > authenticate > afterAuthenticate
     * @param {function} func - function that executes before authentication
     */
    beforeAuthenticate(func) {
        let self = this;
        /* istanbul ignore next */
        return new Promise(function (fulfill, reject) {
            if (typeof func == "function") {
                self.beforeAuth.push(func.bind(self));
                fulfill()
            } else {
                reject(new Error("Cant beforeAuthenticate: not an function"));
            }
        });
    }

    /**
     * When user requests an URL, execute this function after server checks in session if user has authenticates
     * This function always runs regardless user has authenticated or not
     * @param {function} func - function that executes after authentication
     */
    afterAuthenticate(func) {
        let self = this;
        /* istanbul ignore next */
        return new Promise(function (fulfill, reject) {
            if (typeof func == "function") {
                self.afterAuth.push(func.bind(self));
                fulfill()
            } else {
                reject(new Error("Cant afterAuthenticate: not an function"));
            }
        })
    }

    /**
     * Turn object to become property of ArrowApplication
     * @param {object} obj - object parameter
     */

    addGlobal(obj) {
        /* istanbul ignore next */
        return new Promise(function (fulfill, reject) {
            if (_.isObject(obj)) {
                _.assign(Arrow, obj);
                fulfill()
            } else {
                reject(new Error("Cant addGlobal: not an Object"));
            }
        });
    }

    /**
     * Add plugin function to ArrowApplication.plugins, bind this plugin function to ArrowApplication object
     * @param {function} plugin - plugin function
     * <p><a target="_blank" href="http://www.smashingmagazine.com/2014/01/understanding-javascript-function-prototype-bind/">See more about bind</a></p>
     */
    addPlugin(plugin) {
        let self = this;
        /* istanbul ignore next */
        return new Promise(function (fulfill, reject) {
            if (_.isFunction(plugin)) {
                self.plugins.push(plugin.bind(self));
                fulfill()
            } else {
                reject(new Error("Cant addPlugin: not an Function"));
            }
        })

    }

    /**
     * Kick start express application and listen at default port
     * @param {object} setting - {passport: boolean, role: boolean}
     */
    start(setting) {
        let self = this;

        self.arrowSettings = setting;
        let stackBegin;
        return Promise.resolve()
            .then(function connectRedis() {
                //Redis connection
                let redisConfig = self._config.redis || {};
                return RedisCache(redisConfig);
            })
            .then(function (redisFunction) {
                //Make redis cache
                /* istanbul ignore next */
                let redisConfig = self._config.redis || {};
                let redisClient = redisFunction(redisConfig);
                let redisSubscriber = redisFunction.bind(null, redisConfig);

                self.redisClient = redisClient;
                self.redisSubscriber = redisSubscriber;
            })
            .then(function connectDatabase() {
                return require('./database').connectDB(self)
            })
            .then(function setupManager() {
                //Share eventEmitter among all kinds of Managers. This helps Manager object notifies each other
                //when configuration is changed
                eventEmitter = new EventEmitter();

                self.configManager = new ConfigManager(self, "config");
                //subscribes to get notification from shared eventEmitter object
                self.configManager.eventHook(eventEmitter);

                //Create shortcut call
                self.addConfig = addConfig.bind(self);
                self.addConfigFile = addConfigFile.bind(self);
                self.getConfig = self.configManager.getConfig.bind(self.configManager);
                self.setConfig = self.configManager.setConfig.bind(self.configManager);
                self.updateConfig = self.configManager.updateConfig.bind(self.configManager);

                //_componentList contains name property of composite features, singleton features, widgets, plugins
                self._componentList = [];
                Object.keys(self.structure).map(function (managerKey) {
                    let key = managerKey;
                    let managerName = managerKey + "Manager";
                    self[managerName] = new DefaultManager(self, key);
                    self[managerName].eventHook(eventEmitter);
                    //load sub components of
                    self[managerName].loadComponents(key);
                    self[key] = self[managerName]["_" + key];
                    self._componentList.push(key);
                }.bind(self));
            })
            .then(function configRenderLayout() {
                let viewEngineSetting = _.assign(self._config.nunjuckSettings || {}, {express: self._expressApplication});
                let applicationView = ViewEngine(self.arrFolder, viewEngineSetting, self);
                self.applicationENV = applicationView;

                self.render = function (view, options, callback) {
                    let application = self;
                    var done = callback;

                    var opts = options || {};
                    /* istanbul ignore else */
                    if (typeof options === "function") {
                        done = options;
                        opts = {};
                    }

                    _.assign(opts, application.locals);

                    if (application._config.viewExtension && view.indexOf(application._config.viewExtension) === -1 && view.indexOf(".") === -1) {
                        view += "." + application._config.viewExtension;
                    }

                    let arrayPart = view.split(path.sep);
                    arrayPart = arrayPart.map(function (key) {
                        if (key[0] === ":") {
                            key = key.replace(":", "");
                            return application.getConfig(key);
                        } else {
                            return key
                        }
                    });

                    let newLink = arrayPart.join(path.sep);

                    newLink = path.normalize(application.arrFolder + newLink);

                    application.applicationENV.loaders[0].pathsToNames = {};
                    application.applicationENV.loaders[0].cache = {};
                    application.applicationENV.loaders[0].searchPaths = [path.dirname(newLink) + path.sep];
                    return application.applicationENV.render(newLink, opts, done);
                }.bind(self);

                self.renderString = applicationView.renderString.bind(applicationView);
            })
            .then(function () {
                let resolve = Promise.resolve();
                self.plugins.map(function (plug) {
                    resolve = resolve.then(function () {
                        return plug()
                    })
                });
                return resolve
            })
            .then(function () {
                addRoles(self);
                if (self.getConfig("redis.type") !== "fakeredis") {
                    let resolve = self.configManager.getCache();
                    self._componentList.map(function (key) {
                        let managerName = key + "Manager";
                        resolve = resolve.then(function () {
                            return self[managerName].getCache()
                        })
                    });
                    return resolve
                } else {
                    return Promise.resolve();
                }
            })
            .then(function () {
                //init Express app
                return configureExpressApp(self, self.getConfig(), setting)
            })
            .then(function () {
                return associateModels(self);
            })
            .then(function () {
                return circuit_breaker(self)
            })
            .then(function () {
                if (setting && setting.order) {
                    stackBegin = self._router.stack.length;
                }
                return loadRoute_Render(self, setting);
            })
            .then(function () {
                if (setting && setting.order) {
                    let coreRoute = self._router.stack.slice(0, stackBegin);
                    let newRoute = self._router.stack.slice(stackBegin);
                    newRoute = newRoute.sort(function (a, b) {
                        if (a.handle.order) {
                            if (b.handle.order) {
                                if (a.handle.order > b.handle.order) {
                                    return 1
                                } else if (a.handle.order < b.handle.order) {
                                    return -1
                                } else {
                                    return 0
                                }
                            } else {
                                return 0
                            }
                        } else {
                            if (b.handle.order) {
                                return 1
                            } else {
                                return 0
                            }
                        }
                    });
                    coreRoute = coreRoute.concat(newRoute);
                    self._router.stack = coreRoute
                }
                return handleError(self)
            })
            .then(function () {
                return http.createServer(self.expressApp);
            })
            .then(function (server) {
                self.httpServer = server;
                if (self.getConfig('websocket_enable') && self.getConfig('websocket_folder')) {
                    let io = socket_io(server);
                    if (self.getConfig('redis.type') !== 'fakeredis') {
                        let redisConf = {host: self.getConfig('redis.host'), port: self.getConfig('redis.port')};
                        io.adapter(socketRedisAdapter(redisConf));
                    }
                    self.io = io;

                    __.getGlobbedFiles(path.normalize(self.arrFolder + self.getConfig('websocket_folder'))).map(function (link) {
                        let socketFunction = require(link);
                        /* istanbul ignore else */
                        if (_.isFunction(socketFunction)) {
                            socketFunction(io);
                        }
                    })
                }

                server.listen(self.getConfig("port"), function () {
                    logger.info('Application loaded using the "' + process.env.NODE_ENV + '" environment configuration');
                    logger.info('Application started on port ' + self.getConfig("port"), ', Process ID: ' + process.pid);
                });
                /* istanbul ignore next */
            })
            .catch(function (err) {
                logger.error(err)
            });
    }

    close() {
        var self = this;
        return new Promise(function (fulfill, reject) {
            /* istanbul ignore else */
            if (self.httpServer) {
                self.httpServer.close(function (err) {
                    /* istanbul ignore if */
                    if (err) {
                        reject(err)
                    } else {
                        fulfill(self.httpServer)
                    }
                })
            } else {
                fulfill(self.httpServer)
            }
        })
    }
}

/**
 * Associate all models that are loaded into ArrowApplication.models. Associate logic defined in /config/database.js
 * @param {ArrowApplication} arrow
 * @return {ArrowApplication} arrow
 */

function associateModels(arrow) {
    let defaultDatabase = require('./database').db();
    if (arrow.models && Object.keys(arrow.models).length > 0) {
        /* istanbul ignore next */
        let defaultQueryResolve = function () {
            return new Promise(function (fulfill, reject) {
                fulfill("No models")
            })
        };

        //Load model associate rules defined in /config/database.js
        let databaseFunction = require(arrow.arrFolder + "config/database");

        Object.keys(arrow.models).forEach(function (modelName) {
            if ("associate" in arrow.models[modelName]) {
                arrow.models[modelName].associate(arrow.models);
            }
        });

        /* istanbul ignore else */
        if (databaseFunction.associate) {
            let resolve = Promise.resolve();
            resolve.then(function () {
                //Execute models associate logic in /config/database.js
                return databaseFunction.associate(arrow.models)
            }).then(function () {
                defaultDatabase.sync();  //Sequelize.sync: sync all defined models to the DB.
            })
        }


        //Assign raw query function of Sequelize to arrow.models object
        //See Sequelize raw query http://docs.sequelizejs.com/en/latest/docs/raw-queries/
        /* istanbul ignore next */
        if (defaultDatabase.query) {
            arrow.models.rawQuery = defaultDatabase.query.bind(defaultDatabase);
            _.merge(arrow.models, defaultDatabase);
        } else {
            arrow.models.rawQuery = defaultQueryResolve;
        }

    }
    return arrow
}


/**
 * load feature's routes and render
 * @param {ArrowApplication } arrow
 * @param {object} userSetting
 */
function loadRoute_Render(arrow, userSetting) {

    arrow._componentList.map(function (key) {
        Object.keys(arrow[key]).map(function (componentKey) {
            /** display loaded feature in console log when booting Arrowjs application */
            logger.info("Arrow loaded: '" + key + "' - '" + componentKey + "'");
            let routeConfig = arrow[key][componentKey]._structure.route;
            if (routeConfig) {
                Object.keys(routeConfig.path).map(function (second_key) {
                    let defaultRouteConfig = routeConfig.path[second_key];
                    if (arrow[key][componentKey].routes[second_key]) {
                        let componentRouteSetting = arrow[key][componentKey].routes[second_key];
                        handleComponentRouteSetting(arrow, componentRouteSetting, defaultRouteConfig, key, userSetting, componentKey);
                    } else {
                        let componentRouteSetting = arrow[key][componentKey].routes;
                        //Handle Route Path;
                        handleComponentRouteSetting(arrow, componentRouteSetting, defaultRouteConfig, key, userSetting, componentKey);
                    }
                });
            }
        })
    });
    return arrow;
}

/**
 * Run /config/express.js to configure Express object
 * @param app - ArrowApplication object
 * @param config - this.configManager.getConfig
 * @param setting - parameters in application.start(setting);
 */
function configureExpressApp(app, config, setting) {
    return new Promise(function (fulfill, reject) {
        let expressFunction;
        /* istanbul ignore else */
        if (fs.existsSync(path.resolve(app.arrFolder + "config/express.js"))) {
            expressFunction = require(app.arrFolder + "config/express");
        } else {
            reject(new Error("Cant find express.js in folder config"))
        }
        fulfill(expressFunction(app, config, setting));
    });
}


/**
 *
 * @param arrow
 * @param componentRouteSetting
 * @param defaultRouteConfig
 * @param key
 * @param setting
 * @param componentKey
 */
function handleComponentRouteSetting(arrow, componentRouteSetting, defaultRouteConfig, key, setting, componentKey) {
    let component = arrow[key][componentKey];
    let componentName = arrow[key][componentKey].name;
    let viewInfo = arrow[key][componentKey].views;
    //Handle Route Path;
    let route = express.Router();
    route.componentName = componentName;

    Object.keys(componentRouteSetting).map(function (path_name) {
        //Check path_name
        /* istanbul ignore next */
        let routePath = path_name[0] === "/" ? path_name : "/" + componentName + "/" + path_name;

        //handle prefix
        /* istanbul ignore if */
        if (defaultRouteConfig.prefix && defaultRouteConfig.prefix[0] !== "/") {
            defaultRouteConfig.prefix = "/" + defaultRouteConfig.prefix
        }
        let prefix = defaultRouteConfig.prefix || "/";

        let arrayMethod = Object.keys(componentRouteSetting[path_name]).filter(function (method) {
            if (componentRouteSetting[path_name][method].name) {
                arrow._arrRoutes[componentRouteSetting[path_name][method].name] = path.normalize(prefix + routePath);
            }
            //handle function
            let routeHandler;
            componentRouteSetting[path_name][method].handler = componentRouteSetting[path_name][method].handler || function (req, res, next) {
                    next(new Error("Cant find controller"));
                };


            routeHandler = componentRouteSetting[path_name][method].handler;
            let authenticate = componentRouteSetting[path_name][method].authenticate !== undefined ? componentRouteSetting[path_name][method].authenticate : defaultRouteConfig.authenticate;

            let arrayHandler = [];
            if (arrayHandler && _.isArray(routeHandler)) {
                arrayHandler = routeHandler.filter(function (func) {
                    if (_.isFunction(func)) {
                        return func
                    }
                });
            } else if (_.isFunction(routeHandler)) {
                arrayHandler.push(routeHandler)
            } else if (!_.isString(authenticate)) {
                return
            }

            //Add viewRender
            if (!_.isEmpty(viewInfo) && !_.isString(authenticate)) {
                arrayHandler.splice(0, 0, overrideViewRender(arrow, viewInfo, componentName, component, key))
            }


            //handle role
            if (setting && setting.role) {
                let permissions = componentRouteSetting[path_name][method].permissions;
                if (permissions && !_.isString(authenticate)) {
                    arrayHandler.splice(0, 0, arrow.passportSetting.handlePermission);
                    arrayHandler.splice(0, 0, handleRole(arrow, permissions, componentName, key))
                }
            }

            //add middleware after authenticate;
            if (!_.isEmpty(arrow.afterAuth)) {
                arrow.afterAuth.map(function (func) {
                    arrayHandler.splice(0, 0, func)
                })
            }

            //handle Authenticate
            if (setting && setting.passport) {
                if (authenticate) {
                    arrayHandler.splice(0, 0, handleAuthenticate(arrow, authenticate))
                }
            }

            //add middleware before authenticate;
            if (!_.isEmpty(arrow.beforeAuth)) {
                arrow.beforeAuth.map(function (func) {
                    arrayHandler.splice(0, 0, func)
                })
            }

            //Add to route
            if (method === "param") {
                if (_.isString(componentRouteSetting[path_name][method].key) && !_.isArray(componentRouteSetting[path_name][method].handler)) {
                    return route.param(componentRouteSetting[path_name][method].key, componentRouteSetting[path_name][method].handler);
                }
            } else if (method === 'all') {
                return route.route(routePath)
                    [method](arrayHandler);
            } else if (route[method] && ['route', 'use'].indexOf(method) === -1) {
                if (componentRouteSetting[path_name][method].order) {
                    let newRoute = express.Router();
                    newRoute.componentName = componentName;
                    newRoute.order = componentRouteSetting[path_name][method].order;
                    newRoute.route(routePath)
                        [method](arrayHandler);
                    arrow.use(prefix, newRoute)
                }
                return route.route(routePath)
                    [method](arrayHandler)
            }
        });
        !_.isEmpty(arrayMethod) && arrow.use(prefix, route);
    });
    return arrow
}
/**
 *
 * @param application
 * @param componentView
 * @param componentName
 * @param component
 * @returns {Function}
 */
function overrideViewRender(application, componentView, componentName, component, key) {
    return function (req, res, next) {
        // Grab reference of render
        req.arrowUrl = key + "." + componentName;

        let _render = res.render;
        let self = this;
        if (_.isArray(componentView)) {
            res.render = makeRender(req, res, application, componentView, componentName, component);
        } else {
            Object.keys(componentView).map(function (key) {
                res[key] = res[key] || {};
                res[key].render = makeRender(req, res, application, componentView[key], componentName, component[key]);
            });
            res.render = res[Object.keys(componentView)[0]].render
        }
        next();
    }
}
/**
 *
 * @param req
 * @param res
 * @param application
 * @param componentView
 * @param componentName
 * @param component
 * @returns {Function}
 */
function makeRender(req, res, application, componentView, componentName, component) {
    return function (view, options, callback) {

        var done = callback;
        var opts = {};

        //remove flash message
        delete req.session.flash;

        // merge res.locals
        _.assign(opts, application.locals);
        _.assign(opts, res.locals);

        // support callback function as second arg
        /* istanbul ignore if */
        if (typeof options === 'function') {
            done = options;
        } else {
            _.assign(opts, options);
        }
        // default callback to respond
        done = done || function (err, str) {
                if (err) return req.next(err);
                res.send(str);
            };

        if (application._config.viewExtension && view.indexOf(application._config.viewExtension) === -1 && view.indexOf(".") === -1) {
            view += "." + application._config.viewExtension;
        }
        component.viewEngine.loaders[0].pathsToNames = {};
        component.viewEngine.loaders[0].cache = {};
        component.viewEngine.loaders[0].searchPaths = componentView.map(function (obj) {
            return handleView(obj, application, componentName);
        });

        component.viewEngine.render(view, opts, done);
    };
}

/**
 *
 * @param obj
 * @param application
 * @param componentName
 * @returns {*}
 */
function handleView(obj, application, componentName) {
    let miniPath = obj.func(application._config, componentName);
    let normalizePath;
    /* istanbul ignore if */
    if (miniPath[0] === path.sep) {
        normalizePath = path.normalize(obj.base + path.sep + miniPath);
    } else {
        normalizePath = path.normalize(obj.fatherBase + path.sep + miniPath)
    }
    return normalizePath
}

/* istanbul ignore next */
function handleAuthenticate(application, name) {
    let passport = application.passport;
    if (_.isString(name)) {
        if (application.passportSetting[name]) {
            let strategy = application.passportSetting[name].strategy || name;
            let callback = application.passportSetting[name].callback;
            let option = application.passportSetting[name].option || {};
            if (callback) return passport.authenticate(strategy, option, callback);
            return passport.authenticate(strategy, option);
        }
    } else if (_.isBoolean(name)) {
        if (application.passportSetting.checkAuthenticate && _.isFunction(application.passportSetting.checkAuthenticate)) {
            return application.passportSetting.checkAuthenticate
        }
    }
    return function (req, res, next) {
        next()
    }
}
/**
 *
 * @param application
 * @param permissions
 * @param componentName
 * @param key
 * @returns {handleRoles}
 */
/* istanbul ignore next */
function handleRole(application, permissions, componentName, key) {
    let arrayPermissions = [];
    if (_.isArray(permissions)) {
        arrayPermissions = permissions
    } else {
        arrayPermissions.push(permissions);
    }
    return function handleRoles(req, res, next) {
        req.permissions = req.session.permissions;
        if (req.permissions && req.permissions[key] && req.permissions[key][componentName]) {
            let checkedPermission = req.permissions[key][componentName].filter(function (key) {
                if (arrayPermissions.indexOf(key.name) > -1) {
                    return key
                }
            }).map(function (data) {
                return data.name
            });
            if (!_.isEmpty(checkedPermission)) {
                req.permissions = checkedPermission;
                req.hasPermission = true
            }
        } else {
            req.hasPermission = false;
        }
        next();
    }
}
/**
 * Load global functions then append to global.ArrowHelper
 * bind global function to ArrowApplication object so dev can this keyword in that function to refer
 * ArrowApplication object
 * @param self: ArrowApplication object
 */
function loadingGlobalFunction(self) {
    global.ArrowHelper = {};
    __.getGlobbedFiles(path.resolve(__dirname, "..", "helpers/*.js")).map(function (link) {
        let arrowObj = require(link);
        Object.keys(arrowObj).map(function (key) {
            /* istanbul ignore else */
            if (_.isFunction(arrowObj[key])) {
                ArrowHelper[key] = arrowObj[key].bind(self)
            } else {
                ArrowHelper[key] = arrowObj[key]
            }
        })
    });
    __.getGlobbedFiles(path.normalize(__base + self._config.ArrowHelper + "*.js")).map(function (link) {
        let arrowObj = require(link);
        Object.keys(arrowObj).map(function (key) {
            if (_.isFunction(arrowObj[key])) {
                ArrowHelper[key] = arrowObj[key].bind(self)
            } else {
                ArrowHelper[key] = arrowObj[key]
            }
        })
    });

    //Add some support function
    global.__ = ArrowHelper.__;
    return self
}


function addRoles(self) {
    self.permissions = {};
    self._componentList.map(function (key) {
        let managerName = key + "Manager";
        self.permissions[key] = self[managerName].getPermissions();
    });
    return self
}

/**
 * Redirect url when meet same error.
 * @param app
 * @returns {*}
 */
function circuit_breaker(app) {
    if (app.getConfig('fault_tolerant.enable')) {
        app.use(function (req, res, next) {
            if (app.arrowErrorLink[req.url]) {
                let redirectLink = app.getConfig('fault_tolerant.redirect');
                redirectLink = _.isString(redirectLink) ? redirectLink : "404";
                res.redirect(path.normalize(path.sep + redirectLink));
            } else {
                next();
            }
        })
    }
    return app
}

/**
 * Handle Error and redirect error
 * @param app
 */
/* istanbul ignore next */
function handleError(app) {
    /** Assume 'not found' in the error msg is a 404.
     * This is somewhat silly, but valid, you can do whatever you like, set properties, use instanceof etc.
     */
    app.use(function (err, req, res, next) {
        // If the error object doesn't exists
        let error = {};
        if (!err) return next();
        if (app.getConfig('fault_tolerant.enable')) {
            app.arrowErrorLink[req.url] = true;
            error[req.url] = {};
            error[req.url].error = err.stack;
            error[req.url].time = Date.now();
            let arrayInfo = app.getConfig('fault_tolerant.logdata');
            if (_.isArray(arrayInfo)) {
                arrayInfo.map(function (key) {
                    error[req.url][key] = req[key];
                })
            }

        } else {
            error.error = err.stack;
            error.time = Date.now();
            let arrayInfo = app.getConfig('fault_tolerant.logdata');
            if (_.isArray(arrayInfo)) {
                arrayInfo.map(function (key) {
                    error[key] = req[key];
                })
            }
        }
        if (app.getConfig('fault_tolerant.log')) {
            logger.error(error);
        }

        let renderSetting = app.getConfig('fault_tolerant.render');
        if (_.isString(renderSetting)) {
            let arrayPart = renderSetting.split(path.sep);
            arrayPart = arrayPart.map(function (key) {
                if (key[0] === ":") {
                    key = key.replace(":", "");
                    return app.getConfig(key);
                } else {
                    return key
                }
            });
            let link = arrayPart.join(path.sep);
            app.render(path.normalize(app.arrFolder + link), {error: error}, function (err, html) {
                if (err) {
                    res.send(err)
                } else {
                    res.send(html)
                }
            });
        } else {
            res.send(error)
        }
    });
    if (app.getConfig("error")) {
        Object.keys(app.getConfig("error")).map(function (link) {
            let errorConfig = app.getConfig("error");
            if (errorConfig[link].render) {
                if (_.isString(errorConfig[link].render)) {
                    app.get(path.normalize(path.sep + link + `(.html)?`), function (req, res) {
                        app.render(errorConfig[link].render, function (err, html) {
                            if (err) {
                                res.send(err.toString())
                            } else {
                                res.send(html)
                            }
                        });
                    })
                } else if (_.isObject(errorConfig[link].render)) {
                    app.get(link, function (req, res) {
                        res.send(errorConfig[link].render)
                    })
                } else if (_.isNumber(errorConfig[link].render)) {
                    app.get(link, function (req, res) {
                        res.sendStatus(errorConfig[link].render)
                    })
                } else {
                    app.get(link, function (req, res) {
                        res.send(link)
                    })
                }
            }

        })
    }

    if (app.getConfig("error")) {
        Object.keys(app.getConfig("error")).map(function (link) {
            let errorConfig = app.getConfig("error");
            if (errorConfig[link] && errorConfig[link].prefix) {
                app.use(errorConfig[link].prefix, function (req, res) {
                    res.redirect(path.normalize(path.sep + link))
                })
            }
        })
    }

    app.use("*", function (req, res) {
        let h = req.header("Accept");
        try {
            if (h.indexOf('text/html') > -1) {
                res.redirect('/404');
            } else {
                res.sendStatus(404);
            }
        } catch (err) {
            res.sendStatus(404);
        }
    });

    return app
}

function getDataByDotNotation(obj, key) {
    if (_.isString(key)) {
        if (key.indexOf(".") > 0) {
            let arrayKey = key.split(".");
            let self = obj;
            let result;
            arrayKey.map(function (name) {
                if (self[name]) {
                    result = self[name];
                    self = result;
                } else {
                    result = null
                }
            });
            return result
        } else {
            return obj[key];
        }
    } else {
        return null
    }
}
/* istanbul ignore next */
function loadSetting(des, source) {
    let baseFolder = this.arrFolder;
    let setting = {};
    let filePath = path.normalize(baseFolder + des);
    try {
        fs.accessSync(filePath);
        _.assign(setting, require(filePath));
    } catch (err) {
        if (err.code === 'ENOENT') {
            fsExtra.copySync(path.resolve(source), filePath);
            _.assign(setting, require(filePath));
        } else {
            throw err
        }

    }
    return setting;
}

function addConfig(obj) {
    let config = this._config;
    if (_.isObject(obj)) {
        _.assign(config, obj);
    }
}
/* istanbul ignore next */
function addConfigFile(filename) {
    let app = this;
    let configFile = path.normalize(app.arrFolder + "/" + filename);
    fs.readFile(configFile, 'utf8', function (err, data) {
        if (err) throw err;
        let obj = JSON.parse(data);

        app.updateConfig(obj);
    });
    fs.watch(configFile, function (event, filename) {
        if (event === "change") {
            fs.readFile(configFile, 'utf8', function (err, data) {
                if (err) throw err;
                let obj = JSON.parse(data);

                app.updateConfig(obj);
            });
        }
    })
}

module.exports = ArrowApplication;