source: josm/trunk/src/org/openstreetmap/josm/io/OsmApi.java@ 1581

Last change on this file since 1581 was 1581, checked in by stoecker, 15 years ago

fix #2429 - patch by Gubaer - fix upload cancel

File size: 21.0 KB
Line 
1//License: GPL. See README for details.
2package org.openstreetmap.josm.io;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.BufferedReader;
7import java.io.BufferedWriter;
8import java.io.IOException;
9import java.io.InputStream;
10import java.io.InputStreamReader;
11import java.io.OutputStream;
12import java.io.OutputStreamWriter;
13import java.io.PrintWriter;
14import java.io.StringReader;
15import java.io.StringWriter;
16import java.net.ConnectException;
17import java.net.HttpURLConnection;
18import java.net.SocketTimeoutException;
19import java.net.URL;
20import java.net.UnknownHostException;
21import java.util.ArrayList;
22import java.util.Collection;
23import java.util.Properties;
24import java.util.StringTokenizer;
25import java.util.concurrent.FutureTask;
26
27import javax.xml.parsers.SAXParserFactory;
28
29import org.openstreetmap.josm.Main;
30import org.openstreetmap.josm.data.osm.Changeset;
31import org.openstreetmap.josm.data.osm.Node;
32import org.openstreetmap.josm.data.osm.OsmPrimitive;
33import org.openstreetmap.josm.data.osm.Relation;
34import org.openstreetmap.josm.data.osm.Way;
35import org.openstreetmap.josm.data.osm.visitor.CreateOsmChangeVisitor;
36import org.openstreetmap.josm.gui.PleaseWaitRunnable;
37import org.xml.sax.Attributes;
38import org.xml.sax.InputSource;
39import org.xml.sax.SAXException;
40import org.xml.sax.helpers.DefaultHandler;
41
42/**
43 * Class that encapsulates the communications with the OSM API.
44 *
45 * All interaction with the server-side OSM API should go through this class.
46 *
47 * It is conceivable to extract this into an interface later and create various
48 * classes implementing the interface, to be able to talk to various kinds of servers.
49 *
50 */
51public class OsmApi extends OsmConnection {
52
53 /**
54 * Object describing current changeset
55 */
56 private Changeset changeset;
57
58 /**
59 * API version used for server communications
60 */
61 private String version = null;
62
63 /**
64 * Minimum API version accepted by server, from capabilities response
65 */
66 private String minVersion = null;
67
68 /**
69 * Maximum API version accepted by server, from capabilities response
70 */
71 private String maxVersion = null;
72
73 /**
74 * Maximum downloadable area from server (degrees squared), from capabilities response
75 * FIXME: make download dialog use this, instead of hard-coded default.
76 */
77 private String maxArea = null;
78
79 /**
80 * true if successfully initialized
81 */
82 private boolean initialized = false;
83
84 private StringWriter swriter = new StringWriter();
85 private OsmWriter osmWriter = new OsmWriter(new PrintWriter(swriter), true, null);
86
87 /**
88 * A parser for the "capabilities" response XML
89 */
90 private class CapabilitiesParser extends DefaultHandler {
91 @Override public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
92 if (qName.equals("version")) {
93 minVersion = atts.getValue("minimum");
94 maxVersion = atts.getValue("maximum");
95 } else if (qName.equals("area"))
96 maxArea = atts.getValue("maximum");
97 }
98 }
99
100 /**
101 * Helper that returns the lower-case type name of an OsmPrimitive
102 * @param o the primitive
103 * @return "node", "way", "relation", or "changeset"
104 */
105 public static String which(OsmPrimitive o) {
106 if (o instanceof Node) return "node";
107 if (o instanceof Way) return "way";
108 if (o instanceof Relation) return "relation";
109 if (o instanceof Changeset) return "changeset";
110 return "";
111 }
112
113 /**
114 * Returns the OSM protocol version we use to talk to the server.
115 * @return protocol version, or null if not yet negotiated.
116 */
117 public String getVersion() {
118 return version;
119 }
120
121 /**
122 * Returns true if the negotiated version supports changesets.
123 * @return true if the negotiated version supports changesets.
124 */
125 public boolean hasChangesetSupport() {
126 return ((version != null) && (version.compareTo("0.6")>=0));
127 }
128
129 /**
130 * Initializes this component by negotiating a protocol version with the server.
131 */
132 public void initialize() {
133 initAuthentication();
134 try {
135 initialized = true; // note: has to be before the sendRequest or that will throw!
136 String capabilities = sendRequest("GET", "capabilities", null);
137 InputSource inputSource = new InputSource(new StringReader(capabilities));
138 SAXParserFactory.newInstance().newSAXParser().parse(inputSource, new CapabilitiesParser());
139 if (maxVersion.compareTo("0.6") >= 0) {
140 version = "0.6";
141 } else if (minVersion.compareTo("0.5") <= 0) {
142 version = "0.5";
143 } else {
144 System.err.println(tr("This version of JOSM is incompatible with the configured server."));
145 System.err.println(tr("It supports protocol versions 0.5 and 0.6, while the server says it supports {0} to {1}.",
146 minVersion, maxVersion));
147 initialized = false;
148 }
149 System.out.println(tr("Communications with {0} established using protocol version {1}",
150 Main.pref.get("osm-server.url"),
151 version));
152 osmWriter.setVersion(version);
153 } catch (Exception ex) {
154 initialized = false;
155 ex.printStackTrace();
156 }
157 }
158
159 /**
160 * Makes an XML string from an OSM primitive. Uses the OsmWriter class.
161 * @param o the OSM primitive
162 * @param addBody true to generate the full XML, false to only generate the encapsulating tag
163 * @return XML string
164 */
165 private String toXml(OsmPrimitive o, boolean addBody) {
166 swriter.getBuffer().setLength(0);
167 osmWriter.setWithBody(addBody);
168 osmWriter.setChangeset(changeset);
169 osmWriter.header();
170 o.visit(osmWriter);
171 osmWriter.footer();
172 osmWriter.out.flush();
173 return swriter.toString();
174 }
175
176 /**
177 * Helper that makes an int from the first whitespace separated token in a string.
178 * @param s the string
179 * @return the integer represenation of the first token in the string
180 * @throws OsmTransferException if the string is empty or does not represent a number
181 */
182 public static int parseInt(String s) throws OsmTransferException {
183 StringTokenizer t = new StringTokenizer(s);
184 try {
185 return Integer.parseInt(t.nextToken());
186 } catch (Exception x) {
187 throw new OsmTransferException(tr("Cannot read numeric value from response"));
188 }
189 }
190
191 /**
192 * Helper that makes a long from the first whitespace separated token in a string.
193 * @param s the string
194 * @return the long represenation of the first token in the string
195 * @throws OsmTransferException if the string is empty or does not represent a number
196 */
197 public static long parseLong(String s) throws OsmTransferException {
198 StringTokenizer t = new StringTokenizer(s);
199 try {
200 return Long.parseLong(t.nextToken());
201 } catch (Exception x) {
202 throw new OsmTransferException(tr("Cannot read numeric value from response"));
203 }
204 }
205
206 /**
207 * Returns the base URL for API requests, including the negotiated version number.
208 * @return base URL string
209 */
210 public String getBaseUrl() {
211 StringBuffer rv = new StringBuffer(Main.pref.get("osm-server.url"));
212 if (version != null) {
213 rv.append("/");
214 rv.append(version);
215 }
216 rv.append("/");
217 // this works around a ruby (or lighttpd) bug where two consecutive slashes in
218 // an URL will cause a "404 not found" response.
219 int p; while ((p = rv.indexOf("//", 6)) > -1) { rv.delete(p, p + 1); }
220 return rv.toString();
221 }
222
223 /**
224 * Creates an OSM primitive on the server. The OsmPrimitive object passed in
225 * is modified by giving it the server-assigned id.
226 *
227 * @param osm the primitive
228 * @throws OsmTransferException if something goes wrong
229 */
230 public void createPrimitive(OsmPrimitive osm) throws OsmTransferException {
231 osm.id = parseLong(sendRequest("PUT", which(osm)+"/create", toXml(osm, true)));
232 osm.version = 1;
233 }
234
235 /**
236 * Modifies an OSM primitive on the server. For protocols greater than 0.5,
237 * the OsmPrimitive object passed in is modified by giving it the server-assigned
238 * version.
239 *
240 * @param osm the primitive
241 * @throws OsmTransferException if something goes wrong
242 */
243 public void modifyPrimitive(OsmPrimitive osm) throws OsmTransferException {
244 if (version.equals("0.5")) {
245 // legacy mode does not return the new object version.
246 sendRequest("PUT", which(osm)+"/" + osm.id, toXml(osm, true));
247 } else {
248 // normal mode (0.6 and up) returns new object version.
249 osm.version = parseInt(sendRequest("PUT", which(osm)+"/" + osm.id, toXml(osm, true)));
250 }
251 }
252
253 /**
254 * Deletes an OSM primitive on the server.
255 * @param osm the primitive
256 * @throws OsmTransferException if something goes wrong
257 */
258 public void deletePrimitive(OsmPrimitive osm) throws OsmTransferException {
259 // legacy mode does not require payload. normal mode (0.6 and up) requires payload for version matching.
260 sendRequest("DELETE", which(osm)+"/" + osm.id, version.equals("0.5") ? null : toXml(osm, false));
261 }
262
263 /**
264 * Creates a new changeset on the server to use for subsequent calls.
265 * @param comment the "commit comment" for the new changeset
266 * @throws OsmTransferException signifying a non-200 return code, or connection errors
267 */
268 public void createChangeset(String comment) throws OsmTransferException {
269 changeset = new Changeset();
270 Main.pleaseWaitDlg.currentAction.setText(tr("Opening changeset..."));
271 Properties sysProp = System.getProperties();
272 Object ua = sysProp.get("http.agent");
273 changeset.put("created_by", (ua == null) ? "JOSM" : ua.toString());
274 changeset.put("comment", comment);
275 createPrimitive(changeset);
276 }
277
278 /**
279 * Closes a changeset on the server.
280 *
281 * @throws OsmTransferException if something goes wrong.
282 */
283 public void stopChangeset() throws OsmTransferException {
284 Main.pleaseWaitDlg.currentAction.setText(tr("Closing changeset..."));
285 sendRequest("PUT", "changeset" + "/" + changeset.id + "/close", null);
286 changeset = null;
287 }
288
289 /**
290 * Uploads a list of changes in "diff" form to the server.
291 *
292 * @param list the list of changed OSM Primitives
293 * @return list of processed primitives
294 * @throws OsmTransferException if something is wrong
295 * @throws OsmTransferCancelledException if the upload was cancelled by the user
296 */
297 public Collection<OsmPrimitive> uploadDiff(final Collection<OsmPrimitive> list) throws OsmTransferException {
298
299 if (changeset == null) {
300 throw new OsmTransferException(tr("No changeset present for diff upload"));
301 }
302
303
304 final ArrayList<OsmPrimitive> processed = new ArrayList<OsmPrimitive>();
305
306 // this is the asynchronous update task
307 //
308 class UploadDiffTask extends PleaseWaitRunnable {
309
310 private boolean uploadCancelled = false;
311 private boolean uploadFailed = false;
312 private Throwable lastThrowable = null;
313
314 public UploadDiffTask(String title) {
315 super(title,false /* don't ignore exceptions */);
316 }
317
318 @Override protected void realRun() throws SAXException, IOException {
319 CreateOsmChangeVisitor duv = new CreateOsmChangeVisitor(changeset, OsmApi.this);
320
321 for (OsmPrimitive osm : list) {
322 int progress = Main.pleaseWaitDlg.progress.getValue();
323 Main.pleaseWaitDlg.currentAction.setText(tr("Preparing..."));
324 osm.visit(duv);
325 Main.pleaseWaitDlg.progress.setValue(progress+1);
326 }
327
328 Main.pleaseWaitDlg.currentAction.setText(tr("Uploading..."));
329
330 String diff = duv.getDocument();
331 try {
332 String diffresult = sendRequest("POST", "changeset/" + changeset.id + "/upload", diff);
333 DiffResultReader.parseDiffResult(diffresult, list, processed, duv.getNewIdMap(), Main.pleaseWaitDlg);
334 } catch (Exception sxe) {
335 if (isUploadCancelled()) {
336 // ignore exceptions thrown because the connection is aborted,
337 // i.e. IOExceptions or SocketExceptions
338 //
339 System.out.println("Ignoring exception caught because upload is cancelled. Exception is: " + sxe.toString());
340 return;
341 }
342 uploadFailed = true;
343 // remember last exception and don't throw it. If it was thrown again it would
344 // have to be encapsulated in a RuntimeException which would be nested in yet
345 // another RuntimeException by parent classes.
346 // Rather check isUploadFailed() and retrieve getLastThrowable() after the task
347 // is completed
348 //
349 lastThrowable = sxe;
350 }
351 }
352
353 @Override protected void finish() {
354 // do nothing
355 }
356
357 @Override protected void cancel() {
358 activeConnection.disconnect();
359 uploadCancelled = true;
360 }
361
362 public boolean isUploadCancelled() {
363 return uploadCancelled;
364 }
365
366 public boolean isUploadFailed() {
367 return uploadFailed;
368 }
369
370 public Throwable getLastThrowable() {
371 return lastThrowable;
372 }
373 }
374
375 UploadDiffTask uploadTask = new UploadDiffTask(tr("Uploading data"));
376
377 // run data upload as asynchronous task
378 //
379 try {
380 Void result = null;
381 FutureTask<Void> task = new FutureTask<Void>(uploadTask, result);
382 task.run();
383 task.get(); // wait for the task to complete, no return value expected, though
384 } catch(Throwable e) {
385 if (uploadTask.isUploadCancelled()) {
386 throw new OsmTransferCancelledException();
387 }
388 throw new OsmTransferException(e);
389 }
390
391 // handle failed upload
392 //
393 if (uploadTask.isUploadFailed()) {
394 if (uploadTask.getLastThrowable() != null && uploadTask.getLastThrowable() instanceof OsmTransferException) {
395 OsmTransferException e = (OsmTransferException)uploadTask.getLastThrowable();
396 throw e;
397 }
398 // shouldn't happen, but just in case
399 //
400 throw new OsmTransferException(tr("Data upload failed for unknown reason"));
401 }
402
403 // handle cancelled upload
404 //
405 if (uploadTask.isUploadCancelled()) {
406 throw new OsmTransferCancelledException();
407 }
408
409 return processed;
410 }
411
412
413
414 private void sleepAndListen() throws OsmTransferCancelledException {
415 // System.out.print("backing off for 10 seconds...");
416 for(int i=0; i < 10; i++) {
417 if (cancel || isAuthCancelled()) {
418 throw new OsmTransferCancelledException();
419 }
420 try {
421 Thread.sleep(1000);
422 } catch (InterruptedException ex) {}
423 }
424 }
425
426 /**
427 * Generic method for sending requests to the OSM API.
428 *
429 * This method will automatically re-try any requests that are answered with a 5xx
430 * error code, or that resulted in a timeout exception from the TCP layer.
431 *
432 * @param requestMethod The http method used when talking with the server.
433 * @param urlSuffix The suffix to add at the server url, not including the version number,
434 * but including any object ids (e.g. "/way/1234/history").
435 * @param requestBody the body of the HTTP request, if any.
436 *
437 * @return the body of the HTTP response, if and only if the response code was "200 OK".
438 * @exception OsmTransferException if the HTTP return code was not 200 (and retries have
439 * been exhausted), or rewrapping a Java exception.
440 */
441 private String sendRequest(String requestMethod, String urlSuffix,
442 String requestBody) throws OsmTransferException {
443
444 if (!initialized) throw new OsmTransferException(tr("Not initialized"));
445
446 StringBuffer responseBody = new StringBuffer();
447 StringBuffer statusMessage = new StringBuffer();
448
449 int retries = 5; // configurable?
450
451 while(true) { // the retry loop
452 try {
453 URL url = new URL(new URL(getBaseUrl()), urlSuffix, new MyHttpHandler());
454 System.out.print(requestMethod + " " + url + "... ");
455 activeConnection = (HttpURLConnection)url.openConnection();
456 activeConnection.setConnectTimeout(15000);
457 activeConnection.setRequestMethod(requestMethod);
458 addAuth(activeConnection);
459
460 if (requestMethod.equals("PUT") || requestMethod.equals("POST")) {
461 activeConnection.setDoOutput(true);
462 activeConnection.setRequestProperty("Content-type", "text/xml");
463 OutputStream out = activeConnection.getOutputStream();
464
465 // It seems that certain bits of the Ruby API are very unhappy upon
466 // receipt of a PUT/POST message withtout a Content-length header,
467 // even if the request has no payload.
468 // Since Java will not generate a Content-length header unless
469 // we use the output stream, we create an output stream for PUT/POST
470 // even if there is no payload.
471 if (requestBody != null) {
472 BufferedWriter bwr = new BufferedWriter(new OutputStreamWriter(out, "UTF-8"));
473 bwr.write(requestBody);
474 bwr.flush();
475 }
476 out.close();
477 }
478
479 activeConnection.connect();
480 System.out.println(activeConnection.getResponseMessage());
481 int retCode = activeConnection.getResponseCode();
482
483 if (retCode >= 500) {
484 if (retries-- > 0) {
485 sleepAndListen();
486 continue;
487 }
488 }
489
490 // populate return fields.
491 responseBody.setLength(0);
492
493 // If the API returned an error code like 403 forbidden, getInputStream
494 // will fail with an IOException.
495 InputStream i = null;
496 try {
497 i = activeConnection.getInputStream();
498 } catch (IOException ioe) {
499 i = activeConnection.getErrorStream();
500 }
501 BufferedReader in = new BufferedReader(new InputStreamReader(i));
502
503 String s;
504 while((s = in.readLine()) != null) {
505 responseBody.append(s);
506 responseBody.append("\n");
507 }
508
509 statusMessage.setLength(0);
510 // Look for a detailed error message from the server
511 if (activeConnection.getHeaderField("Error") != null) {
512 String er = activeConnection.getHeaderField("Error");
513 System.err.println("Error header: " + er);
514 statusMessage.append(tr(er));
515 } else if (retCode != 200 && responseBody.length()>0) {
516 System.err.println("Error body: " + responseBody);
517 statusMessage.append(tr(responseBody.toString()));
518 } else {
519 statusMessage.append(activeConnection.getResponseMessage());
520 }
521 activeConnection.disconnect();
522
523 if (retCode != 200) {
524 throw new OsmTransferException(statusMessage.toString());
525 }
526 return responseBody.toString();
527 } catch (UnknownHostException e) {
528 throw new OsmTransferException(tr("Unknown host")+": "+e.getMessage(), e);
529 } catch (SocketTimeoutException e) {
530 if (retries-- > 0)
531 continue;
532 throw new OsmTransferException(e.getMessage()+ " " + e.getClass().getCanonicalName(), e);
533 } catch (ConnectException e) {
534 if (retries-- > 0)
535 continue;
536 throw new OsmTransferException(e.getMessage()+ " " + e.getClass().getCanonicalName(), e);
537 } catch (Exception e) {
538 if (e instanceof OsmTransferException) throw (OsmTransferException) e;
539 throw new OsmTransferException(e);
540 }
541 }
542 }
543}
Note: See TracBrowser for help on using the repository browser.