You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
458 lines
16 KiB
458 lines
16 KiB
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._store.allowNoTrailingSlash ? service.servicePath + '/' : 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 doesn't do anything if it is called again |
|
jr.revert(); // revert if there was an error |
|
dirtyObjects = postCommitDirtyObjects; |
|
} |
|
else{ |
|
dojo.forEach(savingObjects, function(obj) { |
|
jr.changing(obj.object, !obj.object); |
|
}); |
|
} |
|
}); |
|
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, 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; |
|
}); |
|
|
|
|