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