123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462 |
- /*
- Copyright (c) 2004-2012, The Dojo Foundation All Rights Reserved.
- Available via Academic Free License >= 2.1 OR the modified BSD license.
- see: http://dojotoolkit.org/license for details
- */
- if(!dojo._hasResource["dojox.rpc.JsonRest"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
- dojo._hasResource["dojox.rpc.JsonRest"] = true;
- dojo.provide("dojox.rpc.JsonRest");
- dojo.require("dojox.json.ref");
- dojo.require("dojox.rpc.Rest");
- 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 = dirtyObjects.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<dirtyObjects.length; i++){
- var dirty = dirtyObjects[i];
- if(object==dirty.object){
- if(_deleting){
- // we are deleting, no object is an indicator of deletiong
- dirty.object = false;
- if(!this._saveNotNeeded){
- dirty.save = true;
- }
- }
- return;
- }
- }
- var old = object instanceof Array ? [] : {};
- for(i in object){
- if(object.hasOwnProperty(i)){
- old[i] = object[i];
- }
- }
- dirtyObjects.push({object: !_deleting && object, old: old, save: !this._saveNotNeeded});
- },
- deleteObject: function(object){
- // summary:
- // deletes an object
- // object:
- // object to delete
- this.changing(object,true);
- },
- getConstructor: function(/*Function|String*/service, schema){
- // summary:
- // Creates or gets a constructor for objects from this service
- if(typeof service == 'string'){
- var servicePath = service;
- service = new dojox.rpc.Rest(service,true);
- this.registerService(service, servicePath, schema);
- }
- if(service._constructor){
- return service._constructor;
- }
- service._constructor = function(data){
- // summary:
- // creates a new object for this table
- //
- // data:
- // object to mixed in
- var self = this;
- var args = arguments;
- var properties;
- var initializeCalled;
- function addDefaults(schema){
- if(schema){
- addDefaults(schema['extends']);
- properties = schema.properties;
- for(var i in properties){
- var propDef = properties[i];
- if(propDef && (typeof propDef == 'object') && ("default" in propDef)){
- self[i] = propDef["default"];
- }
- }
- }
- if(schema && schema.prototype && schema.prototype.initialize){
- initializeCalled = true;
- schema.prototype.initialize.apply(self, args);
- }
- }
- addDefaults(service._schema);
- if(!initializeCalled && data && typeof data == 'object'){
- dojo.mixin(self,data);
- }
- var idAttribute = jr.getIdAttribute(service);
- Rest._index[this.__id = this.__clientId =
- service.servicePath + (this[idAttribute] ||
- Math.random().toString(16).substring(2,14) + '@' + ((dojox.rpc.Client && dojox.rpc.Client.clientId) || "client"))] = this;
- if(dojox.json.schema && properties){
- dojox.json.schema.mustBeValid(dojox.json.schema.validate(this, service._schema));
- }
- dirtyObjects.push({object:this, save: true});
- };
- return dojo.mixin(service._constructor, service._schema, {load:service});
- },
- fetch: function(absoluteId){
- // summary:
- // Fetches a resource by an absolute path/id and returns a dojo.Deferred.
- var serviceAndId = jr.getServiceAndId(absoluteId);
- return this.byId(serviceAndId.service,serviceAndId.id);
- },
- getIdAttribute: function(service){
- // summary:
- // Return the ids attribute used by this service (based on it's schema).
- // Defaults to "id", if not other id is defined
- var schema = service._schema;
- var idAttr;
- if(schema){
- if(!(idAttr = schema._idAttr)){
- for(var i in schema.properties){
- if(schema.properties[i].identity || (schema.properties[i].link == "self")){
- schema._idAttr = idAttr = i;
- }
- }
- }
- }
- return idAttr || 'id';
- },
- getServiceAndId: function(/*String*/absoluteId){
- // summary:
- // Returns the REST service and the local id for the given absolute id. The result
- // is returned as an object with a service property and an id property
- // absoluteId:
- // This is the absolute id of the object
- var serviceName = '';
-
- for(var service in jr.services){
- if((absoluteId.substring(0, service.length) == service) && (service.length >= 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){
- // summary
- // returns true if the item is marked as dirty or true if there are any dirty items
- if(!item){
- return !!dirtyObjects.length;
- }
- return item.__isDirty;
- }
-
- };
- }
|