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

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

remove all these ugly tab stops introduced in the last half year

File size: 18.4 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;
25import java.util.StringTokenizer;
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");
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 * @throws OsmTransferCancelledException if the upload was cancelled by the user
310 */
311 public Collection<OsmPrimitive> uploadDiff(final Collection<OsmPrimitive> list) throws OsmTransferException {
312
313 if (changeset == null)
314 throw new OsmTransferException(tr("No changeset present for diff upload"));
315
316 initialize();
317 final ArrayList<OsmPrimitive> processed = new ArrayList<OsmPrimitive>();
318
319 CreateOsmChangeVisitor duv = new CreateOsmChangeVisitor(changeset, OsmApi.this);
320
321 notifyStatusMessage(tr("Preparing..."));
322 for (OsmPrimitive osm : list) {
323 osm.visit(duv);
324 notifyRelativeProgress(1);
325 }
326 notifyStatusMessage(tr("Uploading..."));
327
328 String diff = duv.getDocument();
329 String diffresult = sendRequest("POST", "changeset/" + changeset.id + "/upload", diff);
330 try {
331 DiffResultReader.parseDiffResult(diffresult, list, processed, duv.getNewIdMap(), Main.pleaseWaitDlg);
332 } catch(Exception e) {
333 throw new OsmTransferException(e);
334 }
335
336 return processed;
337 }
338
339
340
341 private void sleepAndListen() throws OsmTransferCancelledException {
342 // System.out.print("backing off for 10 seconds...");
343 for(int i=0; i < 10; i++) {
344 if (cancel || isAuthCancelled())
345 throw new OsmTransferCancelledException();
346 try {
347 Thread.sleep(1000);
348 } catch (InterruptedException ex) {}
349 }
350 }
351
352 /**
353 * Generic method for sending requests to the OSM API.
354 *
355 * This method will automatically re-try any requests that are answered with a 5xx
356 * error code, or that resulted in a timeout exception from the TCP layer.
357 *
358 * @param requestMethod The http method used when talking with the server.
359 * @param urlSuffix The suffix to add at the server url, not including the version number,
360 * but including any object ids (e.g. "/way/1234/history").
361 * @param requestBody the body of the HTTP request, if any.
362 *
363 * @return the body of the HTTP response, if and only if the response code was "200 OK".
364 * @exception OsmTransferException if the HTTP return code was not 200 (and retries have
365 * been exhausted), or rewrapping a Java exception.
366 */
367 private String sendRequest(String requestMethod, String urlSuffix,
368 String requestBody) throws OsmTransferException {
369
370 StringBuffer responseBody = new StringBuffer();
371
372 int retries = Main.pref.getInteger("osm-server.max-num-retries", DEFAULT_MAX_NUM_RETRIES);
373 retries = Math.max(0,retries);
374
375 while(true) { // the retry loop
376 try {
377 URL url = new URL(new URL(getBaseUrl()), urlSuffix, new MyHttpHandler());
378 System.out.print(requestMethod + " " + url + "... ");
379 activeConnection = (HttpURLConnection)url.openConnection();
380 activeConnection.setConnectTimeout(15000);
381 activeConnection.setRequestMethod(requestMethod);
382 addAuth(activeConnection);
383
384 if (requestMethod.equals("PUT") || requestMethod.equals("POST")) {
385 activeConnection.setDoOutput(true);
386 activeConnection.setRequestProperty("Content-type", "text/xml");
387 OutputStream out = activeConnection.getOutputStream();
388
389 // It seems that certain bits of the Ruby API are very unhappy upon
390 // receipt of a PUT/POST message withtout a Content-length header,
391 // even if the request has no payload.
392 // Since Java will not generate a Content-length header unless
393 // we use the output stream, we create an output stream for PUT/POST
394 // even if there is no payload.
395 if (requestBody != null) {
396 BufferedWriter bwr = new BufferedWriter(new OutputStreamWriter(out, "UTF-8"));
397 bwr.write(requestBody);
398 bwr.flush();
399 }
400 out.close();
401 }
402
403 activeConnection.connect();
404 System.out.println(activeConnection.getResponseMessage());
405 int retCode = activeConnection.getResponseCode();
406
407 if (retCode >= 500) {
408 if (retries-- > 0) {
409 sleepAndListen();
410 continue;
411 }
412 }
413
414 // populate return fields.
415 responseBody.setLength(0);
416
417 // If the API returned an error code like 403 forbidden, getInputStream
418 // will fail with an IOException.
419 InputStream i = null;
420 try {
421 i = activeConnection.getInputStream();
422 } catch (IOException ioe) {
423 i = activeConnection.getErrorStream();
424 }
425 BufferedReader in = new BufferedReader(new InputStreamReader(i));
426
427 String s;
428 while((s = in.readLine()) != null) {
429 responseBody.append(s);
430 responseBody.append("\n");
431 }
432 String errorHeader = null;
433 // Look for a detailed error message from the server
434 if (activeConnection.getHeaderField("Error") != null) {
435 errorHeader = activeConnection.getHeaderField("Error");
436 System.err.println("Error header: " + errorHeader);
437 } else if (retCode != 200 && responseBody.length()>0) {
438 System.err.println("Error body: " + responseBody);
439 }
440 activeConnection.disconnect();
441
442 if (retCode != 200)
443 throw new OsmApiException(retCode,errorHeader,responseBody.toString());
444
445 return responseBody.toString();
446 } catch (UnknownHostException e) {
447 throw new OsmTransferException(e);
448 } catch (SocketTimeoutException e) {
449 if (retries-- > 0) {
450 continue;
451 }
452 throw new OsmTransferException(e);
453 } catch (ConnectException e) {
454 if (retries-- > 0) {
455 continue;
456 }
457 throw new OsmTransferException(e);
458 } catch (Exception e) {
459 if (e instanceof OsmTransferException) throw (OsmTransferException) e;
460 throw new OsmTransferException(e);
461 }
462 }
463 }
464
465 /**
466 * notifies any listeners about the current state of this API. Currently just
467 * displays the message in the global progress dialog, see {@see Main#pleaseWaitDlg}
468 *
469 * @param message a status message.
470 */
471 protected void notifyStatusMessage(String message) {
472 Main.pleaseWaitDlg.currentAction.setText(message);
473 }
474
475 /**
476 * notifies any listeners about the current about a relative progress. Currently just
477 * increments the progress monitor in the in the global progress dialog, see {@see Main#pleaseWaitDlg}
478 *
479 * @param int the delta
480 */
481 protected void notifyRelativeProgress(int delta) {
482 int current= Main.pleaseWaitDlg.progress.getValue();
483 Main.pleaseWaitDlg.progress.setValue(current + delta);
484 }
485}
Note: See TracBrowser for help on using the repository browser.