//>>built define("dojox/rpc/JsonRest", ["dojo", "dojox", "dojox/json/ref", "dojox/rpc/Rest"], function(dojo, dojox) { var dirtyObjects = []; var Rest = dojox.rpc.Rest; var jr; function resolveJson(service, deferred, value, defaultId){ var timeStamp = deferred.ioArgs && deferred.ioArgs.xhr && deferred.ioArgs.xhr.getResponseHeader("Last-Modified"); if(timeStamp && Rest._timeStamps){ Rest._timeStamps[defaultId] = timeStamp; } var hrefProperty = service._schema && service._schema.hrefProperty; if(hrefProperty){ dojox.json.ref.refAttribute = hrefProperty; } value = value && dojox.json.ref.resolveJson(value, { defaultId: defaultId, index: Rest._index, timeStamps: timeStamp && Rest._timeStamps, time: timeStamp, idPrefix: service.servicePath.replace(/[^\/]*$/,''), idAttribute: jr.getIdAttribute(service), schemas: jr.schemas, loader: jr._loader, idAsRef: service.idAsRef, assignAbsoluteIds: true }); dojox.json.ref.refAttribute = "$ref"; return value; } jr = dojox.rpc.JsonRest={ serviceClass: dojox.rpc.Rest, conflictDateHeader: "If-Unmodified-Since", commit: function(kwArgs){ // summary: // Saves the dirty data using REST Ajax methods kwArgs = kwArgs || {}; var actions = []; var alreadyRecorded = {}; var savingObjects = []; for(var i = 0; i < dirtyObjects.length; i++){ var dirty = dirtyObjects[i]; var object = dirty.object; var old = dirty.old; var append = false; if(!(kwArgs.service && (object || old) && (object || old).__id.indexOf(kwArgs.service.servicePath)) && dirty.save){ delete object.__isDirty; if(object){ if(old){ // changed object var pathParts; if((pathParts = object.__id.match(/(.*)#.*/))){ // it is a path reference // this means it is a sub object, we must go to the parent object and save it object = Rest._index[pathParts[1]]; } if(!(object.__id in alreadyRecorded)){// if it has already been saved, we don't want to repeat it // record that we are saving alreadyRecorded[object.__id] = object; if(kwArgs.incrementalUpdates && !pathParts){ // I haven't figured out how we would do incremental updates on sub-objects yet // make an incremental update using a POST var incremental = (typeof kwArgs.incrementalUpdates == 'function' ? kwArgs.incrementalUpdates : function(){ incremental = {}; for(var j in object){ if(object.hasOwnProperty(j)){ if(object[j] !== old[j]){ incremental[j] = object[j]; } }else if(old.hasOwnProperty(j)){ // we can't use incremental updates to remove properties return null; } } return incremental; })(object, old); } if(incremental){ actions.push({method:"post",target:object, content: incremental}); } else{ actions.push({method:"put",target:object,content:object}); } } }else{ // new object var service = jr.getServiceAndId(object.__id).service; var idAttribute = jr.getIdAttribute(service); if((idAttribute in object) && !kwArgs.alwaysPostNewItems){ // if the id attribute is specified, then we should know the location actions.push({method:"put",target:object, content:object}); }else{ actions.push({method:"post",target:{__id:service.servicePath}, content:object}); } } }else if(old){ // deleted object actions.push({method:"delete",target:old}); }//else{ this would happen if an object is created and then deleted, don't do anything savingObjects.push(dirty); dirtyObjects.splice(i--,1); } } dojo.connect(kwArgs,"onError",function(){ if(kwArgs.revertOnError !== false){ var postCommitDirtyObjects = dirtyObjects; dirtyObjects = savingObjects; var numDirty = 0; // make sure this does't do anything if it is called again jr.revert(); // revert if there was an error dirtyObjects = postCommitDirtyObjects; } else{ dirtyObjects = dirtyObject.concat(savingObjects); } }); jr.sendToServer(actions, kwArgs); return actions; }, sendToServer: function(actions, kwArgs){ var xhrSendId; var plainXhr = dojo.xhr; var left = actions.length;// this is how many changes are remaining to be received from the server var i, contentLocation; var timeStamp; var conflictDateHeader = this.conflictDateHeader; // add headers for extra information dojo.xhr = function(method,args){ // keep the transaction open as we send requests args.headers = args.headers || {}; // the last one should commit the transaction args.headers['Transaction'] = actions.length - 1 == i ? "commit" : "open"; if(conflictDateHeader && timeStamp){ args.headers[conflictDateHeader] = timeStamp; } if(contentLocation){ args.headers['Content-ID'] = '<' + contentLocation + '>'; } return plainXhr.apply(dojo,arguments); }; for(i =0; i < actions.length;i++){ // iterate through the actions to execute var action = actions[i]; dojox.rpc.JsonRest._contentId = action.content && action.content.__id; // this is used by OfflineRest var isPost = action.method == 'post'; timeStamp = action.method == 'put' && Rest._timeStamps[action.content.__id]; if(timeStamp){ // update it now Rest._timeStamps[action.content.__id] = (new Date()) + ''; } // send the content location to the server contentLocation = isPost && dojox.rpc.JsonRest._contentId; var serviceAndId = jr.getServiceAndId(action.target.__id); var service = serviceAndId.service; var dfd = action.deferred = service[action.method]( serviceAndId.id.replace(/#/,''), // if we are using references, we need eliminate # dojox.json.ref.toJson(action.content, false, service.servicePath, true) ); (function(object, dfd, service){ dfd.addCallback(function(value){ try{ // Implements id assignment per the HTTP specification var newId = dfd.ioArgs.xhr && dfd.ioArgs.xhr.getResponseHeader("Location"); //TODO: match URLs if the servicePath is relative... if(newId){ // if the path starts in the middle of an absolute URL for Location, we will use the just the path part var startIndex = newId.match(/(^\w+:\/\/)/) && newId.indexOf(service.servicePath); newId = startIndex > 0 ? newId.substring(startIndex) : (service.servicePath + newId). // now do simple relative URL resolution in case of a relative URL. replace(/^(.*\/)?(\w+:\/\/)|[^\/\.]+\/\.\.\/|^.*\/(\/)/,'$2$3'); object.__id = newId; Rest._index[newId] = object; } value = resolveJson(service, dfd, value, object && object.__id); }catch(e){} if(!(--left)){ if(kwArgs.onComplete){ kwArgs.onComplete.call(kwArgs.scope, actions); } } return value; }); })(action.content, dfd, service); dfd.addErrback(function(value){ // on an error we want to revert, first we want to separate any changes that were made since the commit left = -1; // first make sure that success isn't called kwArgs.onError.call(kwArgs.scope, value); }); } // revert back to the normal XHR handler dojo.xhr = plainXhr; }, getDirtyObjects: function(){ return dirtyObjects; }, revert: function(service){ // summary: // Reverts all the changes made to JSON/REST data for(var i = dirtyObjects.length; i > 0;){ i--; var dirty = dirtyObjects[i]; var object = dirty.object; var old = dirty.old; var store = dojox.data._getStoreForItem(object || old); if(!(service && (object || old) && (object || old).__id.indexOf(service.servicePath))){ // if we are in the specified store or if this is a global revert if(object && old){ // changed for(var j in old){ if(old.hasOwnProperty(j) && object[j] !== old[j]){ if(store){ store.onSet(object, j, object[j], old[j]); } object[j] = old[j]; } } for(j in object){ if(!old.hasOwnProperty(j)){ if(store){ store.onSet(object, j, object[j]); } delete object[j]; } } }else if(!old){ // was an addition, remove it if(store){ store.onDelete(object); } }else{ // was a deletion, we will add it back if(store){ store.onNew(old); } } delete (object || old).__isDirty; dirtyObjects.splice(i, 1); } } }, changing: function(object,_deleting){ // summary: // adds an object to the list of dirty objects. This object // contains a reference to the object itself as well as a // cloned and trimmed version of old object for use with // revert. if(!object.__id){ return; } object.__isDirty = true; //if an object is already in the list of dirty objects, don't add it again //or it will overwrite the premodification data set. for(var i=0; i= serviceName.length)){ serviceName = service; } } if (serviceName){ return {service: jr.services[serviceName], id:absoluteId.substring(serviceName.length)}; } var parts = absoluteId.match(/^(.*\/)([^\/]*)$/); return {service: new jr.serviceClass(parts[1], true), id:parts[2]}; }, services:{}, schemas:{}, registerService: function(/*Function*/ service, /*String*/ servicePath, /*Object?*/ schema){ // summary: // Registers a service for as a JsonRest service, mapping it to a path and schema // service: // This is the service to register // servicePath: // This is the path that is used for all the ids for the objects returned by service // schema: // This is a JSON Schema object to associate with objects returned by this service servicePath = service.servicePath = servicePath || service.servicePath; service._schema = jr.schemas[servicePath] = schema || service._schema || {}; jr.services[servicePath] = service; }, byId: function(service, id){ // if caching is allowed, we look in the cache for the result var deferred, result = Rest._index[(service.servicePath || '') + id]; if(result && !result._loadObject){// cache hit deferred = new dojo.Deferred(); deferred.callback(result); return deferred; } return this.query(service, id); }, query: function(service, id, args){ var deferred = service(id, args); deferred.addCallback(function(result){ if(result.nodeType && result.cloneNode){ // return immediately if it is an XML document return result; } return resolveJson(service, deferred, result, typeof id != 'string' || (args && (args.start || args.count)) ? undefined: id); }); return deferred; }, _loader: function(callback){ // load a lazy object var serviceAndId = jr.getServiceAndId(this.__id); var self = this; jr.query(serviceAndId.service, serviceAndId.id).addBoth(function(result){ // if they are the same this means an object was loaded, otherwise it // might be a primitive that was loaded or maybe an error if(result == self){ // we can clear the flag, so it is a loaded object delete result.$ref; delete result._loadObject; }else{ // it is probably a primitive value, we can't change the identity of an object to // the loaded value, so we will keep it lazy, but define the lazy loader to always // return the loaded value self._loadObject = function(callback){ callback(result); }; } callback(result); }); }, isDirty: function(item, store){ // summary // returns true if the item is marked as dirty or true if there are any dirty items if(!item){ if(store){ return dojo.some(dirtyObjects, function(dirty){ return dojox.data._getStoreForItem(dirty.object || dirty.old) == store; }); } return !!dirtyObjects.length; } return item.__isDirty; } }; return dojox.rpc.JsonRest; });