source: Dev/branches/rest-dojo-ui/client/dojox/rpc/JsonRest.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

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