Daniel@0: "use strict"; Daniel@0: Daniel@0: App.module("ContextModule", function(ContextModule, App, Backbone, Marionette, $, _, Logger) { Daniel@0: Daniel@0: // Define private variables Daniel@0: var logger = null; Daniel@0: Daniel@0: ContextModule.addInitializer(function(options){ Daniel@0: Daniel@0: logger = Logger.get("ContextModule.Config"); Daniel@0: logger.setLevel(Logger.WARN); Daniel@0: Daniel@0: /** Daniel@0: * Config embraces common behaviour patterns of an object that can have pending changes – Daniel@0: * it consists of two attributes: parameters and plannedParameterUpdates. Both are simple Daniel@0: * Backbone objects (Models) with a list of scalar attributes. Daniel@0: * Daniel@0: * The ConfigObject itself is identified by its non-changeable clientId (getClientId()) Daniel@0: * and can be serialized (dumped) and unserialized (restored). Daniel@0: * Daniel@0: * It is advised to listen to changes in the Config using "change", "change:parameters" Daniel@0: * and "change:plannedParameterUpdates" listener. Daniel@0: * It will be triggered each time when "parameters or "plannedParameterUpdates" attributes have been changed. Daniel@0: */ Daniel@0: ContextModule.Config = Backbone.Model.extend({ Daniel@0: Daniel@0: // Client id prefix (cf123 instead of standard c123) Daniel@0: cidPrefix: "cf", Daniel@0: Daniel@0: /** Daniel@0: * @memberOf App.ContextModule.Config Daniel@0: */ Daniel@0: constructor: function(attributes, options) { Daniel@0: this._modificationPropagationEnabled = true; Daniel@0: this._parametersWereModified = false; Daniel@0: this._plannedParameterUpdatesWereModified = false; Daniel@0: this._cachedHashForTrimmedParameters = null; Daniel@0: this._cachedHashForParameters = null; Daniel@0: this._cachedHashForPlannedParameterUpdates = null; Daniel@0: this._cachedHashForPermanent = null; Daniel@0: this._cachedHashForTemp = null; Daniel@0: this._cachedHash = null; Daniel@0: Daniel@0: var defaultParameters = (_.isSimpleObject(attributes) && _.isSimpleObject(attributes.parameters)) ? attributes.parameters : undefined; Daniel@0: var defaultPlannedParameterUpdates = (_.isSimpleObject(attributes) && _.isSimpleObject(attributes.plannedParameterUpdates)) ? attributes.plannedParameterUpdates : undefined; Daniel@0: Daniel@0: var realAttributes = {}; Daniel@0: realAttributes.parameters = new Backbone.Model(defaultParameters); Daniel@0: realAttributes.plannedParameterUpdates = new Backbone.Model(defaultPlannedParameterUpdates); Daniel@0: Daniel@0: this.listenTo(realAttributes.parameters, "change", this._registerModificationOfParameters); Daniel@0: this.listenTo(realAttributes.plannedParameterUpdates, "change", this._registerModificationOfPlannedParameterUpdates); Daniel@0: Daniel@0: Backbone.Model.apply(this, [realAttributes, options]); Daniel@0: Daniel@0: if (attributes && attributes.clientId) { Daniel@0: this.cid = attributes.clientId; Daniel@0: _.markUniqueIdAsAlreadyUsed(attributes.clientId); Daniel@0: } Daniel@0: }, Daniel@0: Daniel@0: getClientId: function() { Daniel@0: return this.cid; Daniel@0: }, Daniel@0: Daniel@0: getDimension: function() { Daniel@0: return this.collection ? this.collection.dimension : undefined; Daniel@0: }, Daniel@0: getConfigGridType: function() { Daniel@0: return this.collection ? this.collection.configGridType : undefined; Daniel@0: }, Daniel@0: Daniel@0: /** Daniel@0: * @memberOf App.ContextModule.Config Daniel@0: */ Daniel@0: getParameterValue: function(parameterName) { Daniel@0: return this.attributes.parameters.attributes[parameterName]; Daniel@0: }, Daniel@0: Daniel@0: /** Daniel@0: * @memberOf App.ContextModule.Config Daniel@0: */ Daniel@0: getPlannedParameterValue: function(parameterName) { Daniel@0: var plannedParameterUpdatesAttributes = this.attributes.plannedParameterUpdates.attributes; Daniel@0: if (plannedParameterUpdatesAttributes.hasOwnProperty(parameterName)) { Daniel@0: return plannedParameterUpdatesAttributes[parameterName]; Daniel@0: } else { Daniel@0: return this.attributes.parameters.attributes[parameterName]; Daniel@0: } Daniel@0: }, Daniel@0: Daniel@0: /** Daniel@0: * @memberOf App.ContextModule.Config Daniel@0: * Daniel@0: * XXX clone parameter values? Daniel@0: */ Daniel@0: getPlannedParameterValues: function(parameterName) { Daniel@0: var result = _.clone(this.attributes.parameters.attributes); Daniel@0: var plannedParameterUpdatesAttributes = this.attributes.plannedParameterUpdates.attributes; Daniel@0: for (var key in plannedParameterUpdatesAttributes) { Daniel@0: if (plannedParameterUpdatesAttributes.hasOwnProperty(key)) { Daniel@0: if (plannedParameterUpdatesAttributes[key] === undefined) { Daniel@0: if (result.hasOwnProperty(key)) { Daniel@0: delete result[key]; Daniel@0: } Daniel@0: } else { Daniel@0: result[key] = plannedParameterUpdatesAttributes[key]; Daniel@0: } Daniel@0: } Daniel@0: } Daniel@0: return result; Daniel@0: }, Daniel@0: Daniel@0: /** Daniel@0: * @memberOf App.ContextModule.Config Daniel@0: */ Daniel@0: isPlannedToUpdate: function(parameterName) { Daniel@0: return this.attributes.plannedParameterUpdates.attributes.hasOwnProperty(parameterName); Daniel@0: // var parameterValue = this.getParameterValue(parameterName); Daniel@0: // var plannedParameterValue = this.getPlannedParameterValue(parameterName); Daniel@0: // return (parameterValue !== plannedParameterValue); Daniel@0: }, Daniel@0: Daniel@0: /** Daniel@0: * @memberOf App.ContextModule.Config Daniel@0: */ Daniel@0: hasPlannedParameterUpdates: function() { Daniel@0: return _.size(this.attributes.plannedParameterUpdates.attributes) > 0; Daniel@0: }, Daniel@0: Daniel@0: /** Daniel@0: * @memberOf App.ContextModule.Config Daniel@0: */ Daniel@0: updateParameter: function(parameterName, parameterValue) { Daniel@0: if (!_.isString(parameterName)) { Daniel@0: throw _.str.sprintf("Config::updateParameter called a non-string parameterName: %s", parameterName); Daniel@0: } Daniel@0: var prevModificationPropagationEnabled = this._modificationPropagationEnabled; Daniel@0: this._modificationPropagationEnabled = false; Daniel@0: this.attributes.plannedParameterUpdates.unset(parameterName); Daniel@0: if (typeof parameterValue !== "undefined") { Daniel@0: this.attributes.parameters.set(parameterName, parameterValue); Daniel@0: } else { Daniel@0: this.attributes.parameters.unset(parameterName); Daniel@0: } Daniel@0: if (prevModificationPropagationEnabled) { Daniel@0: this._triggerModificationEventsIfNeeded(); Daniel@0: this._modificationPropagationEnabled = true; Daniel@0: } Daniel@0: }, Daniel@0: Daniel@0: /** Daniel@0: * @memberOf App.ContextModule.Config Daniel@0: */ Daniel@0: planParameterUpdate: function(parameterName, parameterValue) { Daniel@0: if (!_.isString(parameterName)) { Daniel@0: throw _.str.sprintf("Config::planParameterUpdate called a non-string parameterName: %s", parameterName); Daniel@0: } Daniel@0: var prevModificationPropagationEnabled = this._modificationPropagationEnabled; Daniel@0: Daniel@0: this._modificationPropagationEnabled = false; Daniel@0: var plannedParameterUpdatesAttributes = this.attributes.plannedParameterUpdates.attributes; Daniel@0: var parametersAttributes = this.attributes.parameters.attributes; Daniel@0: if (parameterValue === parametersAttributes[parameterName]) { Daniel@0: // special case: backbone won't fire a change event without this hack (due to how _.isEqual works) Daniel@0: if (this.attributes.plannedParameterUpdates.attributes.hasOwnProperty(parameterName) && this.attributes.plannedParameterUpdates.attributes[parameterName] === undefined) { Daniel@0: this.attributes.plannedParameterUpdates.set(parameterName, 42, {silent: true}); Daniel@0: } Daniel@0: this.attributes.plannedParameterUpdates.unset(parameterName); Daniel@0: } else { Daniel@0: // special case: backbone won't fire a change event without this hack (due to how _.isEqual works) Daniel@0: if (parameterValue === undefined && this.attributes.parameters.attributes.hasOwnProperty(parameterName) && !this.attributes.plannedParameterUpdates.attributes.hasOwnProperty(parameterName)) { Daniel@0: this.attributes.plannedParameterUpdates.set(parameterName, 42, {silent: true}); Daniel@0: } Daniel@0: this.attributes.plannedParameterUpdates.set(parameterName, parameterValue); Daniel@0: } Daniel@0: Daniel@0: if (prevModificationPropagationEnabled) { Daniel@0: this._triggerModificationEventsIfNeeded(); Daniel@0: this._modificationPropagationEnabled = true; Daniel@0: } Daniel@0: }, Daniel@0: Daniel@0: /** Daniel@0: * @memberOf App.ContextModule.Config Daniel@0: */ Daniel@0: cancelPlannedParameterUpdate: function(parameterName) { Daniel@0: if (!_.isString(parameterName)) { Daniel@0: throw _.str.sprintf("Config::cancelPlannedParameterUpdate called a non-string parameterName: %s", parameterName); Daniel@0: } Daniel@0: var prevModificationPropagationEnabled = this._modificationPropagationEnabled; Daniel@0: this._modificationPropagationEnabled = false; Daniel@0: Daniel@0: this.attributes.plannedParameterUpdates.unset(parameterName); Daniel@0: if (prevModificationPropagationEnabled) { Daniel@0: this._triggerModificationEventsIfNeeded(); Daniel@0: this._modificationPropagationEnabled = true; Daniel@0: } Daniel@0: }, Daniel@0: Daniel@0: /** Daniel@0: * @memberOf App.ContextModule.Config Daniel@0: */ Daniel@0: updateParameters: function(parameters) { Daniel@0: if (!_.isSimpleObject(parameters)) { Daniel@0: throw _.str.sprintf("Config::updateParameters called a wrong argument: %s", parameters); Daniel@0: } Daniel@0: this._modificationPropagationEnabled = false; Daniel@0: Daniel@0: for (var parameterName in parameters) { Daniel@0: if (parameters.hasOwnProperty(parameterName)) { Daniel@0: this.updateParameter(parameterName, parameters[parameterName]); Daniel@0: } Daniel@0: } Daniel@0: Daniel@0: this._triggerModificationEventsIfNeeded(); Daniel@0: this._modificationPropagationEnabled = true; Daniel@0: }, Daniel@0: Daniel@0: /** Daniel@0: * @memberOf App.ContextModule.Config Daniel@0: */ Daniel@0: planParameterUpdates: function(parameters) { Daniel@0: if (!_.isSimpleObject(parameters)) { Daniel@0: throw _.str.sprintf("Config::planParameterUpdates called with a wrong argument: %s", parameters); Daniel@0: } Daniel@0: this._modificationPropagationEnabled = false; Daniel@0: Daniel@0: for (var parameterName in parameters) { Daniel@0: if (parameters.hasOwnProperty(parameterName)) { Daniel@0: this.planParameterUpdate(parameterName, parameters[parameterName]); Daniel@0: } Daniel@0: } Daniel@0: Daniel@0: this._triggerModificationEventsIfNeeded(); Daniel@0: this._modificationPropagationEnabled = true; Daniel@0: }, Daniel@0: Daniel@0: /** Daniel@0: * @memberOf App.ContextModule.Config Daniel@0: */ Daniel@0: cancelPlannedParameterUpdates: function(parameterNames) { Daniel@0: if (_.isArray(parameterNames)) { Daniel@0: this._modificationPropagationEnabled = false; Daniel@0: Daniel@0: for (var i = 0; i < parameterNames.length; i++) { Daniel@0: this.cancelPlannedParameterUpdate(parameterNames[i]); Daniel@0: } Daniel@0: Daniel@0: this._triggerModificationEventsIfNeeded(); Daniel@0: this._modificationPropagationEnabled = true; Daniel@0: } else if (!_.isUndefined(parameterNames)) { Daniel@0: throw _.str.sprintf("Config::planParameterUpdates called a non-string parameters: %s", parameters); Daniel@0: } else { Daniel@0: if (_.keys(this.attributes.plannedParameterUpdates.attributes).length) { Daniel@0: this.attributes.plannedParameterUpdates.attributes.fix = 42; Daniel@0: } Daniel@0: this.attributes.plannedParameterUpdates.clear(); Daniel@0: } Daniel@0: }, Daniel@0: Daniel@0: /** Daniel@0: * @memberOf App.ContextModule.Config Daniel@0: */ Daniel@0: applyPlannedParameterUpdates: function() { Daniel@0: Daniel@0: // Combine parameters with planned updates Daniel@0: var newParameters = _.extend(this.attributes.parameters.toJSON(), this.attributes.plannedParameterUpdates.toJSON()); Daniel@0: Daniel@0: // Remove "undefined" from the new parameters Daniel@0: for (var key in newParameters) { Daniel@0: if (!newParameters.hasOwnProperty(key)) continue; Daniel@0: Daniel@0: if (typeof newParameters[key] === "undefined") { Daniel@0: delete newParameters[key]; Daniel@0: } Daniel@0: } Daniel@0: Daniel@0: // If there are any existing keys with "undefined" within plannedParameterUpdates, Daniel@0: // replace "undefined" with some value to make sure change:plannedParameterUpdates is triggered Daniel@0: // Another part of this hack is in "planParameterUpdate" method Daniel@0: var attributesInPlannedParameterUpdates = this.attributes.plannedParameterUpdates.attributes; Daniel@0: for (var key in attributesInPlannedParameterUpdates) { Daniel@0: if (attributesInPlannedParameterUpdates[key] === undefined) { Daniel@0: attributesInPlannedParameterUpdates[key] = 42; Daniel@0: break; Daniel@0: } Daniel@0: } Daniel@0: Daniel@0: // Assign parameters and clear planned updates Daniel@0: this.unserialize({ Daniel@0: clientId: this.cid, Daniel@0: parameters: newParameters, Daniel@0: plannedParameterUpdates: {} Daniel@0: }); Daniel@0: }, Daniel@0: Daniel@0: /** Daniel@0: * @memberOf App.ContextModule.Config Daniel@0: */ Daniel@0: serialize: function() { Daniel@0: var result = { Daniel@0: clientId: this.cid, Daniel@0: parameters: this.attributes.parameters.toJSON(), Daniel@0: plannedParameterUpdates: this.attributes.plannedParameterUpdates.toJSON() Daniel@0: }; Daniel@0: Daniel@0: return result; Daniel@0: }, Daniel@0: Daniel@0: /** Daniel@0: * @memberOf App.ContextModule.Config Daniel@0: */ Daniel@0: unserialize: function(serializedAttributes) { Daniel@0: var fixedSerializedAttributes = serializedAttributes; Daniel@0: if (!_.isSimpleObject(serializedAttributes)) { Daniel@0: logger.warn("Config::unserialize called for not an object: ", serializedAttributes); Daniel@0: fixedSerializedAttributes = {}; Daniel@0: } Daniel@0: Daniel@0: if (this.cid != fixedSerializedAttributes.clientId && !_.isUndefined(fixedSerializedAttributes.clientId)) { Daniel@0: throw _.str.sprintf("Parameter bag client id (%s) is not equal to the client id of the serialized object (%s).", this.cid, fixedSerializedAttributes.clientId); Daniel@0: } Daniel@0: Daniel@0: this._parametersWereModified = false; Daniel@0: this._plannedParameterUpdatesWereModified = false; Daniel@0: this._modificationPropagationEnabled = false; Daniel@0: Daniel@0: var fixedSerializedPlannedParameterUpdates = fixedSerializedAttributes.plannedParameterUpdates; Daniel@0: if (!_.isSimpleObject(fixedSerializedPlannedParameterUpdates)) { Daniel@0: if (_.isSimpleObject(serializedAttributes)) { Daniel@0: logger.warn("Config::unserialize called for object with faulty plannedParameterUpdates: ", fixedSerializedPlannedParameterUpdates); Daniel@0: } Daniel@0: fixedSerializedPlannedParameterUpdates = {}; Daniel@0: } Daniel@0: if (!_.isEqual(this.attributes.plannedParameterUpdates.attributes, fixedSerializedPlannedParameterUpdates)) { Daniel@0: this.attributes.plannedParameterUpdates Daniel@0: .set("fix", 42, {silent: true}) Daniel@0: .clear() Daniel@0: .set(fixedSerializedPlannedParameterUpdates); Daniel@0: } Daniel@0: Daniel@0: var fixedSerializedParameters = fixedSerializedAttributes.parameters; Daniel@0: if (!_.isSimpleObject(fixedSerializedParameters)) { Daniel@0: if (_.isSimpleObject(serializedAttributes)) { Daniel@0: logger.warn("Config::unserialize called for object with faulty parameters: ", fixedSerializedParameters); Daniel@0: } Daniel@0: fixedSerializedParameters = {}; Daniel@0: } Daniel@0: if (!_.isEqual(this.attributes.parameters.toJSON(), fixedSerializedParameters)) { Daniel@0: this.attributes.parameters Daniel@0: .clear() Daniel@0: .set(fixedSerializedParameters); Daniel@0: } Daniel@0: Daniel@0: this._triggerModificationEventsIfNeeded(); Daniel@0: this._modificationPropagationEnabled = true; Daniel@0: }, Daniel@0: Daniel@0: clone: function() { Daniel@0: var serializedAttributes = this.serialize(); Daniel@0: delete serializedAttributes.clientId; Daniel@0: return new ContextModule.Config(serializedAttributes); Daniel@0: }, Daniel@0: Daniel@0: getHashForParameters: function() { Daniel@0: if (this._cachedHashForParameters === null) { Daniel@0: this._cachedHashForParameters = JSON.stringify(this.attributes.parameters.attributes); Daniel@0: } Daniel@0: return this._cachedHashForParameters; Daniel@0: }, Daniel@0: Daniel@0: getHashForTrimmedParameters: function() { Daniel@0: if (this._cachedHashForTrimmedParameters === null) { Daniel@0: var attributesToHash = _.clone(this.attributes.parameters.attributes); Daniel@0: for (var key in attributesToHash){ Daniel@0: if (attributesToHash.hasOwnProperty(key) && _.isString(attributesToHash[key])) { Daniel@0: attributesToHash[key] = _.str.trim(attributesToHash[key]); Daniel@0: } Daniel@0: } Daniel@0: this._cachedHashForTrimmedParameters = JSON.stringify(attributesToHash); Daniel@0: } Daniel@0: return this._cachedHashForTrimmedParameters; Daniel@0: }, Daniel@0: Daniel@0: getHashForPlannedParameterUpdates: function() { Daniel@0: if (this._cachedHashForPlannedParameterUpdates === null) { Daniel@0: var attributes = this.attributes.plannedParameterUpdates.attributes; Daniel@0: this._cachedHashForPlannedParameterUpdates = JSON.stringify(attributes); Daniel@0: // special treatment of undefined is needed here Daniel@0: // see http://stackoverflow.com/questions/26540706/json-stringify-removes-hash-keys-with-undefined-values Daniel@0: for (var key in attributes) { Daniel@0: if (attributes.hasOwnProperty(key) && attributes[key] === undefined) { Daniel@0: this._cachedHashForPlannedParameterUpdates += key + "|"; Daniel@0: } Daniel@0: } Daniel@0: } Daniel@0: return this._cachedHashForPlannedParameterUpdates; Daniel@0: }, Daniel@0: Daniel@0: getHashForPermanent: function() { Daniel@0: if (this._cachedHashForPermanent === null) { Daniel@0: this._cachedHashForPermanent = this.getHashForParameters() + this.getHashForPlannedParameterUpdates(); Daniel@0: } Daniel@0: return this._cachedHashForPermanent; Daniel@0: }, Daniel@0: Daniel@0: getHashForTemp: function() { Daniel@0: if (this._cachedHashForTemp === null) { Daniel@0: this._cachedHashForTemp = ""; Daniel@0: } Daniel@0: return this._cachedHashForTemp; Daniel@0: }, Daniel@0: Daniel@0: getHash: function() { Daniel@0: if (!this._cachedHash) { Daniel@0: this._cachedHash = this.getHashForPermanent() + this.getHashForTemp(); Daniel@0: } Daniel@0: return this._cachedHash; Daniel@0: }, Daniel@0: Daniel@0: _registerModificationOfParameters: function() { Daniel@0: this._cachedHashForParameters = null; Daniel@0: this._cachedHashForTrimmedParameters = null; Daniel@0: this._cachedHashForPermanent = null; Daniel@0: this._cachedHash = null; Daniel@0: Daniel@0: this._parametersWereModified = true; Daniel@0: if (this._modificationPropagationEnabled) { Daniel@0: this._triggerModificationEventsIfNeeded(); Daniel@0: }; Daniel@0: }, Daniel@0: Daniel@0: _registerModificationOfPlannedParameterUpdates: function() { Daniel@0: this._cachedHashForPlannedParameterUpdates = null; Daniel@0: this._cachedHashForPermanent = null; Daniel@0: this._cachedHash = null; Daniel@0: Daniel@0: this._plannedParameterUpdatesWereModified = true; Daniel@0: if (this._modificationPropagationEnabled) { Daniel@0: this._triggerModificationEventsIfNeeded(); Daniel@0: }; Daniel@0: }, Daniel@0: Daniel@0: _triggerModificationEventsIfNeeded: function() { Daniel@0: this.changed = [true]; Daniel@0: if (this._parametersWereModified) { Daniel@0: this.trigger("change:parameters"); Daniel@0: } Daniel@0: if (this._plannedParameterUpdatesWereModified) { Daniel@0: this.trigger("change:plannedParameterUpdates"); Daniel@0: } Daniel@0: if (this._tempParametersWereModified) { Daniel@0: this.trigger("change:tempParameters"); Daniel@0: } Daniel@0: Daniel@0: if (this._parametersWereModified || this._plannedParameterUpdatesWereModified) { Daniel@0: this.trigger("change:parametersOrPlannedParameterUpdates"); Daniel@0: this.trigger("change"); Daniel@0: } Daniel@0: this._parametersWereModified = false; Daniel@0: this._plannedParameterUpdatesWereModified = false; Daniel@0: this._tempParametersWereModified = false; Daniel@0: this.changed = null; Daniel@0: }, Daniel@0: }); Daniel@0: }); Daniel@0: }, Logger);