Merge branch 'master' into history
[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         import net.systemeD.halcyon.ExtendedURLLoader;
13
14     /**
15     * XMLConnection provides all the methods required to connect to a live
16     * OSM server. See OSMConnection for connecting to a read-only .osm file
17     */
18         public class XMLConnection extends XMLBaseConnection {
19
20                 public function XMLConnection(name:String,api:String,policy:String,initparams:Object) {
21
22                         super(name,api,policy,initparams);
23                         if (policyURL != "") Security.loadPolicyFile(policyURL);
24
25             var oauthPolicy:String = getParam("oauth_policy", "");
26             if (oauthPolicy != "") Security.loadPolicyFile(oauthPolicy);
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(apiBaseURL+"map");
44             mapRequest.data = mapVars;
45
46             sendLoadRequest(mapRequest);
47                 }
48
49                 override public function loadEntityByID(type:String, id:Number):void {
50                         var url:String=apiBaseURL + type + "/" + id;
51                         if (type=='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(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(this, 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(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                 /** Create XML changeset and send it to the server. Returns the XML string for use in the 'Show data' button.
203                     (We don't mind what's returned as long as it implements .toString() ) */
204
205         override public function uploadChanges():* {
206             var changeset:Changeset = getActiveChangeset();
207             var upload:XML = <osmChange version="0.6"/>
208             upload.appendChild(addCreated(changeset, getAllNodeIDs, getNode, serialiseNode));
209             upload.appendChild(addCreated(changeset, getAllWayIDs, getWay, serialiseWay));
210             upload.appendChild(addCreated(changeset, getAllRelationIDs, getRelation, serialiseRelation));
211             upload.appendChild(addModified(changeset, getAllNodeIDs, getNode, serialiseNode));
212             upload.appendChild(addModified(changeset, getAllWayIDs, getWay, serialiseWay));
213             upload.appendChild(addModified(changeset, getAllRelationIDs, getRelation, serialiseRelation));
214             upload.appendChild(addDeleted(changeset, getAllRelationIDs, getRelation, serialiseEntityRoot, false));
215             upload.appendChild(addDeleted(changeset, getAllRelationIDs, getRelation, serialiseEntityRoot, true));
216             upload.appendChild(addDeleted(changeset, getAllWayIDs, getWay, serialiseEntityRoot, false));
217             upload.appendChild(addDeleted(changeset, getAllWayIDs, getWay, serialiseEntityRoot, true));
218             upload.appendChild(addDeleted(changeset, getAllNodeIDs, getNode, serialiseEntityRoot, false));
219             upload.appendChild(addDeleted(changeset, getAllNodeIDs, getNode, serialiseEntityRoot, true));
220
221             // now actually upload them
222             // make an OAuth query
223             var url:String = apiBaseURL+"changeset/" + changeset.id + "/upload";
224
225             // build the actual request
226                         var serv:HTTPService=new HTTPService();
227                         serv.method="POST";
228                         serv.url=signedOAuthURL(url, "POST");
229                         serv.contentType = "text/xml";
230                         serv.headers={'X-Error-Format':'xml'};
231                         serv.request=" ";
232                         serv.resultFormat="e4x";
233                         serv.requestTimeout=0;
234                         serv.addEventListener(ResultEvent.RESULT, diffUploadComplete);
235                         serv.addEventListener(FaultEvent.FAULT, diffUploadIOError);
236                         serv.send(upload);
237                 
238                         dispatchEvent(new Event(SAVE_STARTED));
239                         return upload;
240         }
241
242         private function diffUploadComplete(event:ResultEvent):void {
243                         var results:XML = XML(event.result);
244
245                         // was it an error document?
246                         if (results.name().localName=='osmError') {
247                         dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
248                                 diffUploadAPIError(results.status, results.message);
249                                 return;
250                         }
251
252             // response should be XML describing the progress
253             
254             for each( var update:XML in results.child("*") ) {
255                 var oldID:Number = Number(update.@old_id);
256                 var newID:Number = Number(update.@new_id);
257                 var version:uint = uint(update.@new_version);
258                 var type:String = update.name();
259
260                                 if (newID==0) {
261                                         // delete
262                         if      (type == "node"    ) { killNode(oldID); }
263                         else if (type == "way"     ) { killWay(oldID); }
264                         else if (type == "relation") { killRelation(oldID); }
265                                         
266                                 } else {
267                                         // create/update
268                         if      (type == "node"    ) { renumberNode(oldID, newID, version); getNode(newID).markClean(); }
269                         else if (type == "way"     ) { renumberWay(oldID, newID, version); getWay(newID).markClean(); }
270                         else if (type == "relation") { renumberRelation(oldID, newID, version); getRelation(newID).markClean(); }
271                                 }
272             }
273
274             dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, true));
275                         freshenActiveChangeset();
276             markClean(); // marks the connection clean. Pressing undo from this point on leads to unexpected results
277             MainUndoStack.getGlobalStack().breakUndo(); // so, for now, break the undo stack
278         }
279
280                 private function diffUploadIOError(event:FaultEvent):void {
281                         trace(event.fault);
282                         dispatchEvent(new MapEvent(MapEvent.ERROR, { message: "Couldn't upload data: "+event.fault.faultString } ));
283                         dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
284                 }
285
286                 private function diffUploadAPIError(status:String, message:String):void {
287                         var matches:Array;
288                         switch (status) {
289
290                                 case '409 Conflict':
291                                         if (message.match(/changeset/i)) { throwChangesetError(message); return; }
292                                         matches=message.match(/mismatch.+had: (\d+) of (\w+) (\d+)/i);
293                                         if (matches) { throwConflictError(findEntity(matches[2],matches[3]), Number(matches[1]), message); return; }
294                                         break;
295                                 
296                                 case '410 Gone':
297                                         matches=message.match(/The (\w+) with the id (\d+)/i);
298                                         if (matches) { throwAlreadyDeletedError(findEntity(matches[1],matches[2]), message); return; }
299                                         break;
300                                 
301                                 case '412 Precondition Failed':
302                                         matches=message.match(/Node (\d+) is still used/i);
303                                         if (matches) { throwInUseError(findEntity('Node',matches[1]), message); return; }
304                                         matches=message.match(/relation (\d+) is used/i);
305                                         if (matches) { throwInUseError(findEntity('Relation',matches[1]), message); return; }
306                                         matches=message.match(/Way (\d+) still used/i);
307                                         if (matches) { throwInUseError(findEntity('Way',matches[1]), message); return; }
308                                         matches=message.match(/Cannot update (\w+) (\d+)/i);
309                                         if (matches) { throwEntityError(findEntity(matches[1],matches[2]), message); return; }
310                                         matches=message.match(/Relation with id (\d+)/i);
311                                         if (matches) { throwEntityError(findEntity('Relation',matches[1]), message); return; }
312                                         matches=message.match(/Way (\d+) requires the nodes/i);
313                                         if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; }
314                                         throwBugError(message); return;
315                                 
316                                 case '404 Not Found':
317                                         throwBugError(message); return;
318                                         
319                                 case '400 Bad Request':
320                                         matches=message.match(/Element (\w+)\/(\d+)/i);
321                                         if (matches) { throwEntityError(findEntity(matches[1],matches[2]), message); return; }
322                                         matches=message.match(/You tried to add \d+ nodes to way (\d+)/i);
323                                         if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; }
324                                         throwBugError(message); return;
325                         }
326
327                         // Not caught, so just throw a generic server error
328                         throwServerError(message);
329                 }
330
331         private function addCreated(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
332             var create:XML = <create version="0.6"/>
333             for each( var id:Number in getIDs() ) {
334                 var entity:Entity = get(id);
335                 if ( id >= 0 || entity.deleted )
336                     continue;
337                     
338                 var xml:XML = serialise(entity);
339                 xml.@changeset = changeset.id;
340                 create.appendChild(xml);
341             }
342             return create.hasComplexContent() ? create : <!-- blank create section -->;
343         }
344
345                 private function addDeleted(changeset:Changeset, getIDs:Function, get:Function, serialise:Function, ifUnused:Boolean):XML {
346             var del:XML = <delete version="0.6"/>
347             if (ifUnused) del.@["if-unused"] = "true";
348             for each( var id:Number in getIDs() ) {
349                 var entity:Entity = get(id);
350                 // creates are already included
351                 if ( id < 0 || !entity.deleted || entity.parentsLoaded==ifUnused)
352                     continue;
353                     
354                 var xml:XML = serialise(entity);
355                 xml.@changeset = changeset.id;
356                 del.appendChild(xml);
357             }
358             return del.hasComplexContent() ? del : <!-- blank delete section -->;
359                 }
360
361         private function addModified(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
362             var modify:XML = <modify version="0.6"/>
363             for each( var id:Number in getIDs() ) {
364                 var entity:Entity = get(id);
365                 // creates and deletes are already included
366                 if ( id < 0 || entity.deleted || !entity.isDirty )
367                     continue;
368                     
369                 var xml:XML = serialise(entity);
370                 xml.@changeset = changeset.id;
371                 modify.appendChild(xml);
372             }
373             return modify.hasComplexContent() ? modify : <!-- blank modify section -->;
374         }
375
376         private function serialiseNode(node:Node):XML {
377             var xml:XML = serialiseEntityRoot(node); //<node/>
378             serialiseEntityTags(node, xml);
379             xml.@lat = node.lat;
380             xml.@lon = node.lon;
381             return xml;
382         }
383
384         private function serialiseWay(way:Way):XML {
385             var xml:XML = serialiseEntityRoot(way); //<node/>
386             serialiseEntityTags(way, xml);
387             for ( var i:uint = 0; i < way.length; i++ ) {
388                 var nd:XML = <nd/>
389                 nd.@ref = way.getNode(i).id;
390                 xml.appendChild(nd);
391             }
392             return xml;
393         }
394
395         private function serialiseRelation(relation:Relation):XML {
396             var xml:XML = serialiseEntityRoot(relation); //<node/>
397             serialiseEntityTags(relation, xml);
398             for ( var i:uint = 0; i < relation.length; i++ ) {
399                 var relMember:RelationMember = relation.getMember(i);
400                 var member:XML = <member/>
401                 member.@ref = relMember.entity.id;
402                 member.@type = relMember.entity.getType();
403                 member.@role = relMember.role;
404                 xml.appendChild(member);
405             }
406             return xml;
407         }
408         
409                 private function serialiseEntityRoot(entity:Object):XML {
410                         var xml:XML;
411                         if      (entity is Way     ) { xml = <way/> }
412                         else if (entity is Node    ) { xml = <node/> }
413                         else if (entity is Relation) { xml = <relation/> }
414                         xml.@id = entity.id;
415                         xml.@version = entity.version;
416                         return xml;
417                 }
418
419         private function serialiseEntityTags(entity:Entity, xml:XML):void {
420             xml.@id = entity.id;
421             xml.@version = entity.version;
422             for each( var tag:Tag in entity.getTagArray() ) {
423               if (tag.key == 'created_by') {
424                 entity.setTag('created_by', null, MainUndoStack.getGlobalStack().addAction);
425                 continue;
426               }
427               var tagXML:XML = <tag/>
428               tagXML.@k = tag.key;
429               tagXML.@v = tag.value;
430               xml.appendChild(tagXML);
431             }
432         }
433
434         override public function fetchUserTraces(refresh:Boolean=false):void {
435             if (traces_loaded && !refresh) {
436               dispatchEvent(new Event(TRACES_LOADED));
437             } else {
438               sendOAuthGet(apiBaseURL+"user/gpx_files", tracesLoadComplete, errorOnMapLoad, mapLoadStatus); //needs error handlers
439               dispatchEvent(new Event(LOAD_STARTED)); //specific to map or reusable?
440             }
441         }
442
443         private function tracesLoadComplete(event:Event):void {
444             clearTraces();
445             var files:XML = new XML(URLLoader(event.target).data);
446             for each(var traceData:XML in files.gpx_file) {
447               var t:Trace = new Trace(this).fromXML(traceData);
448               addTrace(t);
449             }
450             traces_loaded = true;
451             dispatchEvent(new Event(LOAD_COMPLETED));
452             dispatchEvent(new Event(TRACES_LOADED));
453         }
454
455         override public function fetchTrace(id:Number, callback:Function):void {
456             sendOAuthGet(apiBaseURL+"gpx/"+id+"/data.xml", 
457                                 function(e:Event):void { 
458                         dispatchEvent(new Event(LOAD_COMPLETED));
459                                         callback(e);
460                                 }, errorOnMapLoad, mapLoadStatus); // needs error handlers
461             dispatchEvent(new Event(LOAD_STARTED)); //specifc to map or reusable?
462         }
463
464         /** Fetch the history for the given entity. The callback function will be given an array of entities of that type, representing the different versions */
465         override public function fetchHistory(entity:Entity, callback:Function):void {
466             if (entity.id >= 0) {
467               var request:URLRequest = new URLRequest(Connection.apiBaseURL + entity.getType() + "/" + entity.id + "/history");
468               var loader:ExtendedURLLoader = new ExtendedURLLoader();
469               loader.addEventListener(Event.COMPLETE, loadedHistory);
470               loader.addEventListener(IOErrorEvent.IO_ERROR, errorOnMapLoad); //needs error handlers
471               loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, mapLoadStatus);
472               loader.info['callback'] = callback; //store the callback so we can use it later
473               loader.load(request);
474               dispatchEvent(new Event(LOAD_STARTED));
475             } else {
476               // objects created locally only have one state, their current one
477               callback([entity]);
478             }
479         }
480
481         private function loadedHistory(event:Event):void {
482             var _xml:XML = new XML(ExtendedURLLoader(event.target).data);
483             var results:Array = [];
484
485             // only one type of entity should be returned, but this handles any
486
487             for each(var nodeData:XML in _xml.node) {
488                 var newNode:Node = new Node(
489                     Number(nodeData.@id),
490                     uint(nodeData.@version),
491                     parseTags(nodeData.tag),
492                     true,
493                     Number(nodeData.@lat),
494                     Number(nodeData.@lon),
495                     Number(nodeData.@uid),
496                     nodeData.@timestamp,
497                     nodeData.@user
498                     );
499                 results.push(newNode);
500             }
501
502             for each(var wayData:XML in _xml.way) {
503                 var nodes:Array = [];
504                 for each(var nd:XML in wayData.nd) {
505                   nodes.push(new Node(Number(nd.@ref), NaN, null, false, NaN, NaN));
506                 }
507                 var newWay:Way = new Way(
508                     Number(wayData.@id),
509                     uint(wayData.@version),
510                     parseTags(wayData.tag),
511                     true,
512                     nodes,
513                     Number(wayData.@uid),
514                     wayData.@timestamp,
515                     wayData.@user
516                     );
517                 results.push(newWay);
518             }
519
520             for each(var relData:XML in _xml.relation) {
521                 trace("relation history not implemented");
522             }
523
524             // use the callback we stored earlier, and pass it the results
525             ExtendedURLLoader(event.target).info['callback'](results);
526         }
527         }
528 }