[483] | 1 | define("dojox/rpc/JsonRest", ["dojo", "dojox", "dojox/json/ref", "dojox/rpc/Rest"], function(dojo, dojox) { |
---|
| 2 | var dirtyObjects = []; |
---|
| 3 | var Rest = dojox.rpc.Rest; |
---|
| 4 | var jr; |
---|
| 5 | function resolveJson(service, deferred, value, defaultId){ |
---|
| 6 | var timeStamp = deferred.ioArgs && deferred.ioArgs.xhr && deferred.ioArgs.xhr.getResponseHeader("Last-Modified"); |
---|
| 7 | if(timeStamp && Rest._timeStamps){ |
---|
| 8 | Rest._timeStamps[defaultId] = timeStamp; |
---|
| 9 | } |
---|
| 10 | var hrefProperty = service._schema && service._schema.hrefProperty; |
---|
| 11 | if(hrefProperty){ |
---|
| 12 | dojox.json.ref.refAttribute = hrefProperty; |
---|
| 13 | } |
---|
| 14 | value = value && dojox.json.ref.resolveJson(value, { |
---|
| 15 | defaultId: defaultId, |
---|
| 16 | index: Rest._index, |
---|
| 17 | timeStamps: timeStamp && Rest._timeStamps, |
---|
| 18 | time: timeStamp, |
---|
| 19 | idPrefix: service._store.allowNoTrailingSlash ? service.servicePath + '/' : service.servicePath.replace(/[^\/]*$/,''), |
---|
| 20 | idAttribute: jr.getIdAttribute(service), |
---|
| 21 | schemas: jr.schemas, |
---|
| 22 | loader: jr._loader, |
---|
| 23 | idAsRef: service.idAsRef, |
---|
| 24 | assignAbsoluteIds: true |
---|
| 25 | }); |
---|
| 26 | dojox.json.ref.refAttribute = "$ref"; |
---|
| 27 | return value; |
---|
| 28 | } |
---|
| 29 | jr = dojox.rpc.JsonRest={ |
---|
| 30 | serviceClass: dojox.rpc.Rest, |
---|
| 31 | conflictDateHeader: "If-Unmodified-Since", |
---|
| 32 | commit: function(kwArgs){ |
---|
| 33 | // summary: |
---|
| 34 | // Saves the dirty data using REST Ajax methods |
---|
| 35 | |
---|
| 36 | kwArgs = kwArgs || {}; |
---|
| 37 | var actions = []; |
---|
| 38 | var alreadyRecorded = {}; |
---|
| 39 | var savingObjects = []; |
---|
| 40 | for(var i = 0; i < dirtyObjects.length; i++){ |
---|
| 41 | var dirty = dirtyObjects[i]; |
---|
| 42 | var object = dirty.object; |
---|
| 43 | var old = dirty.old; |
---|
| 44 | var append = false; |
---|
| 45 | if(!(kwArgs.service && (object || old) && |
---|
| 46 | (object || old).__id.indexOf(kwArgs.service.servicePath)) && dirty.save){ |
---|
| 47 | delete object.__isDirty; |
---|
| 48 | if(object){ |
---|
| 49 | if(old){ |
---|
| 50 | // changed object |
---|
| 51 | var pathParts; |
---|
| 52 | if((pathParts = object.__id.match(/(.*)#.*/))){ // it is a path reference |
---|
| 53 | // this means it is a sub object, we must go to the parent object and save it |
---|
| 54 | object = Rest._index[pathParts[1]]; |
---|
| 55 | } |
---|
| 56 | if(!(object.__id in alreadyRecorded)){// if it has already been saved, we don't want to repeat it |
---|
| 57 | // record that we are saving |
---|
| 58 | alreadyRecorded[object.__id] = object; |
---|
| 59 | if(kwArgs.incrementalUpdates |
---|
| 60 | && !pathParts){ // I haven't figured out how we would do incremental updates on sub-objects yet |
---|
| 61 | // make an incremental update using a POST |
---|
| 62 | var incremental = (typeof kwArgs.incrementalUpdates == 'function' ? |
---|
| 63 | kwArgs.incrementalUpdates : function(){ |
---|
| 64 | incremental = {}; |
---|
| 65 | for(var j in object){ |
---|
| 66 | if(object.hasOwnProperty(j)){ |
---|
| 67 | if(object[j] !== old[j]){ |
---|
| 68 | incremental[j] = object[j]; |
---|
| 69 | } |
---|
| 70 | }else if(old.hasOwnProperty(j)){ |
---|
| 71 | // we can't use incremental updates to remove properties |
---|
| 72 | return null; |
---|
| 73 | } |
---|
| 74 | } |
---|
| 75 | return incremental; |
---|
| 76 | })(object, old); |
---|
| 77 | } |
---|
| 78 | |
---|
| 79 | if(incremental){ |
---|
| 80 | actions.push({method:"post",target:object, content: incremental}); |
---|
| 81 | } |
---|
| 82 | else{ |
---|
| 83 | actions.push({method:"put",target:object,content:object}); |
---|
| 84 | } |
---|
| 85 | } |
---|
| 86 | }else{ |
---|
| 87 | // new object |
---|
| 88 | var service = jr.getServiceAndId(object.__id).service; |
---|
| 89 | var idAttribute = jr.getIdAttribute(service); |
---|
| 90 | if((idAttribute in object) && !kwArgs.alwaysPostNewItems){ |
---|
| 91 | // if the id attribute is specified, then we should know the location |
---|
| 92 | actions.push({method:"put",target:object, content:object}); |
---|
| 93 | }else{ |
---|
| 94 | actions.push({method:"post",target:{__id:service.servicePath}, |
---|
| 95 | content:object}); |
---|
| 96 | } |
---|
| 97 | } |
---|
| 98 | }else if(old){ |
---|
| 99 | // deleted object |
---|
| 100 | actions.push({method:"delete",target:old}); |
---|
| 101 | }//else{ this would happen if an object is created and then deleted, don't do anything |
---|
| 102 | savingObjects.push(dirty); |
---|
| 103 | dirtyObjects.splice(i--,1); |
---|
| 104 | } |
---|
| 105 | } |
---|
| 106 | dojo.connect(kwArgs,"onError",function(){ |
---|
| 107 | if(kwArgs.revertOnError !== false){ |
---|
| 108 | var postCommitDirtyObjects = dirtyObjects; |
---|
| 109 | dirtyObjects = savingObjects; |
---|
| 110 | var numDirty = 0; // make sure this does't do anything if it is called again |
---|
| 111 | jr.revert(); // revert if there was an error |
---|
| 112 | dirtyObjects = postCommitDirtyObjects; |
---|
| 113 | } |
---|
| 114 | else{ |
---|
| 115 | dojo.forEach(savingObjects, function(obj) { |
---|
| 116 | jr.changing(obj.object, !obj.object); |
---|
| 117 | }); |
---|
| 118 | } |
---|
| 119 | }); |
---|
| 120 | jr.sendToServer(actions, kwArgs); |
---|
| 121 | return actions; |
---|
| 122 | }, |
---|
| 123 | sendToServer: function(actions, kwArgs){ |
---|
| 124 | var xhrSendId; |
---|
| 125 | var plainXhr = dojo.xhr; |
---|
| 126 | var left = actions.length;// this is how many changes are remaining to be received from the server |
---|
| 127 | var i, contentLocation; |
---|
| 128 | var timeStamp; |
---|
| 129 | var conflictDateHeader = this.conflictDateHeader; |
---|
| 130 | // add headers for extra information |
---|
| 131 | dojo.xhr = function(method,args){ |
---|
| 132 | // keep the transaction open as we send requests |
---|
| 133 | args.headers = args.headers || {}; |
---|
| 134 | // the last one should commit the transaction |
---|
| 135 | args.headers['Transaction'] = actions.length - 1 == i ? "commit" : "open"; |
---|
| 136 | if(conflictDateHeader && timeStamp){ |
---|
| 137 | args.headers[conflictDateHeader] = timeStamp; |
---|
| 138 | } |
---|
| 139 | if(contentLocation){ |
---|
| 140 | args.headers['Content-ID'] = '<' + contentLocation + '>'; |
---|
| 141 | } |
---|
| 142 | return plainXhr.apply(dojo,arguments); |
---|
| 143 | }; |
---|
| 144 | for(i =0; i < actions.length;i++){ // iterate through the actions to execute |
---|
| 145 | var action = actions[i]; |
---|
| 146 | dojox.rpc.JsonRest._contentId = action.content && action.content.__id; // this is used by OfflineRest |
---|
| 147 | var isPost = action.method == 'post'; |
---|
| 148 | timeStamp = action.method == 'put' && Rest._timeStamps[action.content.__id]; |
---|
| 149 | if(timeStamp){ |
---|
| 150 | // update it now |
---|
| 151 | Rest._timeStamps[action.content.__id] = (new Date()) + ''; |
---|
| 152 | } |
---|
| 153 | // send the content location to the server |
---|
| 154 | contentLocation = isPost && dojox.rpc.JsonRest._contentId; |
---|
| 155 | var serviceAndId = jr.getServiceAndId(action.target.__id); |
---|
| 156 | var service = serviceAndId.service; |
---|
| 157 | var dfd = action.deferred = service[action.method]( |
---|
| 158 | serviceAndId.id.replace(/#/,''), // if we are using references, we need eliminate # |
---|
| 159 | dojox.json.ref.toJson(action.content, false, service.servicePath, true) |
---|
| 160 | ); |
---|
| 161 | (function(object, dfd, service){ |
---|
| 162 | dfd.addCallback(function(value){ |
---|
| 163 | try{ |
---|
| 164 | // Implements id assignment per the HTTP specification |
---|
| 165 | var newId = dfd.ioArgs.xhr && dfd.ioArgs.xhr.getResponseHeader("Location"); |
---|
| 166 | //TODO: match URLs if the servicePath is relative... |
---|
| 167 | if(newId){ |
---|
| 168 | // if the path starts in the middle of an absolute URL for Location, we will use the just the path part |
---|
| 169 | var startIndex = newId.match(/(^\w+:\/\/)/) && newId.indexOf(service.servicePath); |
---|
| 170 | newId = startIndex > 0 ? newId.substring(startIndex) : (service.servicePath + newId). |
---|
| 171 | // now do simple relative URL resolution in case of a relative URL. |
---|
| 172 | replace(/^(.*\/)?(\w+:\/\/)|[^\/\.]+\/\.\.\/|^.*\/(\/)/,'$2$3'); |
---|
| 173 | object.__id = newId; |
---|
| 174 | Rest._index[newId] = object; |
---|
| 175 | } |
---|
| 176 | value = resolveJson(service, dfd, value, object && object.__id); |
---|
| 177 | }catch(e){} |
---|
| 178 | if(!(--left)){ |
---|
| 179 | if(kwArgs.onComplete){ |
---|
| 180 | kwArgs.onComplete.call(kwArgs.scope, actions); |
---|
| 181 | } |
---|
| 182 | } |
---|
| 183 | return value; |
---|
| 184 | }); |
---|
| 185 | })(action.content, dfd, service); |
---|
| 186 | |
---|
| 187 | dfd.addErrback(function(value){ |
---|
| 188 | |
---|
| 189 | // on an error we want to revert, first we want to separate any changes that were made since the commit |
---|
| 190 | left = -1; // first make sure that success isn't called |
---|
| 191 | kwArgs.onError.call(kwArgs.scope, value); |
---|
| 192 | }); |
---|
| 193 | } |
---|
| 194 | // revert back to the normal XHR handler |
---|
| 195 | dojo.xhr = plainXhr; |
---|
| 196 | |
---|
| 197 | }, |
---|
| 198 | getDirtyObjects: function(){ |
---|
| 199 | return dirtyObjects; |
---|
| 200 | }, |
---|
| 201 | revert: function(service){ |
---|
| 202 | // summary: |
---|
| 203 | // Reverts all the changes made to JSON/REST data |
---|
| 204 | for(var i = dirtyObjects.length; i > 0;){ |
---|
| 205 | i--; |
---|
| 206 | var dirty = dirtyObjects[i]; |
---|
| 207 | var object = dirty.object; |
---|
| 208 | var old = dirty.old; |
---|
| 209 | var store = dojox.data._getStoreForItem(object || old); |
---|
| 210 | |
---|
| 211 | if(!(service && (object || old) && |
---|
| 212 | (object || old).__id.indexOf(service.servicePath))){ |
---|
| 213 | // if we are in the specified store or if this is a global revert |
---|
| 214 | if(object && old){ |
---|
| 215 | // changed |
---|
| 216 | for(var j in old){ |
---|
| 217 | if(old.hasOwnProperty(j) && object[j] !== old[j]){ |
---|
| 218 | if(store){ |
---|
| 219 | store.onSet(object, j, object[j], old[j]); |
---|
| 220 | } |
---|
| 221 | object[j] = old[j]; |
---|
| 222 | } |
---|
| 223 | } |
---|
| 224 | for(j in object){ |
---|
| 225 | if(!old.hasOwnProperty(j)){ |
---|
| 226 | if(store){ |
---|
| 227 | store.onSet(object, j, object[j]); |
---|
| 228 | } |
---|
| 229 | delete object[j]; |
---|
| 230 | } |
---|
| 231 | } |
---|
| 232 | }else if(!old){ |
---|
| 233 | // was an addition, remove it |
---|
| 234 | if(store){ |
---|
| 235 | store.onDelete(object); |
---|
| 236 | } |
---|
| 237 | }else{ |
---|
| 238 | // was a deletion, we will add it back |
---|
| 239 | if(store){ |
---|
| 240 | store.onNew(old); |
---|
| 241 | } |
---|
| 242 | } |
---|
| 243 | delete (object || old).__isDirty; |
---|
| 244 | dirtyObjects.splice(i, 1); |
---|
| 245 | } |
---|
| 246 | } |
---|
| 247 | }, |
---|
| 248 | changing: function(object,_deleting){ |
---|
| 249 | // summary: |
---|
| 250 | // adds an object to the list of dirty objects. This object |
---|
| 251 | // contains a reference to the object itself as well as a |
---|
| 252 | // cloned and trimmed version of old object for use with |
---|
| 253 | // revert. |
---|
| 254 | if(!object.__id){ |
---|
| 255 | return; |
---|
| 256 | } |
---|
| 257 | object.__isDirty = true; |
---|
| 258 | //if an object is already in the list of dirty objects, don't add it again |
---|
| 259 | //or it will overwrite the premodification data set. |
---|
| 260 | for(var i=0; i<dirtyObjects.length; i++){ |
---|
| 261 | var dirty = dirtyObjects[i]; |
---|
| 262 | if(object==dirty.object){ |
---|
| 263 | if(_deleting){ |
---|
| 264 | // we are deleting, no object is an indicator of deletiong |
---|
| 265 | dirty.object = false; |
---|
| 266 | if(!this._saveNotNeeded){ |
---|
| 267 | dirty.save = true; |
---|
| 268 | } |
---|
| 269 | } |
---|
| 270 | return; |
---|
| 271 | } |
---|
| 272 | } |
---|
| 273 | var old = object instanceof Array ? [] : {}; |
---|
| 274 | for(i in object){ |
---|
| 275 | if(object.hasOwnProperty(i)){ |
---|
| 276 | old[i] = object[i]; |
---|
| 277 | } |
---|
| 278 | } |
---|
| 279 | dirtyObjects.push({object: !_deleting && object, old: old, save: !this._saveNotNeeded}); |
---|
| 280 | }, |
---|
| 281 | deleteObject: function(object){ |
---|
| 282 | // summary: |
---|
| 283 | // deletes an object |
---|
| 284 | // object: |
---|
| 285 | // object to delete |
---|
| 286 | this.changing(object,true); |
---|
| 287 | }, |
---|
| 288 | getConstructor: function(/*Function|String*/service, schema){ |
---|
| 289 | // summary: |
---|
| 290 | // Creates or gets a constructor for objects from this service |
---|
| 291 | if(typeof service == 'string'){ |
---|
| 292 | var servicePath = service; |
---|
| 293 | service = new dojox.rpc.Rest(service,true); |
---|
| 294 | this.registerService(service, servicePath, schema); |
---|
| 295 | } |
---|
| 296 | if(service._constructor){ |
---|
| 297 | return service._constructor; |
---|
| 298 | } |
---|
| 299 | service._constructor = function(data){ |
---|
| 300 | // summary: |
---|
| 301 | // creates a new object for this table |
---|
| 302 | // data: |
---|
| 303 | // object to mixed in |
---|
| 304 | var self = this; |
---|
| 305 | var args = arguments; |
---|
| 306 | var properties; |
---|
| 307 | var initializeCalled; |
---|
| 308 | function addDefaults(schema){ |
---|
| 309 | if(schema){ |
---|
| 310 | addDefaults(schema['extends']); |
---|
| 311 | properties = schema.properties; |
---|
| 312 | for(var i in properties){ |
---|
| 313 | var propDef = properties[i]; |
---|
| 314 | if(propDef && (typeof propDef == 'object') && ("default" in propDef)){ |
---|
| 315 | self[i] = propDef["default"]; |
---|
| 316 | } |
---|
| 317 | } |
---|
| 318 | } |
---|
| 319 | if(schema && schema.prototype && schema.prototype.initialize){ |
---|
| 320 | initializeCalled = true; |
---|
| 321 | schema.prototype.initialize.apply(self, args); |
---|
| 322 | } |
---|
| 323 | } |
---|
| 324 | addDefaults(service._schema); |
---|
| 325 | if(!initializeCalled && data && typeof data == 'object'){ |
---|
| 326 | dojo.mixin(self,data); |
---|
| 327 | } |
---|
| 328 | var idAttribute = jr.getIdAttribute(service); |
---|
| 329 | Rest._index[this.__id = this.__clientId = |
---|
| 330 | service.servicePath + (this[idAttribute] || |
---|
| 331 | Math.random().toString(16).substring(2,14) + '@' + ((dojox.rpc.Client && dojox.rpc.Client.clientId) || "client"))] = this; |
---|
| 332 | if(dojox.json.schema && properties){ |
---|
| 333 | dojox.json.schema.mustBeValid(dojox.json.schema.validate(this, service._schema)); |
---|
| 334 | } |
---|
| 335 | dirtyObjects.push({object:this, save: true}); |
---|
| 336 | }; |
---|
| 337 | return dojo.mixin(service._constructor, service._schema, {load:service}); |
---|
| 338 | }, |
---|
| 339 | fetch: function(absoluteId){ |
---|
| 340 | // summary: |
---|
| 341 | // Fetches a resource by an absolute path/id and returns a dojo.Deferred. |
---|
| 342 | var serviceAndId = jr.getServiceAndId(absoluteId); |
---|
| 343 | return this.byId(serviceAndId.service,serviceAndId.id); |
---|
| 344 | }, |
---|
| 345 | getIdAttribute: function(service){ |
---|
| 346 | // summary: |
---|
| 347 | // Return the ids attribute used by this service (based on it's schema). |
---|
| 348 | // Defaults to "id", if not other id is defined |
---|
| 349 | var schema = service._schema; |
---|
| 350 | var idAttr; |
---|
| 351 | if(schema){ |
---|
| 352 | if(!(idAttr = schema._idAttr)){ |
---|
| 353 | for(var i in schema.properties){ |
---|
| 354 | if(schema.properties[i].identity || (schema.properties[i].link == "self")){ |
---|
| 355 | schema._idAttr = idAttr = i; |
---|
| 356 | } |
---|
| 357 | } |
---|
| 358 | } |
---|
| 359 | } |
---|
| 360 | return idAttr || 'id'; |
---|
| 361 | }, |
---|
| 362 | getServiceAndId: function(/*String*/absoluteId){ |
---|
| 363 | // summary: |
---|
| 364 | // Returns the REST service and the local id for the given absolute id. The result |
---|
| 365 | // is returned as an object with a service property and an id property |
---|
| 366 | // absoluteId: |
---|
| 367 | // This is the absolute id of the object |
---|
| 368 | var serviceName = ''; |
---|
| 369 | |
---|
| 370 | for(var service in jr.services){ |
---|
| 371 | if((absoluteId.substring(0, service.length) == service) && (service.length >= serviceName.length)){ |
---|
| 372 | serviceName = service; |
---|
| 373 | } |
---|
| 374 | } |
---|
| 375 | if (serviceName){ |
---|
| 376 | return {service: jr.services[serviceName], id:absoluteId.substring(serviceName.length)}; |
---|
| 377 | } |
---|
| 378 | var parts = absoluteId.match(/^(.*\/)([^\/]*)$/); |
---|
| 379 | return {service: new jr.serviceClass(parts[1], true), id:parts[2]}; |
---|
| 380 | }, |
---|
| 381 | services:{}, |
---|
| 382 | schemas:{}, |
---|
| 383 | registerService: function(/*Function*/ service, /*String*/ servicePath, /*Object?*/ schema){ |
---|
| 384 | // summary: |
---|
| 385 | // Registers a service for as a JsonRest service, mapping it to a path and schema |
---|
| 386 | // service: |
---|
| 387 | // This is the service to register |
---|
| 388 | // servicePath: |
---|
| 389 | // This is the path that is used for all the ids for the objects returned by service |
---|
| 390 | // schema: |
---|
| 391 | // This is a JSON Schema object to associate with objects returned by this service |
---|
| 392 | servicePath = service.servicePath = servicePath || service.servicePath; |
---|
| 393 | service._schema = jr.schemas[servicePath] = schema || service._schema || {}; |
---|
| 394 | jr.services[servicePath] = service; |
---|
| 395 | }, |
---|
| 396 | byId: function(service, id){ |
---|
| 397 | // if caching is allowed, we look in the cache for the result |
---|
| 398 | var deferred, result = Rest._index[(service.servicePath || '') + id]; |
---|
| 399 | if(result && !result._loadObject){// cache hit |
---|
| 400 | deferred = new dojo.Deferred(); |
---|
| 401 | deferred.callback(result); |
---|
| 402 | return deferred; |
---|
| 403 | } |
---|
| 404 | return this.query(service, id); |
---|
| 405 | }, |
---|
| 406 | query: function(service, id, args){ |
---|
| 407 | var deferred = service(id, args); |
---|
| 408 | |
---|
| 409 | deferred.addCallback(function(result){ |
---|
| 410 | if(result.nodeType && result.cloneNode){ |
---|
| 411 | // return immediately if it is an XML document |
---|
| 412 | return result; |
---|
| 413 | } |
---|
| 414 | return resolveJson(service, deferred, result, typeof id != 'string' || (args && (args.start || args.count)) ? undefined: id); |
---|
| 415 | }); |
---|
| 416 | return deferred; |
---|
| 417 | }, |
---|
| 418 | _loader: function(callback){ |
---|
| 419 | // load a lazy object |
---|
| 420 | var serviceAndId = jr.getServiceAndId(this.__id); |
---|
| 421 | var self = this; |
---|
| 422 | jr.query(serviceAndId.service, serviceAndId.id).addBoth(function(result){ |
---|
| 423 | // if they are the same this means an object was loaded, otherwise it |
---|
| 424 | // might be a primitive that was loaded or maybe an error |
---|
| 425 | if(result == self){ |
---|
| 426 | // we can clear the flag, so it is a loaded object |
---|
| 427 | delete result.$ref; |
---|
| 428 | delete result._loadObject; |
---|
| 429 | }else{ |
---|
| 430 | // it is probably a primitive value, we can't change the identity of an object to |
---|
| 431 | // the loaded value, so we will keep it lazy, but define the lazy loader to always |
---|
| 432 | // return the loaded value |
---|
| 433 | self._loadObject = function(callback){ |
---|
| 434 | callback(result); |
---|
| 435 | }; |
---|
| 436 | } |
---|
| 437 | callback(result); |
---|
| 438 | }); |
---|
| 439 | }, |
---|
| 440 | isDirty: function(item, store){ |
---|
| 441 | // summary: |
---|
| 442 | // returns true if the item is marked as dirty or true if there are any dirty items |
---|
| 443 | if(!item){ |
---|
| 444 | if(store){ |
---|
| 445 | return dojo.some(dirtyObjects, function(dirty){ |
---|
| 446 | return dojox.data._getStoreForItem(dirty.object || dirty.old) == store; |
---|
| 447 | }); |
---|
| 448 | } |
---|
| 449 | return !!dirtyObjects.length; |
---|
| 450 | } |
---|
| 451 | return item.__isDirty; |
---|
| 452 | } |
---|
| 453 | |
---|
| 454 | }; |
---|
| 455 | |
---|
| 456 | return dojox.rpc.JsonRest; |
---|
| 457 | }); |
---|
| 458 | |
---|