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