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