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

Last change on this file since 1811 was 1811, checked in by jttt, 15 years ago

PleaseWait refactoring. Progress is now reported using ProgressMonitor interface, that is available through PleaseWaitRunnable.

File size: 18.6 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.HashMap;
24import java.util.Properties;
25
26import javax.xml.parsers.SAXParserFactory;
27
28import org.openstreetmap.josm.Main;
29import org.openstreetmap.josm.data.osm.Changeset;
30import org.openstreetmap.josm.data.osm.OsmPrimitive;
31import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
32import org.openstreetmap.josm.data.osm.visitor.CreateOsmChangeVisitor;
33import org.openstreetmap.josm.gui.progress.ProgressMonitor;
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, ProgressMonitor progressMonitor) throws OsmTransferException {
282 progressMonitor.beginTask((tr("Opening changeset...")));
283 try {
284 changeset = new Changeset();
285 Properties sysProp = System.getProperties();
286 Object ua = sysProp.get("http.agent");
287 changeset.put("created_by", (ua == null) ? "JOSM" : ua.toString());
288 changeset.put("comment", comment);
289 createPrimitive(changeset);
290 } finally {
291 progressMonitor.finishTask();
292 }
293 }
294
295 /**
296 * Closes a changeset on the server.
297 *
298 * @throws OsmTransferException if something goes wrong.
299 */
300 public void stopChangeset(ProgressMonitor progressMonitor) throws OsmTransferException {
301 progressMonitor.beginTask(tr("Closing changeset..."));
302 try {
303 initialize();
304 sendRequest("PUT", "changeset" + "/" + changeset.id + "/close", null);
305 changeset = null;
306 } finally {
307 progressMonitor.finishTask();
308 }
309 }
310
311 /**
312 * Uploads a list of changes in "diff" form to the server.
313 *
314 * @param list the list of changed OSM Primitives
315 * @return list of processed primitives
316 * @throws OsmTransferException if something is wrong
317 */
318 public Collection<OsmPrimitive> uploadDiff(final Collection<OsmPrimitive> list, ProgressMonitor progressMonitor) throws OsmTransferException {
319
320 progressMonitor.beginTask("", list.size() * 2);
321 try {
322 if (changeset == null)
323 throw new OsmTransferException(tr("No changeset present for diff upload"));
324
325 initialize();
326 final ArrayList<OsmPrimitive> processed = new ArrayList<OsmPrimitive>();
327
328 CreateOsmChangeVisitor duv = new CreateOsmChangeVisitor(changeset, OsmApi.this);
329
330 progressMonitor.subTask(tr("Preparing..."));
331 for (OsmPrimitive osm : list) {
332 osm.visit(duv);
333 progressMonitor.worked(1);
334 }
335 progressMonitor.indeterminateSubTask(tr("Uploading..."));
336
337 String diff = duv.getDocument();
338 try {
339 String diffresult = sendRequest("POST", "changeset/" + changeset.id + "/upload", diff);
340 DiffResultReader.parseDiffResult(diffresult, list, processed, duv.getNewIdMap(),
341 progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
342 } catch(OsmTransferException e) {
343 throw e;
344 } catch(Exception e) {
345 throw new OsmTransferException(e);
346 }
347
348 return processed;
349 } finally {
350 progressMonitor.finishTask();
351 }
352 }
353
354
355
356 private void sleepAndListen() throws OsmTransferCancelledException {
357 // System.out.print("backing off for 10 seconds...");
358 for(int i=0; i < 10; i++) {
359 if (cancel || isAuthCancelled())
360 throw new OsmTransferCancelledException();
361 try {
362 Thread.sleep(1000);
363 } catch (InterruptedException ex) {}
364 }
365 }
366
367 /**
368 * Generic method for sending requests to the OSM API.
369 *
370 * This method will automatically re-try any requests that are answered with a 5xx
371 * error code, or that resulted in a timeout exception from the TCP layer.
372 *
373 * @param requestMethod The http method used when talking with the server.
374 * @param urlSuffix The suffix to add at the server url, not including the version number,
375 * but including any object ids (e.g. "/way/1234/history").
376 * @param requestBody the body of the HTTP request, if any.
377 *
378 * @return the body of the HTTP response, if and only if the response code was "200 OK".
379 * @exception OsmTransferException if the HTTP return code was not 200 (and retries have
380 * been exhausted), or rewrapping a Java exception.
381 */
382 private String sendRequest(String requestMethod, String urlSuffix,
383 String requestBody) throws OsmTransferException {
384
385 StringBuffer responseBody = new StringBuffer();
386
387 int retries = Main.pref.getInteger("osm-server.max-num-retries", DEFAULT_MAX_NUM_RETRIES);
388 retries = Math.max(0,retries);
389
390 while(true) { // the retry loop
391 try {
392 URL url = new URL(new URL(getBaseUrl()), urlSuffix, new MyHttpHandler());
393 System.out.print(requestMethod + " " + url + "... ");
394 activeConnection = (HttpURLConnection)url.openConnection();
395 activeConnection.setConnectTimeout(15000);
396 activeConnection.setRequestMethod(requestMethod);
397 addAuth(activeConnection);
398
399 if (requestMethod.equals("PUT") || requestMethod.equals("POST") || requestMethod.equals("DELETE")) {
400 activeConnection.setDoOutput(true);
401 activeConnection.setRequestProperty("Content-type", "text/xml");
402 OutputStream out = activeConnection.getOutputStream();
403
404 // It seems that certain bits of the Ruby API are very unhappy upon
405 // receipt of a PUT/POST message withtout a Content-length header,
406 // even if the request has no payload.
407 // Since Java will not generate a Content-length header unless
408 // we use the output stream, we create an output stream for PUT/POST
409 // even if there is no payload.
410 if (requestBody != null) {
411 BufferedWriter bwr = new BufferedWriter(new OutputStreamWriter(out, "UTF-8"));
412 bwr.write(requestBody);
413 bwr.flush();
414 }
415 out.close();
416 }
417
418 activeConnection.connect();
419 System.out.println(activeConnection.getResponseMessage());
420 int retCode = activeConnection.getResponseCode();
421
422 if (retCode >= 500) {
423 if (retries-- > 0) {
424 sleepAndListen();
425 continue;
426 }
427 }
428
429 // populate return fields.
430 responseBody.setLength(0);
431
432 // If the API returned an error code like 403 forbidden, getInputStream
433 // will fail with an IOException.
434 InputStream i = null;
435 try {
436 i = activeConnection.getInputStream();
437 } catch (IOException ioe) {
438 i = activeConnection.getErrorStream();
439 }
440 BufferedReader in = new BufferedReader(new InputStreamReader(i));
441
442 String s;
443 while((s = in.readLine()) != null) {
444 responseBody.append(s);
445 responseBody.append("\n");
446 }
447 String errorHeader = null;
448 // Look for a detailed error message from the server
449 if (activeConnection.getHeaderField("Error") != null) {
450 errorHeader = activeConnection.getHeaderField("Error");
451 System.err.println("Error header: " + errorHeader);
452 } else if (retCode != 200 && responseBody.length()>0) {
453 System.err.println("Error body: " + responseBody);
454 }
455 activeConnection.disconnect();
456
457 if (retCode != 200)
458 throw new OsmApiException(retCode,errorHeader.trim(),responseBody.toString().trim());
459
460 return responseBody.toString();
461 } catch (UnknownHostException e) {
462 throw new OsmTransferException(e);
463 } catch (SocketTimeoutException e) {
464 if (retries-- > 0) {
465 continue;
466 }
467 throw new OsmTransferException(e);
468 } catch (ConnectException e) {
469 if (retries-- > 0) {
470 continue;
471 }
472 throw new OsmTransferException(e);
473 } catch (Exception e) {
474 if (e instanceof OsmTransferException) throw (OsmTransferException) e;
475 throw new OsmTransferException(e);
476 }
477 }
478 }
479
480 /**
481 * returns the API capabilities; null, if the API is not initialized yet
482 *
483 * @return the API capabilities
484 */
485 public Capabilities getCapabilities() {
486 return capabilities;
487 }
488}
Note: See TracBrowser for help on using the repository browser.