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

Last change on this file since 1529 was 1529, checked in by framm, 15 years ago

fix API 0.6 upload bug, patch by Matt Amos <zerebubuth@…>

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