<commit_before>/*jshint regexp:false*/

/**
 * Controller abstract class.
 */
define([
    './Joint',
    'services/state',
    'mout/string/startsWith',
    'mout/object/size',
    'mout/object/pick',
    'mout/object/fillIn',
    'mout/object/mixIn',
    'has'
], function (Joint, stateRegistry, startsWith, size, pick, fillIn, mixIn, has) {

    'use strict';

    /**
     * Constructor.
     */
    function Controller() {
        Joint.call(this);

        this._parseStates();
        this._parseDefaultState();
    }

    Controller.prototype = Object.create(Joint.prototype);
    Controller.prototype.constructor = Controller;

    /**
     * Get the current state or null if none is set.
     *
     * @return {State} The state
     */
    Controller.prototype.getState = function () {
        return this._currentState;
    };

    /**
     * Generates an URL for a state.
     *
     * @param {String}  name       The state name
     * @param {Object}  [params]   The state params
     * @param {Boolean} [absolute] True to generate an absolute URL, false otherwise
     *
     * @return {String} The generated URL
     */
    Controller.prototype.generateUrl = function (name, params, absolute) {
        var resolved;

        // Resolve the state
        resolved = this._resolveState(name, params);

        if (resolved.internal) {
            throw new Error('Cannot generate a URL for internal state "' + name + '" in "' + this.$name + '".');
        }

        return stateRegistry.generateUrl(resolved.fullName, resolved.params, absolute);
    };

    /**
     * Sets the current state.
     * If the state is the same, nothing happens.
     *
     * @param {String} [name]    The state name
     * @param {Object} [params]  The state params
     * @param {Object} [options] The options
     *
     * @return {Controller} The instance itself to allow chaining
     */
    Controller.prototype.setState = function (name, params, options) {
        var resolved,
            state;

        // Resolve the state
        // Note that parameters are shallow cloned to avoid side-effects
        resolved = this._resolveState(name, mixIn({}, params));

        // If the state is not ours, simply set it on the state registry
        if (resolved.absolute || resolved.relative) {
            stateRegistry.setCurrent(resolved.fullName, resolved.params, options);
            return this;
        }

        // Act differently if this state is global
        if (resolved.global) {
            // If so attempt to change the global state, aborting if it succeeded
            if (stateRegistry.setCurrent(resolved.fullName, resolved.params, options)) {
                return this;
            }

            // Since the global state is equal, grab it to avoid creating unnecessary
            // state objects
            state = stateRegistry.getCurrent().seekTo(resolved.name);
        // Otherwise state is local
        } else {
            state = stateRegistry._createStateInstance(resolved.name, resolved.params);

            // Generate local metadata
            state.getParams().$info = {
                newState: state,
                previousState: this._previousState
            };
        }

        return this.delegateState(state);
    };

    /**
     * Delegates a state to be handled by the controller.
     *
     * @param {Object|State} state The state parameter bag or a state instance
     *
     * @return {Controller} The instance itself to allow chaining
     */
    Controller.prototype.delegateState = function (state) {
        var name,
            currentState;

        // Assume app state if not passed
        if (state.$info) {
            state = state.$info.newState;
        }

        // Ensure state is filled with the defaults
        this._fillStateIfEmpty(state);

        name = state.getName();

        // If still has no name it means there's no default state defined
        if (!name) {
            if (has('debug') && this._nrStates) {
                console.warn('[spoonjs] No default state defined in "' + this.$name + '".');
            }

            return;
        }

        // Check if state exists
        if (!this._states[name]) {
            if (has('debug')) {
                console.warn('[spoonjs] Unknown state "' + name + '" on controller "' + this.$name + '".');
            }

            return;
        }

        // If the current state is not the same, transition to it
        if (!this._isSameState(state)) {
            this._performStateChange(state);
        // Otherwise propagate it to child controllers
        } else {
            this._propagateState(state);
        }

        // Sync up the full state name with the application one
        // This is needed because default states might have been translated down the chain
        // Note that the current state might not be set or be changed meanwhile if the user
        // override "_performStateChange()" or "_propagateState()"
        currentState = this._currentState;
        if (stateRegistry.getCurrent() === state && currentState && currentState.getName() === name) {
            this._currentState.setFullName(state.getFullName());
        }

        return this;
    };

    /**
     * Instruct the extend to merge states.
     *
     * {@inheritDoc}
     */
    Controller.extend = function (parent, props, merge) {
        merge = merge || [];
        merge.push('_states');

        return Joint.extend.call(this, parent, props, merge);
    };

    // --------------------------------------------

    /**
     * Fills the state object with the default state if it's name is empty.
     *
     * @param {State} state The state
     */
    Controller.prototype._fillStateIfEmpty = function (state) {
        if (!this._defaultState) {
            return;
        }

        if (state.getName() === this._defaultState.name) {
            fillIn(state.getParams(), this._defaultState.params);
        } else if (!state.getName()) {
            state.setFullName(state.getFullName() + '.' + this._defaultState.name);
            fillIn(state.getParams(), this._defaultState.params);
        }
    };

    /**
     * Parses the controller states.
     */
    Controller.prototype._parseStates = function () {
        var key,
            func,
            matches,
            regExp = this.constructor._stateParamsRegExp || Controller._stateParamsRegExp,
            states = this._states,
            internal;

        this._states = {};
        this._nrStates = size(states);

        // Process the states object
        for (key in states) {
            func = states[key];

            // Check if this state is internal
            if (!key.indexOf('!')) {
                key = key.substr(1);
                internal = true;
            } else {
                internal = false;
            }

            // Process the params specified in the parentheses
            matches = key.match(regExp);
            if (matches) {
                key = key.substr(0, key.indexOf('('));
                this._states[key] = { internal: internal };

                // If user specified state(*), then the state changes every time
                // even if the params haven't changed
                if (matches[1] === '*') {
                    this._states[key].wildcard = true;
                } else {
                    this._states[key].params = matches[1].split(/\s*,\s*/);
                }
            } else {
                this._states[key] = { internal: internal };
            }

            if (has('debug')) {
                if (!stateRegistry.isValid(key)) {
                    throw new Error('State name "' + key + '" of "' + this.$name + '" has an invalid format.');
                }
                if (key.indexOf('.') !== -1) {
                    throw new Error('State name "' + key + '" of "' + this.$name + '" must be local (cannot contain dots).');
                }
            }

            // Check if it is a string or already a function
            if (typeof func === 'string') {
                func = this[func];
                this._states[key].fn = func;
            }

            if (has('debug') && typeof func !== 'function') {
                throw new Error('State handler "' + key + '" of "' + this.$name + '" references a nonexistent function.');
            }

            this._states[key].fn = func;
            this._states[key].params = this._states[key].params || [];
        }
    };

    /**
     * Parse the default state.
     */
    Controller.prototype._parseDefaultState = function () {
        // Convert default state as a string to an object
        if (typeof this._defaultState === 'string') {
            this._defaultState = {
                name: this._defaultState,
                params: {}
            };
        }

        if (has('debug') && this._defaultState) {
            if (!this._defaultState.name) {
                throw new Error('The default state of "' + this.$name + '" cannot be empty.');
            }
            if (!this._states[this._defaultState.name]) {
                throw new Error('The default state of "' + this.$name + '" points to an nonexistent state.');
            }
        }
    },

    /**
     * Resolves a state.
     *
     * If name starts with a / then state is absolute.
     * If name starts with ../ then state is relative.
     * If empty will try to map to the default state.
     * If not empty, strict validation will be made against the name
     *
     * The full state name will be resolved by navigating through the ancestors,
     * unless the state is flagged as internal.
     *
     * @param {String} [name]   The state name
     * @param {Object} [params] The state params
     *
     * @return {Object} An object with the name, params and some flags
     */
    Controller.prototype._resolveState = function (name, params) {
        var resolved,
            ancestor,
            fullName,
            localName,
            ancestorState;

        name = name || '';
        params = params || {};

        // Absolute
        if (name.charAt(0) === '/') {
            name = name.substr(1);

            if (has('debug') && !stateRegistry.isRegistered(name)) {
                throw new Error('Resolved "' + name + '" as absolute but its not registered in the state registry.');
            }

            return { fullName: name, params: params, absolute: true };
        }

        // Relative which will be resolved
        if (startsWith(name, '../')) {
            if (has('debug') && (!this._uplink || !(this._uplink instanceof Controller))) {
                throw new Error('Cannot resolve relative state "' + name + '" in "' + this.$name + '" because controller has no parent.');
            }

            resolved = this._uplink._resolveState(name.substr(3), params);
            resolved.relative = true;
            delete resolved.name;

            if (has('debug') && !resolved.global) {
                throw new Error('Relative state "' + name + '" in "' + this.$name + '" is not a global state (only global are supported for now).');
            }

            return resolved;
        }

        // Local
        // Fill in with the default state
        if (!name && this._defaultState) {
            name = this._defaultState.name;
            params = fillIn(params, this._defaultState.params);
        }

        // If name was specified, do some special stuff
        if (name) {
            localName = name.split('.')[0];

            // Validate if state exists
            if (has('debug') && !this._states[localName]) {
                throw new Error('Unknown state "' + localName + '" in "' + this.$name + '".');
            }

            // Is this an internal state? If so end it here..
            if (this._states[localName].internal) {
                return { name: name, params: params, internal: true };
            }
        }

        // If we end up here, then we need to build the full name based on the ancestors
        fullName = name;

        if (this._uplink) {
            ancestor = this._uplink;

            while (ancestor && ancestor instanceof Controller) {
                ancestorState = ancestor.getState();

                if (!ancestorState) {
                    throw new Error('Unable to resolve full state when controller "' + this.$name + '" is not in any state.');
                }

                // Concatenate name & mix in relevant params
                fullName = ancestorState.getName() + (fullName ? '.' + fullName : '');
                fillIn(params, ancestor._currentStateParams);

                ancestor = ancestor._uplink;
            }
        }

        return { fullName: fullName, name: name, params: params, global: stateRegistry.isRegistered(fullName) };
    };

    /**
     * Checks if a given state is the same as the current controller state.
     *
     * @param {State} state       The state
     * @param {State} [baseState] The state to compare against, defaults to the current state
     *
     * @return {Boolean} True if the same, false otherwise
     */
    Controller.prototype._isSameState = function (state, baseState) {
        var stateMeta;

        baseState = baseState || this._currentState;

        if (!baseState) {
            return false;
        }

        stateMeta = this._states[state.getName()] || {};

        // Check if state is a wildcard
        if (stateMeta.wildcard) {
            return false;
        }

        // Check if equal
        return baseState.isEqual(state, stateMeta.params);
    };

    /**
     * Sets the current state based on the passed in state.
     * Updates all the necessary properties used internally.
     *
     * @param {State} state The state
     */
    Controller.prototype._setCurrentState = function (state) {
        var params;

        // Update current state
        this._previousState = this._currentState;
        this._currentState = state.clone();
        this._currentStateParams = pick(this._currentState.getParams(), this._states[this._currentState.getName()].params);

        // Ensure $info gets up to date
        params = this._currentState.getParams();
        if (params.$info) {
            params.$info.newState = this._currentState;
        }

        // Update the state registry one
        if (state === stateRegistry.getCurrent() && state.getFullName() !== this._currentState.getFullName()) {
            state.setFullName(this._currentState.getFullName());
        }
    };

    /**
     * Performs the state change, calling the state handler if any.
     *
     * @param {State} state The state
     */
    Controller.prototype._performStateChange = function (state) {
        var stateMeta;

        // Update internal state
        this._setCurrentState(state);

        // Advance pointer
        state.next();

        // Execute handler
        stateMeta = this._states[this._currentState.getName()];
        stateMeta.fn.call(this, state.getParams());
    };

    /**
     * Attempts to propagate the state to one of the downlinks.
     *
     * @param {State} state The state
     */
    Controller.prototype._propagateState = function (state) {
        var name,
            leadingName,
            curr,
            length,
            x;

        // Update internal state
        this._setCurrentState(state);

        // Advance pointer
        state.next();

        // Find suitable child controller to handle the state
        name = state.getName();
        leadingName = state.getLeadingName();
        length = this._downlinks.length;

        // The first cycle on the down links are more strict to ensure we
        // don't match the wrong child
        for (x = 0; x < length; x += 1) {
            curr = this._downlinks[x];

            if (!(curr instanceof Controller)) {
                continue;
            }

            if (!name) {
                if (curr._defaultState && stateRegistry.isRegistered(state.getFullName() + '.' + curr._defaultState.name)) {
                    return curr.delegateState(state);
                }
            } else if (curr._currentState && curr._currentState.getLeadingName() === leadingName) {
                return curr.delegateState(state);
            }
        }

        // The second cycle is loose and only works if we got a state
        if (name) {
            for (x = 0; x < length; x += 1) {
                curr = this._downlinks[x];

                if (!(curr instanceof Controller)) {
                    continue;
                }

                if (curr._states[name]) {
                    return curr.delegateState(state);
                }
            }

            if (has('debug')) {
                console.warn('[spoonjs] No child controller of "' + this.$name + '" can handle the "' + name + '" state.');
            }
        }
    };

    Controller._stateParamsRegExp = /\((.+?)\)/;

    return Controller;
});
<commit_msg>Improve previous commit.<commit_after>/*jshint regexp:false*/

/**
 * Controller abstract class.
 */
define([
    './Joint',
    'services/state',
    'mout/string/startsWith',
    'mout/object/size',
    'mout/object/pick',
    'mout/object/fillIn',
    'mout/object/mixIn',
    'has'
], function (Joint, stateRegistry, startsWith, size, pick, fillIn, mixIn, has) {

    'use strict';

    /**
     * Constructor.
     */
    function Controller() {
        Joint.call(this);

        this._parseStates();
        this._parseDefaultState();
    }

    Controller.prototype = Object.create(Joint.prototype);
    Controller.prototype.constructor = Controller;

    /**
     * Get the current state or null if none is set.
     *
     * @return {State} The state
     */
    Controller.prototype.getState = function () {
        return this._currentState;
    };

    /**
     * Generates an URL for a state.
     *
     * @param {String}  name       The state name
     * @param {Object}  [params]   The state params
     * @param {Boolean} [absolute] True to generate an absolute URL, false otherwise
     *
     * @return {String} The generated URL
     */
    Controller.prototype.generateUrl = function (name, params, absolute) {
        var resolved;

        // Resolve the state
        resolved = this._resolveState(name, params);

        if (resolved.internal) {
            throw new Error('Cannot generate a URL for internal state "' + name + '" in "' + this.$name + '".');
        }

        return stateRegistry.generateUrl(resolved.fullName, resolved.params, absolute);
    };

    /**
     * Sets the current state.
     * If the state is the same, nothing happens.
     *
     * @param {String} [name]    The state name
     * @param {Object} [params]  The state params
     * @param {Object} [options] The options
     *
     * @return {Controller} The instance itself to allow chaining
     */
    Controller.prototype.setState = function (name, params, options) {
        var resolved,
            state;

        // Resolve the state
        // Note that parameters are shallow cloned to avoid side-effects
        resolved = this._resolveState(name, mixIn({}, params));

        // If the state is not ours, simply set it on the state registry
        if (resolved.absolute || resolved.relative) {
            stateRegistry.setCurrent(resolved.fullName, resolved.params, options);
            return this;
        }

        // Check if state is internal and the resolved controller is not ourselves
        if (resolved.internal && resolved.controller !== this) {
            resolved.controller.setState(resolved.fullName, resolved.params);
            return this;
        }

        // Act differently if this state is global
        if (resolved.global) {
            // If so attempt to change the global state, aborting if it succeeded
            if (stateRegistry.setCurrent(resolved.fullName, resolved.params, options)) {
                return this;
            }

            // Since the global state is equal, grab it to avoid creating unnecessary
            // state objects
            state = stateRegistry.getCurrent().seekTo(resolved.name);
        } else {
            state = stateRegistry._createStateInstance(resolved.name, resolved.params);

            // Generate local metadata
            state.getParams().$info = {
                newState: state,
                previousState: this._previousState
            };
        }

        return this.delegateState(state);
    };

    /**
     * Delegates a state to be handled by the controller.
     *
     * @param {Object|State} state The state parameter bag or a state instance
     *
     * @return {Controller} The instance itself to allow chaining
     */
    Controller.prototype.delegateState = function (state) {
        var name,
            currentState;

        // Assume app state if not passed
        if (state.$info) {
            state = state.$info.newState;
        }

        // Ensure state is filled with the defaults
        this._fillStateIfEmpty(state);

        name = state.getName();

        // If still has no name it means there's no default state defined
        if (!name) {
            if (has('debug') && this._nrStates) {
                console.warn('[spoonjs] No default state defined in "' + this.$name + '".');
            }

            return;
        }

        // Check if state exists
        if (!this._states[name]) {
            if (has('debug')) {
                console.warn('[spoonjs] Unknown state "' + name + '" on controller "' + this.$name + '".');
            }

            return;
        }

        // If the current state is not the same, transition to it
        if (!this._isSameState(state)) {
            this._performStateChange(state);
        // Otherwise propagate it to child controllers
        } else {
            this._propagateState(state);
        }

        // Sync up the full state name with the application one
        // This is needed because default states might have been translated down the chain
        // Note that the current state might not be set or be changed meanwhile if the user
        // override "_performStateChange()" or "_propagateState()"
        currentState = this._currentState;
        if (stateRegistry.getCurrent() === state && currentState && currentState.getName() === name) {
            this._currentState.setFullName(state.getFullName());
        }

        return this;
    };

    /**
     * Instruct the extend to merge states.
     *
     * {@inheritDoc}
     */
    Controller.extend = function (parent, props, merge) {
        merge = merge || [];
        merge.push('_states');

        return Joint.extend.call(this, parent, props, merge);
    };

    // --------------------------------------------

    /**
     * Fills the state object with the default state if it's name is empty.
     *
     * @param {State} state The state
     */
    Controller.prototype._fillStateIfEmpty = function (state) {
        if (!this._defaultState) {
            return;
        }

        if (state.getName() === this._defaultState.name) {
            fillIn(state.getParams(), this._defaultState.params);
        } else if (!state.getName()) {
            state.setFullName(state.getFullName() + '.' + this._defaultState.name);
            fillIn(state.getParams(), this._defaultState.params);
        }
    };

    /**
     * Parses the controller states.
     */
    Controller.prototype._parseStates = function () {
        var key,
            func,
            matches,
            regExp = this.constructor._stateParamsRegExp || Controller._stateParamsRegExp,
            states = this._states,
            internal;

        this._states = {};
        this._nrStates = size(states);

        // Process the states object
        for (key in states) {
            func = states[key];

            // Check if this state is internal
            if (!key.indexOf('!')) {
                key = key.substr(1);
                internal = true;
            } else {
                internal = false;
            }

            // Process the params specified in the parentheses
            matches = key.match(regExp);
            if (matches) {
                key = key.substr(0, key.indexOf('('));
                this._states[key] = { internal: internal };

                // If user specified state(*), then the state changes every time
                // even if the params haven't changed
                if (matches[1] === '*') {
                    this._states[key].wildcard = true;
                } else {
                    this._states[key].params = matches[1].split(/\s*,\s*/);
                }
            } else {
                this._states[key] = { internal: internal };
            }

            if (has('debug')) {
                if (!stateRegistry.isValid(key)) {
                    throw new Error('State name "' + key + '" of "' + this.$name + '" has an invalid format.');
                }
                if (key.indexOf('.') !== -1) {
                    throw new Error('State name "' + key + '" of "' + this.$name + '" must be local (cannot contain dots).');
                }
            }

            // Check if it is a string or already a function
            if (typeof func === 'string') {
                func = this[func];
                this._states[key].fn = func;
            }

            if (has('debug') && typeof func !== 'function') {
                throw new Error('State handler "' + key + '" of "' + this.$name + '" references a nonexistent function.');
            }

            this._states[key].fn = func;
            this._states[key].params = this._states[key].params || [];
        }
    };

    /**
     * Parse the default state.
     */
    Controller.prototype._parseDefaultState = function () {
        // Convert default state as a string to an object
        if (typeof this._defaultState === 'string') {
            this._defaultState = {
                name: this._defaultState,
                params: {}
            };
        }

        if (has('debug') && this._defaultState) {
            if (!this._defaultState.name) {
                throw new Error('The default state of "' + this.$name + '" cannot be empty.');
            }
            if (!this._states[this._defaultState.name]) {
                throw new Error('The default state of "' + this.$name + '" points to an nonexistent state.');
            }
        }
    },

    /**
     * Resolves a state.
     *
     * If name starts with a / then state is absolute.
     * If name starts with ../ then state is relative.
     * If empty will try to map to the default state.
     * If not empty, strict validation will be made against the name
     *
     * The full state name will be resolved by navigating through the ancestors,
     * unless the state is flagged as internal.
     *
     * @param {String} [name]   The state name
     * @param {Object} [params] The state params
     *
     * @return {Object} An object with the name, params and some flags
     */
    Controller.prototype._resolveState = function (name, params) {
        var resolved,
            ancestor,
            controller,
            fullName,
            localName,
            ancestorState;

        name = name || '';
        params = params || {};

        // Absolute
        if (name.charAt(0) === '/') {
            name = name.substr(1);

            if (has('debug') && !stateRegistry.isRegistered(name)) {
                throw new Error('Resolved "' + name + '" as absolute but its not registered in the state registry.');
            }

            return { fullName: name, params: params, absolute: true };
        }

        // Relative which will be resolved
        if (startsWith(name, '../')) {
            if (has('debug') && (!this._uplink || !(this._uplink instanceof Controller))) {
                throw new Error('Cannot resolve relative state "' + name + '" in "' + this.$name + '" because controller has no parent.');
            }

            resolved = this._uplink._resolveState(name.substr(3), params);
            resolved.relative = true;
            delete resolved.name;

            if (has('debug') && !resolved.global) {
                throw new Error('Relative state "' + name + '" in "' + this.$name + '" is not a global state (only global are supported for now).');
            }

            return resolved;
        }

        // Local
        // Fill in with the default state
        if (!name && this._defaultState) {
            name = this._defaultState.name;
            params = fillIn(params, this._defaultState.params);
        }

        // If name was specified, do some special stuff
        if (name) {
            localName = name.split('.')[0];

            // Validate if state exists
            if (has('debug') && !this._states[localName]) {
                throw new Error('Unknown state "' + localName + '" in "' + this.$name + '".');
            }

            // Is this an internal state? If so end it here..
            if (this._states[localName].internal) {
                return {
                    fullName: name,
                    name: name,
                    params: params,
                    internal: true,
                    controller: this
                };
            }
        }

        // If we end up here, then we need to build the full name based on the ancestors
        fullName = name;

        if (this._uplink) {
            ancestor = this._uplink;

            while (ancestor && ancestor instanceof Controller) {
                ancestorState = ancestor.getState();

                if (!ancestorState) {
                    throw new Error('Unable to resolve full state when controller "' + this.$name + '" is not in any state.');
                }

                // Concatenate name & mix in relevant params
                fullName = ancestorState.getName() + (fullName ? '.' + fullName : '');
                fillIn(params, ancestor._currentStateParams);

                // If the ancestor state is internal, stop here
                if (ancestor._states[ancestorState.getName()].internal) {
                    controller = ancestor;
                    break;
                }

                ancestor = ancestor._uplink;
            }
        }

        // Internal state detected..
        if (controller) {
            return {
                fullName: fullName,
                name: name,
                params: params,
                internal: true,
                controller: controller
            };
        }

        return {
            fullName: fullName,
            name: name,
            params: params,
            global: stateRegistry.isRegistered(fullName)
        };
    };

    /**
     * Checks if a given state is the same as the current controller state.
     *
     * @param {State} state       The state
     * @param {State} [baseState] The state to compare against, defaults to the current state
     *
     * @return {Boolean} True if the same, false otherwise
     */
    Controller.prototype._isSameState = function (state, baseState) {
        var stateMeta;

        baseState = baseState || this._currentState;

        if (!baseState) {
            return false;
        }

        stateMeta = this._states[state.getName()] || {};

        // Check if state is a wildcard
        if (stateMeta.wildcard) {
            return false;
        }

        // Check if equal
        return baseState.isEqual(state, stateMeta.params);
    };

    /**
     * Sets the current state based on the passed in state.
     * Updates all the necessary properties used internally.
     *
     * @param {State} state The state
     */
    Controller.prototype._setCurrentState = function (state) {
        var params;

        // Update current state
        this._previousState = this._currentState;
        this._currentState = state.clone();
        this._currentStateParams = pick(this._currentState.getParams(), this._states[this._currentState.getName()].params);

        // Ensure $info gets up to date
        params = this._currentState.getParams();
        if (params.$info) {
            params.$info.newState = this._currentState;
        }

        // Update the state registry one
        if (state === stateRegistry.getCurrent() && state.getFullName() !== this._currentState.getFullName()) {
            state.setFullName(this._currentState.getFullName());
        }
    };

    /**
     * Performs the state change, calling the state handler if any.
     *
     * @param {State} state The state
     */
    Controller.prototype._performStateChange = function (state) {
        var stateMeta;

        // Update internal state
        this._setCurrentState(state);

        // Advance pointer
        state.next();

        // Execute handler
        stateMeta = this._states[this._currentState.getName()];
        stateMeta.fn.call(this, state.getParams());
    };

    /**
     * Attempts to propagate the state to one of the downlinks.
     *
     * @param {State} state The state
     */
    Controller.prototype._propagateState = function (state) {
        var name,
            leadingName,
            curr,
            length,
            x;

        // Update internal state
        this._setCurrentState(state);

        // Advance pointer
        state.next();

        // Find suitable child controller to handle the state
        name = state.getName();
        leadingName = state.getLeadingName();
        length = this._downlinks.length;

        // The first cycle on the down links are more strict to ensure we
        // don't match the wrong child
        for (x = 0; x < length; x += 1) {
            curr = this._downlinks[x];

            if (!(curr instanceof Controller)) {
                continue;
            }

            if (!name) {
                if (curr._defaultState && stateRegistry.isRegistered(state.getFullName() + '.' + curr._defaultState.name)) {
                    return curr.delegateState(state);
                }
            } else if (curr._currentState && curr._currentState.getLeadingName() === leadingName) {
                return curr.delegateState(state);
            }
        }

        // The second cycle is loose and only works if we got a state
        if (name) {
            for (x = 0; x < length; x += 1) {
                curr = this._downlinks[x];

                if (!(curr instanceof Controller)) {
                    continue;
                }

                if (curr._states[name]) {
                    return curr.delegateState(state);
                }
            }

            if (has('debug')) {
                console.warn('[spoonjs] No child controller of "' + this.$name + '" can handle the "' + name + '" state.');
            }
        }
    };

    Controller._stateParamsRegExp = /\((.+?)\)/;

    return Controller;
});
<|endoftext|>


<commit_before>/**
 * The functions here are global because they need to be
 * accesed from outside (in reports view).
 */

var STORAGE_TREE_KEY = "scinote-sidebar-tree-collapsed-ids";
var STORAGE_TOGGLE_KEY = "scinote-sidebar-toggled";
var SCREEN_SIZE_LARGE = 928;

/**
 * Get all collapsed sidebar elements.
 * @return An array of sidebar element IDs.
 */
function sessionGetCollapsedSidebarElements() {
  var val = sessionStorage.getItem(STORAGE_TREE_KEY);
  console.log(val);
  if (val === null) {
    val = "[]";
    sessionStorage.setItem(STORAGE_TREE_KEY, val);
  }
  val = JSON.parse(val);
  // console.log(val);
  // val = val.filter(Number);
  return val;
}

/**
 * Collapse a specified element in the sidebar.
 * @param id - The collapsed element's ID.
 */
function sessionCollapseSidebarElement(project, id) {
  var ids = sessionGetCollapsedSidebarElements();
  if (_.indexOf(ids[project], id) === -1) {
    var stored_projects = _.pluck(ids, 'name');
    if( _.contains(stored_projects, project)){
      _.findWhere(ids, {name: project}).ids.push(id);
    } else {
      var collapsed = { name: project, ids: [] };
      collapsed.ids.push(id);
      ids.push(collapsed);
    }
    sessionStorage.setItem(STORAGE_TREE_KEY, JSON.stringify(ids));
  }
}

/**
 * Expand a specified element in the sidebar.
 * @param id - The expanded element's ID.
 */
function sessionExpandSidebarElement(project, id) {
  var ids = sessionGetCollapsedSidebarElements();
  var index = _.indexOf(ids, id);
  if (index !== -1) {

    ids.splice(index, 1);
    var items = {};
    items[project] = ids;
    sessionStorage.setItem(STORAGE_TREE_KEY, JSON.stringify(items));
  }
}

/**
 * Get the session stored toggled boolean or null value if
 * sidebar toggle state was not changed by user. It allow for
 * automatic toggling for small devices.
 *
 * @return True if sidebar is toggled; false otherwise.
 */
function sessionIsSidebarToggled() {
  var val = sessionStorage.getItem(STORAGE_TOGGLE_KEY);

  if (val === null) {
    return null;
  }

  return val === "toggled";
}

/**
 * Store the sidebar toggled boolean to session storage.
 */
function sessionToggleSidebar() {
  if (sessionIsSidebarToggled()) {
    sessionStorage.setItem(STORAGE_TOGGLE_KEY, "not_toggled");
  } else {
    sessionStorage.setItem(STORAGE_TOGGLE_KEY, "toggled");
  }
}

/**
 * Setup the sidebar collapsing & expanding functionality.
 */
function setupSidebarTree() {
  function toggleLi(el, collapse, animate) {
    var children = el
    .find(" > ul > li");

    if (collapse) {
      if (animate) {
      children.hide("fast");
      } else {
        children.hide();
      }
      el
      .find(" > span i")
      .attr("title", "Expand this branch")
      .removeClass("expanded");
    } else {
      if (animate) {
        children.show("fast");
      } else {
        children.show();
      }
      el
      .find(" > span i")
      .attr("title", "Collapse this branch")
      .addClass("expanded");
    }
  }

  // Add triangle icons and titles to every parent node
  $(".tree li:has(ul)")
  .addClass("parent_li")
  .find(" > span i")
  .attr("title", "Collapse this branch");
  $(".tree li.parent_li ")
  .find("> span i")
  .addClass("glyphicon glyphicon-triangle-right expanded");

  // Add IDs to all parent <lis>
  var i = 1;
  _.each($('[data-parent="candidate"]'), function(el) {
    $(el).attr("data-toggle-id", i++);
  });

  // Collapse session-stored elements
  var project = $('#sidebar-project-name').text();
  var collapsedIds = sessionGetCollapsedSidebarElements();
  _.each($('li.parent_li[data-parent="candidate"]'), function(el) {
    var id = $(el).data("toggle-id");
    var li = $(".tree li.parent_li[data-toggle-id='" + id + "']");
    if( li.hasClass("active") ){
      console.log("-------------- active -------");
      console.log(id);
      console.log("-------------------------------");
      // Only collapse element if it's descendants don't contain the currently
      // active element
      toggleLi(li,
        false,
        false);
        sessionCollapseSidebarElement(project, id);
    } else if ($.inArray( id, collapsedIds) === 0 ) {
      console.log("-------------- expanded -------");
      console.log(id);
      console.log("-------------------------------");
      toggleLi(li,
        false,
        false);
        sessionExpandSidebarElement(project, id);
    } else {
      console.log("-------- colapsed -----");
      console.log(id);
      console.log("-------------------");
      // collapse all element
      toggleLi(li,
        true,
        false);
      sessionCollapseSidebarElement(project, id);
    }
  });

  // Add onclick callback to every triangle icon
  $(".tree li.parent_li ")
  .find("> span i")
  .on("click", function (e) {
    var el = $(this)
    .parent("span")
    .parent("li.parent_li");

    if (el.find(" > ul > li").is(":visible")) {
      toggleLi(el, true, true);
      sessionCollapseSidebarElement(project, el.data("toggle-id"));
    } else {
      toggleLi(el, false, true);
      sessionExpandSidebarElement(project, el.data("toggle-id"));
    }

    e.stopPropagation();
    return false;
  });
}

/**
 * Initialize the show/hide toggling of sidebar.
 */
function initializeSidebarToggle() {
  var wrapper = $("#wrapper");
  var toggled = sessionIsSidebarToggled();

  if (toggled || toggled === null && $(window).width() < SCREEN_SIZE_LARGE) {
    wrapper.addClass("no-animation");
    wrapper.addClass("toggled");
    // Cause reflow of the wrapper element
    wrapper[0].offsetHeight;
    wrapper.removeClass("no-animation");
    $(".navbar-secondary").addClass("navbar-without-sidebar");
  }

  $("#toggle-sidebar-link").on("click", function() {
    $("#wrapper").toggleClass("toggled");
    sessionToggleSidebar();
    $(".navbar-secondary").toggleClass("navbar-without-sidebar", sessionIsSidebarToggled());
    return false;
  });
}

// Resize the sidebar to accomodate to the page size
function resizeSidebarContents() {
  var wrapper = $("#wrapper");
  var tree = $("#sidebar-wrapper .tree");
  var toggled = sessionIsSidebarToggled();

  if (tree.length && tree.length == 1) {
    tree.css(
      "height",
      ($(window).height() - tree.position().top - 50) + "px"
    );
  }
  // Automatic toggling of sidebar for smaller devices
  if (toggled === null) {
    if ($(window).width() < SCREEN_SIZE_LARGE) {
      wrapper.addClass("toggled");
    } else {
      wrapper.removeClass("toggled");
    }
  }
}

(function () {
  // Initialize click listeners
  setupSidebarTree();
  initializeSidebarToggle();

  // Actually display wrapper, which is, up to now,
  // hidden
  $("#wrapper").show();

  // Resize the sidebar automatically
  resizeSidebarContents();

  // Bind onto window resize function
  $(window).resize(function() {
    resizeSidebarContents();
  });
}());
<commit_msg>fixes object duplication<commit_after>/**
 * The functions here are global because they need to be
 * accesed from outside (in reports view).
 */

var STORAGE_TREE_KEY = "scinote-sidebar-tree-collapsed-ids";
var STORAGE_TOGGLE_KEY = "scinote-sidebar-toggled";
var SCREEN_SIZE_LARGE = 928;

/**
 * Get all collapsed sidebar elements.
 * @return An array of sidebar element IDs.
 */
function sessionGetCollapsedSidebarElements() {
  var val = sessionStorage.getItem(STORAGE_TREE_KEY);
  console.log(val);
  if (val === null) {
    val = "[]";
    sessionStorage.setItem(STORAGE_TREE_KEY, val);
  }
  val = JSON.parse(val);
  // console.log(val);
  // val = val.filter(Number);
  return val;
}

/**
 * Collapse a specified element in the sidebar.
 * @param id - The collapsed element's ID.
 */
function sessionCollapseSidebarElement(project, id) {
  console.log("collapsed:   " +  id);
  var ids = sessionGetCollapsedSidebarElements();
  var item = _.findWhere( ids, {name: project});
  var collapsed = { name: project, ids: [] };
  var stored_projects = _.pluck(ids, 'name');

  if( _.contains(stored_projects, project)){
    if ( item && _.indexOf(item.ids, id) === -1) {
      _.findWhere(ids, {name: project}).ids.push(id);
    }
  } else {
    collapsed.ids.push(id);
    ids.push(collapsed);
  }
  sessionStorage.setItem(STORAGE_TREE_KEY, JSON.stringify(ids));
}

/**
 * Expand a specified element in the sidebar.
 * @param id - The expanded element's ID.
 */
function sessionExpandSidebarElement(project, id) {
  var ids = sessionGetCollapsedSidebarElements();
  var item = _.findWhere( ids, {name: project});
  var index = -1;
  if ( item ) {
  index = _.indexOf(
              item.ids,
              id
            );
  }

  if (index !== -1) {
    item.ids.splice(index, 1);
    sessionStorage.setItem(STORAGE_TREE_KEY, JSON.stringify(ids));
  }
}

/**
 * Get the session stored toggled boolean or null value if
 * sidebar toggle state was not changed by user. It allow for
 * automatic toggling for small devices.
 *
 * @return True if sidebar is toggled; false otherwise.
 */
function sessionIsSidebarToggled() {
  var val = sessionStorage.getItem(STORAGE_TOGGLE_KEY);

  if (val === null) {
    return null;
  }

  return val === "toggled";
}

/**
 * Store the sidebar toggled boolean to session storage.
 */
function sessionToggleSidebar() {
  if (sessionIsSidebarToggled()) {
    sessionStorage.setItem(STORAGE_TOGGLE_KEY, "not_toggled");
  } else {
    sessionStorage.setItem(STORAGE_TOGGLE_KEY, "toggled");
  }
}

/**
 * Setup the sidebar collapsing & expanding functionality.
 */
function setupSidebarTree() {
  function toggleLi(el, collapse, animate) {
    var children = el
    .find(" > ul > li");

    if (collapse) {
      if (animate) {
      children.hide("fast");
      } else {
        children.hide();
      }
      el
      .find(" > span i")
      .attr("title", "Expand this branch")
      .removeClass("expanded");
    } else {
      if (animate) {
        children.show("fast");
      } else {
        children.show();
      }
      el
      .find(" > span i")
      .attr("title", "Collapse this branch")
      .addClass("expanded");
    }
  }

  // Add triangle icons and titles to every parent node
  $(".tree li:has(ul)")
  .addClass("parent_li")
  .find(" > span i")
  .attr("title", "Collapse this branch");
  $(".tree li.parent_li ")
  .find("> span i")
  .addClass("glyphicon glyphicon-triangle-right expanded");

  // Add IDs to all parent <lis>
  var i = 1;
  _.each($('[data-parent="candidate"]'), function(el) {
    $(el).attr("data-toggle-id", i++);
  });

  // Collapse session-stored elements
  var project = $('#sidebar-project-name').text();
  var collapsedIds = sessionGetCollapsedSidebarElements();
  _.each($('li.parent_li[data-parent="candidate"]'), function(el) {
    var id = $(el).data("toggle-id");
    var li = $(".tree li.parent_li[data-toggle-id='" + id + "']");
    if( li.hasClass("active") ){
      console.log("-------------- active -------");
      console.log(id);
      console.log("-------------------------------");
      // Only collapse element if it's descendants don't contain the currently
      // active element
      toggleLi(li,
        false,
        false);
        sessionCollapseSidebarElement(project, id);
    } else if ($.inArray( id, collapsedIds) === 0 ) {
      console.log("-------------- expanded -------");
      console.log(id);
      console.log("-------------------------------");
      toggleLi(li,
        false,
        false);
        sessionExpandSidebarElement(project, id);
    } else {
      console.log("-------- colapsed -----");
      console.log(id);
      console.log("-------------------");
      // collapse all element
      toggleLi(li,
        true,
        false);
      sessionCollapseSidebarElement(project, id);
    }
  });

  // Add onclick callback to every triangle icon
  $(".tree li.parent_li ")
  .find("> span i")
  .on("click", function (e) {
    var el = $(this)
    .parent("span")
    .parent("li.parent_li");

    if (el.find(" > ul > li").is(":visible")) {
      toggleLi(el, true, true);
      sessionCollapseSidebarElement(project, el.data("toggle-id"));
    } else {
      toggleLi(el, false, true);
      sessionExpandSidebarElement(project, el.data("toggle-id"));
    }

    e.stopPropagation();
    return false;
  });
}

/**
 * Initialize the show/hide toggling of sidebar.
 */
function initializeSidebarToggle() {
  var wrapper = $("#wrapper");
  var toggled = sessionIsSidebarToggled();

  if (toggled || toggled === null && $(window).width() < SCREEN_SIZE_LARGE) {
    wrapper.addClass("no-animation");
    wrapper.addClass("toggled");
    // Cause reflow of the wrapper element
    wrapper[0].offsetHeight;
    wrapper.removeClass("no-animation");
    $(".navbar-secondary").addClass("navbar-without-sidebar");
  }

  $("#toggle-sidebar-link").on("click", function() {
    $("#wrapper").toggleClass("toggled");
    sessionToggleSidebar();
    $(".navbar-secondary").toggleClass("navbar-without-sidebar", sessionIsSidebarToggled());
    return false;
  });
}

// Resize the sidebar to accomodate to the page size
function resizeSidebarContents() {
  var wrapper = $("#wrapper");
  var tree = $("#sidebar-wrapper .tree");
  var toggled = sessionIsSidebarToggled();

  if (tree.length && tree.length == 1) {
    tree.css(
      "height",
      ($(window).height() - tree.position().top - 50) + "px"
    );
  }
  // Automatic toggling of sidebar for smaller devices
  if (toggled === null) {
    if ($(window).width() < SCREEN_SIZE_LARGE) {
      wrapper.addClass("toggled");
    } else {
      wrapper.removeClass("toggled");
    }
  }
}

(function () {
  // Initialize click listeners
  setupSidebarTree();
  initializeSidebarToggle();

  // Actually display wrapper, which is, up to now,
  // hidden
  $("#wrapper").show();

  // Resize the sidebar automatically
  resizeSidebarContents();

  // Bind onto window resize function
  $(window).resize(function() {
    resizeSidebarContents();
  });
}());
<|endoftext|>


<commit_before>/*jshint regexp:false*/

/**
 * Controller abstract class.
 */
define([
    './Joint',
    'services/state',
    'mout/string/startsWith',
    'mout/object/size',
    'mout/object/pick',
    'mout/object/mixIn',
    'mout/object/fillIn',
    'mout/array/find',
    'has'
], function (Joint, stateRegistry, startsWith, size, pick, mixIn, fillIn, find, has) {

    'use strict';

    /**
     * Constructor.
     */
    function Controller() {
        Joint.call(this);

        this._parseStates();
        this._parseDefaultState();
    }

    Controller.prototype = Object.create(Joint.prototype);
    Controller.prototype.constructor = Controller;

    /**
     * Get the current state or null if none is set.
     *
     * @return {State} The state
     */
    Controller.prototype.getState = function () {
        return this._currentState;
    };

    /**
     * Generates an URL for a state.
     *
     * @param {String}  name       The state name
     * @param {Object}  [params]   The state params
     * @param {Boolean} [absolute] True to generate an absolute URL, false otherwise
     *
     * @return {String} The generated URL
     */
    Controller.prototype.generateUrl = function (name, params, absolute) {
        var state;

        // Resolve the state
        state = this._resolveFullState(name);
        mixIn(state.params, params);

        return stateRegistry.generateUrl(state.fullName, state.params, absolute);
    };

    /**
     * Sets the current state.
     * If the state is the same, nothing happens.
     *
     * @param {String} [name]    The state name
     * @param {Object} [params]  The state params
     * @param {Object} [options] The options
     *
     * @return {Controller} The instance itself to allow chaining
     */
    Controller.prototype.setState = function (name, params, options) {
        var stateMeta,
            state;

        // Resolve the state
        stateMeta = this._resolveFullState(name);
        mixIn(stateMeta.params, params);

        // If the state is absolute, simply set it on the state registry
        if (stateMeta.name == null) {
            stateRegistry.setCurrent(stateMeta.fullName, stateMeta.params, options);
            return this;
        }

        // Check if the state is globally registered
        if (stateRegistry.isRegistered(stateMeta.fullName)) {
            // If so attempt to change the global state, aborting if it succeeded
            if (stateRegistry.setCurrent(stateMeta.fullName, stateMeta.params, options)) {
                return this;
            }

            // Since the global state is equal, grab it to avoid creating unnecessary
            // state objects
            state = stateRegistry.getCurrent().seekTo(stateMeta.name);
        } else {
            state = stateRegistry._createStateInstance(stateMeta.name, stateMeta.params);

            // Generate local metadata
            state.getParams().$info = {
                newState: state,
                previousState: this._previousState
            };
        }

        return this.delegateState(state);
    };

    /**
     * Delegates a state to be handled by the controller.
     *
     * @param {Object|State} state The state parameter bag or a state instance
     *
     * @return {Controller} The instance itself to allow chaining
     */
    Controller.prototype.delegateState = function (state) {
        var name,
            currentState;

        // Assume app state if not passed
        if (!state) {
            state = stateRegistry.getCurrent();
        } else if (state.$info) {
            state = state.$info.newState;
        }

        // Ensure state is filled with the defaults
        this._fillStateIfEmpty(state);

        name = state.getName();

        // If still has no name it means there's no default state defined
        if (!name) {
            if (has('debug') && this._nrStates) {
                console.warn('[spoonjs] No default state defined in "' + this.$name + '".');
            }

            return;
        }

        // Check if state exists
        if (!this._states[name]) {
            if (has('debug')) {
                console.warn('[spoonjs] Unknown state "' + name + '" on controller "' + this.$name + '".');
            }

            return;
        }

        // If the current state is not the same, transition to it
        if (!this._isSameState(state)) {
            this._performStateChange(state);
        // Otherwise propagate it to child controllers
        } else {
            this._propagateState(state);
        }

        // Sync up the full state name with the application one
        // This is needed because default states might have been translated down the chain
        // Note that the current state might not be set or be changed meanwhile if the user
        // override "_performStateChange()" or "_propagateState()"
        currentState = this._currentState;
        if (stateRegistry.getCurrent() === state && currentState && currentState.getName() === name) {
            this._currentState.setFullName(state.getFullName());
        }

        return this;
    };

    /**
     * Instruct the extend to merge states.
     *
     * {@inheritDoc}
     */
    Controller.extend = function (parent, props, merge) {
        merge = merge || [];
        merge.push('_states');

        return Joint.extend.call(this, parent, props, merge);
    };

    // --------------------------------------------

    /**
     * Fills the state object with the default state if it's name is empty.
     *
     * @param {State} state The state
     */
    Controller.prototype._fillStateIfEmpty = function (state) {
        if (!this._defaultState) {
            return;
        }

        if (state.getName() === this._defaultState.name) {
            fillIn(state.getParams(), this._defaultState.params);
        } else if (!state.getName()) {
            state.setFullName(state.getFullName() + '.' + this._defaultState.name);
            fillIn(state.getParams(), this._defaultState.params);
        }
    };

    /**
     * Parses the controller states.
     */
    Controller.prototype._parseStates = function () {
        var key,
            func,
            matches,
            regExp = this.constructor._stateParamsRegExp || Controller._stateParamsRegExp,
            states = this._states;

        this._states = {};
        this._nrStates = size(states);

        // Process the states object
        for (key in states) {
            func = states[key];

            // Process the params specified in the parentheses
            matches = key.match(regExp);
            if (matches) {
                key = key.substr(0, key.indexOf('('));
                this._states[key] = {};

                // If user specified state(*), then the state changes every time
                // even if the params haven't changed
                if (matches[1] === '*') {
                    this._states[key].wildcard = true;
                } else {
                    this._states[key].params = matches[1].split(/\s*,\s*/);
                }
            } else {
                this._states[key] = {};
            }

            if (has('debug')) {
                if (!stateRegistry.isValid(key)) {
                    throw new Error('State name "' + key + '" of "' + this.$name + '" has an invalid format.');
                }
                if (key.indexOf('.') !== -1) {
                    throw new Error('State name "' + key + '" of "' + this.$name + '" must be local (cannot contain dots).');
                }
            }

            // Check if it is a string or already a function
            if (typeof func === 'string') {
                func = this[func];
                this._states[key].fn = func;
            }

            if (has('debug') && typeof func !== 'function') {
                throw new Error('State handler "' + key + '" of "' + this.$name + '" references a nonexistent function.');
            }

            this._states[key].fn = func;
            this._states[key].params = this._states[key].params || [];
        }
    };

    /**
     * Parse the default state.
     */
    Controller.prototype._parseDefaultState = function () {
        // Convert default state as a string to an object
        if (typeof this._defaultState === 'string') {
            this._defaultState = {
                name: this._defaultState,
                params: {}
            };
        }

        if (has('debug') && this._defaultState) {
            if (!this._defaultState.name) {
                throw new Error('The default state of "' + this.$name + '" cannot be empty.');
            }
            if (!this._states[this._defaultState.name]) {
                throw new Error('The default state of "' + this.$name + '" points to an nonexistent state.');
            }
        }
    },

    /**
     * Resolves a full state name.
     *
     * If name starts with a / then state is absolute.
     * If name starts with ../ then state is relative.
     * If empty will try to map to the default state.
     * Otherwise the full state name will be resolved from the local name.
     *
     * @param {String} [name] The state name
     *
     * @return {Object} The full state name and params
     */
    Controller.prototype._resolveFullState = function (name) {
        var state,
            ancestor,
            ancestorState;

        name = name || '';

        // Absolute
        if (name.charAt(0) === '/') {
            return {
                fullName: name.substr(1),
                params: {}
            };
        }

        // Relative
        if (startsWith(name, '../')) {
            if (has('debug') && (!this._uplink || !(this._uplink instanceof Controller))) {
                throw new Error('Cannot resolve relative state "' + name + '" in "' + this.$name + '".');
            }

            state = this._uplink._resolveFullState(name.substr(3));
            delete state.name;  // Remove name because state is not local

            return state;
        }

        state = {
            name: name,
            fullName: name,
            params: this._currentState ? mixIn({}, this._currentState.getParams()) : {}
        };

        // Local
        ancestor = this._uplink;
        while (ancestor && ancestor instanceof Controller) {
            ancestorState = ancestor.getState();
            if (!ancestorState) {
                // Break here, the ancestor is not in any state
                break;
            }

            // Concatenate name & mix in relevant params
            state.fullName = ancestorState.getName() + (state.fullName ? '.' + state.fullName : '');
            fillIn(state.params, ancestorState.getParams());

            ancestor = ancestor._uplink;
        }

        // If no state name is set, use default state
        if (!state.name && this._defaultState) {
            state.name = this._defaultState.name;
            state.fullName = state.fullName || this._defaultState.name;
            mixIn(state.params, this._defaultState.params);
        }

        return state;
    };

    /**
     * Checks if a given state is the same as the current controller state.
     *
     * @param {State} state       The state
     * @param {State} [baseState] The state to compare against, defaults to the current state
     *
     * @return {Boolean} True if the same, false otherwise
     */
    Controller.prototype._isSameState = function (state, baseState) {
        var stateMeta;

        baseState = baseState || this._currentState;

        if (!baseState) {
            return false;
        }

        stateMeta = this._states[state.getName()] || {};

        // Check if state is a wildcard
        if (stateMeta.wildcard) {
            return false;
        }

        // Check if equal
        return baseState.isEqual(state, stateMeta.params);
    };

    /**
     * Sets the current state based on the passed in state.
     * Updates all the necessary properties used internally.
     *
     * @param {State} state The state
     */
    Controller.prototype._setCurrentState = function (state) {
        var params;

        // Update current state
        this._previousState = this._currentState;
        this._currentState = state.clone();

        // Ensure $info gets up to date
        params = this._currentState.getParams();
        if (params.$info) {
            params.$info.newState = this._currentState;
        }

        // Update the state registry one
        if (state === stateRegistry.getCurrent() && state.getFullName() !== this._currentState.getFullName()) {
            state.setFullName(this._currentState.getFullName());
        }
    };

    /**
     * Performs the state change, calling the state handler if any.
     *
     * @param {State} state The state
     */
    Controller.prototype._performStateChange = function (state) {
        var stateMeta;

        // Update internal state
        this._setCurrentState(state);

        // Advance pointer
        state.next();

        // Execute handler
        stateMeta = this._states[this._currentState.getName()];
        stateMeta.fn.call(this, state.getParams());
    };

    /**
     * Attempts to propagate the state to one of the downlinks.
     *
     * @param {State} state The state
     */
    Controller.prototype._propagateState = function (state) {
        var name,
            leadingName,
            curr,
            length,
            x;

        // Update internal state
        this._setCurrentState(state);

        // Advance pointer
        state.next();

        // Find suitable child controller to handle the state
        name = state.getName();
        leadingName = state.getLeadingName();
        length = this._downlinks.length;

        for (x = 0; x < length; x += 1) {
            curr = this._downlinks[x];

            if (!(curr instanceof Controller)) {
                continue;
            }

            // If the state has no name, check if this child has a registered default state
            if (!name) {
                if (curr._defaultState && stateRegistry.isRegistered(state.getFullName() + '.' + curr._defaultState.name)) {
                    curr.delegateState(state);
                    return;
                }
            // Otherwise check if this child has the wanted state
            } else if (curr._currentState && curr._currentState.getLeadingName() === leadingName) {
                curr.delegateState(state);
                return;
            }
        }

        if (name && has('debug')) {
            console.warn('[spoonjs] No child controller of "' + this.$name + '" can handle the "' + name + '" state.');
        }
    };

    Controller._stateParamsRegExp = /\((.+?)\)/;

    return Controller;
});
<commit_msg>fix propagate state when the child has no current state<commit_after>/*jshint regexp:false*/

/**
 * Controller abstract class.
 */
define([
    './Joint',
    'services/state',
    'mout/string/startsWith',
    'mout/object/size',
    'mout/object/pick',
    'mout/object/mixIn',
    'mout/object/fillIn',
    'mout/array/find',
    'has'
], function (Joint, stateRegistry, startsWith, size, pick, mixIn, fillIn, find, has) {

    'use strict';

    /**
     * Constructor.
     */
    function Controller() {
        Joint.call(this);

        this._parseStates();
        this._parseDefaultState();
    }

    Controller.prototype = Object.create(Joint.prototype);
    Controller.prototype.constructor = Controller;

    /**
     * Get the current state or null if none is set.
     *
     * @return {State} The state
     */
    Controller.prototype.getState = function () {
        return this._currentState;
    };

    /**
     * Generates an URL for a state.
     *
     * @param {String}  name       The state name
     * @param {Object}  [params]   The state params
     * @param {Boolean} [absolute] True to generate an absolute URL, false otherwise
     *
     * @return {String} The generated URL
     */
    Controller.prototype.generateUrl = function (name, params, absolute) {
        var state;

        // Resolve the state
        state = this._resolveFullState(name);
        mixIn(state.params, params);

        return stateRegistry.generateUrl(state.fullName, state.params, absolute);
    };

    /**
     * Sets the current state.
     * If the state is the same, nothing happens.
     *
     * @param {String} [name]    The state name
     * @param {Object} [params]  The state params
     * @param {Object} [options] The options
     *
     * @return {Controller} The instance itself to allow chaining
     */
    Controller.prototype.setState = function (name, params, options) {
        var stateMeta,
            state;

        // Resolve the state
        stateMeta = this._resolveFullState(name);
        mixIn(stateMeta.params, params);

        // If the state is absolute, simply set it on the state registry
        if (stateMeta.name == null) {
            stateRegistry.setCurrent(stateMeta.fullName, stateMeta.params, options);
            return this;
        }

        // Check if the state is globally registered
        if (stateRegistry.isRegistered(stateMeta.fullName)) {
            // If so attempt to change the global state, aborting if it succeeded
            if (stateRegistry.setCurrent(stateMeta.fullName, stateMeta.params, options)) {
                return this;
            }

            // Since the global state is equal, grab it to avoid creating unnecessary
            // state objects
            state = stateRegistry.getCurrent().seekTo(stateMeta.name);
        } else {
            state = stateRegistry._createStateInstance(stateMeta.name, stateMeta.params);

            // Generate local metadata
            state.getParams().$info = {
                newState: state,
                previousState: this._previousState
            };
        }

        return this.delegateState(state);
    };

    /**
     * Delegates a state to be handled by the controller.
     *
     * @param {Object|State} state The state parameter bag or a state instance
     *
     * @return {Controller} The instance itself to allow chaining
     */
    Controller.prototype.delegateState = function (state) {
        var name,
            currentState;

        // Assume app state if not passed
        if (!state) {
            state = stateRegistry.getCurrent();
        } else if (state.$info) {
            state = state.$info.newState;
        }

        // Ensure state is filled with the defaults
        this._fillStateIfEmpty(state);

        name = state.getName();

        // If still has no name it means there's no default state defined
        if (!name) {
            if (has('debug') && this._nrStates) {
                console.warn('[spoonjs] No default state defined in "' + this.$name + '".');
            }

            return;
        }

        // Check if state exists
        if (!this._states[name]) {
            if (has('debug')) {
                console.warn('[spoonjs] Unknown state "' + name + '" on controller "' + this.$name + '".');
            }

            return;
        }

        // If the current state is not the same, transition to it
        if (!this._isSameState(state)) {
            this._performStateChange(state);
        // Otherwise propagate it to child controllers
        } else {
            this._propagateState(state);
        }

        // Sync up the full state name with the application one
        // This is needed because default states might have been translated down the chain
        // Note that the current state might not be set or be changed meanwhile if the user
        // override "_performStateChange()" or "_propagateState()"
        currentState = this._currentState;
        if (stateRegistry.getCurrent() === state && currentState && currentState.getName() === name) {
            this._currentState.setFullName(state.getFullName());
        }

        return this;
    };

    /**
     * Instruct the extend to merge states.
     *
     * {@inheritDoc}
     */
    Controller.extend = function (parent, props, merge) {
        merge = merge || [];
        merge.push('_states');

        return Joint.extend.call(this, parent, props, merge);
    };

    // --------------------------------------------

    /**
     * Fills the state object with the default state if it's name is empty.
     *
     * @param {State} state The state
     */
    Controller.prototype._fillStateIfEmpty = function (state) {
        if (!this._defaultState) {
            return;
        }

        if (state.getName() === this._defaultState.name) {
            fillIn(state.getParams(), this._defaultState.params);
        } else if (!state.getName()) {
            state.setFullName(state.getFullName() + '.' + this._defaultState.name);
            fillIn(state.getParams(), this._defaultState.params);
        }
    };

    /**
     * Parses the controller states.
     */
    Controller.prototype._parseStates = function () {
        var key,
            func,
            matches,
            regExp = this.constructor._stateParamsRegExp || Controller._stateParamsRegExp,
            states = this._states;

        this._states = {};
        this._nrStates = size(states);

        // Process the states object
        for (key in states) {
            func = states[key];

            // Process the params specified in the parentheses
            matches = key.match(regExp);
            if (matches) {
                key = key.substr(0, key.indexOf('('));
                this._states[key] = {};

                // If user specified state(*), then the state changes every time
                // even if the params haven't changed
                if (matches[1] === '*') {
                    this._states[key].wildcard = true;
                } else {
                    this._states[key].params = matches[1].split(/\s*,\s*/);
                }
            } else {
                this._states[key] = {};
            }

            if (has('debug')) {
                if (!stateRegistry.isValid(key)) {
                    throw new Error('State name "' + key + '" of "' + this.$name + '" has an invalid format.');
                }
                if (key.indexOf('.') !== -1) {
                    throw new Error('State name "' + key + '" of "' + this.$name + '" must be local (cannot contain dots).');
                }
            }

            // Check if it is a string or already a function
            if (typeof func === 'string') {
                func = this[func];
                this._states[key].fn = func;
            }

            if (has('debug') && typeof func !== 'function') {
                throw new Error('State handler "' + key + '" of "' + this.$name + '" references a nonexistent function.');
            }

            this._states[key].fn = func;
            this._states[key].params = this._states[key].params || [];
        }
    };

    /**
     * Parse the default state.
     */
    Controller.prototype._parseDefaultState = function () {
        // Convert default state as a string to an object
        if (typeof this._defaultState === 'string') {
            this._defaultState = {
                name: this._defaultState,
                params: {}
            };
        }

        if (has('debug') && this._defaultState) {
            if (!this._defaultState.name) {
                throw new Error('The default state of "' + this.$name + '" cannot be empty.');
            }
            if (!this._states[this._defaultState.name]) {
                throw new Error('The default state of "' + this.$name + '" points to an nonexistent state.');
            }
        }
    },

    /**
     * Resolves a full state name.
     *
     * If name starts with a / then state is absolute.
     * If name starts with ../ then state is relative.
     * If empty will try to map to the default state.
     * Otherwise the full state name will be resolved from the local name.
     *
     * @param {String} [name] The state name
     *
     * @return {Object} The full state name and params
     */
    Controller.prototype._resolveFullState = function (name) {
        var state,
            ancestor,
            ancestorState;

        name = name || '';

        // Absolute
        if (name.charAt(0) === '/') {
            return {
                fullName: name.substr(1),
                params: {}
            };
        }

        // Relative
        if (startsWith(name, '../')) {
            if (has('debug') && (!this._uplink || !(this._uplink instanceof Controller))) {
                throw new Error('Cannot resolve relative state "' + name + '" in "' + this.$name + '".');
            }

            state = this._uplink._resolveFullState(name.substr(3));
            delete state.name;  // Remove name because state is not local

            return state;
        }

        state = {
            name: name,
            fullName: name,
            params: this._currentState ? mixIn({}, this._currentState.getParams()) : {}
        };

        // Local
        ancestor = this._uplink;
        while (ancestor && ancestor instanceof Controller) {
            ancestorState = ancestor.getState();
            if (!ancestorState) {
                // Break here, the ancestor is not in any state
                break;
            }

            // Concatenate name & mix in relevant params
            state.fullName = ancestorState.getName() + (state.fullName ? '.' + state.fullName : '');
            fillIn(state.params, ancestorState.getParams());

            ancestor = ancestor._uplink;
        }

        // If no state name is set, use default state
        if (!state.name && this._defaultState) {
            state.name = this._defaultState.name;
            state.fullName = state.fullName || this._defaultState.name;
            mixIn(state.params, this._defaultState.params);
        }

        return state;
    };

    /**
     * Checks if a given state is the same as the current controller state.
     *
     * @param {State} state       The state
     * @param {State} [baseState] The state to compare against, defaults to the current state
     *
     * @return {Boolean} True if the same, false otherwise
     */
    Controller.prototype._isSameState = function (state, baseState) {
        var stateMeta;

        baseState = baseState || this._currentState;

        if (!baseState) {
            return false;
        }

        stateMeta = this._states[state.getName()] || {};

        // Check if state is a wildcard
        if (stateMeta.wildcard) {
            return false;
        }

        // Check if equal
        return baseState.isEqual(state, stateMeta.params);
    };

    /**
     * Sets the current state based on the passed in state.
     * Updates all the necessary properties used internally.
     *
     * @param {State} state The state
     */
    Controller.prototype._setCurrentState = function (state) {
        var params;

        // Update current state
        this._previousState = this._currentState;
        this._currentState = state.clone();

        // Ensure $info gets up to date
        params = this._currentState.getParams();
        if (params.$info) {
            params.$info.newState = this._currentState;
        }

        // Update the state registry one
        if (state === stateRegistry.getCurrent() && state.getFullName() !== this._currentState.getFullName()) {
            state.setFullName(this._currentState.getFullName());
        }
    };

    /**
     * Performs the state change, calling the state handler if any.
     *
     * @param {State} state The state
     */
    Controller.prototype._performStateChange = function (state) {
        var stateMeta;

        // Update internal state
        this._setCurrentState(state);

        // Advance pointer
        state.next();

        // Execute handler
        stateMeta = this._states[this._currentState.getName()];
        stateMeta.fn.call(this, state.getParams());
    };

    /**
     * Attempts to propagate the state to one of the downlinks.
     *
     * @param {State} state The state
     */
    Controller.prototype._propagateState = function (state) {
        var name,
            leadingName,
            curr,
            length,
            x;

        // Update internal state
        this._setCurrentState(state);

        // Advance pointer
        state.next();

        // Find suitable child controller to handle the state
        name = state.getName();
        leadingName = state.getLeadingName();
        length = this._downlinks.length;

        // The first cycle on the down links are more strict to ensure we
        // don't match the wrong child
        for (x = 0; x < length; x += 1) {
            curr = this._downlinks[x];

            if (!(curr instanceof Controller)) {
                continue;
            }

            if (!name) {
                if (curr._defaultState && stateRegistry.isRegistered(state.getFullName() + '.' + curr._defaultState.name)) {
                    return curr.delegateState(state);
                }
            } else if (curr._currentState && curr._currentState.getLeadingName() === leadingName) {
                return curr.delegateState(state);
            }
        }

        // The second cycle is loose
        for (x = 0; x < length; x += 1) {
            curr = this._downlinks[x];

            if (!(curr instanceof Controller)) {
                continue;
            }

            // If the state has no name, check if this child has a registered default state
            if ((!name && curr._defaultState) || (name && curr._states[name])) {
                return curr.delegateState(state);
            }
        }

        if (name && has('debug')) {
            console.warn('[spoonjs] No child controller of "' + this.$name + '" can handle the "' + name + '" state.');
        }
    };

    Controller._stateParamsRegExp = /\((.+?)\)/;

    return Controller;
});
<|endoftext|>


<commit_before>/*
 * Copyright 2015 Telefonica Investigación y Desarrollo, S.A.U
 *
 * This file is part of perseo-fe
 *
 * perseo-fe is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * perseo-fe is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with perseo-fe.
 * If not, see http://www.gnu.org/licenses/.
 *
 * For those usages not covered by the GNU Affero General Public License
 * please contact with::[contacto@tid.es]
 */
'use strict';

var util = require('util'),
    async = require('async'),
    appContext = require('../appContext'),
    rulesCollectionName = require('../../config').collections.rules,
    myutils = require('../myutils'),
    logger = require('logops'),
    errors = {};

function parsePostAxnParams(rule) {
    if (rule && rule.action && rule.action.type === 'post') {
        if (rule.action.parameters.json) {
            try {
                rule.action.parameters.json = JSON.parse(rule.action.parameters.json);
            } catch (ex) {
                myutils.logErrorIf(ex);
            }
        }
        if (rule.action.parameters.qs) {
            try {
                rule.action.parameters.qs = JSON.parse(rule.action.parameters.qs);
            } catch (ex) {
                myutils.logErrorIf(ex);
            }
        }
        if (rule.action.parameters.headers) {
            try {
                rule.action.parameters.headers = JSON.parse(rule.action.parameters.headers);
            } catch (ex) {
                myutils.logErrorIf(ex);
            }
        }
    }
}

function stringifyPostAxnParams(rule) {
    if (rule && rule.action && rule.action.type === 'post') {
        if (rule.action.parameters.json) {
            try {
                rule.action.parameters.json = JSON.stringify(rule.action.parameters.json);
            } catch (e) {
                myutils.logErrorIf(e);
            }
        }
        if (rule.action.parameters.qs) {
            try {
                rule.action.parameters.qs = JSON.stringify(rule.action.parameters.qs);
            } catch (e) {
                myutils.logErrorIf(e);
            }
        }
        if (rule.action.parameters.headers) {
            try {
                rule.action.parameters.headers = JSON.stringify(rule.action.parameters.headers);
            } catch (e) {
                myutils.logErrorIf(e);
            }
        }
    }
}

function search(rule, callback) {
    var db = appContext.Db();
    async.waterfall(
        [
            db.collection.bind(db, rulesCollectionName, { strict: true }),
            function(col, cb) {
                col.findOne({ name: rule.name, subservice: rule.subservice, service: rule.service }, cb);
            }
        ],
        callback
    );
}

function findAll(service, subservice, limit, offset, count, callback) {
    var criterion = {};

    if (typeof service === 'function') {
        callback = service;
    } else {
        criterion.service = service;
        criterion.subservice = subservice;
    }
    var db = appContext.Db();
    async.waterfall(
        [
            db.collection.bind(db, rulesCollectionName, { strict: true }),
            function(col, cb) {
                col.find(criterion).toArray(function(err, rules) {
                    if (rules && util.isArray(rules)) {
                        rules.forEach(function(r) {
                            parsePostAxnParams(r);
                        });
                    }
                    cb(err, rules);
                });
            }
        ],
        function(err, result) {
            myutils.logErrorIf(err);
            logger.info('rulesStore.FindAll %s', myutils.firstChars(result));
            return callback(err, result);
        }
    );
}

module.exports = {
    Find: function Find(rule, callback) {
        search(rule, function(err, result) {
            myutils.logErrorIf(err);
            if (err) {
                return callback(err, null);
            }
            if (!result) {
                return callback(new errors.NotFoundRule(rule.name), null);
            }
            parsePostAxnParams(result);
            logger.info('rulesStore.Find %s', myutils.firstChars(result));
            return callback(err, result);
        });
    },
    Exists: function Exists(rule, callback) {
        search(rule, function(err, result) {
            myutils.logErrorIf(err);
            callback(err, Boolean(result));
        });
    },
    FindAll: findAll,
    Remove: function Remove(rule, callback) {
        var db = appContext.Db();
        async.waterfall(
            [
                db.collection.bind(db, rulesCollectionName, { strict: true }),
                function(col, cb) {
                    col.remove({ name: rule.name, subservice: rule.subservice, service: rule.service }, cb);
                }
            ],
            function(err, result) {
                myutils.logErrorIf(err);
                logger.info('rulesStore.Remove %j', myutils.firstChars(result));
                return callback(err, result);
            }
        );
    },
    Save: function Save(r, callback) {
        var db = appContext.Db();
        stringifyPostAxnParams(r);
        async.waterfall(
            [
                db.collection.bind(db, rulesCollectionName, { strict: true }),
                function(col, cb) {
                    col.save(r, cb);
                }
            ],
            function(err, result) {
                myutils.logErrorIf(err);
                logger.info('rulesStore.Save %j', myutils.firstChars(result));
                return callback(err, result);
            }
        );
    },
    Update: function Update(id, r, callback) {
        var db = appContext.Db();
        stringifyPostAxnParams(r);
        async.waterfall(
            [
                db.collection.bind(db, rulesCollectionName, { strict: true }),
                function(col, cb) {
                    col.findOneAndUpdate(
                        { name: id },
                        r,
                        {
                            upsert: false,
                            returnOriginal: false
                        },
                        function(err, result) {
                            if (result && result.lastErrorObject && result.lastErrorObject.updatedExisting === false) {
                                return cb(new errors.NotFoundRule(id), null);
                            }
                            parsePostAxnParams(r);
                            return cb(err, r);
                        }
                    );
                }
            ],
            function(err, result) {
                myutils.logErrorIf(err);
                logger.info('rulesStore.Update %j', myutils.firstChars(result));
                return callback(err, result);
            }
        );
    }
};
/**
 * Constructors for possible errors from this module
 *
 * @type {Object}
 */
module.exports.errors = errors;

(function() {
    errors.NotFoundRule = function NotFoundRule(msg) {
        this.name = 'RULE_NOTFOUND';
        this.message = 'rule not found ' + msg;
        this.httpCode = 404;
    };
    Object.keys(errors).forEach(function(element) {
        util.inherits(errors[element], Error);
    });
})();
<commit_msg>use limit and offset<commit_after>/*
 * Copyright 2015 Telefonica Investigación y Desarrollo, S.A.U
 *
 * This file is part of perseo-fe
 *
 * perseo-fe is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * perseo-fe is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with perseo-fe.
 * If not, see http://www.gnu.org/licenses/.
 *
 * For those usages not covered by the GNU Affero General Public License
 * please contact with::[contacto@tid.es]
 */
'use strict';

var util = require('util'),
    async = require('async'),
    appContext = require('../appContext'),
    rulesCollectionName = require('../../config').collections.rules,
    myutils = require('../myutils'),
    logger = require('logops'),
    errors = {};

function parsePostAxnParams(rule) {
    if (rule && rule.action && rule.action.type === 'post') {
        if (rule.action.parameters.json) {
            try {
                rule.action.parameters.json = JSON.parse(rule.action.parameters.json);
            } catch (ex) {
                myutils.logErrorIf(ex);
            }
        }
        if (rule.action.parameters.qs) {
            try {
                rule.action.parameters.qs = JSON.parse(rule.action.parameters.qs);
            } catch (ex) {
                myutils.logErrorIf(ex);
            }
        }
        if (rule.action.parameters.headers) {
            try {
                rule.action.parameters.headers = JSON.parse(rule.action.parameters.headers);
            } catch (ex) {
                myutils.logErrorIf(ex);
            }
        }
    }
}

function stringifyPostAxnParams(rule) {
    if (rule && rule.action && rule.action.type === 'post') {
        if (rule.action.parameters.json) {
            try {
                rule.action.parameters.json = JSON.stringify(rule.action.parameters.json);
            } catch (e) {
                myutils.logErrorIf(e);
            }
        }
        if (rule.action.parameters.qs) {
            try {
                rule.action.parameters.qs = JSON.stringify(rule.action.parameters.qs);
            } catch (e) {
                myutils.logErrorIf(e);
            }
        }
        if (rule.action.parameters.headers) {
            try {
                rule.action.parameters.headers = JSON.stringify(rule.action.parameters.headers);
            } catch (e) {
                myutils.logErrorIf(e);
            }
        }
    }
}

function search(rule, callback) {
    var db = appContext.Db();
    async.waterfall(
        [
            db.collection.bind(db, rulesCollectionName, { strict: true }),
            function(col, cb) {
                col.findOne({ name: rule.name, subservice: rule.subservice, service: rule.service }, cb);
            }
        ],
        callback
    );
}

function findAll(service, subservice, limit, offset, count, callback) {
    var criterion = {};

    if (typeof service === 'function') {
        callback = service;
    } else {
        criterion.service = service;
        criterion.subservice = subservice;
    }
    var db = appContext.Db();
    async.waterfall(
        [
            db.collection.bind(db, rulesCollectionName, { strict: true }),
            function(col, cb) {
                col.find(criterion).toArray(function(err, rules) {
                    if (rules && util.isArray(rules)) {
                        // apply limit and offset over rules array
                        rules = rules.splice(offset, limit);
                        rules.forEach(function(r) {
                            parsePostAxnParams(r);
                        });
                    }
                    cb(err, rules);
                });
            }
        ],
        function(err, result) {
            myutils.logErrorIf(err);
            logger.info('rulesStore.FindAll %s', myutils.firstChars(result));
            return callback(err, result);
        }
    );
}

module.exports = {
    Find: function Find(rule, callback) {
        search(rule, function(err, result) {
            myutils.logErrorIf(err);
            if (err) {
                return callback(err, null);
            }
            if (!result) {
                return callback(new errors.NotFoundRule(rule.name), null);
            }
            parsePostAxnParams(result);
            logger.info('rulesStore.Find %s', myutils.firstChars(result));
            return callback(err, result);
        });
    },
    Exists: function Exists(rule, callback) {
        search(rule, function(err, result) {
            myutils.logErrorIf(err);
            callback(err, Boolean(result));
        });
    },
    FindAll: findAll,
    Remove: function Remove(rule, callback) {
        var db = appContext.Db();
        async.waterfall(
            [
                db.collection.bind(db, rulesCollectionName, { strict: true }),
                function(col, cb) {
                    col.remove({ name: rule.name, subservice: rule.subservice, service: rule.service }, cb);
                }
            ],
            function(err, result) {
                myutils.logErrorIf(err);
                logger.info('rulesStore.Remove %j', myutils.firstChars(result));
                return callback(err, result);
            }
        );
    },
    Save: function Save(r, callback) {
        var db = appContext.Db();
        stringifyPostAxnParams(r);
        async.waterfall(
            [
                db.collection.bind(db, rulesCollectionName, { strict: true }),
                function(col, cb) {
                    col.save(r, cb);
                }
            ],
            function(err, result) {
                myutils.logErrorIf(err);
                logger.info('rulesStore.Save %j', myutils.firstChars(result));
                return callback(err, result);
            }
        );
    },
    Update: function Update(id, r, callback) {
        var db = appContext.Db();
        stringifyPostAxnParams(r);
        async.waterfall(
            [
                db.collection.bind(db, rulesCollectionName, { strict: true }),
                function(col, cb) {
                    col.findOneAndUpdate(
                        { name: id },
                        r,
                        {
                            upsert: false,
                            returnOriginal: false
                        },
                        function(err, result) {
                            if (result && result.lastErrorObject && result.lastErrorObject.updatedExisting === false) {
                                return cb(new errors.NotFoundRule(id), null);
                            }
                            parsePostAxnParams(r);
                            return cb(err, r);
                        }
                    );
                }
            ],
            function(err, result) {
                myutils.logErrorIf(err);
                logger.info('rulesStore.Update %j', myutils.firstChars(result));
                return callback(err, result);
            }
        );
    }
};
/**
 * Constructors for possible errors from this module
 *
 * @type {Object}
 */
module.exports.errors = errors;

(function() {
    errors.NotFoundRule = function NotFoundRule(msg) {
        this.name = 'RULE_NOTFOUND';
        this.message = 'rule not found ' + msg;
        this.httpCode = 404;
    };
    Object.keys(errors).forEach(function(element) {
        util.inherits(errors[element], Error);
    });
})();
<|endoftext|>