Path: blob/trunk/third_party/closure/goog/datasource/datamanager.js
2868 views
// Copyright 2006 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* Central class for registering and accessing data sources17* Also handles processing of data events.18*19* There is a shared global instance that most client code should access via20* goog.ds.DataManager.getInstance(). However you can also create your own21* DataManager using new22*23* Implements DataNode to provide the top element in a data registry24* Prepends '$' to top level data names in path to denote they are root object25*26*/27goog.provide('goog.ds.DataManager');2829goog.require('goog.ds.BasicNodeList');30goog.require('goog.ds.DataNode');31goog.require('goog.ds.Expr');32goog.require('goog.object');33goog.require('goog.string');34goog.require('goog.structs');35goog.require('goog.structs.Map');36373839/**40* Create a DataManger41* @extends {goog.ds.DataNode}42* @constructor43* @final44*/45goog.ds.DataManager = function() {46this.dataSources_ = new goog.ds.BasicNodeList();47this.autoloads_ = new goog.structs.Map();48this.listenerMap_ = {};49this.listenersByFunction_ = {};50this.aliases_ = {};51this.eventCount_ = 0;52this.indexedListenersByFunction_ = {};53};545556/**57* Global instance58* @private59*/60goog.ds.DataManager.instance_ = null;61goog.inherits(goog.ds.DataManager, goog.ds.DataNode);626364/**65* Get the global instance66* @return {!goog.ds.DataManager} The data manager singleton.67*/68goog.ds.DataManager.getInstance = function() {69if (!goog.ds.DataManager.instance_) {70goog.ds.DataManager.instance_ = new goog.ds.DataManager();71}72return goog.ds.DataManager.instance_;73};747576/**77* Clears the global instance (for unit tests to reset state).78*/79goog.ds.DataManager.clearInstance = function() {80goog.ds.DataManager.instance_ = null;81};828384/**85* Add a data source86* @param {goog.ds.DataNode} ds The data source.87* @param {boolean=} opt_autoload Whether to automatically load the data,88* defaults to false.89* @param {string=} opt_name Optional name, can also get name90* from the datasource.91*/92goog.ds.DataManager.prototype.addDataSource = function(93ds, opt_autoload, opt_name) {94var autoload = !!opt_autoload;95var name = opt_name || ds.getDataName();96if (!goog.string.startsWith(name, '$')) {97name = '$' + name;98}99ds.setDataName(name);100this.dataSources_.add(ds);101this.autoloads_.set(name, autoload);102};103104105/**106* Create an alias for a data path, very similar to assigning a variable.107* For example, you can set $CurrentContact -> $Request/Contacts[5], and all108* references to $CurrentContact will be procesed on $Request/Contacts[5].109*110* Aliases will hide datasources of the same name.111*112* @param {string} name Alias name, must be a top level path ($Foo).113* @param {string} dataPath Data path being aliased.114*/115goog.ds.DataManager.prototype.aliasDataSource = function(name, dataPath) {116if (!this.aliasListener_) {117this.aliasListener_ = goog.bind(this.listenForAlias_, this);118}119if (this.aliases_[name]) {120var oldPath = this.aliases_[name].getSource();121this.removeListeners(this.aliasListener_, oldPath + '/...', name);122}123this.aliases_[name] = goog.ds.Expr.create(dataPath);124this.addListener(this.aliasListener_, dataPath + '/...', name);125this.fireDataChange(name);126};127128129/**130* Listener function for matches of paths that have been aliased.131* Fires a data change on the alias as well.132*133* @param {string} dataPath Path of data event fired.134* @param {string} name Name of the alias.135* @private136*/137goog.ds.DataManager.prototype.listenForAlias_ = function(dataPath, name) {138var aliasedExpr = this.aliases_[name];139140if (aliasedExpr) {141// If it's a subpath, appends the subpath to the alias name142// otherwise just fires on the top level alias143var aliasedPath = aliasedExpr.getSource();144if (dataPath.indexOf(aliasedPath) == 0) {145this.fireDataChange(name + dataPath.substring(aliasedPath.length));146} else {147this.fireDataChange(name);148}149}150};151152153/**154* Gets a named child node of the current node.155*156* @param {string} name The node name.157* @return {goog.ds.DataNode} The child node,158* or null if no node of this name exists.159*/160goog.ds.DataManager.prototype.getDataSource = function(name) {161if (this.aliases_[name]) {162return this.aliases_[name].getNode();163} else {164return this.dataSources_.get(name);165}166};167168169/**170* Get the value of the node171* @return {!Object} The value of the node.172* @override173*/174goog.ds.DataManager.prototype.get = function() {175return this.dataSources_;176};177178179/** @override */180goog.ds.DataManager.prototype.set = function(value) {181throw Error('Can\'t set on DataManager');182};183184185/** @override */186goog.ds.DataManager.prototype.getChildNodes = function(opt_selector) {187if (opt_selector) {188return new goog.ds.BasicNodeList(189[this.getChildNode(/** @type {string} */ (opt_selector))]);190} else {191return this.dataSources_;192}193};194195196/**197* Gets a named child node of the current node198* @param {string} name The node name.199* @return {goog.ds.DataNode} The child node,200* or null if no node of this name exists.201* @override202*/203goog.ds.DataManager.prototype.getChildNode = function(name) {204return this.getDataSource(name);205};206207208/** @override */209goog.ds.DataManager.prototype.getChildNodeValue = function(name) {210var ds = this.getDataSource(name);211return ds ? ds.get() : null;212};213214215/**216* Get the name of the node relative to the parent node217* @return {string} The name of the node.218* @override219*/220goog.ds.DataManager.prototype.getDataName = function() {221return '';222};223224225/**226* Gets the a qualified data path to this node227* @return {string} The data path.228* @override229*/230goog.ds.DataManager.prototype.getDataPath = function() {231return '';232};233234235/**236* Load or reload the backing data for this node237* only loads datasources flagged with autoload238* @override239*/240goog.ds.DataManager.prototype.load = function() {241var len = this.dataSources_.getCount();242for (var i = 0; i < len; i++) {243var ds = this.dataSources_.getByIndex(i);244var autoload = this.autoloads_.get(ds.getDataName());245if (autoload) {246ds.load();247}248}249};250251252/**253* Gets the state of the backing data for this node254* @return {goog.ds.LoadState} The state.255* @override256*/257goog.ds.DataManager.prototype.getLoadState = goog.abstractMethod;258259260/**261* Whether the value of this node is a homogeneous list of data262* @return {boolean} True if a list.263* @override264*/265goog.ds.DataManager.prototype.isList = function() {266return false;267};268269270/**271* Get the total count of events fired (mostly for debugging)272* @return {number} Count of events.273*/274goog.ds.DataManager.prototype.getEventCount = function() {275return this.eventCount_;276};277278279/**280* Adds a listener281* Listeners should fire when any data with path that has dataPath as substring282* is changed.283* TODO(user) Look into better listener handling284*285* @param {Function} fn Callback function, signature function(dataPath, id).286* @param {string} dataPath Fully qualified data path.287* @param {string=} opt_id A value passed back to the listener when the dataPath288* is matched.289*/290goog.ds.DataManager.prototype.addListener = function(fn, dataPath, opt_id) {291// maxAncestor sets how distant an ancestor you can be of the fired event292// and still fire (you always fire if you are a descendant).293// 0 means you don't fire if you are an ancestor294// 1 means you only fire if you are parent295// 1000 means you will fire if you are ancestor (effectively infinite)296var maxAncestors = 0;297if (goog.string.endsWith(dataPath, '/...')) {298maxAncestors = 1000;299dataPath = dataPath.substring(0, dataPath.length - 4);300} else if (goog.string.endsWith(dataPath, '/*')) {301maxAncestors = 1;302dataPath = dataPath.substring(0, dataPath.length - 2);303}304305opt_id = opt_id || '';306var key = dataPath + ':' + opt_id + ':' + goog.getUid(fn);307var listener = {dataPath: dataPath, id: opt_id, fn: fn};308var expr = goog.ds.Expr.create(dataPath);309310var fnUid = goog.getUid(fn);311if (!this.listenersByFunction_[fnUid]) {312this.listenersByFunction_[fnUid] = {};313}314this.listenersByFunction_[fnUid][key] = {listener: listener, items: []};315316while (expr) {317var listenerSpec = {listener: listener, maxAncestors: maxAncestors};318var matchingListeners = this.listenerMap_[expr.getSource()];319if (matchingListeners == null) {320matchingListeners = {};321this.listenerMap_[expr.getSource()] = matchingListeners;322}323matchingListeners[key] = listenerSpec;324maxAncestors = 0;325expr = expr.getParent();326this.listenersByFunction_[fnUid][key].items.push(327{key: key, obj: matchingListeners});328}329};330331332/**333* Adds an indexed listener.334*335* Indexed listeners allow for '*' in data paths. If a * exists, will match336* all values and return the matched values in an array to the callback.337*338* Currently uses a promiscuous match algorithm: Matches everything before the339* first '*', and then does a regex match for all of the returned events.340* Although this isn't optimized, it is still an improvement as you can collapse341* 100's of listeners into a single regex match342*343* @param {Function} fn Callback function, signature (dataPath, id, indexes).344* @param {string} dataPath Fully qualified data path.345* @param {string=} opt_id A value passed back to the listener when the dataPath346* is matched.347*/348goog.ds.DataManager.prototype.addIndexedListener = function(349fn, dataPath, opt_id) {350var firstStarPos = dataPath.indexOf('*');351// Just need a regular listener352if (firstStarPos == -1) {353this.addListener(fn, dataPath, opt_id);354return;355}356357var listenPath = dataPath.substring(0, firstStarPos) + '...';358359// Create regex that matches * to any non '\' character360var ext = '$';361if (goog.string.endsWith(dataPath, '/...')) {362dataPath = dataPath.substring(0, dataPath.length - 4);363ext = '';364}365var regExpPath = goog.string.regExpEscape(dataPath);366var matchRegExp = regExpPath.replace(/\\\*/g, '([^\\\/]+)') + ext;367368// Matcher function applies the regex and calls back the original function369// if the regex matches, passing in an array of the matched values370var matchRegExpRe = new RegExp(matchRegExp);371var matcher = function(path, id) {372var match = matchRegExpRe.exec(path);373if (match) {374match.shift();375fn(path, opt_id, match);376}377};378this.addListener(matcher, listenPath, opt_id);379380// Add the indexed listener to the map so that we can remove it later.381var fnUid = goog.getUid(fn);382if (!this.indexedListenersByFunction_[fnUid]) {383this.indexedListenersByFunction_[fnUid] = {};384}385var key = dataPath + ':' + opt_id;386this.indexedListenersByFunction_[fnUid][key] = {387listener: {dataPath: listenPath, fn: matcher, id: opt_id}388};389};390391392/**393* Removes indexed listeners with a given callback function, and optional394* matching datapath and matching id.395*396* @param {Function} fn Callback function, signature function(dataPath, id).397* @param {string=} opt_dataPath Fully qualified data path.398* @param {string=} opt_id A value passed back to the listener when the dataPath399* is matched.400*/401goog.ds.DataManager.prototype.removeIndexedListeners = function(402fn, opt_dataPath, opt_id) {403this.removeListenersByFunction_(404this.indexedListenersByFunction_, true, fn, opt_dataPath, opt_id);405};406407408/**409* Removes listeners with a given callback function, and optional410* matching dataPath and matching id411*412* @param {Function} fn Callback function, signature function(dataPath, id).413* @param {string=} opt_dataPath Fully qualified data path.414* @param {string=} opt_id A value passed back to the listener when the dataPath415* is matched.416*/417goog.ds.DataManager.prototype.removeListeners = function(418fn, opt_dataPath, opt_id) {419420// Normalize data path root421if (opt_dataPath && goog.string.endsWith(opt_dataPath, '/...')) {422opt_dataPath = opt_dataPath.substring(0, opt_dataPath.length - 4);423} else if (opt_dataPath && goog.string.endsWith(opt_dataPath, '/*')) {424opt_dataPath = opt_dataPath.substring(0, opt_dataPath.length - 2);425}426427this.removeListenersByFunction_(428this.listenersByFunction_, false, fn, opt_dataPath, opt_id);429};430431432/**433* Removes listeners with a given callback function, and optional434* matching dataPath and matching id from the given listenersByFunction435* data structure.436*437* @param {Object} listenersByFunction The listeners by function.438* @param {boolean} indexed Indicates whether the listenersByFunction are439* indexed or not.440* @param {Function} fn Callback function, signature function(dataPath, id).441* @param {string=} opt_dataPath Fully qualified data path.442* @param {string=} opt_id A value passed back to the listener when the dataPath443* is matched.444* @private445*/446goog.ds.DataManager.prototype.removeListenersByFunction_ = function(447listenersByFunction, indexed, fn, opt_dataPath, opt_id) {448var fnUid = goog.getUid(fn);449var functionMatches = listenersByFunction[fnUid];450if (functionMatches != null) {451for (var key in functionMatches) {452var functionMatch = functionMatches[key];453var listener = functionMatch.listener;454if ((!opt_dataPath || opt_dataPath == listener.dataPath) &&455(!opt_id || opt_id == listener.id)) {456if (indexed) {457this.removeListeners(listener.fn, listener.dataPath, listener.id);458}459if (functionMatch.items) {460for (var i = 0; i < functionMatch.items.length; i++) {461var item = functionMatch.items[i];462delete item.obj[item.key];463}464}465delete functionMatches[key];466}467}468}469};470471472/**473* Get the total number of listeners (per expression listened to, so may be474* more than number of times addListener() has been called475* @return {number} Number of listeners.476*/477goog.ds.DataManager.prototype.getListenerCount = function() {478var /** number */ count = 0;479goog.object.forEach(this.listenerMap_, function(matchingListeners) {480count += goog.structs.getCount(matchingListeners);481});482return count;483};484485486/**487* Disables the sending of all data events during the execution of the given488* callback. This provides a way to avoid useless notifications of small changes489* when you will eventually send a data event manually that encompasses them490* all.491*492* Note that this function can not be called reentrantly.493*494* @param {Function} callback Zero-arg function to execute.495*/496goog.ds.DataManager.prototype.runWithoutFiringDataChanges = function(callback) {497if (this.disableFiring_) {498throw Error('Can not nest calls to runWithoutFiringDataChanges');499}500501this.disableFiring_ = true;502try {503callback();504} finally {505this.disableFiring_ = false;506}507};508509510/**511* Fire a data change event to all listeners512*513* If the path matches the path of a listener, the listener will fire514*515* If your path is the parent of a listener, the listener will fire. I.e.516* if $Contacts/[email protected] changes, then we will fire listener for517* $Contacts/[email protected]/Name as well, as the assumption is that when518* a parent changes, all children are invalidated.519*520* If your path is the child of a listener, the listener may fire, depending521* on the ancestor depth.522*523* A listener for $Contacts might only be interested if the contact name changes524* (i.e. $Contacts doesn't fire on $Contacts/[email protected]/Name),525* while a listener for a specific contact might526* (i.e. $Contacts/[email protected] would fire on $Contacts/[email protected]/Name).527* Adding "/..." to a lisetener path listens to all children, and adding "/*" to528* a listener path listens only to direct children529*530* @param {string} dataPath Fully qualified data path.531*/532goog.ds.DataManager.prototype.fireDataChange = function(dataPath) {533if (this.disableFiring_) {534return;535}536537var expr = goog.ds.Expr.create(dataPath);538var ancestorDepth = 0;539540// Look for listeners for expression and all its parents.541// Parents of listener expressions are all added to the listenerMap as well,542// so this will evaluate inner loop every time the dataPath is a child or543// an ancestor of the original listener path544while (expr) {545var matchingListeners = this.listenerMap_[expr.getSource()];546if (matchingListeners) {547for (var id in matchingListeners) {548var match = matchingListeners[id];549var listener = match.listener;550if (ancestorDepth <= match.maxAncestors) {551listener.fn(dataPath, listener.id);552}553}554}555ancestorDepth++;556expr = expr.getParent();557}558this.eventCount_++;559};560561562