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