datamanager.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. // Copyright 2006 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview
  16. * Central class for registering and accessing data sources
  17. * Also handles processing of data events.
  18. *
  19. * There is a shared global instance that most client code should access via
  20. * goog.ds.DataManager.getInstance(). However you can also create your own
  21. * DataManager using new
  22. *
  23. * Implements DataNode to provide the top element in a data registry
  24. * Prepends '$' to top level data names in path to denote they are root object
  25. *
  26. */
  27. goog.provide('goog.ds.DataManager');
  28. goog.require('goog.ds.BasicNodeList');
  29. goog.require('goog.ds.DataNode');
  30. goog.require('goog.ds.Expr');
  31. goog.require('goog.object');
  32. goog.require('goog.string');
  33. goog.require('goog.structs');
  34. goog.require('goog.structs.Map');
  35. /**
  36. * Create a DataManger
  37. * @extends {goog.ds.DataNode}
  38. * @constructor
  39. * @final
  40. */
  41. goog.ds.DataManager = function() {
  42. this.dataSources_ = new goog.ds.BasicNodeList();
  43. this.autoloads_ = new goog.structs.Map();
  44. this.listenerMap_ = {};
  45. this.listenersByFunction_ = {};
  46. this.aliases_ = {};
  47. this.eventCount_ = 0;
  48. this.indexedListenersByFunction_ = {};
  49. };
  50. /**
  51. * Global instance
  52. * @private
  53. */
  54. goog.ds.DataManager.instance_ = null;
  55. goog.inherits(goog.ds.DataManager, goog.ds.DataNode);
  56. /**
  57. * Get the global instance
  58. * @return {!goog.ds.DataManager} The data manager singleton.
  59. */
  60. goog.ds.DataManager.getInstance = function() {
  61. if (!goog.ds.DataManager.instance_) {
  62. goog.ds.DataManager.instance_ = new goog.ds.DataManager();
  63. }
  64. return goog.ds.DataManager.instance_;
  65. };
  66. /**
  67. * Clears the global instance (for unit tests to reset state).
  68. */
  69. goog.ds.DataManager.clearInstance = function() {
  70. goog.ds.DataManager.instance_ = null;
  71. };
  72. /**
  73. * Add a data source
  74. * @param {goog.ds.DataNode} ds The data source.
  75. * @param {boolean=} opt_autoload Whether to automatically load the data,
  76. * defaults to false.
  77. * @param {string=} opt_name Optional name, can also get name
  78. * from the datasource.
  79. */
  80. goog.ds.DataManager.prototype.addDataSource = function(
  81. ds, opt_autoload, opt_name) {
  82. var autoload = !!opt_autoload;
  83. var name = opt_name || ds.getDataName();
  84. if (!goog.string.startsWith(name, '$')) {
  85. name = '$' + name;
  86. }
  87. ds.setDataName(name);
  88. this.dataSources_.add(ds);
  89. this.autoloads_.set(name, autoload);
  90. };
  91. /**
  92. * Create an alias for a data path, very similar to assigning a variable.
  93. * For example, you can set $CurrentContact -> $Request/Contacts[5], and all
  94. * references to $CurrentContact will be procesed on $Request/Contacts[5].
  95. *
  96. * Aliases will hide datasources of the same name.
  97. *
  98. * @param {string} name Alias name, must be a top level path ($Foo).
  99. * @param {string} dataPath Data path being aliased.
  100. */
  101. goog.ds.DataManager.prototype.aliasDataSource = function(name, dataPath) {
  102. if (!this.aliasListener_) {
  103. this.aliasListener_ = goog.bind(this.listenForAlias_, this);
  104. }
  105. if (this.aliases_[name]) {
  106. var oldPath = this.aliases_[name].getSource();
  107. this.removeListeners(this.aliasListener_, oldPath + '/...', name);
  108. }
  109. this.aliases_[name] = goog.ds.Expr.create(dataPath);
  110. this.addListener(this.aliasListener_, dataPath + '/...', name);
  111. this.fireDataChange(name);
  112. };
  113. /**
  114. * Listener function for matches of paths that have been aliased.
  115. * Fires a data change on the alias as well.
  116. *
  117. * @param {string} dataPath Path of data event fired.
  118. * @param {string} name Name of the alias.
  119. * @private
  120. */
  121. goog.ds.DataManager.prototype.listenForAlias_ = function(dataPath, name) {
  122. var aliasedExpr = this.aliases_[name];
  123. if (aliasedExpr) {
  124. // If it's a subpath, appends the subpath to the alias name
  125. // otherwise just fires on the top level alias
  126. var aliasedPath = aliasedExpr.getSource();
  127. if (dataPath.indexOf(aliasedPath) == 0) {
  128. this.fireDataChange(name + dataPath.substring(aliasedPath.length));
  129. } else {
  130. this.fireDataChange(name);
  131. }
  132. }
  133. };
  134. /**
  135. * Gets a named child node of the current node.
  136. *
  137. * @param {string} name The node name.
  138. * @return {goog.ds.DataNode} The child node,
  139. * or null if no node of this name exists.
  140. */
  141. goog.ds.DataManager.prototype.getDataSource = function(name) {
  142. if (this.aliases_[name]) {
  143. return this.aliases_[name].getNode();
  144. } else {
  145. return this.dataSources_.get(name);
  146. }
  147. };
  148. /**
  149. * Get the value of the node
  150. * @return {!Object} The value of the node.
  151. * @override
  152. */
  153. goog.ds.DataManager.prototype.get = function() {
  154. return this.dataSources_;
  155. };
  156. /** @override */
  157. goog.ds.DataManager.prototype.set = function(value) {
  158. throw Error('Can\'t set on DataManager');
  159. };
  160. /** @override */
  161. goog.ds.DataManager.prototype.getChildNodes = function(opt_selector) {
  162. if (opt_selector) {
  163. return new goog.ds.BasicNodeList(
  164. [this.getChildNode(/** @type {string} */ (opt_selector))]);
  165. } else {
  166. return this.dataSources_;
  167. }
  168. };
  169. /**
  170. * Gets a named child node of the current node
  171. * @param {string} name The node name.
  172. * @return {goog.ds.DataNode} The child node,
  173. * or null if no node of this name exists.
  174. * @override
  175. */
  176. goog.ds.DataManager.prototype.getChildNode = function(name) {
  177. return this.getDataSource(name);
  178. };
  179. /** @override */
  180. goog.ds.DataManager.prototype.getChildNodeValue = function(name) {
  181. var ds = this.getDataSource(name);
  182. return ds ? ds.get() : null;
  183. };
  184. /**
  185. * Get the name of the node relative to the parent node
  186. * @return {string} The name of the node.
  187. * @override
  188. */
  189. goog.ds.DataManager.prototype.getDataName = function() {
  190. return '';
  191. };
  192. /**
  193. * Gets the a qualified data path to this node
  194. * @return {string} The data path.
  195. * @override
  196. */
  197. goog.ds.DataManager.prototype.getDataPath = function() {
  198. return '';
  199. };
  200. /**
  201. * Load or reload the backing data for this node
  202. * only loads datasources flagged with autoload
  203. * @override
  204. */
  205. goog.ds.DataManager.prototype.load = function() {
  206. var len = this.dataSources_.getCount();
  207. for (var i = 0; i < len; i++) {
  208. var ds = this.dataSources_.getByIndex(i);
  209. var autoload = this.autoloads_.get(ds.getDataName());
  210. if (autoload) {
  211. ds.load();
  212. }
  213. }
  214. };
  215. /**
  216. * Gets the state of the backing data for this node
  217. * @return {goog.ds.LoadState} The state.
  218. * @override
  219. */
  220. goog.ds.DataManager.prototype.getLoadState = goog.abstractMethod;
  221. /**
  222. * Whether the value of this node is a homogeneous list of data
  223. * @return {boolean} True if a list.
  224. * @override
  225. */
  226. goog.ds.DataManager.prototype.isList = function() {
  227. return false;
  228. };
  229. /**
  230. * Get the total count of events fired (mostly for debugging)
  231. * @return {number} Count of events.
  232. */
  233. goog.ds.DataManager.prototype.getEventCount = function() {
  234. return this.eventCount_;
  235. };
  236. /**
  237. * Adds a listener
  238. * Listeners should fire when any data with path that has dataPath as substring
  239. * is changed.
  240. * TODO(user) Look into better listener handling
  241. *
  242. * @param {Function} fn Callback function, signature function(dataPath, id).
  243. * @param {string} dataPath Fully qualified data path.
  244. * @param {string=} opt_id A value passed back to the listener when the dataPath
  245. * is matched.
  246. */
  247. goog.ds.DataManager.prototype.addListener = function(fn, dataPath, opt_id) {
  248. // maxAncestor sets how distant an ancestor you can be of the fired event
  249. // and still fire (you always fire if you are a descendant).
  250. // 0 means you don't fire if you are an ancestor
  251. // 1 means you only fire if you are parent
  252. // 1000 means you will fire if you are ancestor (effectively infinite)
  253. var maxAncestors = 0;
  254. if (goog.string.endsWith(dataPath, '/...')) {
  255. maxAncestors = 1000;
  256. dataPath = dataPath.substring(0, dataPath.length - 4);
  257. } else if (goog.string.endsWith(dataPath, '/*')) {
  258. maxAncestors = 1;
  259. dataPath = dataPath.substring(0, dataPath.length - 2);
  260. }
  261. opt_id = opt_id || '';
  262. var key = dataPath + ':' + opt_id + ':' + goog.getUid(fn);
  263. var listener = {dataPath: dataPath, id: opt_id, fn: fn};
  264. var expr = goog.ds.Expr.create(dataPath);
  265. var fnUid = goog.getUid(fn);
  266. if (!this.listenersByFunction_[fnUid]) {
  267. this.listenersByFunction_[fnUid] = {};
  268. }
  269. this.listenersByFunction_[fnUid][key] = {listener: listener, items: []};
  270. while (expr) {
  271. var listenerSpec = {listener: listener, maxAncestors: maxAncestors};
  272. var matchingListeners = this.listenerMap_[expr.getSource()];
  273. if (matchingListeners == null) {
  274. matchingListeners = {};
  275. this.listenerMap_[expr.getSource()] = matchingListeners;
  276. }
  277. matchingListeners[key] = listenerSpec;
  278. maxAncestors = 0;
  279. expr = expr.getParent();
  280. this.listenersByFunction_[fnUid][key].items.push(
  281. {key: key, obj: matchingListeners});
  282. }
  283. };
  284. /**
  285. * Adds an indexed listener.
  286. *
  287. * Indexed listeners allow for '*' in data paths. If a * exists, will match
  288. * all values and return the matched values in an array to the callback.
  289. *
  290. * Currently uses a promiscuous match algorithm: Matches everything before the
  291. * first '*', and then does a regex match for all of the returned events.
  292. * Although this isn't optimized, it is still an improvement as you can collapse
  293. * 100's of listeners into a single regex match
  294. *
  295. * @param {Function} fn Callback function, signature (dataPath, id, indexes).
  296. * @param {string} dataPath Fully qualified data path.
  297. * @param {string=} opt_id A value passed back to the listener when the dataPath
  298. * is matched.
  299. */
  300. goog.ds.DataManager.prototype.addIndexedListener = function(
  301. fn, dataPath, opt_id) {
  302. var firstStarPos = dataPath.indexOf('*');
  303. // Just need a regular listener
  304. if (firstStarPos == -1) {
  305. this.addListener(fn, dataPath, opt_id);
  306. return;
  307. }
  308. var listenPath = dataPath.substring(0, firstStarPos) + '...';
  309. // Create regex that matches * to any non '\' character
  310. var ext = '$';
  311. if (goog.string.endsWith(dataPath, '/...')) {
  312. dataPath = dataPath.substring(0, dataPath.length - 4);
  313. ext = '';
  314. }
  315. var regExpPath = goog.string.regExpEscape(dataPath);
  316. var matchRegExp = regExpPath.replace(/\\\*/g, '([^\\\/]+)') + ext;
  317. // Matcher function applies the regex and calls back the original function
  318. // if the regex matches, passing in an array of the matched values
  319. var matchRegExpRe = new RegExp(matchRegExp);
  320. var matcher = function(path, id) {
  321. var match = matchRegExpRe.exec(path);
  322. if (match) {
  323. match.shift();
  324. fn(path, opt_id, match);
  325. }
  326. };
  327. this.addListener(matcher, listenPath, opt_id);
  328. // Add the indexed listener to the map so that we can remove it later.
  329. var fnUid = goog.getUid(fn);
  330. if (!this.indexedListenersByFunction_[fnUid]) {
  331. this.indexedListenersByFunction_[fnUid] = {};
  332. }
  333. var key = dataPath + ':' + opt_id;
  334. this.indexedListenersByFunction_[fnUid][key] = {
  335. listener: {dataPath: listenPath, fn: matcher, id: opt_id}
  336. };
  337. };
  338. /**
  339. * Removes indexed listeners with a given callback function, and optional
  340. * matching datapath and matching id.
  341. *
  342. * @param {Function} fn Callback function, signature function(dataPath, id).
  343. * @param {string=} opt_dataPath Fully qualified data path.
  344. * @param {string=} opt_id A value passed back to the listener when the dataPath
  345. * is matched.
  346. */
  347. goog.ds.DataManager.prototype.removeIndexedListeners = function(
  348. fn, opt_dataPath, opt_id) {
  349. this.removeListenersByFunction_(
  350. this.indexedListenersByFunction_, true, fn, opt_dataPath, opt_id);
  351. };
  352. /**
  353. * Removes listeners with a given callback function, and optional
  354. * matching dataPath and matching id
  355. *
  356. * @param {Function} fn Callback function, signature function(dataPath, id).
  357. * @param {string=} opt_dataPath Fully qualified data path.
  358. * @param {string=} opt_id A value passed back to the listener when the dataPath
  359. * is matched.
  360. */
  361. goog.ds.DataManager.prototype.removeListeners = function(
  362. fn, opt_dataPath, opt_id) {
  363. // Normalize data path root
  364. if (opt_dataPath && goog.string.endsWith(opt_dataPath, '/...')) {
  365. opt_dataPath = opt_dataPath.substring(0, opt_dataPath.length - 4);
  366. } else if (opt_dataPath && goog.string.endsWith(opt_dataPath, '/*')) {
  367. opt_dataPath = opt_dataPath.substring(0, opt_dataPath.length - 2);
  368. }
  369. this.removeListenersByFunction_(
  370. this.listenersByFunction_, false, fn, opt_dataPath, opt_id);
  371. };
  372. /**
  373. * Removes listeners with a given callback function, and optional
  374. * matching dataPath and matching id from the given listenersByFunction
  375. * data structure.
  376. *
  377. * @param {Object} listenersByFunction The listeners by function.
  378. * @param {boolean} indexed Indicates whether the listenersByFunction are
  379. * indexed or not.
  380. * @param {Function} fn Callback function, signature function(dataPath, id).
  381. * @param {string=} opt_dataPath Fully qualified data path.
  382. * @param {string=} opt_id A value passed back to the listener when the dataPath
  383. * is matched.
  384. * @private
  385. */
  386. goog.ds.DataManager.prototype.removeListenersByFunction_ = function(
  387. listenersByFunction, indexed, fn, opt_dataPath, opt_id) {
  388. var fnUid = goog.getUid(fn);
  389. var functionMatches = listenersByFunction[fnUid];
  390. if (functionMatches != null) {
  391. for (var key in functionMatches) {
  392. var functionMatch = functionMatches[key];
  393. var listener = functionMatch.listener;
  394. if ((!opt_dataPath || opt_dataPath == listener.dataPath) &&
  395. (!opt_id || opt_id == listener.id)) {
  396. if (indexed) {
  397. this.removeListeners(listener.fn, listener.dataPath, listener.id);
  398. }
  399. if (functionMatch.items) {
  400. for (var i = 0; i < functionMatch.items.length; i++) {
  401. var item = functionMatch.items[i];
  402. delete item.obj[item.key];
  403. }
  404. }
  405. delete functionMatches[key];
  406. }
  407. }
  408. }
  409. };
  410. /**
  411. * Get the total number of listeners (per expression listened to, so may be
  412. * more than number of times addListener() has been called
  413. * @return {number} Number of listeners.
  414. */
  415. goog.ds.DataManager.prototype.getListenerCount = function() {
  416. var /** number */ count = 0;
  417. goog.object.forEach(this.listenerMap_, function(matchingListeners) {
  418. count += goog.structs.getCount(matchingListeners);
  419. });
  420. return count;
  421. };
  422. /**
  423. * Disables the sending of all data events during the execution of the given
  424. * callback. This provides a way to avoid useless notifications of small changes
  425. * when you will eventually send a data event manually that encompasses them
  426. * all.
  427. *
  428. * Note that this function can not be called reentrantly.
  429. *
  430. * @param {Function} callback Zero-arg function to execute.
  431. */
  432. goog.ds.DataManager.prototype.runWithoutFiringDataChanges = function(callback) {
  433. if (this.disableFiring_) {
  434. throw Error('Can not nest calls to runWithoutFiringDataChanges');
  435. }
  436. this.disableFiring_ = true;
  437. try {
  438. callback();
  439. } finally {
  440. this.disableFiring_ = false;
  441. }
  442. };
  443. /**
  444. * Fire a data change event to all listeners
  445. *
  446. * If the path matches the path of a listener, the listener will fire
  447. *
  448. * If your path is the parent of a listener, the listener will fire. I.e.
  449. * if $Contacts/bob@bob.com changes, then we will fire listener for
  450. * $Contacts/bob@bob.com/Name as well, as the assumption is that when
  451. * a parent changes, all children are invalidated.
  452. *
  453. * If your path is the child of a listener, the listener may fire, depending
  454. * on the ancestor depth.
  455. *
  456. * A listener for $Contacts might only be interested if the contact name changes
  457. * (i.e. $Contacts doesn't fire on $Contacts/bob@bob.com/Name),
  458. * while a listener for a specific contact might
  459. * (i.e. $Contacts/bob@bob.com would fire on $Contacts/bob@bob.com/Name).
  460. * Adding "/..." to a lisetener path listens to all children, and adding "/*" to
  461. * a listener path listens only to direct children
  462. *
  463. * @param {string} dataPath Fully qualified data path.
  464. */
  465. goog.ds.DataManager.prototype.fireDataChange = function(dataPath) {
  466. if (this.disableFiring_) {
  467. return;
  468. }
  469. var expr = goog.ds.Expr.create(dataPath);
  470. var ancestorDepth = 0;
  471. // Look for listeners for expression and all its parents.
  472. // Parents of listener expressions are all added to the listenerMap as well,
  473. // so this will evaluate inner loop every time the dataPath is a child or
  474. // an ancestor of the original listener path
  475. while (expr) {
  476. var matchingListeners = this.listenerMap_[expr.getSource()];
  477. if (matchingListeners) {
  478. for (var id in matchingListeners) {
  479. var match = matchingListeners[id];
  480. var listener = match.listener;
  481. if (ancestorDepth <= match.maxAncestors) {
  482. listener.fn(dataPath, listener.id);
  483. }
  484. }
  485. }
  486. ancestorDepth++;
  487. expr = expr.getParent();
  488. }
  489. this.eventCount_++;
  490. };