support reverting individual entities - makes conflict resolution work
[potlatch2.git] / net / systemeD / halcyon / connection / XMLConnection.as
1 package net.systemeD.halcyon.connection {
2
3     import flash.events.*;
4
5         import flash.system.Security;
6         import flash.net.*;
7     import org.iotashan.oauth.*;
8
9         import net.systemeD.halcyon.AttentionEvent;
10         import net.systemeD.halcyon.MapEvent;
11
12     /**
13     * XMLConnection provides all the methods required to connect to a live
14     * OSM server. See OSMConnection for connecting to a read-only .osm file
15     */
16         public class XMLConnection extends XMLBaseConnection {
17
18         //public var readConnection:NetConnection;
19
20                 public function XMLConnection() {
21
22                         if (Connection.policyURL!='')
23                 Security.loadPolicyFile(Connection.policyURL);
24             var oauthPolicy:String = Connection.getParam("oauth_policy", "");
25             if ( oauthPolicy != "" ) {
26                 Security.loadPolicyFile(oauthPolicy);
27             }
28                 }
29                 
30                 override public function loadBbox(left:Number,right:Number,
31                                                                 top:Number,bottom:Number):void {
32             var mapVars:URLVariables = new URLVariables();
33             mapVars.bbox= left+","+bottom+","+right+","+top;
34
35             var mapRequest:URLRequest = new URLRequest(Connection.apiBaseURL+"map");
36             mapRequest.data = mapVars;
37
38             sendLoadRequest(mapRequest);
39                 }
40
41                 override public function loadEntity(entity:Entity):void {
42                         var url:String=Connection.apiBaseURL + entity.getType() + "/" + entity.id;
43                         if (entity is Relation || entity is Way) url+="/full";
44                         sendLoadRequest(new URLRequest(url));
45                 }
46
47                 private function sendLoadRequest(request:URLRequest):void {
48                         var mapLoader:URLLoader = new URLLoader();
49                         mapLoader.addEventListener(Event.COMPLETE, loadedMap);
50                         mapLoader.addEventListener(IOErrorEvent.IO_ERROR, errorOnMapLoad);
51                         mapLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, mapLoadStatus);
52                         mapLoader.load(request);
53                         dispatchEvent(new Event(LOAD_STARTED));
54                 }
55
56         private function errorOnMapLoad(event:Event):void {
57                         dispatchEvent(new MapEvent(MapEvent.ERROR, { message: "Couldn't load the map" } ));
58         }
59         private function mapLoadStatus(event:HTTPStatusEvent):void {
60             trace("loading map status = "+event.status);
61         }
62
63         protected var appID:OAuthConsumer;
64         protected var authToken:OAuthToken;
65
66             override public function setAuthToken(id:Object):void {
67                 authToken = OAuthToken(id);
68             }
69
70         override public function hasAccessToken():Boolean {
71             return !(getAccessToken() == null);
72         }
73
74         override public function setAccessToken(key:String, secret:String):void {
75             if (key && secret) {
76               authToken = new OAuthToken(key, secret);
77             }
78         }
79
80         /* Get the stored access token, or try setting it up from loader params */
81         private function getAccessToken():OAuthToken {
82             if (authToken == null) {
83               var key:String = getParam("oauth_token", null);
84               var secret:String = getParam("oauth_token_secret", null);
85
86               if ( key != null && secret != null ) {
87                   authToken = new OAuthToken(key, secret);
88               }
89             }
90             return authToken;
91         }
92
93         private function getConsumer():OAuthConsumer {
94             if (appID == null) {
95               var key:String = getParam("oauth_consumer_key", null);
96               var secret:String = getParam("oauth_consumer_secret", null);
97
98               if ( key != null && secret != null ) {
99                   appID = new OAuthConsumer(key, secret);
100               }
101             }
102             return appID;
103         }
104
105         private var httpStatus:int = 0;
106         
107         private function recordStatus(event:HTTPStatusEvent):void {
108             httpStatus = event.status;
109         }
110         
111         private var lastUploadedChangesetTags:Object;
112         
113         override public function createChangeset(tags:Object):void {
114             lastUploadedChangesetTags = tags;
115             
116                 var changesetXML:XML = <osm version="0.6"><changeset /></osm>;
117                 var changeset:XML = <changeset />;
118                 for (var tagKey:Object in tags) {
119               var tagXML:XML = <tag/>;
120               tagXML.@k = tagKey;
121               tagXML.@v = tags[tagKey];
122               changesetXML.changeset.appendChild(tagXML);
123             }        
124
125                         sendOAuthPut(Connection.apiBaseURL+"changeset/create",
126                                                  changesetXML,
127                                                  changesetCreateComplete, changesetCreateError, recordStatus);
128             }
129
130         private function changesetCreateComplete(event:Event):void {
131             // response should be a Number changeset id
132             var id:Number = Number(URLLoader(event.target).data);
133             
134             // which means we now have a new changeset!
135             setActiveChangeset(new Changeset(id, lastUploadedChangesetTags));
136         }
137
138         private function changesetCreateError(event:IOErrorEvent):void {
139             dispatchEvent(new Event(NEW_CHANGESET_ERROR));
140         }
141
142                 override public function closeChangeset():void {
143             var cs:Changeset = getActiveChangeset();
144                         if (!cs) return;
145                         
146                         sendOAuthPut(Connection.apiBaseURL+"changeset/"+cs.id+"/close",
147                                                  null,
148                                                  changesetCloseComplete, changesetCloseError, recordStatus);
149                         closeActiveChangeset();
150                 }
151                 
152                 private function changesetCloseComplete(event:Event):void { 
153                         dispatchEvent(new AttentionEvent(AttentionEvent.ALERT, null, "Changeset closed"));
154                 }
155                 private function changesetCloseError(event:Event):void { 
156                         dispatchEvent(new AttentionEvent(AttentionEvent.ALERT, null, "Couldn't close changeset", 1));
157                 }
158
159         private function signedOAuthURL(url:String, method:String):String {
160             // method should be PUT, GET, POST or DELETE
161             var sig:IOAuthSignatureMethod = new OAuthSignatureMethod_HMAC_SHA1();
162             var oauthRequest:OAuthRequest = new OAuthRequest(method, url, null, getConsumer(), authToken);
163             var urlStr:Object = oauthRequest.buildRequest(sig, OAuthRequest.RESULT_TYPE_URL_STRING);
164             return String(urlStr);
165         }
166
167                 private function sendOAuthPut(url:String, xml:XML, onComplete:Function, onError:Function, onStatus:Function):void {
168             // build the request
169             var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "PUT"));
170             urlReq.method = "POST";
171                         if (xml) { urlReq.data = xml.toXMLString(); } else { urlReq.data = true; }
172             urlReq.contentType = "application/xml";
173             urlReq.requestHeaders = new Array(new URLRequestHeader("X_HTTP_METHOD_OVERRIDE", "PUT"));
174             var loader:URLLoader = new URLLoader();
175             loader.addEventListener(Event.COMPLETE, onComplete);
176             loader.addEventListener(IOErrorEvent.IO_ERROR, onError);
177             loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, onStatus);
178                 loader.load(urlReq);
179                 }
180
181         private function sendOAuthGet(url:String, onComplete:Function, onError:Function, onStatus:Function):void {
182             var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "GET"));
183             urlReq.method = "GET";
184             var loader:URLLoader = new URLLoader();
185             loader.addEventListener(Event.COMPLETE, onComplete);
186             loader.addEventListener(IOErrorEvent.IO_ERROR, onError);
187             loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, onStatus);
188             loader.load(urlReq);
189         }
190
191         override public function uploadChanges():void {
192             var changeset:Changeset = getActiveChangeset();
193             var upload:XML = <osmChange version="0.6"/>
194             upload.appendChild(addCreated(changeset, getAllNodeIDs, getNode, serialiseNode));
195             upload.appendChild(addCreated(changeset, getAllWayIDs, getWay, serialiseWay));
196             upload.appendChild(addCreated(changeset, getAllRelationIDs, getRelation, serialiseRelation));
197             upload.appendChild(addModified(changeset, getAllNodeIDs, getNode, serialiseNode));
198             upload.appendChild(addModified(changeset, getAllWayIDs, getWay, serialiseWay));
199             upload.appendChild(addModified(changeset, getAllRelationIDs, getRelation, serialiseRelation));
200             upload.appendChild(addDeleted(changeset, getAllRelationIDs, getRelation, serialiseEntityRoot, false));
201             upload.appendChild(addDeleted(changeset, getAllRelationIDs, getRelation, serialiseEntityRoot, true));
202             upload.appendChild(addDeleted(changeset, getAllWayIDs, getWay, serialiseEntityRoot, false));
203             upload.appendChild(addDeleted(changeset, getAllWayIDs, getWay, serialiseEntityRoot, true));
204             upload.appendChild(addDeleted(changeset, getAllNodeIDs, getNode, serialiseEntityRoot, false));
205             upload.appendChild(addDeleted(changeset, getAllNodeIDs, getNode, serialiseEntityRoot, true));
206
207             // now actually upload them
208             // make an OAuth query
209
210             var url:String = Connection.apiBaseURL+"changeset/" + changeset.id + "/upload";
211
212             // build the actual request
213             var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "POST"));
214             urlReq.method = "POST";
215             urlReq.data = upload.toXMLString();
216             urlReq.contentType = "text/xml";
217             urlReq.requestHeaders = [new URLRequestHeader("X-Error-Format","xml")];
218             var loader:URLLoader = new URLLoader();
219             loader.dataFormat = URLLoaderDataFormat.BINARY;
220             loader.addEventListener(Event.COMPLETE, diffUploadComplete);
221             loader.addEventListener(IOErrorEvent.IO_ERROR, function(event:IOErrorEvent):void { trace(urlReq.data); diffUploadIOError(event); } );
222             loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, recordStatus);
223                 loader.load(urlReq);
224                 
225                 dispatchEvent(new Event(SAVE_STARTED));
226         }
227
228         private function diffUploadComplete(event:Event):void {
229                         var results:XML = new XML(URLLoader(event.target).data);
230
231                         // was it an error document?
232                         if (results.name().localName=='osmError') {
233                         dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
234                                 diffUploadAPIError(results.status, results.message);
235                                 return;
236                         }
237
238             // response should be XML describing the progress
239             
240             for each( var update:XML in results.child("*") ) {
241                 var oldID:Number = Number(update.@old_id);
242                 var newID:Number = Number(update.@new_id);
243                 var version:uint = uint(update.@new_version);
244                 var type:String = update.name();
245
246                                 if (newID==0) {
247                                         // delete
248                         if      (type == "node"    ) { killNode(oldID); }
249                         else if (type == "way"     ) { killWay(oldID); }
250                         else if (type == "relation") { killRelation(oldID); }
251                                         
252                                 } else {
253                                         // create/update
254                         if      (type == "node"    ) { renumberNode(oldID, newID, version); getNode(newID).markClean(); }
255                         else if (type == "way"     ) { renumberWay(oldID, newID, version); getWay(newID).markClean(); }
256                         else if (type == "relation") { renumberRelation(oldID, newID, version); getRelation(newID).markClean(); }
257                                 }
258             }
259
260             dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, true));
261                         freshenActiveChangeset();
262             markClean(); // marks the connection clean. Pressing undo from this point on leads to unexpected results
263             MainUndoStack.getGlobalStack().breakUndo(); // so, for now, break the undo stack
264         }
265
266         private function diffUploadIOError(event:IOErrorEvent):void {
267                         dispatchEvent(new MapEvent(MapEvent.ERROR, { message: "Couldn't upload data: "+httpStatus+" "+event.text } ));
268                 dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
269         }
270
271                 private function diffUploadAPIError(status:String, message:String):void {
272                         var matches:Array;
273                         switch (status) {
274
275                                 case '409 Conflict':
276                                         if (message.match(/changeset/i)) { throwChangesetError(message); return; }
277                                         matches=message.match(/mismatch.+had: (\d+) of (\w+) (\d+)/i);
278                                         if (matches) { throwConflictError(findEntity(matches[2],matches[3]), Number(matches[1]), message); return; }
279                                         break;
280                                 
281                                 case '410 Gone':
282                                         matches=message.match(/The (\w+) with the id (\d+)/i);
283                                         if (matches) { throwAlreadyDeletedError(findEntity(matches[1],matches[2]), message); return; }
284                                         break;
285                                 
286                                 case '412 Precondition Failed':
287                                         matches=message.match(/Node (\d+) is still used/i);
288                                         if (matches) { throwInUseError(findEntity('Node',matches[1]), message); return; }
289                                         matches=message.match(/relation (\d+) is used/i);
290                                         if (matches) { throwInUseError(findEntity('Relation',matches[1]), message); return; }
291                                         matches=message.match(/Way (\d+) still used/i);
292                                         if (matches) { throwInUseError(findEntity('Way',matches[1]), message); return; }
293                                         matches=message.match(/Cannot update (\w+) (\d+)/i);
294                                         if (matches) { throwEntityError(findEntity(matches[1],matches[2]), message); return; }
295                                         matches=message.match(/Relation with id (\d+)/i);
296                                         if (matches) { throwEntityError(findEntity('Relation',matches[1]), message); return; }
297                                         matches=message.match(/Way (\d+) requires the nodes/i);
298                                         if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; }
299                                         throwBugError(message); return;
300                                 
301                                 case '404 Not Found':
302                                         throwBugError(message); return;
303                                         
304                                 case '400 Bad Request':
305                                         matches=message.match(/Element (\w+)\/(\d+)/i);
306                                         if (matches) { throwEntityError(findEntity(matches[1],matches[2]), message); return; }
307                                         matches=message.match(/You tried to add \d+ nodes to way (\d+)/i);
308                                         if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; }
309                                         throwBugError(message); return;
310                         }
311
312                         // Not caught, so just throw a generic server error
313                         throwServerError(message);
314                 }
315
316         private function addCreated(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
317             var create:XML = <create version="0.6"/>
318             for each( var id:Number in getIDs() ) {
319                 var entity:Entity = get(id);
320                 if ( id >= 0 || entity.deleted )
321                     continue;
322                     
323                 var xml:XML = serialise(entity);
324                 xml.@changeset = changeset.id;
325                 create.appendChild(xml);
326             }
327             return create.hasComplexContent() ? create : <!-- blank create section -->;
328         }
329
330                 private function addDeleted(changeset:Changeset, getIDs:Function, get:Function, serialise:Function, ifUnused:Boolean):XML {
331             var del:XML = <delete version="0.6"/>
332             if (ifUnused) del.@["if-unused"] = "true";
333             for each( var id:Number in getIDs() ) {
334                 var entity:Entity = get(id);
335                 // creates are already included
336                 if ( id < 0 || !entity.deleted || entity.parentsLoaded==ifUnused)
337                     continue;
338                     
339                 var xml:XML = serialise(entity);
340                 xml.@changeset = changeset.id;
341                 del.appendChild(xml);
342             }
343             return del.hasComplexContent() ? del : <!-- blank delete section -->;
344                 }
345
346         private function addModified(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
347             var modify:XML = <modify version="0.6"/>
348             for each( var id:Number in getIDs() ) {
349                 var entity:Entity = get(id);
350                 // creates and deletes are already included
351                 if ( id < 0 || entity.deleted || !entity.isDirty )
352                     continue;
353                     
354                 var xml:XML = serialise(entity);
355                 xml.@changeset = changeset.id;
356                 modify.appendChild(xml);
357             }
358             return modify.hasComplexContent() ? modify : <!-- blank modify section -->;
359         }
360
361         private function serialiseNode(node:Node):XML {
362             var xml:XML = serialiseEntityRoot(node); //<node/>
363             serialiseEntityTags(node, xml);
364             xml.@lat = node.lat;
365             xml.@lon = node.lon;
366             return xml;
367         }
368
369         private function serialiseWay(way:Way):XML {
370             var xml:XML = serialiseEntityRoot(way); //<node/>
371             serialiseEntityTags(way, xml);
372             for ( var i:uint = 0; i < way.length; i++ ) {
373                 var nd:XML = <nd/>
374                 nd.@ref = way.getNode(i).id;
375                 xml.appendChild(nd);
376             }
377             return xml;
378         }
379
380         private function serialiseRelation(relation:Relation):XML {
381             var xml:XML = serialiseEntityRoot(relation); //<node/>
382             serialiseEntityTags(relation, xml);
383             for ( var i:uint = 0; i < relation.length; i++ ) {
384                 var relMember:RelationMember = relation.getMember(i);
385                 var member:XML = <member/>
386                 member.@ref = relMember.entity.id;
387                 member.@type = relMember.entity.getType();
388                 member.@role = relMember.role;
389                 xml.appendChild(member);
390             }
391             return xml;
392         }
393         
394                 private function serialiseEntityRoot(entity:Object):XML {
395                         var xml:XML;
396                         if      (entity is Way     ) { xml = <way/> }
397                         else if (entity is Node    ) { xml = <node/> }
398                         else if (entity is Relation) { xml = <relation/> }
399                         xml.@id = entity.id;
400                         xml.@version = entity.version;
401                         return xml;
402                 }
403
404         private function serialiseEntityTags(entity:Entity, xml:XML):void {
405             xml.@id = entity.id;
406             xml.@version = entity.version;
407             for each( var tag:Tag in entity.getTagArray() ) {
408               if (tag.key == 'created_by') {
409                 entity.setTag('created_by', null, MainUndoStack.getGlobalStack().addAction);
410                 continue;
411               }
412               var tagXML:XML = <tag/>
413               tagXML.@k = tag.key;
414               tagXML.@v = tag.value;
415               xml.appendChild(tagXML);
416             }
417         }
418
419         override public function fetchUserTraces(refresh:Boolean=false):void {
420             if (traces_loaded && !refresh) {
421               dispatchEvent(new Event(TRACES_LOADED));
422             } else {
423               sendOAuthGet(Connection.apiBaseURL+"user/gpx_files",
424                           tracesLoadComplete, errorOnMapLoad, mapLoadStatus); //needs error handlers
425               dispatchEvent(new Event(LOAD_STARTED)); //specific to map or reusable?
426             }
427         }
428
429         private function tracesLoadComplete(event:Event):void {
430             clearTraces();
431             var files:XML = new XML(URLLoader(event.target).data);
432             for each(var traceData:XML in files.gpx_file) {
433               var t:Trace = new Trace().fromXML(traceData);
434               addTrace(t);
435             }
436             traces_loaded = true;
437             dispatchEvent(new Event(LOAD_COMPLETED));
438             dispatchEvent(new Event(TRACES_LOADED));
439         }
440
441         override public function fetchTrace(id:Number, callback:Function):void {
442             sendOAuthGet(Connection.apiBaseURL+"gpx/"+id+"/data.xml", 
443                                 function(e:Event):void { 
444                         dispatchEvent(new Event(LOAD_COMPLETED));
445                                         callback(e);
446                                 }, errorOnMapLoad, mapLoadStatus); // needs error handlers
447             dispatchEvent(new Event(LOAD_STARTED)); //specifc to map or reusable?
448         }
449         }
450 }