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