source: Dev/branches/rest-dojo-ui/client/dojox/data/OpmlStore.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

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