From e44a7e37b6c7b5961adaffc62b9042b8d442938e Mon Sep 17 00:00:00 2001 From: mensonge Date: Thu, 13 Nov 2008 09:49:11 +0000 Subject: New feature: basic Ajax suggestion for tags and implementation of Dojo toolkit git-svn-id: https://semanticscuttle.svn.sourceforge.net/svnroot/semanticscuttle/trunk@151 b3834d28-1941-0410-a4f8-b48e95affb8f --- includes/js/dojox/rpc/JsonRestStore.js | 661 +++++++++++++++++++++++++++++++++ 1 file changed, 661 insertions(+) create mode 100644 includes/js/dojox/rpc/JsonRestStore.js (limited to 'includes/js/dojox/rpc/JsonRestStore.js') 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 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 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