diff options
Diffstat (limited to 'includes/js/dojox/rpc/JsonRestStore.js')
-rw-r--r-- | includes/js/dojox/rpc/JsonRestStore.js | 661 |
1 files changed, 661 insertions, 0 deletions
diff --git a/includes/js/dojox/rpc/JsonRestStore.js b/includes/js/dojox/rpc/JsonRestStore.js new file mode 100644 index 0000000..dd14874 --- /dev/null +++ b/includes/js/dojox/rpc/JsonRestStore.js @@ -0,0 +1,661 @@ +if(!dojo._hasResource["dojox.data.JsonRestStore"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojox.data.JsonRestStore"] = true; +dojo.provide("dojox.data.JsonRestStore"); +dojo.require("dojox.rpc.Rest"); +dojo.require("dojox.rpc.JsonReferencing"); // TODO: Make it work without this dependency + +// A JsonRestStore takes a REST service and uses it the remote communication for a +// read/write dojo.data implementation. To use a JsonRestStore you should create a +// service with a REST transport. This can be configured with an SMD: +//{ +// services: { +// jsonRestStore: { +// transport: "REST", +// envelope: "URL", +// target: "store.php", +// contentType:"application/json", +// parameters: [ +// {name: "location", type: "string", optional: true} +// ] +// } +// } +//} +// The SMD can then be used to create service, and the service can be passed to a JsonRestStore. For example: +// var myServices = new dojox.rpc.Service(dojo.moduleUrl("dojox.rpc.tests.resources", "test.smd")); +// var jsonStore = new dojox.data.JsonRestStore({service:myServices.jsonRestStore}); +// +// The JsonRestStore will then cause all saved modifications to be server using Rest commands (PUT, POST, or DELETE). +// The JsonRestStore also supports lazy loading. References can be made to objects that have not been loaded. +// For example if a service returned: +// {"name":"Example","lazyLoadedObject":{"$ref":"obj2"}} +// +// And this object has accessed using the dojo.data API: +// var obj = jsonStore.getValue(myObject,"lazyLoadedObject"); +// The object would automatically be requested from the server (with an object id of "obj2"). +// +// When using a Rest store on a public network, it is important to implement proper security measures to +// control access to resources + +dojox.data.ASYNC_MODE = 0; +dojox.data.SYNC_MODE = 1; +dojo.declare("dojox.data.JsonRestStore", + null, + { + mode: dojox.data.ASYNC_MODE, + constructor: function(options){ + //summary: + // JsonRestStore constructor, instantiate a new JsonRestStore + // A JsonRestStore can be configured from a JSON Schema. Queries are just + // passed through as URLs for XHR requests, + // so there is nothing to configure, just plug n play. + // Of course there are some options to fiddle with if you want: + // + // jsonSchema: /* object */ + // + // service: /* function */ + // This is the service object that is used to retrieve lazy data and save results + // The function should be directly callable with a single parameter of an object id to be loaded + // The function should also have the following methods: + // put(id,value) - puts the value at the given id + // post(id,value) - posts (appends) the value at the given id + // delete(id) - deletes the value corresponding to the given id + // + // 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. + + // 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 the advanced json parser is enabled, we can pass through object updates as onSet events + dojo.connect(dojox.rpc,"onUpdate",this,function(obj,attrName,oldValue,newValue){ + var prefix = this.service.serviceName + '/'; + if (!obj._id){ + console.log("no id on updated object ", obj); + } + else if (obj._id.substring(0,prefix.length) == prefix) + this.onSet(obj,attrName,oldValue,newValue); + }); + if (options){ + dojo.mixin(this,options); + } + if (!this.service) + throw Error("A service is required for JsonRestStore"); + if (!(this.service.contentType + '').match(/application\/json/)) + throw Error("A service must use a contentType of 'application/json' in order to be used in a JsonRestStore"); + this.idAttribute = (this.service._schema && this.service._schema._idAttr) || 'id'; + var arrayModifyingMethodNames = ["splice","push","pop","unshift","shift","reverse","sort"]; + this._arrayModifyingMethods = {}; + var array = []; + var _this = this; + // setup array augmentation, for catching mods and setting arrays as dirty + for (var i = 0; i < arrayModifyingMethodNames.length; i++){ + (function(key){ // closure for the method to be bound correctly + var method = array[key]; + _this._arrayModifyingMethods[key] = function(){ + _this._setDirty(this); // set the array as dirty before the native modifying operation + return method.apply(this,arguments); + } + _this._arrayModifyingMethods[key]._augmented = 1; + })(arrayModifyingMethodNames[i]); + } + this._deletedItems=[]; + this._dirtyItems=[]; + //given a url, load json data from as the store + }, + + _loadById: function(id,callback){ + var slashIndex = id.indexOf('/'); + var serviceName = id.substring(0,slashIndex); + var id = id.substring(slashIndex + 1); + (this.service.serviceName == serviceName ? + this.service : // use the current service if it is the right one + dojox.rpc.services[serviceName])(id) // otherwise call by looking up the service + .addCallback(callback); + }, + getValue: function(item, property,lazyCallback){ + // summary: + // Gets the value of an item's 'property' + // + // item: /* object */ + // property: /* string */ + // property to look up value for + // lazyCallback: /* function*/ + // not part of the API, but if you are using lazy loading properties, you may provide a callback to resume, in order to have asynchronous loading + var value = item[property]; + if (value && value.$ref){ + dojox.rpc._sync = !lazyCallback; // tell the service to operate synchronously (I have some concerns about the "thread" safety with FF3, as I think it does event stacking on sync calls) + this._loadById((value && value._id) || (item._id + '.' + property),lazyCallback); + delete dojox.rpc._sync; // revert to normal async behavior + } else if (lazyCallback){lazyCallback(value);} + return value; + }, + + getValues: function(item, property){ + // summary: + // Gets the value of an item's 'property' and returns + // it. If this value is an array it is just returned, + // if not, the value is added to an array and that is returned. + // + // item: /* object */ + // property: /* string */ + // property to look up value for + + var val = this.getValue(item,property); + return dojo.isArray(val) ? val : [val]; + }, + + getAttributes: function(item){ + // summary: + // Gets the available attributes of an item's 'property' and returns + // it as an array. + // + // item: /* object */ + + var res = []; + for (var i in item){ + res.push(i); + } + return res; + }, + + hasAttribute: function(item,attribute){ + // summary: + // Checks to see if item has attribute + // + // item: /* object */ + // attribute: /* string */ + return attribute in item; + }, + + containsValue: function(item, attribute, value){ + // summary: + // Checks to see if 'item' has 'value' at 'attribute' + // + // item: /* object */ + // attribute: /* string */ + // value: /* anything */ + return getValue(item,attribute)==value; + }, + + + isItem: function(item){ + // summary: + // Checks to see if a passed 'item' + // is really a JsonRestStore item. + // + // item: /* object */ + // attribute: /* string */ + + return !!(dojo.isObject(item) && item._id); + }, + + isItemLoaded: function(item){ + // summary: + // returns isItem() :) + // + // item: /* object */ + + return !item.$ref; + }, + + loadItem: function(item){ + // summary: + // Loads an item that has not been loaded yet. Lazy loading should happen through getValue, and if used properly, this should never need to be called + // returns true. Note this does not work with lazy loaded primitives! + if (item.$ref){ + dojox.rpc._sync = true; // tell the service to operate synchronously + this._loadById(item._id) + delete dojox.rpc._sync; // revert to normal async behavior + } + + return true; + }, + + _walk : function(value,forEach){ + // walk the graph, avoiding duplication + var walked=[]; + function walk(value){ + if (value && typeof value == 'object' && !value.__walked){ + value.__walked = true; + walked.push(value); + for (var i in value){ + if (walk(value[i])){ + forEach(value,i,value[i]); + } + } + return true; + } + } + walk(value); + forEach({},null,value); + for (var i = 0; i < walked.length;i++) + delete walked[i].__walked; + }, + fetch: function(args){ + //console.log("fetch() ", args); + // summary + // + // fetch takes either a string argument or a keywordArgs + // object containing the parameters for the search. + // If passed a string, fetch will interpret this string + // as the query to be performed and will do so in + // SYNC_MODE returning the results immediately. + // If an object is supplied as 'args', its options will be + // parsed and then contained query executed. + // + // query: /* string or object */ + // Defaults to "". This is basically passed to the XHR request as the URL to get the data + // + // start: /* int */ + // Starting item in result set + // + // count: /* int */ + // Maximum number of items to return + // + // cache: /* boolean */ + // + // sort: /* function */ + // Not Implemented yet + // + // The following only apply to ASYNC requests (the default) + // + // onBegin: /* function */ + // called before any results are returned. Parameters + // will be the count and the original fetch request + // + // onItem: /*function*/ + // called for each returned item. Parameters will be + // the item and the fetch request + // + // onComplete: /* function */ + // called on completion of the request. Parameters will + // be the complete result set and the request + // + // onError: /* function */ + // colled in the event of an error + + if(dojo.isString(args)){ + query = args; + args={query: query, mode: dojox.data.SYNC_MODE}; + + } + + var query; + if (!args || !args.query){ + if (!args){ + var args={}; + } + + if (!args.query){ + args.query=""; + query=args.query; + } + + } + + if (dojo.isObject(args.query)){ + if (args.query.query){ + query = args.query.query; + }else{ + query = args.query = ""; + } + if (args.query.queryOptions){ + args.queryOptions=args.query.queryOptions + } + }else{ + query=args.query; + } + if (args.start || args.count){ + query += '[' + (args.start ? args.start : '') + ':' + (args.count ? ((args.start || 0) + args.count) : '') + ']'; + } + var results = dojox.rpc._index[this.service.serviceName + '/' + query]; + if (!args.mode){args.mode = this.mode;} + var _this = this; + var defResult; + dojox.rpc._sync = this.mode; + dojox._newId = query; + if (results && !("cache" in args && !args.cache)){ // TODO: Add TTL maybe? + defResult = new dojo.Deferred; + defResult.callback(results); + } + else { + defResult = this.service(query); + } + defResult.addCallback(function(results){ + delete dojox._newId; // cleanup + if (args.onBegin){ + args["onBegin"].call(_this, results.length, args); + } + _this._walk(results,function(obj,i,value){ + if (value instanceof Array){ + for (var i in _this._arrayModifyingMethods){ + if (!value[i]._augmented){ + value[i] = _this._arrayModifyingMethods[i]; + } + } + + } + }); + if (args.onItem){ + for (var i=0; i<results.length;i++){ + args["onItem"].call(_this, results[i], args); + } + } + if (args.onComplete){ + args["onComplete"].call(_this, results, args); + } + return results; + }); + defResult.addErrback(args.onError); + return args; + }, + + + getFeatures: function(){ + // summary: + // return the store feature set + + return { + "dojo.data.api.Read": true, + "dojo.data.api.Identity": true, + "dojo.data.api.Write": true, + "dojo.data.api.Notification": true + } + }, + + getLabel: function(item){ + // summary + // returns the label for an item. Just gets the "label" attribute. + // + return this.getValue(item,"label"); + }, + + getLabelAttributes: function(item){ + // summary: + // returns an array of attributes that are used to create the label of an item + return ["label"]; + }, + + 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. + var prefix = this.service.serviceName + '/'; + if (!item._id){// generate a good random id + item._id = prefix + Math.random().toString(16).substring(2,14)+Math.random().toString(16).substring(2,14); + } + if (item._id.substring(0,prefix.length) != prefix){ + throw Error("Identity attribute not found"); + } + return item._id.substring(prefix.length); + }, + + 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. fetch and fetchItemByIdentity work exactly the same + return this.fetch(args); + }, + + //Write API Support + newItem: function(data, parentInfo){ + // summary: + // adds a new item to the store at the specified point. + // Takes two parameters, data, and options. + // + // data: /* object */ + // The data to be added in as an item. + // parentInfo: + // An optional javascript object defining what item is the parent of this item (in a hierarchical store. Not all stores do hierarchical items), + // and what attribute of that parent to assign the new item to. If this is present, and the attribute specified + // is a multi-valued attribute, it will append this item into the array of values for that attribute. The structure + // of the object is as follows: + // { + // parent: someItem, + // } + // or + // { + // parentId: someItemId, + // } + + if (this.service._schema && this.service._schema.clazz && data.constructor != this.service._schema.clazz) + data = dojo.mixin(new this.service._schema.clazz,data); + this.getIdentity(data); + this._getParent(parentInfo).push(data); // essentially what newItem really means + this.onNew(data); + return data; + }, + _getParent : function(parentInfo){ + + var parentId = (parentInfo && parentInfo.parentId) || this.parentId || ''; + var parent = (parentInfo && parentInfo.parent) || dojox.rpc._index[this.service.serviceName + '/' + parentId] || []; + if (!parent._id){ + parent._id = this.service.serviceName + '/' + parentId; + this._setDirty(parent); // set it dirty so it will be post + } + return parent; + }, + deleteItem: function(item,/*array*/parentInfo){ + // summary + // deletes item any references to that item from the store. + // + // item: + // item to delete + // + // removeFrom: This an item or items from which to remove references to this object. This store does not record references, + // so if this parameter the entire object graph from load items will be searched for references. Providing this parameter + // is vastly faster. An empty object or truthy primitive can be passed if no references need to be removed + + // If the desire is to delete only one reference, unsetAttribute or + // setValue is the way to go. + if (this.isItem(item)) + this._deletedItems.push(item); + var _this = this; + this._walk(((parentInfo || this.parentId) && this._getParent(parentInfo)) || dojox.rpc._index,function(obj,i,val){ + if (obj[i] === item){ // find a reference to this object + if (_this.isItem(obj)){ + if (isNaN(i) || !obj.splice){ // remove a property + _this.unsetAttribute(obj,i); + delete obj[i]; + } + else {// remove an array entry + obj.splice(i,1); + } + } + } + }); + this.onDelete(item); + }, + + _setDirty: function(item){ + // summary: + // adds an item to the list of dirty items. This item + // contains a reference to the item itself as well as a + // cloned and trimmed version of old item for use with + // revert. + var i; + if (!item._id) + return; + //if an item is already in the list of dirty items, don't add it again + //or it will overwrite the premodification data set. + for (i=0; i<this._dirtyItems.length; i++){ + if (item==this._dirtyItems[i].item){ + return; + } + } + var old = item instanceof Array ? [] : {}; + for (i in item) + if (item.hasOwnProperty(i)) + old[i] = item[i]; + this._dirtyItems.push({item: item, old: old}); + }, + + setValue: function(item, attribute, value){ + // summary: + // sets 'attribute' on 'item' to 'value' + + var old = item[attribute]; + if (old != value){ + this._setDirty(item); + item[attribute]=value; + this.onSet(item,attribute,old,value); + } + }, + + setValues: function(item, attribute, values){ + // summary: + // sets 'attribute' on 'item' to 'value' value + // must be an array. + + + if (!dojo.isArray(values)){throw new Error("setValues expects to be passed an Array object as its value");} + this._setDirty(item); + var old = item[attribute]; + item[attribute]=values; + this.onSet(item,attribute,old,values); + }, + + unsetAttribute: function(item, attribute){ + // summary: + // unsets 'attribute' on 'item' + + this._setDirty(item); + var old = item[attribute]; + delete item[attribute]; + this.onSet(item,attribute,old,undefined); + }, + _commitAppend: function(listId,item){ + return this.service.post(listId,item); + }, + save: function(kwArgs){ + // summary: + // Saves the dirty data using REST Ajax methods + + var data = []; + + var left = 0; // this is how many changes are remaining to be received from the server + var _this = this; + function finishOne(){ + if (!(--left)) + _this.onSave(data); + } + while (this._dirtyItems.length > 0){ + var dirty = this._dirtyItems.pop(); + var item = dirty.item; + var append = false; + left++; + var deferred; + if (item instanceof Array && dirty.old instanceof Array){ + // see if we can just append the item with a post + append = true; + for (var i = 0, l = dirty.old.length; i < l; i++){ + if (item[i] != dirty.old[i]){ + append = false; + } + } + if (append){ // if we can, we will do posts to add from here + for (;i<item.length;i++){ + deferred = this._commitAppend(this.getIdentity(item),item[i]); + deferred.addCallback(finishOne); + } + } + } + if (!append){ + deferred = this.service.put(this.getIdentity(item),item); + deferred.addCallback(finishOne); + } + + data.push(item); + } + while (this._deletedItems.length > 0){ + left++; + this.service['delete'](this.getIdentity(this._deletedItems.pop())).addCallback(finishOne); + } + }, + + + revert: function(){ + // summary + // returns any modified data to its original state prior to a save(); + + while (this._dirtyItems.length>0){ + var i; + var d = this._dirtyItems.pop(); + for (i in d.old){ + d.item[i] = d.old[i]; + } + for (i in d.item){ + if (!d.old.hasOwnProperty(i)) + delete d.item[i] + } + } + this.onRevert(); + }, + + + isDirty: function(item){ + // summary + // returns true if the item is marked as dirty. + for (var i=0, l=this._dirtyItems.length; i<l; i++){ + if (this._dirtyItems[i]==item){return true}; + } + }, + + + //Notifcation Support + + onSet: function(){ + }, + + onNew: function(){ + + }, + + onDelete: function(){ + + }, + + onSave: function(items){ + // summary: + // notification of the save event..not part of the notification api, + // but probably should be. + //console.log("onSave() ", items); + }, + + onRevert: function(){ + // summary: + // notification of the revert event..not part of the notification api, + // but probably should be. + + } + } +); + + +} |