dojo.provide("dojox.data.jsonPathStore"); dojo.require("dojox.jsonPath"); dojo.require("dojo.date"); dojo.require("dojo.date.locale"); dojo.require("dojo.date.stamp"); dojox.data.ASYNC_MODE = 0; dojox.data.SYNC_MODE = 1; dojo.declare("dojox.data.jsonPathStore", // summary: // The jsonPathStore implements dojo.data.read, write, notify, and identity api's. It is a local (in memory) store // and can take a javascript object with any arbitrary format and attach to it to provide a dojo.data interface to that object // data. It uses jsonPath as the query language to search agains this store. null, { mode: dojox.data.ASYNC_MODE, metaLabel: "_meta", hideMetaAttributes: false, autoIdPrefix: "_auto_", autoIdentity: true, idAttribute: "_id", indexOnLoad: true, labelAttribute: "", url: "", _replaceRegex: /\'\]/gi, noRevert : false, constructor: function(options){ //summary: // jsonPathStore constructor, instantiate a new jsonPathStore // // Takes a single optional parameter in the form of a Javascript object // containing one or more of the following properties. // // data: /*JSON String*/ || /* Javascript Object */, // JSON String or Javascript object this store will control // JSON is converted into an object, and an object passed to // the store will be used directly. If no data and no url // is provide, an empty object, {}, will be used as the initial // store. // // url: /* string url */ // Load data from this url in JSON format and use the Object // created from the data as the data source. // // indexOnLoad: /* boolean */ // Defaults to true, but this may change in the near future. // Parse the data object and set individual objects up as // appropriate. This will add meta data and assign // id's to objects that dont' have them as defined by the // idAttribute option. Disabling this option will keep this // parsing from happening until a query is performed at which // time only the top level of an item has meta info stored. // This might work in some situations, but you will almost // always want to indexOnLoad or use another option which // will create an index. In the future we will support a // generated index that maps by jsonPath allowing the // server to take some of this load for larger data sets. // // idAttribute: /* string */ // Defaults to '_id'. The name of the attribute that holds an objects id. // This can be a preexisting id provided by the server. // If an ID isn't already provided when an object // is fetched or added to the store, the autoIdentity system // will generate an id for it and add it to the index. There // are utility routines for exporting data from the store // that can clean any generated IDs before exporting and leave // preexisting id's in tact. // // metaLabel: /* string */ // Defaults to '_meta' overrides the attribute name that is used by the store // for attaching meta information to an object while // in the store's control. Defaults to '_meta'. // // hideMetaAttributes: /* boolean */ // Defaults to False. When enabled, calls to getAttributes() will not // include the meta attribute. // // autoIdPrefix: /*string*/ // Defaults to "_auto_". This string is used as the prefix to any // objects which have a generated id. A numeric index is appended // to this string to complete the ID // // mode: dojox.data.ASYNC_MODE || dojox.data.SYNC_MODE // Defaults to ASYNC_MODE. This option sets the default mode for this store. // Sync calls return their data immediately from the calling function // instead of calling the callback functions. Functions such as // fetchItemByIdentity() and fetch() both accept a string parameter in addtion // to the normal keywordArgs parameter. When passed this option, SYNC_MODE will // automatically be used even when the default mode of the system is ASYNC_MODE. // A normal request to fetch or fetchItemByIdentity (with kwArgs object) can also // include a mode property to override this setting for that one request. //setup a byId alias to the api call this.byId=this.fetchItemByIdentity; if (options){ dojo.mixin(this,options); } this._dirtyItems=[]; this._autoId=0; this._referenceId=0; this._references={}; this._fetchQueue=[]; this.index={}; //regex to identify when we're travelling down metaObject (which we don't want to do) var expr="("+this.metaLabel+"\'\])"; this.metaRegex = new RegExp(expr); //no data or url, start with an empty object for a store if (!this.data && !this.url){ this.setData({}); } //we have data, but no url, set the store as the data if (this.data && !this.url){ this.setData(this.data); //remove the original refernce, we're now using _data from here on out delete this.data; } //given a url, load json data from as the store if (this.url){ dojo.xhrGet({ url: options.url, handleAs: "json", load: dojo.hitch(this, "setData"), sync: this.mode }); } }, _loadData: function(data){ // summary: // load data into the store. Index it if appropriate. if (this._data){ delete this._data; } if (dojo.isString(data)){ this._data = dojo.fromJson(data); }else{ this._data = data; } if (this.indexOnLoad){ this.buildIndex(); } this._updateMeta(this._data, {path: "$"}); this.onLoadData(this._data); }, onLoadData: function(data){ // summary // Called after data has been loaded in the store. // If any requests happened while the startup is happening // then process them now. while (this._fetchQueue.length>0){ var req = this._fetchQueue.shift(); this.fetch(req); } }, setData: function(data){ // summary: // set the stores' data to the supplied object and then // load and/or setup that data with the required meta info this._loadData(data); }, buildIndex: function(path, item){ //summary: // parse the object structure, and turn any objects into // jsonPathStore items. Basically this just does a recursive // series of fetches which itself already examines any items // as they are retrieved and setups up the required meta information. // // path: /* string */ // jsonPath Query for the starting point of this index construction. if (!this.idAttribute){ throw new Error("buildIndex requires idAttribute for the store"); } item = item || this._data; var origPath = path; path = path||"$"; path += "[*]"; var data = this.fetch({query: path,mode: dojox.data.SYNC_MODE}); for(var i=0; i= args.count)) { continue; } var item = results[i]["value"]; var path = results[i]["path"]; if (!dojo.isObject(item)){continue;} if(this.metaRegex.exec(path)){continue;} //this automatically records the objects path this._updateMeta(item,{path: results[i].path}); //if autoIdentity and no id, generate one and add it to the item if(this.autoIdentity && !item[this.idAttribute]){ var newId = this.autoIdPrefix + this._autoId++; item[this.idAttribute]=newId; item[this.metaLabel].autoId=true; } //add item to the item index if appropriate if(item[this.idAttribute]){this.index[item[this.idAttribute]]=item} count++; tmp.push(item); } results = tmp; var scope = args.scope || dojo.global; if ("sort" in args){ console.log("TODO::add support for sorting in the fetch"); } if (args.mode==dojox.data.SYNC_MODE){ return results; }; if (args.onBegin){ args["onBegin"].call(scope, results.length, args); } if (args.onItem){ for (var i=0; i0) { label+=" ";} label += item[this.labelAttribute[i]]; } return label; }else{ return item[this.labelAttribute]; } } return item.toString(); }, getLabelAttributes: function(item){ // summary: // returns an array of attributes that are used to create the label of an item item = this._correctReference(item); return dojo.isArray(this.labelAttribute) ? this.labelAttribute : [this.labelAttribute]; }, sort: function(a,b){ console.log("TODO::implement default sort algo"); }, //Identity API Support getIdentity: function(item){ // summary // returns the identity of an item or throws // a not found error. if (this.isItem(item)){ return item[this.idAttribute]; } throw new Error("Id not found for item"); }, getIdentityAttributes: function(item){ // summary: // returns the attributes which are used to make up the // identity of an item. Basically returns this.idAttribute return [this.idAttribute]; }, fetchItemByIdentity: function(args){ // summary: // fetch an item by its identity. This store also provides // a much more finger friendly alias, 'byId' which does the // same thing as this function. If provided a string // this call will be treated as a SYNC request and will // return the identified item immediatly. Alternatively it // takes a object as a set of keywordArgs: // // identity: /* string */ // the id of the item you want to retrieve // // mode: dojox.data.SYNC_MODE || dojox.data.ASYNC_MODE // overrides the default store fetch mode // // onItem: /* function */ // Result call back. Passed the fetched item. // // onError: /* function */ // error callback. var id; if (dojo.isString(args)){ id = args; args = {identity: id, mode: dojox.data.SYNC_MODE} }else{ if (args){ id = args["identity"]; } if (!args.mode){args.mode = this.mode} } if (this.index && (this.index[id] || this.index["identity"])){ if (args.mode==dojox.data.SYNC_MODE){ return this.index[id]; } if (args.onItem){ args["onItem"].call(args.scope || dojo.global, this.index[id], args); } return args; }else{ if (args.mode==dojox.data.SYNC_MODE){ return false; } } if(args.onError){ args["onItem"].call(args.scope || dojo.global, new Error("Item Not Found: " + id), args); } return args; }, _makeItAnItem: function(data, pInfo){ // Summary: // add the meta data to the item and descendants var meta={}; if(this.idAttribute && !data[this.idAttribute]){ if(this.requireId){ throw new Error("requireId is enabled, new items must have an id defined to be added"); } if(this.autoIdentity){ var newId = this.autoIdPrefix + this._autoId++; data[this.idAttribute]=newId; meta.autoId=true; } } if(!pInfo && !pInfo.attribute && !this.idAttribute && !data[this.idAttribute]){ throw new Error("Adding a new item requires, at a minimum, either the pInfo information, including the pInfo.attribute, or an id on the item in the field identified by idAttribute"); } if(!pInfo.attribute){ pInfo.attribute = data[this.idAttribute]; } //add this item to the index if(data[this.idAttribute]){ this.index[data[this.idAttribute]]=data; } this._updateMeta(data, meta) //keep track of all references in the store so we can delete them as necessary this._addReference(data, {parent: pInfo.item, attribute: pInfo.attribute}); //mark this new item as dirty this._setDirty(data); //Itemize the children the children if any if(data[pInfo.attribute] && dojo.isArray(data[pInfo.attribute])){ for(var i=0; i0){ console.log("refs map: " , this._references); console.log("item to delete: ", item); var rid = item[this.metaLabel]["referenceIds"].pop(); var pInfo = this._references[rid]; console.log("deleteItem(): ", pInfo, pInfo.parent); var parentItem = pInfo.parent; var attribute = pInfo.attribute; if(parentItem && parentItem[attribute] && !dojo.isArray(parentItem[attribute])){ this._setDirty(parentItem); this.unsetAttribute(parentItem, attribute); delete parentItem[attribute]; } if (dojo.isArray(parentItem[attribute])){ console.log("Parent is array"); var oldValue = this._trimItem(parentItem[attribute]); var found=false; for (var i=0; i 0){ var item = this._dirtyItems.pop()["item"]; var t = this._trimItem(item); var d; switch(kwArgs.format){ case "json": d = dojo.toJson(t); break; case "raw": default: d = t; } data.push(d); this._markClean(item); } this.onSave(data); }, _markClean: function(item){ // summary // remove this meta information marking an item as "dirty" if (item && item[this.metaLabel] && item[this.metaLabel]["isDirty"]){ delete item[this.metaLabel]["isDirty"]; } }, revert: function(){ // summary // returns any modified data to its original state prior to a save(); while (this._dirtyItems.length>0){ var d = this._dirtyItems.pop(); this._mixin(d.item, d.old); } this.onRevert(); }, _mixin: function(target, data){ // summary: // specialized mixin that hooks up objects in the store where references are identified. var mix; if (dojo.isObject(data)){ if (dojo.isArray(data)){ while(target.length>0){target.pop();} for (var i=0; i