JsonRest.js 16 KB

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