// Copyright 2007 The Closure Library Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @fileoverview * Efficient implementation of DataNode API. * * The implementation consists of three concrete classes for modelling * DataNodes with different characteristics: FastDataNode, * FastPrimitiveDataNode and FastListNode. * * FastDataNode is for bean-like or map-like objects that consists of * key/value mappings and where the primary access pattern is by key. * * FastPrimitiveDataNode wraps primitives like strings, boolean, and numbers. * * FastListNode is for array-like data nodes. It also supports key-based * lookups if the data nodes have an "id" property or if child nodes are * explicitly added by name. It is most efficient if these features are not * used. * * FastDataNodes can be constructed from JSON-like objects via the function * goog.ds.FastDataNode.fromJs. */ goog.provide('goog.ds.AbstractFastDataNode'); goog.provide('goog.ds.FastDataNode'); goog.provide('goog.ds.FastListNode'); goog.provide('goog.ds.PrimitiveFastDataNode'); goog.require('goog.ds.DataManager'); goog.require('goog.ds.DataNodeList'); goog.require('goog.ds.EmptyNodeList'); goog.require('goog.string'); /* * Implementation note: In order to reduce the number of objects, * FastDataNode stores its key/value mappings directly in the FastDataNode * object iself (instead of a separate map). To make this work we have to * sure that there are no name clashes with other attribute names used by * FastDataNode (like dataName and parent). This is especially difficult in * the light of automatic renaming by the JavaScript compiler. For this reason, * all internal attributes start with "__" so that they are not renamed * by the compiler. */ /** * Creates a new abstract data node. * @param {string} dataName Name of the datanode. * @param {goog.ds.DataNode=} opt_parent Parent of this data node. * @constructor * @extends {goog.ds.DataNodeList} */ // TODO(arv): Use interfaces when available. goog.ds.AbstractFastDataNode = function(dataName, opt_parent) { if (!dataName) { throw Error('Cannot create a fast data node without a data name'); } this['__dataName'] = dataName; this['__parent'] = opt_parent; }; /** * Return the name of this data node. * @return {string} Name of this data noden. * @override */ goog.ds.AbstractFastDataNode.prototype.getDataName = function() { return this['__dataName']; }; /** * Set the name of this data node. * @param {string} value Name. * @override */ goog.ds.AbstractFastDataNode.prototype.setDataName = function(value) { this['__dataName'] = value; }; /** * Get the path leading to this data node. * @return {string} Data path. * @override */ goog.ds.AbstractFastDataNode.prototype.getDataPath = function() { var parentPath; if (this['__parent']) { parentPath = this['__parent'].getDataPath() + goog.ds.STR_PATH_SEPARATOR; } else { parentPath = ''; } return parentPath + this.getDataName(); }; /** * Creates a new fast data node, using the properties of root. * @param {Object} root JSON-like object to initialize data node from. * @param {string} dataName Name of this data node. * @param {goog.ds.DataNode=} opt_parent Parent of this data node. * @extends {goog.ds.AbstractFastDataNode} * @constructor */ goog.ds.FastDataNode = function(root, dataName, opt_parent) { goog.ds.AbstractFastDataNode.call(this, dataName, opt_parent); this.extendWith(root); }; goog.inherits(goog.ds.FastDataNode, goog.ds.AbstractFastDataNode); /** * Add all attributes of object to this data node. * @param {Object} object Object to add attributes from. * @protected */ goog.ds.FastDataNode.prototype.extendWith = function(object) { for (var key in object) { this[key] = object[key]; } }; /** * Creates a new FastDataNode structure initialized from object. This will * return an instance of the most suitable sub-class of FastDataNode. * * You should not modify object after creating a fast data node from it * or assume that changing object changes the data node. Doing so results * in undefined behaviour. * * @param {Object|number|boolean|string} object Object to initialize data * node from. * @param {string} dataName Name of data node. * @param {goog.ds.DataNode=} opt_parent Parent of data node. * @return {!goog.ds.AbstractFastDataNode} Data node representing object. */ goog.ds.FastDataNode.fromJs = function(object, dataName, opt_parent) { if (goog.isArray(object)) { return new goog.ds.FastListNode(object, dataName, opt_parent); } else if (goog.isObject(object)) { return new goog.ds.FastDataNode(object, dataName, opt_parent); } else { return new goog.ds.PrimitiveFastDataNode( object || !!object, dataName, opt_parent); } }; /** * Static instance of an empty list. * @type {!goog.ds.EmptyNodeList} * @private */ goog.ds.FastDataNode.emptyList_ = new goog.ds.EmptyNodeList(); /** * Not supported for normal FastDataNodes. * @param {*} value Value to set data node to. * @override */ goog.ds.FastDataNode.prototype.set = function(value) { throw new Error('Not implemented yet'); }; /** @override */ goog.ds.FastDataNode.prototype.getChildNodes = function(opt_selector) { if (!opt_selector || opt_selector == goog.ds.STR_ALL_CHILDREN_SELECTOR) { return this; } else if (opt_selector.indexOf(goog.ds.STR_WILDCARD) == -1) { var child = this.getChildNode(opt_selector); return child ? new goog.ds.FastListNode([child], '') : new goog.ds.EmptyNodeList(); } else { throw Error('Unsupported selector: ' + opt_selector); } }; /** * Makes sure that a named child is wrapped in a data node structure. * @param {string} name Name of child to wrap. * @private */ goog.ds.FastDataNode.prototype.wrapChild_ = function(name) { var child = this[name]; if (child != null && !child.getDataName) { this[name] = goog.ds.FastDataNode.fromJs(this[name], name, this); } }; /** * Get a child node by name. * @param {string} name Name of child node. * @param {boolean=} opt_create Whether to create the child if it does not * exist. * @return {goog.ds.DataNode} Child node. * @override */ goog.ds.FastDataNode.prototype.getChildNode = function(name, opt_create) { this.wrapChild_(name); // this[name] always is a data node object, so using "||" is fine. var child = this[name] || null; if (child == null && opt_create) { child = new goog.ds.FastDataNode({}, name, this); this[name] = child; } return child; }; /** * Sets a child node. Creates the child if it does not exist. * * Calling this function makes any child nodes previously obtained for name * invalid. You should not use these child nodes but instead obtain a new * instance by calling getChildNode. * * @override */ goog.ds.FastDataNode.prototype.setChildNode = function(name, value) { if (value != null) { this[name] = value; } else { delete this[name]; } goog.ds.DataManager.getInstance().fireDataChange( this.getDataPath() + goog.ds.STR_PATH_SEPARATOR + name); return null; }; /** * Returns the value of a child node. By using this method you can avoid * the need to create PrimitiveFastData nodes. * @param {string} name Name of child node. * @return {Object} Value of child node. * @override */ goog.ds.FastDataNode.prototype.getChildNodeValue = function(name) { var child = this[name]; if (child != null) { return (child.getDataName ? child.get() : child); } else { return null; } }; /** * Returns whether this data node is a list. Always returns false for * instances of FastDataNode but may return true for subclasses. * @return {boolean} Whether this data node is array-like. * @override */ goog.ds.FastDataNode.prototype.isList = function() { return false; }; /** * Returns a javascript object representation of this data node. You should * not modify the object returned by this function. * @return {!Object} Javascript object representation of this data node. */ goog.ds.FastDataNode.prototype.getJsObject = function() { var result = {}; for (var key in this) { if (!goog.string.startsWith(key, '__') && !goog.isFunction(this[key])) { result[key] = (this[key]['__dataName'] ? this[key].getJsObject() : this[key]); } } return result; }; /** * Creates a deep copy of this data node. * @return {goog.ds.FastDataNode} Clone of this data node. */ goog.ds.FastDataNode.prototype.clone = function() { return /** @type {!goog.ds.FastDataNode} */ ( goog.ds.FastDataNode.fromJs(this.getJsObject(), this.getDataName())); }; /* * Implementation of goog.ds.DataNodeList for FastDataNode. */ /** * Adds a child to this data node. * @param {goog.ds.DataNode} value Child node to add. * @override */ goog.ds.FastDataNode.prototype.add = function(value) { this.setChildNode(value.getDataName(), value); }; /** * Gets the value of this data node (if called without opt_key) or * gets a child node (if called with opt_key). * @param {string=} opt_key Name of child node. * @return {*} This data node or a child node. * @override */ goog.ds.FastDataNode.prototype.get = function(opt_key) { if (!goog.isDef(opt_key)) { // if there is no key, DataNode#get was called return this; } else { return this.getChildNode(opt_key); } }; /** * Gets a child node by index. This method has a complexity of O(n) where * n is the number of children. If you need a faster implementation of this * method, you should use goog.ds.FastListNode. * @param {number} index Index of child node (starting from 0). * @return {goog.ds.DataNode} Child node at specified index. * @override */ goog.ds.FastDataNode.prototype.getByIndex = function(index) { var i = 0; for (var key in this) { if (!goog.string.startsWith(key, '__') && !goog.isFunction(this[key])) { if (i == index) { this.wrapChild_(key); return this[key]; } ++i; } } return null; }; /** * Gets the number of child nodes. This method has a complexity of O(n) where * n is the number of children. If you need a faster implementation of this * method, you should use goog.ds.FastListNode. * @return {number} Number of child nodes. * @override */ goog.ds.FastDataNode.prototype.getCount = function() { var count = 0; for (var key in this) { if (!goog.string.startsWith(key, '__') && !goog.isFunction(this[key])) { ++count; } } // maybe cache this? return count; }; /** * Sets a child node. * @param {string} name Name of child node. * @param {Object} value Value of child node. * @override */ goog.ds.FastDataNode.prototype.setNode = function(name, value) { this.setChildNode(name, value); }; /** * Removes a child node. * @override */ goog.ds.FastDataNode.prototype.removeNode = function(name) { delete this[name]; return false; }; /** * Creates a new data node wrapping a primitive value. * @param {number|boolean|string} value Value the value to wrap. * @param {string} dataName name Name of this data node. * @param {goog.ds.DataNode=} opt_parent Parent of this data node. * @extends {goog.ds.AbstractFastDataNode} * @constructor * @final */ goog.ds.PrimitiveFastDataNode = function(value, dataName, opt_parent) { this.value_ = value; goog.ds.AbstractFastDataNode.call(this, dataName, opt_parent); }; goog.inherits(goog.ds.PrimitiveFastDataNode, goog.ds.AbstractFastDataNode); /** * Returns the value of this data node. * @return {(boolean|number|string)} Value of this data node. * @override */ goog.ds.PrimitiveFastDataNode.prototype.get = function() { return this.value_; }; /** * Sets this data node to a new value. * @param {*} value Value to set data node to. * @override */ goog.ds.PrimitiveFastDataNode.prototype.set = function(value) { if (goog.isArray(value) || goog.isObject(value)) { throw Error('can only set PrimitiveFastDataNode to primitive values'); } this.value_ = value; goog.ds.DataManager.getInstance().fireDataChange(this.getDataPath()); }; /** * Returns child nodes of this data node. Always returns an unmodifiable, * empty list. * @return {!goog.ds.DataNodeList} (Empty) list of child nodes. * @override */ goog.ds.PrimitiveFastDataNode.prototype.getChildNodes = function() { return goog.ds.FastDataNode.emptyList_; }; /** * Get a child node by name. Always returns null. * @param {string} name Name of child node. * @return {goog.ds.DataNode} Child node. * @override */ goog.ds.PrimitiveFastDataNode.prototype.getChildNode = function(name) { return null; }; /** * Returns the value of a child node. Always returns null. * @param {string} name Name of child node. * @return {Object} Value of child node. * @override */ goog.ds.PrimitiveFastDataNode.prototype.getChildNodeValue = function(name) { return null; }; /** * Not supported by primitive data nodes. * @param {string} name Name of child node. * @param {Object} value Value of child node. * @override */ goog.ds.PrimitiveFastDataNode.prototype.setChildNode = function(name, value) { throw Error('Cannot set a child node for a PrimitiveFastDataNode'); }; /** * Returns whether this data node is a list. Always returns false for * instances of PrimitiveFastDataNode. * @return {boolean} Whether this data node is array-like. * @override */ goog.ds.PrimitiveFastDataNode.prototype.isList = function() { return false; }; /** * Returns a javascript object representation of this data node. You should * not modify the object returned by this function. * @return {*} Javascript object representation of this data node. */ goog.ds.PrimitiveFastDataNode.prototype.getJsObject = function() { return this.value_; }; /** * Creates a new list node from an array. * @param {Array} values values hold by this list node. * @param {string} dataName name of this node. * @param {goog.ds.DataNode=} opt_parent parent of this node. * @extends {goog.ds.AbstractFastDataNode} * @constructor * @final */ // TODO(arv): Use interfaces when available. This implements DataNodeList // as well. goog.ds.FastListNode = function(values, dataName, opt_parent) { this.values_ = []; for (var i = 0; i < values.length; ++i) { var name = values[i].id || ('[' + i + ']'); this.values_.push(goog.ds.FastDataNode.fromJs(values[i], name, this)); if (values[i].id) { if (!this.map_) { this.map_ = {}; } this.map_[values[i].id] = i; } } goog.ds.AbstractFastDataNode.call(this, dataName, opt_parent); }; goog.inherits(goog.ds.FastListNode, goog.ds.AbstractFastDataNode); /** * Not supported for FastListNodes. * @param {*} value Value to set data node to. * @override */ goog.ds.FastListNode.prototype.set = function(value) { throw Error('Cannot set a FastListNode to a new value'); }; /** * Returns child nodes of this data node. Currently, only supports * returning all children. * @return {!goog.ds.DataNodeList} List of child nodes. * @override */ goog.ds.FastListNode.prototype.getChildNodes = function() { return this; }; /** * Get a child node by name. * @param {string} key Name of child node. * @param {boolean=} opt_create Whether to create the child if it does not * exist. * @return {goog.ds.DataNode} Child node. * @override */ goog.ds.FastListNode.prototype.getChildNode = function(key, opt_create) { var index = this.getKeyAsNumber_(key); if (index == null && this.map_) { index = this.map_[key]; } if (index != null && this.values_[index]) { return this.values_[index]; } else if (opt_create) { this.setChildNode(key, {}); return this.getChildNode(key); } else { return null; } }; /** * Returns the value of a child node. * @param {string} key Name of child node. * @return {*} Value of child node. * @override */ goog.ds.FastListNode.prototype.getChildNodeValue = function(key) { var child = this.getChildNode(key); return (child ? child.get() : null); }; /** * Tries to interpret key as a numeric index enclosed by square brakcets. * @param {string} key Key that should be interpreted as a number. * @return {?number} Numeric index or null if key is not of the form * described above. * @private */ goog.ds.FastListNode.prototype.getKeyAsNumber_ = function(key) { if (key.charAt(0) == '[' && key.charAt(key.length - 1) == ']') { return Number(key.substring(1, key.length - 1)); } else { return null; } }; /** * Sets a child node. Creates the child if it does not exist. To set * children at a certain index, use a key of the form '[index]'. Note, that * you can only set values at existing numeric indices. To add a new node * to this list, you have to use the add method. * * Calling this function makes any child nodes previously obtained for name * invalid. You should not use these child nodes but instead obtain a new * instance by calling getChildNode. * * @override */ goog.ds.FastListNode.prototype.setChildNode = function(key, value) { var count = this.values_.length; if (value != null) { if (!value.getDataName) { value = goog.ds.FastDataNode.fromJs(value, key, this); } var index = this.getKeyAsNumber_(key); if (index != null) { if (index < 0 || index >= this.values_.length) { throw Error('List index out of bounds: ' + index); } // NOTE: This code here appears to want to use "index" rather than // "key" here (which would be better for an array. However, changing // that would require knowing that there wasn't a mix of non-number // keys, as using index that would risk overwriting those values if // they were set first. Instead we loosen the type so we can use // strings as indexes. /** @type {!Object} */ var values = this.values_; values[key] = value; } else { if (!this.map_) { this.map_ = {}; } this.values_.push(value); this.map_[key] = this.values_.length - 1; } } else { this.removeNode(key); } var dm = goog.ds.DataManager.getInstance(); dm.fireDataChange(this.getDataPath() + goog.ds.STR_PATH_SEPARATOR + key); if (this.values_.length != count) { this.listSizeChanged_(); } return null; }; /** * Fire data changes that are appropriate when the size of this list changes. * Should be called whenever the list size has changed. * @private */ goog.ds.FastListNode.prototype.listSizeChanged_ = function() { var dm = goog.ds.DataManager.getInstance(); dm.fireDataChange(this.getDataPath()); dm.fireDataChange( this.getDataPath() + goog.ds.STR_PATH_SEPARATOR + 'count()'); }; /** * Returns whether this data node is a list. Always returns true. * @return {boolean} Whether this data node is array-like. * @override */ goog.ds.FastListNode.prototype.isList = function() { return true; }; /** * Returns a javascript object representation of this data node. You should * not modify the object returned by this function. * @return {!Object} Javascript object representation of this data node. */ goog.ds.FastListNode.prototype.getJsObject = function() { var result = []; for (var i = 0; i < this.values_.length; ++i) { result.push(this.values_[i].getJsObject()); } return result; }; /* * Implementation of goog.ds.DataNodeList for FastListNode. */ /** * Adds a child to this data node * @param {goog.ds.DataNode} value Child node to add. * @override */ goog.ds.FastListNode.prototype.add = function(value) { if (!value.getDataName) { value = goog.ds.FastDataNode.fromJs( value, String('[' + (this.values_.length) + ']'), this); } this.values_.push(value); var dm = goog.ds.DataManager.getInstance(); dm.fireDataChange( this.getDataPath() + goog.ds.STR_PATH_SEPARATOR + '[' + (this.values_.length - 1) + ']'); this.listSizeChanged_(); }; /** * Gets the value of this data node (if called without opt_key) or * gets a child node (if called with opt_key). * @param {string=} opt_key Name of child node. * @return {Array|goog.ds.DataNode} Array of child nodes (if called without * opt_key), or a named child node otherwise. * @override */ goog.ds.FastListNode.prototype.get = function(opt_key) { // if there are no arguments, DataNode.get was called if (!goog.isDef(opt_key)) { return this.values_; } else { return this.getChildNode(opt_key); } }; /** * Gets a child node by (numeric) index. * @param {number} index Index of child node (starting from 0). * @return {goog.ds.DataNode} Child node at specified index. * @override */ goog.ds.FastListNode.prototype.getByIndex = function(index) { var child = this.values_[index]; return (child != null ? child : null); // never return undefined }; /** * Gets the number of child nodes. * @return {number} Number of child nodes. * @override */ goog.ds.FastListNode.prototype.getCount = function() { return this.values_.length; }; /** * Sets a child node. * @param {string} name Name of child node. * @param {Object} value Value of child node. * @override */ goog.ds.FastListNode.prototype.setNode = function(name, value) { throw Error('Setting child nodes of a FastListNode is not implemented, yet'); }; /** * Removes a child node. * @override */ goog.ds.FastListNode.prototype.removeNode = function(name) { var index = this.getKeyAsNumber_(name); if (index == null && this.map_) { index = this.map_[name]; } if (index != null) { this.values_.splice(index, 1); if (this.map_) { var keyToDelete = null; for (var key in this.map_) { if (this.map_[key] == index) { keyToDelete = key; } else if (this.map_[key] > index) { --this.map_[key]; } } if (keyToDelete) { delete this.map_[keyToDelete]; } } var dm = goog.ds.DataManager.getInstance(); dm.fireDataChange( this.getDataPath() + goog.ds.STR_PATH_SEPARATOR + '[' + index + ']'); this.listSizeChanged_(); } return false; }; /** * Returns the index of a named child nodes. This method only works if * this list uses mixed name/indexed lookup, i.e. if its child node have * an 'id' attribute. * @param {string} name Name of child node to determine index of. * @return {number} Index of child node named name. */ goog.ds.FastListNode.prototype.indexOf = function(name) { var index = this.getKeyAsNumber_(name); if (index == null && this.map_) { index = this.map_[name]; } if (index == null) { throw Error('Cannot determine index for: ' + name); } return /** @type {number} */ (index); };