source: Dev/trunk/src/client/dojox/rpc/JsonRest.js @ 532

Last change on this file since 532 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 15.7 KB
Line 
1define("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
Note: See TracBrowser for help on using the repository browser.