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

Last change on this file since 1691 was 1691, checked in by Gubaer, 15 years ago

#2703: patch (slightly extended) by dmuecke
clean up: few issues in Capabilities and OsmApi

File size: 19.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.awt.EventQueue;
7import java.io.BufferedReader;
8import java.io.BufferedWriter;
9import java.io.IOException;
10import java.io.InputStream;
11import java.io.InputStreamReader;
12import java.io.OutputStream;
13import java.io.OutputStreamWriter;
14import java.io.PrintWriter;
15import java.io.StringReader;
16import java.io.StringWriter;
17import java.net.ConnectException;
18import java.net.HttpURLConnection;
19import java.net.SocketTimeoutException;
20import java.net.URL;
21import java.net.UnknownHostException;
22import java.util.ArrayList;
23import java.util.Collection;
24import java.util.HashMap;
25import java.util.Properties;
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.OsmPrimitive;
32import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
33import org.openstreetmap.josm.data.osm.visitor.CreateOsmChangeVisitor;
34import org.xml.sax.Attributes;
35import org.xml.sax.InputSource;
36import org.xml.sax.SAXException;
37import org.xml.sax.helpers.DefaultHandler;
38
39/**
40 * Class that encapsulates the communications with the OSM API.
41 *
42 * All interaction with the server-side OSM API should go through this class.
43 *
44 * It is conceivable to extract this into an interface later and create various
45 * classes implementing the interface, to be able to talk to various kinds of servers.
46 *
47 */
48public class OsmApi extends OsmConnection {
49 /** max number of retries to send a request in case of HTTP 500 errors or timeouts */
50 static public final int DEFAULT_MAX_NUM_RETRIES = 5;
51
52 /** the collection of instantiated OSM APIs */
53 private static HashMap<String, OsmApi> instances = new HashMap<String, OsmApi>();
54
55 /**
56 * replies the {@see OsmApi} for a given server URL
57 *
58 * @param serverUrl the server URL
59 * @return the OsmApi
60 * @throws IllegalArgumentException thrown, if serverUrl is null
61 *
62 */
63 static public OsmApi getOsmApi(String serverUrl) {
64 OsmApi api = instances.get(serverUrl);
65 if (api == null) {
66 api = new OsmApi(serverUrl);
67 instances.put(serverUrl,api);
68 }
69 return api;
70 }
71 /**
72 * replies the {@see OsmApi} for the URL given by the preference <code>osm-server.url</code>
73 *
74 * @return the OsmApi
75 * @exception IllegalStateException thrown, if the preference <code>osm-server.url</code> is not set
76 *
77 */
78 static public OsmApi getOsmApi() {
79 String serverUrl = Main.pref.get("osm-server.url", "http://api.openstreetmap.org/api");
80 if (serverUrl == null)
81 throw new IllegalStateException(tr("preference ''{0}'' missing. Can't initialize OsmApi", "osm-server.url"));
82 return getOsmApi(serverUrl);
83 }
84
85 /** the server URL */
86 private String serverUrl;
87
88 /**
89 * Object describing current changeset
90 */
91 private Changeset changeset;
92
93 /**
94 * API version used for server communications
95 */
96 private String version = null;
97
98 /** the api capabilities */
99 private Capabilities capabilities = new Capabilities();
100
101 /**
102 * true if successfully initialized
103 */
104 private boolean initialized = false;
105
106 private StringWriter swriter = new StringWriter();
107 private OsmWriter osmWriter = new OsmWriter(new PrintWriter(swriter), true, null);
108
109 /**
110 * A parser for the "capabilities" response XML
111 */
112 private class CapabilitiesParser extends DefaultHandler {
113 @Override
114 public void startDocument() throws SAXException {
115 capabilities.clear();
116 }
117
118 @Override public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
119 for (int i=0; i< qName.length(); i++) {
120 capabilities.put(qName, atts.getQName(i), atts.getValue(i));
121 }
122 }
123 }
124
125 /**
126 * creates an OSM api for a specific server URL
127 *
128 * @param serverUrl the server URL. Must not be null
129 * @exception IllegalArgumentException thrown, if serverUrl is null
130 */
131 protected OsmApi(String serverUrl) {
132 if (serverUrl == null)
133 throw new IllegalArgumentException(tr("parameter '{0}' must not be null", "serverUrl"));
134 this.serverUrl = serverUrl;
135 }
136
137 /**
138 * Returns the OSM protocol version we use to talk to the server.
139 * @return protocol version, or null if not yet negotiated.
140 */
141 public String getVersion() {
142 return version;
143 }
144
145 /**
146 * Returns true if the negotiated version supports changesets.
147 * @return true if the negotiated version supports changesets.
148 */
149 public boolean hasChangesetSupport() {
150 return ((version != null) && (version.compareTo("0.6")>=0));
151 }
152
153 /**
154 * Initializes this component by negotiating a protocol version with the server.
155 *
156 * @exception OsmApiInitializationException thrown, if an exception occurs
157 */
158 public void initialize() throws OsmApiInitializationException {
159 if (initialized)
160 return;
161 initAuthentication();
162 try {
163 String s = sendRequest("GET", "capabilities", null);
164 InputSource inputSource = new InputSource(new StringReader(s));
165 SAXParserFactory.newInstance().newSAXParser().parse(inputSource, new CapabilitiesParser());
166 if (capabilities.supportsVersion("0.6")) {
167 version = "0.6";
168 } else if (capabilities.supportsVersion("0.5")) {
169 version = "0.5";
170 } else {
171 System.err.println(tr("This version of JOSM is incompatible with the configured server."));
172 System.err.println(tr("It supports protocol versions 0.5 and 0.6, while the server says it supports {0} to {1}.",
173 capabilities.get("version", "minimum"), capabilities.get("version", "maximum")));
174 initialized = false;
175 }
176 System.out.println(tr("Communications with {0} established using protocol version {1}",
177 serverUrl,
178 version));
179 osmWriter.setVersion(version);
180 initialized = true;
181 } catch (Exception ex) {
182 initialized = false;
183 throw new OsmApiInitializationException(ex);
184 }
185 }
186
187 /**
188 * Makes an XML string from an OSM primitive. Uses the OsmWriter class.
189 * @param o the OSM primitive
190 * @param addBody true to generate the full XML, false to only generate the encapsulating tag
191 * @return XML string
192 */
193 private String toXml(OsmPrimitive o, boolean addBody) {
194 swriter.getBuffer().setLength(0);
195 osmWriter.setWithBody(addBody);
196 osmWriter.setChangeset(changeset);
197 osmWriter.header();
198 o.visit(osmWriter);
199 osmWriter.footer();
200 osmWriter.out.flush();
201 return swriter.toString();
202 }
203
204 /**
205 * Returns the base URL for API requests, including the negotiated version number.
206 * @return base URL string
207 */
208 public String getBaseUrl() {
209 StringBuffer rv = new StringBuffer(serverUrl);
210 if (version != null) {
211 rv.append("/");
212 rv.append(version);
213 }
214 rv.append("/");
215 // this works around a ruby (or lighttpd) bug where two consecutive slashes in
216 // an URL will cause a "404 not found" response.
217 int p; while ((p = rv.indexOf("//", 6)) > -1) { rv.delete(p, p + 1); }
218 return rv.toString();
219 }
220
221 /**
222 * Creates an OSM primitive on the server. The OsmPrimitive object passed in
223 * is modified by giving it the server-assigned id.
224 *
225 * @param osm the primitive
226 * @throws OsmTransferException if something goes wrong
227 */
228 public void createPrimitive(OsmPrimitive osm) throws OsmTransferException {
229 initialize();
230 String ret = "";
231 try {
232 ret = sendRequest("PUT", OsmPrimitiveType.from(osm).getAPIName()+"/create", toXml(osm, true));
233 osm.id = Long.parseLong(ret.trim());
234 osm.version = 1;
235 } catch(NumberFormatException e){
236 throw new OsmTransferException(tr("unexpected format of id replied by the server, got ''{0}''", ret));
237 }
238 }
239
240 /**
241 * Modifies an OSM primitive on the server. For protocols greater than 0.5,
242 * the OsmPrimitive object passed in is modified by giving it the server-assigned
243 * version.
244 *
245 * @param osm the primitive
246 * @throws OsmTransferException if something goes wrong
247 */
248 public void modifyPrimitive(OsmPrimitive osm) throws OsmTransferException {
249 initialize();
250 if (version.equals("0.5")) {
251 // legacy mode does not return the new object version.
252 sendRequest("PUT", OsmPrimitiveType.from(osm).getAPIName()+"/" + osm.id, toXml(osm, true));
253 } else {
254 String ret = null;
255 // normal mode (0.6 and up) returns new object version.
256 try {
257 ret = sendRequest("PUT", OsmPrimitiveType.from(osm).getAPIName()+"/" + osm.id, toXml(osm, true));
258 osm.version = Integer.parseInt(ret.trim());
259 } catch(NumberFormatException e) {
260 throw new OsmTransferException(tr("unexpected format of new version of modified primitive ''{0}'', got ''{1}''", osm.id, ret));
261 }
262 }
263 }
264
265 /**
266 * Deletes an OSM primitive on the server.
267 * @param osm the primitive
268 * @throws OsmTransferException if something goes wrong
269 */
270 public void deletePrimitive(OsmPrimitive osm) throws OsmTransferException {
271 initialize();
272 // legacy mode does not require payload. normal mode (0.6 and up) requires payload for version matching.
273 sendRequest("DELETE", OsmPrimitiveType.from(osm).getAPIName()+"/" + osm.id, version.equals("0.5") ? null : toXml(osm, false));
274 }
275
276 /**
277 * Creates a new changeset on the server to use for subsequent calls.
278 * @param comment the "commit comment" for the new changeset
279 * @throws OsmTransferException signifying a non-200 return code, or connection errors
280 */
281 public void createChangeset(String comment) throws OsmTransferException {
282 changeset = new Changeset();
283 notifyStatusMessage(tr("Opening changeset..."));
284 Properties sysProp = System.getProperties();
285 Object ua = sysProp.get("http.agent");
286 changeset.put("created_by", (ua == null) ? "JOSM" : ua.toString());
287 changeset.put("comment", comment);
288 createPrimitive(changeset);
289 }
290
291 /**
292 * Closes a changeset on the server.
293 *
294 * @throws OsmTransferException if something goes wrong.
295 */
296 public void stopChangeset() throws OsmTransferException {
297 initialize();
298 notifyStatusMessage(tr("Closing changeset..."));
299 sendRequest("PUT", "changeset" + "/" + changeset.id + "/close", null);
300 changeset = null;
301 }
302
303 /**
304 * Uploads a list of changes in "diff" form to the server.
305 *
306 * @param list the list of changed OSM Primitives
307 * @return list of processed primitives
308 * @throws OsmTransferException if something is wrong
309 */
310 public Collection<OsmPrimitive> uploadDiff(final Collection<OsmPrimitive> list) throws OsmTransferException {
311
312 if (changeset == null)
313 throw new OsmTransferException(tr("No changeset present for diff upload"));
314
315 initialize();
316 final ArrayList<OsmPrimitive> processed = new ArrayList<OsmPrimitive>();
317
318 CreateOsmChangeVisitor duv = new CreateOsmChangeVisitor(changeset, OsmApi.this);
319
320 notifyStatusMessage(tr("Preparing..."));
321 for (OsmPrimitive osm : list) {
322 osm.visit(duv);
323 notifyRelativeProgress(1);
324 }
325 notifyStatusMessage(tr("Uploading..."));
326 setAutoProgressIndication(true);
327
328 String diff = duv.getDocument();
329 try {
330 String diffresult = sendRequest("POST", "changeset/" + changeset.id + "/upload", diff);
331 DiffResultReader.parseDiffResult(diffresult, list, processed, duv.getNewIdMap(), Main.pleaseWaitDlg);
332 } catch(Exception e) {
333 throw new OsmTransferException(e);
334 } finally {
335 setAutoProgressIndication(false);
336 }
337
338 return processed;
339 }
340
341
342
343 private void sleepAndListen() throws OsmTransferCancelledException {
344 // System.out.print("backing off for 10 seconds...");
345 for(int i=0; i < 10; i++) {
346 if (cancel || isAuthCancelled())
347 throw new OsmTransferCancelledException();
348 try {
349 Thread.sleep(1000);
350 } catch (InterruptedException ex) {}
351 }
352 }
353
354 /**
355 * Generic method for sending requests to the OSM API.
356 *
357 * This method will automatically re-try any requests that are answered with a 5xx
358 * error code, or that resulted in a timeout exception from the TCP layer.
359 *
360 * @param requestMethod The http method used when talking with the server.
361 * @param urlSuffix The suffix to add at the server url, not including the version number,
362 * but including any object ids (e.g. "/way/1234/history").
363 * @param requestBody the body of the HTTP request, if any.
364 *
365 * @return the body of the HTTP response, if and only if the response code was "200 OK".
366 * @exception OsmTransferException if the HTTP return code was not 200 (and retries have
367 * been exhausted), or rewrapping a Java exception.
368 */
369 private String sendRequest(String requestMethod, String urlSuffix,
370 String requestBody) throws OsmTransferException {
371
372 StringBuffer responseBody = new StringBuffer();
373
374 int retries = Main.pref.getInteger("osm-server.max-num-retries", DEFAULT_MAX_NUM_RETRIES);
375 retries = Math.max(0,retries);
376
377 while(true) { // the retry loop
378 try {
379 URL url = new URL(new URL(getBaseUrl()), urlSuffix, new MyHttpHandler());
380 System.out.print(requestMethod + " " + url + "... ");
381 activeConnection = (HttpURLConnection)url.openConnection();
382 activeConnection.setConnectTimeout(15000);
383 activeConnection.setRequestMethod(requestMethod);
384 addAuth(activeConnection);
385
386 if (requestMethod.equals("PUT") || requestMethod.equals("POST") || requestMethod.equals("DELETE")) {
387 activeConnection.setDoOutput(true);
388 activeConnection.setRequestProperty("Content-type", "text/xml");
389 OutputStream out = activeConnection.getOutputStream();
390
391 // It seems that certain bits of the Ruby API are very unhappy upon
392 // receipt of a PUT/POST message withtout a Content-length header,
393 // even if the request has no payload.
394 // Since Java will not generate a Content-length header unless
395 // we use the output stream, we create an output stream for PUT/POST
396 // even if there is no payload.
397 if (requestBody != null) {
398 BufferedWriter bwr = new BufferedWriter(new OutputStreamWriter(out, "UTF-8"));
399 bwr.write(requestBody);
400 bwr.flush();
401 }
402 out.close();
403 }
404
405 activeConnection.connect();
406 System.out.println(activeConnection.getResponseMessage());
407 int retCode = activeConnection.getResponseCode();
408
409 if (retCode >= 500) {
410 if (retries-- > 0) {
411 sleepAndListen();
412 continue;
413 }
414 }
415
416 // populate return fields.
417 responseBody.setLength(0);
418
419 // If the API returned an error code like 403 forbidden, getInputStream
420 // will fail with an IOException.
421 InputStream i = null;
422 try {
423 i = activeConnection.getInputStream();
424 } catch (IOException ioe) {
425 i = activeConnection.getErrorStream();
426 }
427 BufferedReader in = new BufferedReader(new InputStreamReader(i));
428
429 String s;
430 while((s = in.readLine()) != null) {
431 responseBody.append(s);
432 responseBody.append("\n");
433 }
434 String errorHeader = null;
435 // Look for a detailed error message from the server
436 if (activeConnection.getHeaderField("Error") != null) {
437 errorHeader = activeConnection.getHeaderField("Error");
438 System.err.println("Error header: " + errorHeader);
439 } else if (retCode != 200 && responseBody.length()>0) {
440 System.err.println("Error body: " + responseBody);
441 }
442 activeConnection.disconnect();
443
444 if (retCode != 200)
445 throw new OsmApiException(retCode,errorHeader,responseBody.toString());
446
447 return responseBody.toString();
448 } catch (UnknownHostException e) {
449 throw new OsmTransferException(e);
450 } catch (SocketTimeoutException e) {
451 if (retries-- > 0) {
452 continue;
453 }
454 throw new OsmTransferException(e);
455 } catch (ConnectException e) {
456 if (retries-- > 0) {
457 continue;
458 }
459 throw new OsmTransferException(e);
460 } catch (Exception e) {
461 if (e instanceof OsmTransferException) throw (OsmTransferException) e;
462 throw new OsmTransferException(e);
463 }
464 }
465 }
466
467 /**
468 * notifies any listeners about the current state of this API. Currently just
469 * displays the message in the global progress dialog, see {@see Main#pleaseWaitDlg}
470 *
471 * @param message a status message.
472 */
473 protected void notifyStatusMessage(String message) {
474 Main.pleaseWaitDlg.currentAction.setText(message);
475 }
476
477 /**
478 * notifies any listeners about the current about a relative progress. Currently just
479 * increments the progress monitor in the in the global progress dialog, see {@see Main#pleaseWaitDlg}
480 *
481 * @param int the delta
482 */
483 protected void notifyRelativeProgress(int delta) {
484 int current= Main.pleaseWaitDlg.progress.getValue();
485 Main.pleaseWaitDlg.progress.setValue(current + delta);
486 }
487
488
489 protected void setAutoProgressIndication(final boolean enabled) {
490 EventQueue.invokeLater(
491 new Runnable() {
492 public void run() {
493 Main.pleaseWaitDlg.setIndeterminate(enabled);
494 }
495 }
496 );
497 }
498
499 /**
500 * returns the API capabilities; null, if the API is not initialized yet
501 *
502 * @return the API capabilities
503 */
504 public Capabilities getCapabilities() {
505 return capabilities;
506 }
507}
Note: See TracBrowser for help on using the repository browser.