JsonRest.js 16 KB

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