OpmlStore.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. define("dojox/data/OpmlStore", ["dojo/_base/declare", "dojo/_base/lang", "dojo/_base/xhr", "dojo/data/util/simpleFetch", "dojo/data/util/filter",
  2. "dojo/_base/window"],
  3. function(declare, lang, xhr, simpleFetch, filterUtil, winUtil) {
  4. var OpmlStore = declare("dojox.data.OpmlStore", null, {
  5. /* summary:
  6. * The OpmlStore implements the dojo.data.api.Read API.
  7. */
  8. /* examples:
  9. * var opmlStore = new dojo.data.OpmlStore({url:"geography.xml"});
  10. * var opmlStore = new dojo.data.OpmlStore({url:"http://example.com/geography.xml"});
  11. */
  12. constructor: function(/* Object */ keywordParameters){
  13. // summary: constructor
  14. // keywordParameters: {url: String, label: String} Where label is optional and configures what should be used as the return from getLabel()
  15. this._xmlData = null;
  16. this._arrayOfTopLevelItems = [];
  17. this._arrayOfAllItems = [];
  18. this._metadataNodes = null;
  19. this._loadFinished = false;
  20. this.url = keywordParameters.url;
  21. this._opmlData = keywordParameters.data; // XML DOM Document
  22. if(keywordParameters.label){
  23. this.label = keywordParameters.label;
  24. }
  25. this._loadInProgress = false; //Got to track the initial load to prevent duelling loads of the dataset.
  26. this._queuedFetches = [];
  27. this._identityMap = {};
  28. this._identCount = 0;
  29. this._idProp = "_I";
  30. if(keywordParameters && "urlPreventCache" in keywordParameters){
  31. this.urlPreventCache = keywordParameters.urlPreventCache?true:false;
  32. }
  33. },
  34. // label: [public] string
  35. // The attribute of the Opml item to act as a label.
  36. label: "text",
  37. // url: [public] string
  38. // The location from which to fetch the Opml document.
  39. url: "",
  40. // urlPreventCache: [public] boolean
  41. // Flag to denote if the underlying xhrGet call should set preventCache.
  42. urlPreventCache: false,
  43. _assertIsItem: function(/* item */ item){
  44. if(!this.isItem(item)){
  45. throw new Error("dojo.data.OpmlStore: a function was passed an item argument that was not an item");
  46. }
  47. },
  48. _assertIsAttribute: function(/* item || String */ attribute){
  49. // summary:
  50. // This function tests whether the item passed in is indeed a valid 'attribute' like type for the store.
  51. // attribute:
  52. // The attribute to test for being contained by the store.
  53. if(!lang.isString(attribute)){
  54. throw new Error("dojox.data.OpmlStore: a function was passed an attribute argument that was not an attribute object nor an attribute name string");
  55. }
  56. },
  57. _removeChildNodesThatAreNotElementNodes: function(/* node */ node, /* boolean */ recursive){
  58. var childNodes = node.childNodes;
  59. if(childNodes.length === 0){
  60. return;
  61. }
  62. var nodesToRemove = [];
  63. var i, childNode;
  64. for(i = 0; i < childNodes.length; ++i){
  65. childNode = childNodes[i];
  66. if(childNode.nodeType != 1){
  67. nodesToRemove.push(childNode);
  68. }
  69. }
  70. for(i = 0; i < nodesToRemove.length; ++i){
  71. childNode = nodesToRemove[i];
  72. node.removeChild(childNode);
  73. }
  74. if(recursive){
  75. for(i = 0; i < childNodes.length; ++i){
  76. childNode = childNodes[i];
  77. this._removeChildNodesThatAreNotElementNodes(childNode, recursive);
  78. }
  79. }
  80. },
  81. _processRawXmlTree: function(/* xmlDoc */ rawXmlTree){
  82. this._loadFinished = true;
  83. this._xmlData = rawXmlTree;
  84. var headNodes = rawXmlTree.getElementsByTagName('head');
  85. var headNode = headNodes[0];
  86. if(headNode){
  87. this._removeChildNodesThatAreNotElementNodes(headNode);
  88. this._metadataNodes = headNode.childNodes;
  89. }
  90. var bodyNodes = rawXmlTree.getElementsByTagName('body');
  91. var bodyNode = bodyNodes[0];
  92. if(bodyNode){
  93. this._removeChildNodesThatAreNotElementNodes(bodyNode, true);
  94. var bodyChildNodes = bodyNodes[0].childNodes;
  95. for(var i = 0; i < bodyChildNodes.length; ++i){
  96. var node = bodyChildNodes[i];
  97. if(node.tagName == 'outline'){
  98. this._identityMap[this._identCount] = node;
  99. this._identCount++;
  100. this._arrayOfTopLevelItems.push(node);
  101. this._arrayOfAllItems.push(node);
  102. this._checkChildNodes(node);
  103. }
  104. }
  105. }
  106. },
  107. _checkChildNodes: function(node /*Node*/){
  108. // summary:
  109. // Internal function to recurse over all child nodes from the store and add them
  110. // As non-toplevel items
  111. // description:
  112. // Internal function to recurse over all child nodes from the store and add them
  113. // As non-toplevel items
  114. //
  115. // node:
  116. // The child node to walk.
  117. if(node.firstChild){
  118. for(var i = 0; i < node.childNodes.length; i++){
  119. var child = node.childNodes[i];
  120. if(child.tagName == 'outline'){
  121. this._identityMap[this._identCount] = child;
  122. this._identCount++;
  123. this._arrayOfAllItems.push(child);
  124. this._checkChildNodes(child);
  125. }
  126. }
  127. }
  128. },
  129. _getItemsArray: function(/*object?*/queryOptions){
  130. // summary:
  131. // Internal function to determine which list of items to search over.
  132. // queryOptions: The query options parameter, if any.
  133. if(queryOptions && queryOptions.deep){
  134. return this._arrayOfAllItems;
  135. }
  136. return this._arrayOfTopLevelItems;
  137. },
  138. /***************************************
  139. dojo.data.api.Read API
  140. ***************************************/
  141. getValue: function( /* item */ item,
  142. /* attribute || attribute-name-string */ attribute,
  143. /* value? */ defaultValue){
  144. // summary:
  145. // See dojo.data.api.Read.getValue()
  146. this._assertIsItem(item);
  147. this._assertIsAttribute(attribute);
  148. if(attribute == 'children'){
  149. return (item.firstChild || defaultValue); //Object
  150. }else{
  151. var value = item.getAttribute(attribute);
  152. return (value !== undefined) ? value : defaultValue; //Object
  153. }
  154. },
  155. getValues: function(/* item */ item,
  156. /* attribute || attribute-name-string */ attribute){
  157. // summary:
  158. // See dojo.data.api.Read.getValues()
  159. this._assertIsItem(item);
  160. this._assertIsAttribute(attribute);
  161. var array = [];
  162. if(attribute == 'children'){
  163. for(var i = 0; i < item.childNodes.length; ++i){
  164. array.push(item.childNodes[i]);
  165. }
  166. } else if(item.getAttribute(attribute) !== null){
  167. array.push(item.getAttribute(attribute));
  168. }
  169. return array; // Array
  170. },
  171. getAttributes: function(/* item */ item){
  172. // summary:
  173. // See dojo.data.api.Read.getAttributes()
  174. this._assertIsItem(item);
  175. var attributes = [];
  176. var xmlNode = item;
  177. var xmlAttributes = xmlNode.attributes;
  178. for(var i = 0; i < xmlAttributes.length; ++i){
  179. var xmlAttribute = xmlAttributes.item(i);
  180. attributes.push(xmlAttribute.nodeName);
  181. }
  182. if(xmlNode.childNodes.length > 0){
  183. attributes.push('children');
  184. }
  185. return attributes; //Array
  186. },
  187. hasAttribute: function( /* item */ item,
  188. /* attribute || attribute-name-string */ attribute){
  189. // summary:
  190. // See dojo.data.api.Read.hasAttribute()
  191. return (this.getValues(item, attribute).length > 0); //Boolean
  192. },
  193. containsValue: function(/* item */ item,
  194. /* attribute || attribute-name-string */ attribute,
  195. /* anything */ value){
  196. // summary:
  197. // See dojo.data.api.Read.containsValue()
  198. var regexp = undefined;
  199. if(typeof value === "string"){
  200. regexp = filterUtil.patternToRegExp(value, false);
  201. }
  202. return this._containsValue(item, attribute, value, regexp); //boolean.
  203. },
  204. _containsValue: function( /* item */ item,
  205. /* attribute || attribute-name-string */ attribute,
  206. /* anything */ value,
  207. /* RegExp?*/ regexp){
  208. // summary:
  209. // Internal function for looking at the values contained by the item.
  210. // description:
  211. // Internal function for looking at the values contained by the item. This
  212. // function allows for denoting if the comparison should be case sensitive for
  213. // strings or not (for handling filtering cases where string case should not matter)
  214. //
  215. // item:
  216. // The data item to examine for attribute values.
  217. // attribute:
  218. // The attribute to inspect.
  219. // value:
  220. // The value to match.
  221. // regexp:
  222. // Optional regular expression generated off value if value was of string type to handle wildcarding.
  223. // If present and attribute values are string, then it can be used for comparison instead of 'value'
  224. var values = this.getValues(item, attribute);
  225. for(var i = 0; i < values.length; ++i){
  226. var possibleValue = values[i];
  227. if(typeof possibleValue === "string" && regexp){
  228. return (possibleValue.match(regexp) !== null);
  229. }else{
  230. //Non-string matching.
  231. if(value === possibleValue){
  232. return true; // Boolean
  233. }
  234. }
  235. }
  236. return false; // Boolean
  237. },
  238. isItem: function(/* anything */ something){
  239. // summary:
  240. // See dojo.data.api.Read.isItem()
  241. // description:
  242. // Four things are verified to ensure that "something" is an item:
  243. // something can not be null, the nodeType must be an XML Element,
  244. // the tagName must be "outline", and the node must be a member of
  245. // XML document for this datastore.
  246. return (something &&
  247. something.nodeType == 1 &&
  248. something.tagName == 'outline' &&
  249. something.ownerDocument === this._xmlData); //Boolean
  250. },
  251. isItemLoaded: function(/* anything */ something){
  252. // summary:
  253. // See dojo.data.api.Read.isItemLoaded()
  254. // OpmlStore loads every item, so if it's an item, then it's loaded.
  255. return this.isItem(something); //Boolean
  256. },
  257. loadItem: function(/* item */ item){
  258. // summary:
  259. // See dojo.data.api.Read.loadItem()
  260. // description:
  261. // The OpmlStore always loads all items, so if it's an item, then it's loaded.
  262. // From the dojo.data.api.Read.loadItem docs:
  263. // If a call to isItemLoaded() returns true before loadItem() is even called,
  264. // then loadItem() need not do any work at all and will not even invoke the callback handlers.
  265. },
  266. getLabel: function(/* item */ item){
  267. // summary:
  268. // See dojo.data.api.Read.getLabel()
  269. if(this.isItem(item)){
  270. return this.getValue(item,this.label); //String
  271. }
  272. return undefined; //undefined
  273. },
  274. getLabelAttributes: function(/* item */ item){
  275. // summary:
  276. // See dojo.data.api.Read.getLabelAttributes()
  277. return [this.label]; //array
  278. },
  279. // The dojo.data.api.Read.fetch() function is implemented as
  280. // a mixin from dojo.data.util.simpleFetch.
  281. // That mixin requires us to define _fetchItems().
  282. _fetchItems: function( /* Object */ keywordArgs,
  283. /* Function */ findCallback,
  284. /* Function */ errorCallback){
  285. // summary:
  286. // See dojo.data.util.simpleFetch.fetch()
  287. var self = this;
  288. var filter = function(requestArgs, arrayOfItems){
  289. var items = null;
  290. if(requestArgs.query){
  291. items = [];
  292. var ignoreCase = requestArgs.queryOptions ? requestArgs.queryOptions.ignoreCase : false;
  293. //See if there are any string values that can be regexp parsed first to avoid multiple regexp gens on the
  294. //same value for each item examined. Much more efficient.
  295. var regexpList = {};
  296. for(var key in requestArgs.query){
  297. var value = requestArgs.query[key];
  298. if(typeof value === "string"){
  299. regexpList[key] = filterUtil.patternToRegExp(value, ignoreCase);
  300. }
  301. }
  302. for(var i = 0; i < arrayOfItems.length; ++i){
  303. var match = true;
  304. var candidateItem = arrayOfItems[i];
  305. for(var key in requestArgs.query){
  306. var value = requestArgs.query[key];
  307. if(!self._containsValue(candidateItem, key, value, regexpList[key])){
  308. match = false;
  309. }
  310. }
  311. if(match){
  312. items.push(candidateItem);
  313. }
  314. }
  315. }else{
  316. // We want a copy to pass back in case the parent wishes to sort the array. We shouldn't allow resort
  317. // of the internal list so that multiple callers can get lists and sort without affecting each other.
  318. if(arrayOfItems.length> 0){
  319. items = arrayOfItems.slice(0,arrayOfItems.length);
  320. }
  321. }
  322. findCallback(items, requestArgs);
  323. };
  324. if(this._loadFinished){
  325. filter(keywordArgs, this._getItemsArray(keywordArgs.queryOptions));
  326. }else{
  327. //If fetches come in before the loading has finished, but while
  328. //a load is in progress, we have to defer the fetching to be
  329. //invoked in the callback.
  330. if(this._loadInProgress){
  331. this._queuedFetches.push({args: keywordArgs, filter: filter});
  332. }else{
  333. if(this.url !== ""){
  334. this._loadInProgress = true;
  335. var getArgs = {
  336. url: self.url,
  337. handleAs: "xml",
  338. preventCache: self.urlPreventCache
  339. };
  340. var getHandler = xhr.get(getArgs);
  341. getHandler.addCallback(function(data){
  342. self._processRawXmlTree(data);
  343. filter(keywordArgs, self._getItemsArray(keywordArgs.queryOptions));
  344. self._handleQueuedFetches();
  345. });
  346. getHandler.addErrback(function(error){
  347. throw error;
  348. });
  349. }else if(this._opmlData){
  350. this._processRawXmlTree(this._opmlData);
  351. this._opmlData = null;
  352. filter(keywordArgs, this._getItemsArray(keywordArgs.queryOptions));
  353. }else{
  354. throw new Error("dojox.data.OpmlStore: No OPML source data was provided as either URL or XML data input.");
  355. }
  356. }
  357. }
  358. },
  359. getFeatures: function(){
  360. // summary: See dojo.data.api.Read.getFeatures()
  361. var features = {
  362. 'dojo.data.api.Read': true,
  363. 'dojo.data.api.Identity': true
  364. };
  365. return features; //Object
  366. },
  367. /***************************************
  368. dojo.data.api.Identity API
  369. ***************************************/
  370. getIdentity: function(/* item */ item){
  371. // summary:
  372. // See dojo.data.api.Identity.getIdentity()
  373. if(this.isItem(item)){
  374. //No ther way to do this other than O(n) without
  375. //complete rework of how the tree stores nodes.
  376. for(var i in this._identityMap){
  377. if(this._identityMap[i] === item){
  378. return i;
  379. }
  380. }
  381. }
  382. return null; //null
  383. },
  384. fetchItemByIdentity: function(/* Object */ keywordArgs){
  385. // summary:
  386. // See dojo.data.api.Identity.fetchItemByIdentity()
  387. //Hasn't loaded yet, we have to trigger the load.
  388. if(!this._loadFinished){
  389. var self = this;
  390. if(this.url !== ""){
  391. //If fetches come in before the loading has finished, but while
  392. //a load is in progress, we have to defer the fetching to be
  393. //invoked in the callback.
  394. if(this._loadInProgress){
  395. this._queuedFetches.push({args: keywordArgs});
  396. }else{
  397. this._loadInProgress = true;
  398. var getArgs = {
  399. url: self.url,
  400. handleAs: "xml"
  401. };
  402. var getHandler = xhr.get(getArgs);
  403. getHandler.addCallback(function(data){
  404. var scope = keywordArgs.scope ? keywordArgs.scope : winUtil.global;
  405. try{
  406. self._processRawXmlTree(data);
  407. var item = self._identityMap[keywordArgs.identity];
  408. if(!self.isItem(item)){
  409. item = null;
  410. }
  411. if(keywordArgs.onItem){
  412. keywordArgs.onItem.call(scope, item);
  413. }
  414. self._handleQueuedFetches();
  415. }catch(error){
  416. if(keywordArgs.onError){
  417. keywordArgs.onError.call(scope, error);
  418. }
  419. }
  420. });
  421. getHandler.addErrback(function(error){
  422. this._loadInProgress = false;
  423. if(keywordArgs.onError){
  424. var scope = keywordArgs.scope ? keywordArgs.scope : winUtil.global;
  425. keywordArgs.onError.call(scope, error);
  426. }
  427. });
  428. }
  429. }else if(this._opmlData){
  430. this._processRawXmlTree(this._opmlData);
  431. this._opmlData = null;
  432. var item = this._identityMap[keywordArgs.identity];
  433. if(!self.isItem(item)){
  434. item = null;
  435. }
  436. if(keywordArgs.onItem){
  437. var scope = keywordArgs.scope ? keywordArgs.scope : winUtil.global;
  438. keywordArgs.onItem.call(scope, item);
  439. }
  440. }
  441. }else{
  442. //Already loaded. We can just look it up and call back.
  443. var item = this._identityMap[keywordArgs.identity];
  444. if(!this.isItem(item)){
  445. item = null;
  446. }
  447. if(keywordArgs.onItem){
  448. var scope = keywordArgs.scope ? keywordArgs.scope : winUtil.global;
  449. keywordArgs.onItem.call(scope, item);
  450. }
  451. }
  452. },
  453. getIdentityAttributes: function(/* item */ item){
  454. // summary:
  455. // See dojo.data.api.Identity.getIdentifierAttributes()
  456. //Identity isn't a public attribute in the item, it's the node count.
  457. //So, return null.
  458. return null;
  459. },
  460. _handleQueuedFetches: function(){
  461. // summary:
  462. // Internal function to execute delayed request in the store.
  463. //Execute any deferred fetches now.
  464. if(this._queuedFetches.length > 0){
  465. for(var i = 0; i < this._queuedFetches.length; i++){
  466. var fData = this._queuedFetches[i];
  467. var delayedQuery = fData.args;
  468. var delayedFilter = fData.filter;
  469. if(delayedFilter){
  470. delayedFilter(delayedQuery, this._getItemsArray(delayedQuery.queryOptions));
  471. }else{
  472. this.fetchItemByIdentity(delayedQuery);
  473. }
  474. }
  475. this._queuedFetches = [];
  476. }
  477. },
  478. close: function(/*dojo.data.api.Request || keywordArgs || null */ request){
  479. // summary:
  480. // See dojo.data.api.Read.close()
  481. }
  482. });
  483. //Mix in the simple fetch implementation to this class.
  484. lang.extend(OpmlStore, simpleFetch);
  485. return OpmlStore;
  486. });