Source: data/service/authorization-manager.js

var Montage = require("core/core").Montage,
    Promise = require("core/promise").Promise,
    Map = require("collections/map"),
    application = require("core/application").application,
    AuthorizationPolicy = require("data/service/authorization-policy").AuthorizationPolicy,
    MANAGER_PANEL_MODULE = "ui/authorization-manager-panel.reel";


/**
 * Helps coordinates the needs for DataServices to get the authorization they
 * need to access data. It is meant to be a singleton, so the constructor
 * enforces that.
 *
 * @class
 * @extends external:Montage
 */
var AuthorizationManager = Montage.specialize(/** @lends AuthorizationManager.prototype */ {

    constructor: {
        value: function () {
            this._providersByModuleID = new Map();
            this._panelsByModuleID = new Map();
            this._authorizationsByProviderModuleID = new Map();
            this.defineBinding("hasPendingServices", {"<-": "_pendingServicesCount != 0"});
            return this;
        }
    },

    /********************************************
     * Caching
     */

    // Provider Module ID to Authorization Promise
    _authorizationsByProviderModuleID: {
        value: undefined
    },

    // Module ID to Panel
    _panelsByModuleID: {
        value: undefined
    },

    // Module ID to Service
    _providersByModuleID: {
        value: undefined
    },

    /********************************************
     * Panels
     */

    _managerPanel: {
        value: function () {
            var self = this,
                moduleId;

            if (!this._managerPanelPromise && this.authorizationManagerPanel) {
                this.authorizationManagerPanel.authorizationManager = this;
                this._managerPanelPromise = Promise.resolve(this.authorizationManagerPanel);
            } else if (!this._managerPanelPromise) {
                moduleId = this.callDelegateMethod("authorizationManagerWillLoadAuthorizationManagerPanel", this, MANAGER_PANEL_MODULE) || MANAGER_PANEL_MODULE;
                this._managerPanelPromise = require.async(moduleId).bind(this).then(function (exports) {
                    var panel = new exports.AuthorizationManagerPanel();
                    self.authorizationManagerPanel = panel;
                    panel.authorizationManager = self;
                    return panel;
                }).catch(function(error) {
                    console.log(error);
                });
            }

            return this._managerPanelPromise;
        }
    },

    _panelForProvider: {
        value: function (provider) {
            var moduleId = this._panelModuleIDForProvider(provider),
                panel = this._panelsByModuleID.get(moduleId);

            return panel ? Promise.resolve(panel) : this._makePanelForProvider(moduleId, provider);
        }
    },

    _panelModuleIDForProvider: {
        value: function (provider) {
            var moduleID = provider.authorizationPanel;
            return this.callDelegateMethod("authorizationManagerWillAuthorizeServiceWithPanelModuleId", this,  provider, moduleID) || moduleID;
        }
    },

    _makePanelForProvider: {
        value: function (panelModuleID, provider) {
            var self = this,
                providerInfo = Montage.getInfoForObject(provider),
                panelPromise;

            if (panelModuleID) {
                panelPromise = providerInfo.require.async(panelModuleID).then(function (exports) {
                    var exportNames = Object.keys(exports),
                        panel, i, n;

                    for (i = 0, n = exportNames.length; i < n && !panel; ++i) {
                        panel = self._panelForConstructorAndProvider(exports[exportNames[i]], provider);
                    }
                    panel.service = provider;
                    self._panelsByModuleID.set(panelModuleID, panel);
                    return panel;
                });
            }

            return panelPromise;
        }
    },

    _panelForConstructorAndProvider: {
        value: function (constructor, provider) {
            return this.callDelegateMethod("authorizationManagerWillInstantiateAuthorizationPanelForService", this, constructor, provider) || new constructor();
        }
    },

    /********************************************
     * Services/Providers
     */

    _canNotifyDataService: {
        value: function (dataService) {
            return dataService.authorizationManagerWillAuthorizeWithService && typeof dataService.authorizationManagerWillAuthorizeWithService === "function";
        }
    },

    _providersForDataService: {
        value: function (dataService) {
            var promises = [],
                dataServiceInfo = Montage.getInfoForObject(dataService),
                providerIDs = dataService.authorizationServices,
                providerPromise, i, n;

            for (i = 0, n = providerIDs.length; i < n; ++i) {
                providerPromise = this._providerWithModuleID(providerIDs[i], dataServiceInfo.require);
                promises.push(providerPromise);
            }

            return Promise.all(promises);
        }
    },

    _providerWithModuleID: {
        value: function (moduleID, require) {
            var existingService = this._providersByModuleID.get(moduleID);
            return existingService ? Promise.resolve(existingService) :
                                     this._makeProviderWithModuleID(moduleID, require);
        }
    },


    _authorizationWithProvider: {
        value: function (moduleID, require) {
            var self = this,
                provider;
            return this._providerWithModuleID(moduleID, require).then(function (instance) {
                provider = instance;
                return provider.authorize();
            }).then(function (authorization) {
                return authorization || self._authorizeProviderWithManagerPanel(provider);

            });
        }
    },

    _authorizeProviderWithManagerPanel: {
        value: function (provider) {
            var self = this,
                managerPanel;
            self._pendingServicesCount++;
            return this._managerPanel().then(function (authManagerPanel) {
                managerPanel = authManagerPanel;
                return self._panelForProvider(provider);
            }).then(function (panel) {
                return managerPanel.authorizeWithPanel(panel);
            }).then(function (authorization) {
                self._pendingServicesCount--;
                return authorization;
            });
        }
    },

    _makeProviderWithModuleID: {
        value: function (moduleID, require) {
            var self = this;
            return require.async(moduleID).then(function (exports) {
                var provider, i, providerName,
                    providerNames = Object.keys(exports);
                for (i = 0; (providerName = providerNames[i]); ++i) {
                    provider = provider || new exports[providerName]();
                }
                self.registerAuthorizationService(provider);
                return provider;
            });
        }
    },

    _notifyDataService: {
        value: function (dataService) {
            var i, n;

            if (this._canNotifyDataService(dataService)) {
                return this._providersForDataService(dataService).then(function (services) {
                    for (i = 0, n = services.length; i < n; i++) {
                        //We tell the data service we're authorizing about authorizationService we create and are about to use.
                        dataService.authorizationManagerWillAuthorizeWithService(this, services[i]);
                    }
                });
            }

            return Promise.resolve(null);
        }
    },

    /********************************************
     * Public
     */

    /**
     * Module ID of the panel that displays the UI specific to the authorization providers.
     *
     *
     * Each authorization provider has a module ID pointing to a component which displays whatever
     * UI is required to get authorization through that provider. The most common UI for
     * applications with a single auth provider is a login form. The most common UI for
     * applications with multiple providers is the view that shows a button for each provider
     * and allows the user to select their preferred provider. For example, a button for each
     * of Facebook, Linkedin, and Google. The authorizationManagerPanel is the container
     * into which the provider-specific components are placed.
     *
     * @type {string}
     */
    authorizationManagerPanel: {
        value: undefined
    },

    /**
     * Takes care of obtaining authorization for a DataService. Returns a promise of Authorization
     *
     * TODO:
     * 1. Handle repeated calls: if a DataService authorizes on-demand it's likely
     * it would come from fetching data. Multiple independent fetches could trigger repeated
     * attempts to authorize: The promise should be cached and returned when pending.
     *
     * TODO:
     * 2. A service could require mandatory authorization from 2 dataService, right now it's implemented
     * in a way that we expect user to make a choice in one of aDataService.authorizationServices,
     * not a combination of. We need another structure to represent that.
     *
     * TODO
     * right now, Promises for existing objects are resolved, meaning that the loops could see different
     * types of objects coming in. Existing objects could be just added to array filled after the Promise.all().then..
     *
     * @method
     * @argument {DataService} dataService - A dataService for which to get an authorization
     */


    authorizeService: {
        value: function (dataService, didFailAuthorization) {
            var self = this,
                authorizationPromises = [];


            if (dataService.authorizationPolicy === AuthorizationPolicy.NONE) {
                return Promise.resolve(null);
            } else {

                //[TJ] This will only work for data services with a single authorization-service
                authorizationPromises = this._authorizationsForDataService(dataService);

                if (authorizationPromises.length) {
                    return Promise.all(authorizationPromises);
                } else if (dataService.authorizationPolicy === AuthorizationPolicy.ON_DEMAND && !didFailAuthorization) {
                    return Promise.resolve(null);
                } else {

                    authorizationPromises = this._authorizationsForDataService(dataService, true);
                    return self._notifyDataService(dataService).then(function () {
                        var useModal = application.applicationModal && self.authorizationManagerPanel.runModal;
                        return useModal ? self.authorizationManagerPanel.runModal() : Promise.all(authorizationPromises);
                    }).then(function(authorizations) {
                        self.callDelegateMethod("authorizationManagerDidAuthorizeService", self, dataService);
                        //TODO [TJ] How to concatenate authorizations from different auth services
                        //TODO      into a single Authorization Object for the data-service
                        return authorizations;
                    }).catch(function () {
                        self.hasPendingServices = false;
                    });
                }
            }
        }
    },

    _authorizationsForDataService: {
        value: function (dataService, requestIfAbsent) {
            var promises = [],
                dataServiceInfo = Montage.getInfoForObject(dataService),
                promise, moduleID, i, n;
            for (i = 0, n = dataService.authorizationServices.length; i < n; i++) {
                moduleID = dataService.authorizationServices[i];
                promise = this._authorizationForServiceFromProvider(moduleID, dataServiceInfo.require, requestIfAbsent);
                if (promise) {
                    promises.push(promise);
                }
            }
            return promises;
        }
    },

    _authorizationForServiceFromProvider: {
        value: function (moduleID, require, requestIfAbsent) {
            var promise = null;
            if (this._authorizationsByProviderModuleID.has(moduleID)) {
                promise = this._authorizationsByProviderModuleID.get(moduleID);
            } else if (requestIfAbsent) {
                promise = this._authorizationWithProvider(moduleID, require);
                this._authorizationsByProviderModuleID.set(moduleID, promise);
            }
            return promise;
        }
    },


    _pendingServicesCount: {
        value: 0
    },

    delegate: {
        value: null
    },


    /**
     * Flag to track the number of services pending on the AuthorizationManagerPanel.
     * The value is true if 1 or more services is currently pending. It can be used as
     * an alternative to runModal()/closeModal()
     * @type {boolean}
     */
    hasPendingServices: {
        value: false
    },


    /**
     *
     * @method
     * @argument {DataService} dataService - A dataService to be registered as an Authorization provider
     */
    registerAuthorizationService: {
        value: function(dataService) {
            var info = Montage.getInfoForObject(dataService);
            this._providersByModuleID.set(info.moduleId, dataService);
        }
    }

});

exports.AuthorizationManager = new AuthorizationManager();