Path: blob/trunk/third_party/closure/goog/datasource/fastdatanode.js
2868 views
// Copyright 2007 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview16* Efficient implementation of DataNode API.17*18* The implementation consists of three concrete classes for modelling19* DataNodes with different characteristics: FastDataNode,20* FastPrimitiveDataNode and FastListNode.21*22* FastDataNode is for bean-like or map-like objects that consists of23* key/value mappings and where the primary access pattern is by key.24*25* FastPrimitiveDataNode wraps primitives like strings, boolean, and numbers.26*27* FastListNode is for array-like data nodes. It also supports key-based28* lookups if the data nodes have an "id" property or if child nodes are29* explicitly added by name. It is most efficient if these features are not30* used.31*32* FastDataNodes can be constructed from JSON-like objects via the function33* goog.ds.FastDataNode.fromJs.3435*/3637goog.provide('goog.ds.AbstractFastDataNode');38goog.provide('goog.ds.FastDataNode');39goog.provide('goog.ds.FastListNode');40goog.provide('goog.ds.PrimitiveFastDataNode');4142goog.require('goog.ds.DataManager');43goog.require('goog.ds.DataNodeList');44goog.require('goog.ds.EmptyNodeList');45goog.require('goog.string');4647/*48* Implementation note: In order to reduce the number of objects,49* FastDataNode stores its key/value mappings directly in the FastDataNode50* object iself (instead of a separate map). To make this work we have to51* sure that there are no name clashes with other attribute names used by52* FastDataNode (like dataName and parent). This is especially difficult in53* the light of automatic renaming by the JavaScript compiler. For this reason,54* all internal attributes start with "__" so that they are not renamed55* by the compiler.56*/5758/**59* Creates a new abstract data node.60* @param {string} dataName Name of the datanode.61* @param {goog.ds.DataNode=} opt_parent Parent of this data node.62* @constructor63* @extends {goog.ds.DataNodeList}64*/65// TODO(arv): Use interfaces when available.66goog.ds.AbstractFastDataNode = function(dataName, opt_parent) {67if (!dataName) {68throw Error('Cannot create a fast data node without a data name');69}70this['__dataName'] = dataName;71this['__parent'] = opt_parent;72};737475/**76* Return the name of this data node.77* @return {string} Name of this data noden.78* @override79*/80goog.ds.AbstractFastDataNode.prototype.getDataName = function() {81return this['__dataName'];82};838485/**86* Set the name of this data node.87* @param {string} value Name.88* @override89*/90goog.ds.AbstractFastDataNode.prototype.setDataName = function(value) {91this['__dataName'] = value;92};939495/**96* Get the path leading to this data node.97* @return {string} Data path.98* @override99*/100goog.ds.AbstractFastDataNode.prototype.getDataPath = function() {101var parentPath;102if (this['__parent']) {103parentPath = this['__parent'].getDataPath() + goog.ds.STR_PATH_SEPARATOR;104} else {105parentPath = '';106}107return parentPath + this.getDataName();108};109110111112/**113* Creates a new fast data node, using the properties of root.114* @param {Object} root JSON-like object to initialize data node from.115* @param {string} dataName Name of this data node.116* @param {goog.ds.DataNode=} opt_parent Parent of this data node.117* @extends {goog.ds.AbstractFastDataNode}118* @constructor119*/120goog.ds.FastDataNode = function(root, dataName, opt_parent) {121goog.ds.AbstractFastDataNode.call(this, dataName, opt_parent);122this.extendWith(root);123};124goog.inherits(goog.ds.FastDataNode, goog.ds.AbstractFastDataNode);125126127/**128* Add all attributes of object to this data node.129* @param {Object} object Object to add attributes from.130* @protected131*/132goog.ds.FastDataNode.prototype.extendWith = function(object) {133for (var key in object) {134this[key] = object[key];135}136};137138139/**140* Creates a new FastDataNode structure initialized from object. This will141* return an instance of the most suitable sub-class of FastDataNode.142*143* You should not modify object after creating a fast data node from it144* or assume that changing object changes the data node. Doing so results145* in undefined behaviour.146*147* @param {Object|number|boolean|string} object Object to initialize data148* node from.149* @param {string} dataName Name of data node.150* @param {goog.ds.DataNode=} opt_parent Parent of data node.151* @return {!goog.ds.AbstractFastDataNode} Data node representing object.152*/153goog.ds.FastDataNode.fromJs = function(object, dataName, opt_parent) {154if (goog.isArray(object)) {155return new goog.ds.FastListNode(object, dataName, opt_parent);156} else if (goog.isObject(object)) {157return new goog.ds.FastDataNode(object, dataName, opt_parent);158} else {159return new goog.ds.PrimitiveFastDataNode(160object || !!object, dataName, opt_parent);161}162};163164165/**166* Static instance of an empty list.167* @type {!goog.ds.EmptyNodeList}168* @private169*/170goog.ds.FastDataNode.emptyList_ = new goog.ds.EmptyNodeList();171172173/**174* Not supported for normal FastDataNodes.175* @param {*} value Value to set data node to.176* @override177*/178goog.ds.FastDataNode.prototype.set = function(value) {179throw new Error('Not implemented yet');180};181182183/** @override */184goog.ds.FastDataNode.prototype.getChildNodes = function(opt_selector) {185if (!opt_selector || opt_selector == goog.ds.STR_ALL_CHILDREN_SELECTOR) {186return this;187} else if (opt_selector.indexOf(goog.ds.STR_WILDCARD) == -1) {188var child = this.getChildNode(opt_selector);189return child ? new goog.ds.FastListNode([child], '') :190new goog.ds.EmptyNodeList();191} else {192throw Error('Unsupported selector: ' + opt_selector);193}194};195196197/**198* Makes sure that a named child is wrapped in a data node structure.199* @param {string} name Name of child to wrap.200* @private201*/202goog.ds.FastDataNode.prototype.wrapChild_ = function(name) {203var child = this[name];204if (child != null && !child.getDataName) {205this[name] = goog.ds.FastDataNode.fromJs(this[name], name, this);206}207};208209210/**211* Get a child node by name.212* @param {string} name Name of child node.213* @param {boolean=} opt_create Whether to create the child if it does not214* exist.215* @return {goog.ds.DataNode} Child node.216* @override217*/218goog.ds.FastDataNode.prototype.getChildNode = function(name, opt_create) {219this.wrapChild_(name);220// this[name] always is a data node object, so using "||" is fine.221var child = this[name] || null;222if (child == null && opt_create) {223child = new goog.ds.FastDataNode({}, name, this);224this[name] = child;225}226return child;227};228229230/**231* Sets a child node. Creates the child if it does not exist.232*233* Calling this function makes any child nodes previously obtained for name234* invalid. You should not use these child nodes but instead obtain a new235* instance by calling getChildNode.236*237* @override238*/239goog.ds.FastDataNode.prototype.setChildNode = function(name, value) {240if (value != null) {241this[name] = value;242} else {243delete this[name];244}245goog.ds.DataManager.getInstance().fireDataChange(246this.getDataPath() + goog.ds.STR_PATH_SEPARATOR + name);247return null;248};249250251/**252* Returns the value of a child node. By using this method you can avoid253* the need to create PrimitiveFastData nodes.254* @param {string} name Name of child node.255* @return {Object} Value of child node.256* @override257*/258goog.ds.FastDataNode.prototype.getChildNodeValue = function(name) {259var child = this[name];260if (child != null) {261return (child.getDataName ? child.get() : child);262} else {263return null;264}265};266267268/**269* Returns whether this data node is a list. Always returns false for270* instances of FastDataNode but may return true for subclasses.271* @return {boolean} Whether this data node is array-like.272* @override273*/274goog.ds.FastDataNode.prototype.isList = function() {275return false;276};277278279/**280* Returns a javascript object representation of this data node. You should281* not modify the object returned by this function.282* @return {!Object} Javascript object representation of this data node.283*/284goog.ds.FastDataNode.prototype.getJsObject = function() {285var result = {};286for (var key in this) {287if (!goog.string.startsWith(key, '__') && !goog.isFunction(this[key])) {288result[key] =289(this[key]['__dataName'] ? this[key].getJsObject() : this[key]);290}291}292return result;293};294295296/**297* Creates a deep copy of this data node.298* @return {goog.ds.FastDataNode} Clone of this data node.299*/300goog.ds.FastDataNode.prototype.clone = function() {301return /** @type {!goog.ds.FastDataNode} */ (302goog.ds.FastDataNode.fromJs(this.getJsObject(), this.getDataName()));303};304305306/*307* Implementation of goog.ds.DataNodeList for FastDataNode.308*/309310311/**312* Adds a child to this data node.313* @param {goog.ds.DataNode} value Child node to add.314* @override315*/316goog.ds.FastDataNode.prototype.add = function(value) {317this.setChildNode(value.getDataName(), value);318};319320321/**322* Gets the value of this data node (if called without opt_key) or323* gets a child node (if called with opt_key).324* @param {string=} opt_key Name of child node.325* @return {*} This data node or a child node.326* @override327*/328goog.ds.FastDataNode.prototype.get = function(opt_key) {329if (!goog.isDef(opt_key)) {330// if there is no key, DataNode#get was called331return this;332} else {333return this.getChildNode(opt_key);334}335};336337338/**339* Gets a child node by index. This method has a complexity of O(n) where340* n is the number of children. If you need a faster implementation of this341* method, you should use goog.ds.FastListNode.342* @param {number} index Index of child node (starting from 0).343* @return {goog.ds.DataNode} Child node at specified index.344* @override345*/346goog.ds.FastDataNode.prototype.getByIndex = function(index) {347var i = 0;348for (var key in this) {349if (!goog.string.startsWith(key, '__') && !goog.isFunction(this[key])) {350if (i == index) {351this.wrapChild_(key);352return this[key];353}354++i;355}356}357return null;358};359360361/**362* Gets the number of child nodes. This method has a complexity of O(n) where363* n is the number of children. If you need a faster implementation of this364* method, you should use goog.ds.FastListNode.365* @return {number} Number of child nodes.366* @override367*/368goog.ds.FastDataNode.prototype.getCount = function() {369var count = 0;370for (var key in this) {371if (!goog.string.startsWith(key, '__') && !goog.isFunction(this[key])) {372++count;373}374}375// maybe cache this?376return count;377};378379380/**381* Sets a child node.382* @param {string} name Name of child node.383* @param {Object} value Value of child node.384* @override385*/386goog.ds.FastDataNode.prototype.setNode = function(name, value) {387this.setChildNode(name, value);388};389390391/**392* Removes a child node.393* @override394*/395goog.ds.FastDataNode.prototype.removeNode = function(name) {396delete this[name];397return false;398};399400401402/**403* Creates a new data node wrapping a primitive value.404* @param {number|boolean|string} value Value the value to wrap.405* @param {string} dataName name Name of this data node.406* @param {goog.ds.DataNode=} opt_parent Parent of this data node.407* @extends {goog.ds.AbstractFastDataNode}408* @constructor409* @final410*/411goog.ds.PrimitiveFastDataNode = function(value, dataName, opt_parent) {412this.value_ = value;413goog.ds.AbstractFastDataNode.call(this, dataName, opt_parent);414};415goog.inherits(goog.ds.PrimitiveFastDataNode, goog.ds.AbstractFastDataNode);416417418/**419* Returns the value of this data node.420* @return {(boolean|number|string)} Value of this data node.421* @override422*/423goog.ds.PrimitiveFastDataNode.prototype.get = function() {424return this.value_;425};426427428/**429* Sets this data node to a new value.430* @param {*} value Value to set data node to.431* @override432*/433goog.ds.PrimitiveFastDataNode.prototype.set = function(value) {434if (goog.isArray(value) || goog.isObject(value)) {435throw Error('can only set PrimitiveFastDataNode to primitive values');436}437this.value_ = value;438goog.ds.DataManager.getInstance().fireDataChange(this.getDataPath());439};440441442/**443* Returns child nodes of this data node. Always returns an unmodifiable,444* empty list.445* @return {!goog.ds.DataNodeList} (Empty) list of child nodes.446* @override447*/448goog.ds.PrimitiveFastDataNode.prototype.getChildNodes = function() {449return goog.ds.FastDataNode.emptyList_;450};451452453/**454* Get a child node by name. Always returns null.455* @param {string} name Name of child node.456* @return {goog.ds.DataNode} Child node.457* @override458*/459goog.ds.PrimitiveFastDataNode.prototype.getChildNode = function(name) {460return null;461};462463464/**465* Returns the value of a child node. Always returns null.466* @param {string} name Name of child node.467* @return {Object} Value of child node.468* @override469*/470goog.ds.PrimitiveFastDataNode.prototype.getChildNodeValue = function(name) {471return null;472};473474475/**476* Not supported by primitive data nodes.477* @param {string} name Name of child node.478* @param {Object} value Value of child node.479* @override480*/481goog.ds.PrimitiveFastDataNode.prototype.setChildNode = function(name, value) {482throw Error('Cannot set a child node for a PrimitiveFastDataNode');483};484485486/**487* Returns whether this data node is a list. Always returns false for488* instances of PrimitiveFastDataNode.489* @return {boolean} Whether this data node is array-like.490* @override491*/492goog.ds.PrimitiveFastDataNode.prototype.isList = function() {493return false;494};495496497/**498* Returns a javascript object representation of this data node. You should499* not modify the object returned by this function.500* @return {*} Javascript object representation of this data node.501*/502goog.ds.PrimitiveFastDataNode.prototype.getJsObject = function() {503return this.value_;504};505506507/**508* Creates a new list node from an array.509* @param {Array<?>} values values hold by this list node.510* @param {string} dataName name of this node.511* @param {goog.ds.DataNode=} opt_parent parent of this node.512* @extends {goog.ds.AbstractFastDataNode}513* @constructor514* @final515*/516// TODO(arv): Use interfaces when available. This implements DataNodeList517// as well.518goog.ds.FastListNode = function(values, dataName, opt_parent) {519this.values_ = [];520for (var i = 0; i < values.length; ++i) {521var name = values[i].id || ('[' + i + ']');522this.values_.push(goog.ds.FastDataNode.fromJs(values[i], name, this));523if (values[i].id) {524if (!this.map_) {525this.map_ = {};526}527this.map_[values[i].id] = i;528}529}530goog.ds.AbstractFastDataNode.call(this, dataName, opt_parent);531};532goog.inherits(goog.ds.FastListNode, goog.ds.AbstractFastDataNode);533534535/**536* Not supported for FastListNodes.537* @param {*} value Value to set data node to.538* @override539*/540goog.ds.FastListNode.prototype.set = function(value) {541throw Error('Cannot set a FastListNode to a new value');542};543544545/**546* Returns child nodes of this data node. Currently, only supports547* returning all children.548* @return {!goog.ds.DataNodeList} List of child nodes.549* @override550*/551goog.ds.FastListNode.prototype.getChildNodes = function() {552return this;553};554555556/**557* Get a child node by name.558* @param {string} key Name of child node.559* @param {boolean=} opt_create Whether to create the child if it does not560* exist.561* @return {goog.ds.DataNode} Child node.562* @override563*/564goog.ds.FastListNode.prototype.getChildNode = function(key, opt_create) {565var index = this.getKeyAsNumber_(key);566if (index == null && this.map_) {567index = this.map_[key];568}569if (index != null && this.values_[index]) {570return this.values_[index];571} else if (opt_create) {572this.setChildNode(key, {});573return this.getChildNode(key);574} else {575return null;576}577};578579580/**581* Returns the value of a child node.582* @param {string} key Name of child node.583* @return {*} Value of child node.584* @override585*/586goog.ds.FastListNode.prototype.getChildNodeValue = function(key) {587var child = this.getChildNode(key);588return (child ? child.get() : null);589};590591592/**593* Tries to interpret key as a numeric index enclosed by square brakcets.594* @param {string} key Key that should be interpreted as a number.595* @return {?number} Numeric index or null if key is not of the form596* described above.597* @private598*/599goog.ds.FastListNode.prototype.getKeyAsNumber_ = function(key) {600if (key.charAt(0) == '[' && key.charAt(key.length - 1) == ']') {601return Number(key.substring(1, key.length - 1));602} else {603return null;604}605};606607608/**609* Sets a child node. Creates the child if it does not exist. To set610* children at a certain index, use a key of the form '[index]'. Note, that611* you can only set values at existing numeric indices. To add a new node612* to this list, you have to use the add method.613*614* Calling this function makes any child nodes previously obtained for name615* invalid. You should not use these child nodes but instead obtain a new616* instance by calling getChildNode.617*618* @override619*/620goog.ds.FastListNode.prototype.setChildNode = function(key, value) {621var count = this.values_.length;622if (value != null) {623if (!value.getDataName) {624value = goog.ds.FastDataNode.fromJs(value, key, this);625}626var index = this.getKeyAsNumber_(key);627if (index != null) {628if (index < 0 || index >= this.values_.length) {629throw Error('List index out of bounds: ' + index);630}631// NOTE: This code here appears to want to use "index" rather than632// "key" here (which would be better for an array. However, changing633// that would require knowing that there wasn't a mix of non-number634// keys, as using index that would risk overwriting those values if635// they were set first. Instead we loosen the type so we can use636// strings as indexes.637638/** @type {!Object} */639var values = this.values_;640641values[key] = value;642} else {643if (!this.map_) {644this.map_ = {};645}646this.values_.push(value);647this.map_[key] = this.values_.length - 1;648}649} else {650this.removeNode(key);651}652var dm = goog.ds.DataManager.getInstance();653dm.fireDataChange(this.getDataPath() + goog.ds.STR_PATH_SEPARATOR + key);654if (this.values_.length != count) {655this.listSizeChanged_();656}657return null;658};659660661/**662* Fire data changes that are appropriate when the size of this list changes.663* Should be called whenever the list size has changed.664* @private665*/666goog.ds.FastListNode.prototype.listSizeChanged_ = function() {667var dm = goog.ds.DataManager.getInstance();668dm.fireDataChange(this.getDataPath());669dm.fireDataChange(670this.getDataPath() + goog.ds.STR_PATH_SEPARATOR + 'count()');671};672673674/**675* Returns whether this data node is a list. Always returns true.676* @return {boolean} Whether this data node is array-like.677* @override678*/679goog.ds.FastListNode.prototype.isList = function() {680return true;681};682683684/**685* Returns a javascript object representation of this data node. You should686* not modify the object returned by this function.687* @return {!Object} Javascript object representation of this data node.688*/689goog.ds.FastListNode.prototype.getJsObject = function() {690var result = [];691for (var i = 0; i < this.values_.length; ++i) {692result.push(this.values_[i].getJsObject());693}694return result;695};696697698/*699* Implementation of goog.ds.DataNodeList for FastListNode.700*/701702703/**704* Adds a child to this data node705* @param {goog.ds.DataNode} value Child node to add.706* @override707*/708goog.ds.FastListNode.prototype.add = function(value) {709if (!value.getDataName) {710value = goog.ds.FastDataNode.fromJs(711value, String('[' + (this.values_.length) + ']'), this);712}713this.values_.push(value);714var dm = goog.ds.DataManager.getInstance();715dm.fireDataChange(716this.getDataPath() + goog.ds.STR_PATH_SEPARATOR + '[' +717(this.values_.length - 1) + ']');718this.listSizeChanged_();719};720721722/**723* Gets the value of this data node (if called without opt_key) or724* gets a child node (if called with opt_key).725* @param {string=} opt_key Name of child node.726* @return {Array|goog.ds.DataNode} Array of child nodes (if called without727* opt_key), or a named child node otherwise.728* @override729*/730goog.ds.FastListNode.prototype.get = function(opt_key) {731// if there are no arguments, DataNode.get was called732if (!goog.isDef(opt_key)) {733return this.values_;734} else {735return this.getChildNode(opt_key);736}737};738739740/**741* Gets a child node by (numeric) index.742* @param {number} index Index of child node (starting from 0).743* @return {goog.ds.DataNode} Child node at specified index.744* @override745*/746goog.ds.FastListNode.prototype.getByIndex = function(index) {747var child = this.values_[index];748return (child != null ? child : null); // never return undefined749};750751752/**753* Gets the number of child nodes.754* @return {number} Number of child nodes.755* @override756*/757goog.ds.FastListNode.prototype.getCount = function() {758return this.values_.length;759};760761762/**763* Sets a child node.764* @param {string} name Name of child node.765* @param {Object} value Value of child node.766* @override767*/768goog.ds.FastListNode.prototype.setNode = function(name, value) {769throw Error('Setting child nodes of a FastListNode is not implemented, yet');770};771772773/**774* Removes a child node.775* @override776*/777goog.ds.FastListNode.prototype.removeNode = function(name) {778var index = this.getKeyAsNumber_(name);779if (index == null && this.map_) {780index = this.map_[name];781}782if (index != null) {783this.values_.splice(index, 1);784if (this.map_) {785var keyToDelete = null;786for (var key in this.map_) {787if (this.map_[key] == index) {788keyToDelete = key;789} else if (this.map_[key] > index) {790--this.map_[key];791}792}793if (keyToDelete) {794delete this.map_[keyToDelete];795}796}797var dm = goog.ds.DataManager.getInstance();798dm.fireDataChange(799this.getDataPath() + goog.ds.STR_PATH_SEPARATOR + '[' + index + ']');800this.listSizeChanged_();801}802return false;803};804805806/**807* Returns the index of a named child nodes. This method only works if808* this list uses mixed name/indexed lookup, i.e. if its child node have809* an 'id' attribute.810* @param {string} name Name of child node to determine index of.811* @return {number} Index of child node named name.812*/813goog.ds.FastListNode.prototype.indexOf = function(name) {814var index = this.getKeyAsNumber_(name);815if (index == null && this.map_) {816index = this.map_[name];817}818if (index == null) {819throw Error('Cannot determine index for: ' + name);820}821return /** @type {number} */ (index);822};823824825