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

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