source: josm/trunk/src/org/openstreetmap/josm/io/OsmReader.java

Last change on this file was 18801, checked in by taylor.smock, 3 years ago

Fix #22832: Code cleanup and some simplification, documentation fixes (patch by gaben)

There should not be any functional changes in this patch; it is intended to do
the following:

  • Simplify and cleanup code (example: Arrays.asList(item) -> Collections.singletonList(item))
  • Fix typos in documentation (which also corrects the documentation to match what actually happens, in some cases)
  • Property svn:eol-style set to native
File size: 20.5 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.InputStream;
7import java.util.Arrays;
8import java.util.Collection;
9import java.util.Collections;
10import java.util.Objects;
11import java.util.Set;
12import java.util.TreeSet;
13import java.util.regex.Matcher;
14import java.util.regex.Pattern;
15
16import javax.xml.stream.Location;
17import javax.xml.stream.XMLStreamConstants;
18import javax.xml.stream.XMLStreamException;
19import javax.xml.stream.XMLStreamReader;
20
21import org.openstreetmap.josm.data.osm.Changeset;
22import org.openstreetmap.josm.data.osm.DataSet;
23import org.openstreetmap.josm.data.osm.Node;
24import org.openstreetmap.josm.data.osm.NodeData;
25import org.openstreetmap.josm.data.osm.PrimitiveData;
26import org.openstreetmap.josm.data.osm.Relation;
27import org.openstreetmap.josm.data.osm.RelationData;
28import org.openstreetmap.josm.data.osm.RelationMemberData;
29import org.openstreetmap.josm.data.osm.Tagged;
30import org.openstreetmap.josm.data.osm.Way;
31import org.openstreetmap.josm.data.osm.WayData;
32import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
33import org.openstreetmap.josm.gui.progress.ProgressMonitor;
34import org.openstreetmap.josm.tools.Logging;
35import org.openstreetmap.josm.tools.UncheckedParseException;
36import org.openstreetmap.josm.tools.XmlUtils;
37
38/**
39 * Parser for the Osm API (XML output). Read from an input stream and construct a dataset out of it.
40 *
41 * For each xml element, there is a dedicated method.
42 * The XMLStreamReader cursor points to the start of the element, when the method is
43 * entered, and it must point to the end of the same element, when it is exited.
44 */
45public class OsmReader extends AbstractReader {
46
47 /**
48 * Options are used to change how the xml data is parsed.
49 * For example, {@link Options#CONVERT_UNKNOWN_TO_TAGS} is used to convert unknown XML attributes to a tag for the object.
50 * @since 16641
51 */
52 public enum Options {
53 /**
54 * Convert unknown XML attributes to tags
55 */
56 CONVERT_UNKNOWN_TO_TAGS,
57 /**
58 * Save the original id of an object (currently stored in `current_id`)
59 */
60 SAVE_ORIGINAL_ID
61 }
62
63 protected XMLStreamReader parser;
64
65 /** The {@link OsmReader.Options} to use when parsing the xml data */
66 protected final Collection<Options> options;
67
68 private static final Set<String> COMMON_XML_ATTRIBUTES = new TreeSet<>();
69
70 static {
71 COMMON_XML_ATTRIBUTES.add("id");
72 COMMON_XML_ATTRIBUTES.add("timestamp");
73 COMMON_XML_ATTRIBUTES.add("user");
74 COMMON_XML_ATTRIBUTES.add("uid");
75 COMMON_XML_ATTRIBUTES.add("visible");
76 COMMON_XML_ATTRIBUTES.add("version");
77 COMMON_XML_ATTRIBUTES.add("action");
78 COMMON_XML_ATTRIBUTES.add("changeset");
79 COMMON_XML_ATTRIBUTES.add("lat");
80 COMMON_XML_ATTRIBUTES.add("lon");
81 }
82
83 /**
84 * constructor (for private and subclasses use only)
85 *
86 * @see #parseDataSet(InputStream, ProgressMonitor)
87 */
88 protected OsmReader() {
89 this((Options) null);
90 }
91
92 /**
93 * constructor (for private and subclasses use only)
94 * @param options The options to use when reading data
95 *
96 * @see #parseDataSet(InputStream, ProgressMonitor)
97 * @since 16641
98 */
99 protected OsmReader(Options... options) {
100 // Restricts visibility
101 this.options = options == null ? Collections.emptyList() : Arrays.asList(options);
102 }
103
104 protected void setParser(XMLStreamReader parser) {
105 this.parser = parser;
106 }
107
108 protected void throwException(Throwable th) throws XMLStreamException {
109 throw new XmlStreamParsingException(th.getMessage(), parser.getLocation(), th);
110 }
111
112 protected void throwException(String msg, Throwable th) throws XMLStreamException {
113 throw new XmlStreamParsingException(msg, parser.getLocation(), th);
114 }
115
116 protected void throwException(String msg) throws XMLStreamException {
117 throw new XmlStreamParsingException(msg, parser.getLocation());
118 }
119
120 protected void parse() throws XMLStreamException {
121 int event = parser.getEventType();
122 while (true) {
123 if (event == XMLStreamConstants.START_ELEMENT) {
124 parseRoot();
125 } else if (event == XMLStreamConstants.END_ELEMENT)
126 return;
127 if (parser.hasNext()) {
128 event = parser.next();
129 } else {
130 break;
131 }
132 }
133 parser.close();
134 }
135
136 protected void parseRoot() throws XMLStreamException {
137 if ("osm".equals(parser.getLocalName())) {
138 parseOsm();
139 } else {
140 parseUnknown();
141 }
142 }
143
144 private void parseOsm() throws XMLStreamException {
145 try {
146 parseVersion(parser.getAttributeValue(null, "version"));
147 parseDownloadPolicy("download", parser.getAttributeValue(null, "download"));
148 parseUploadPolicy("upload", parser.getAttributeValue(null, "upload"));
149 parseLocked(parser.getAttributeValue(null, "locked"));
150 } catch (IllegalDataException e) {
151 throwException(e);
152 }
153 String generator = parser.getAttributeValue(null, "generator");
154 Long uploadChangesetId = null;
155 if (parser.getAttributeValue(null, "upload-changeset") != null) {
156 uploadChangesetId = getLong("upload-changeset");
157 }
158 while (parser.hasNext()) {
159 int event = parser.next();
160
161 if (cancel) {
162 cancel = false;
163 throw new OsmParsingCanceledException(tr("Reading was canceled"), parser.getLocation());
164 }
165
166 if (event == XMLStreamConstants.START_ELEMENT) {
167 switch (parser.getLocalName()) {
168 case "bounds":
169 parseBounds(generator);
170 break;
171 case "node":
172 parseNode();
173 break;
174 case "way":
175 parseWay();
176 break;
177 case "relation":
178 parseRelation();
179 break;
180 case "changeset":
181 parseChangeset(uploadChangesetId);
182 break;
183 case "remark": // Used by Overpass API
184 parseRemark();
185 break;
186 default:
187 parseUnknown();
188 }
189 } else if (event == XMLStreamConstants.END_ELEMENT) {
190 return;
191 }
192 }
193 }
194
195 private void handleIllegalDataException(IllegalDataException e) throws XMLStreamException {
196 Throwable cause = e.getCause();
197 if (cause instanceof XMLStreamException) {
198 throw (XMLStreamException) cause;
199 } else {
200 throwException(e);
201 }
202 }
203
204 private void parseError() throws XMLStreamException {
205 while (parser.hasNext()) {
206 int event = parser.next();
207 if (event == XMLStreamConstants.CHARACTERS) {
208 throwException(parser.getText());
209 } else {
210 throwException("Unknown error element type");
211 }
212 }
213 }
214
215 private void parseRemark() throws XMLStreamException {
216 while (parser.hasNext()) {
217 int event = parser.next();
218 if (event == XMLStreamConstants.CHARACTERS) {
219 ds.setRemark(parser.getText());
220 } else if (event == XMLStreamConstants.END_ELEMENT) {
221 return;
222 }
223 }
224 }
225
226 private void parseBounds(String generator) throws XMLStreamException {
227 String minlon = parser.getAttributeValue(null, "minlon");
228 String minlat = parser.getAttributeValue(null, "minlat");
229 String maxlon = parser.getAttributeValue(null, "maxlon");
230 String maxlat = parser.getAttributeValue(null, "maxlat");
231 String origin = parser.getAttributeValue(null, "origin");
232 try {
233 parseBounds(generator, minlon, minlat, maxlon, maxlat, origin);
234 } catch (IllegalDataException e) {
235 handleIllegalDataException(e);
236 }
237 jumpToEnd();
238 }
239
240 protected Node parseNode() throws XMLStreamException {
241 String lat = parser.getAttributeValue(null, "lat");
242 String lon = parser.getAttributeValue(null, "lon");
243 try {
244 return parseNode(lat, lon, this::readCommon, this::parseNodeTags);
245 } catch (IllegalDataException e) {
246 handleIllegalDataException(e);
247 }
248 return null;
249 }
250
251 private void parseNodeTags(NodeData n) throws IllegalDataException {
252 try {
253 while (parser.hasNext()) {
254 int event = parser.next();
255 if (event == XMLStreamConstants.START_ELEMENT) {
256 if ("tag".equals(parser.getLocalName())) {
257 parseTag(n);
258 } else {
259 parseUnknown();
260 }
261 } else if (event == XMLStreamConstants.END_ELEMENT) {
262 return;
263 }
264 }
265 } catch (XMLStreamException e) {
266 throw new IllegalDataException(e);
267 }
268 }
269
270 protected Way parseWay() throws XMLStreamException {
271 try {
272 return parseWay(this::readCommon, this::parseWayNodesAndTags);
273 } catch (IllegalDataException e) {
274 handleIllegalDataException(e);
275 }
276 return null;
277 }
278
279 private void parseWayNodesAndTags(WayData w, Collection<Long> nodeIds) throws IllegalDataException {
280 try {
281 while (parser.hasNext()) {
282 int event = parser.next();
283 if (event == XMLStreamConstants.START_ELEMENT) {
284 switch (parser.getLocalName()) {
285 case "nd":
286 nodeIds.add(parseWayNode(w));
287 break;
288 case "tag":
289 parseTag(w);
290 break;
291 default:
292 parseUnknown();
293 }
294 } else if (event == XMLStreamConstants.END_ELEMENT) {
295 break;
296 }
297 }
298 } catch (XMLStreamException e) {
299 throw new IllegalDataException(e);
300 }
301 }
302
303 private long parseWayNode(WayData w) throws XMLStreamException {
304 if (parser.getAttributeValue(null, "ref") == null) {
305 throwException(
306 tr("Missing mandatory attribute ''{0}'' on <nd> of way {1}.", "ref", Long.toString(w.getUniqueId()))
307 );
308 }
309 long id = getLong("ref");
310 if (id == 0) {
311 throwException(
312 tr("Illegal value of attribute ''ref'' of element <nd>. Got {0}.", Long.toString(id))
313 );
314 }
315 jumpToEnd();
316 return id;
317 }
318
319 protected Relation parseRelation() throws XMLStreamException {
320 try {
321 return parseRelation(this::readCommon, this::parseRelationMembersAndTags);
322 } catch (IllegalDataException e) {
323 handleIllegalDataException(e);
324 }
325 return null;
326 }
327
328 private void parseRelationMembersAndTags(RelationData r, Collection<RelationMemberData> members) throws IllegalDataException {
329 try {
330 while (parser.hasNext()) {
331 int event = parser.next();
332 if (event == XMLStreamConstants.START_ELEMENT) {
333 switch (parser.getLocalName()) {
334 case "member":
335 members.add(parseRelationMember(r));
336 break;
337 case "tag":
338 parseTag(r);
339 break;
340 default:
341 parseUnknown();
342 }
343 } else if (event == XMLStreamConstants.END_ELEMENT) {
344 break;
345 }
346 }
347 } catch (XMLStreamException e) {
348 throw new IllegalDataException(e);
349 }
350 }
351
352 private RelationMemberData parseRelationMember(RelationData r) throws XMLStreamException {
353 RelationMemberData result = null;
354 try {
355 String ref = parser.getAttributeValue(null, "ref");
356 String type = parser.getAttributeValue(null, "type");
357 String role = parser.getAttributeValue(null, "role");
358 result = parseRelationMember(r, ref, type, role);
359 jumpToEnd();
360 } catch (IllegalDataException e) {
361 handleIllegalDataException(e);
362 }
363 return result;
364 }
365
366 private void parseChangeset(Long uploadChangesetId) throws XMLStreamException {
367
368 Long id = null;
369 if (parser.getAttributeValue(null, "id") != null) {
370 id = getLong("id");
371 }
372 // Read changeset info if neither upload-changeset nor id are set, or if they are both set to the same value
373 if (Objects.equals(id, uploadChangesetId)) {
374 uploadChangeset = new Changeset(id != null ? id.intValue() : 0);
375 while (true) {
376 int event = parser.next();
377 if (event == XMLStreamConstants.START_ELEMENT) {
378 if ("tag".equals(parser.getLocalName())) {
379 parseTag(uploadChangeset);
380 } else {
381 parseUnknown();
382 }
383 } else if (event == XMLStreamConstants.END_ELEMENT)
384 return;
385 }
386 } else {
387 jumpToEnd(false);
388 }
389 }
390
391 private void parseTag(Tagged t) throws XMLStreamException {
392 String key = parser.getAttributeValue(null, "k");
393 String value = parser.getAttributeValue(null, "v");
394 try {
395 parseTag(t, key, value);
396 } catch (IllegalDataException e) {
397 throwException(e);
398 }
399 jumpToEnd();
400 }
401
402 protected void parseUnknown(boolean printWarning) throws XMLStreamException {
403 final String element = parser.getLocalName();
404 if (printWarning && ("note".equals(element) || "meta".equals(element))) {
405 // we know that Overpass API returns those elements
406 Logging.debug(tr("Undefined element ''{0}'' found in input stream. Skipping.", element));
407 } else if ("error".equals(element)) {
408 parseError();
409 } else if (printWarning) {
410 Logging.info(tr("Undefined element ''{0}'' found in input stream. Skipping.", element));
411 }
412 while (true) {
413 int event = parser.next();
414 if (event == XMLStreamConstants.START_ELEMENT) {
415 parseUnknown(false); /* no more warning for inner elements */
416 } else if (event == XMLStreamConstants.END_ELEMENT)
417 return;
418 }
419 }
420
421 protected void parseUnknown() throws XMLStreamException {
422 parseUnknown(true);
423 }
424
425 /**
426 * When cursor is at the start of an element, moves it to the end tag of that element.
427 * Nested content is skipped.
428 *
429 * This is basically the same code as parseUnknown(), except for the warnings, which
430 * are displayed for inner elements and not at top level.
431 * @param printWarning if {@code true}, a warning message will be printed if an unknown element is met
432 * @throws XMLStreamException if there is an error processing the underlying XML source
433 */
434 protected final void jumpToEnd(boolean printWarning) throws XMLStreamException {
435 while (true) {
436 int event = parser.next();
437 if (event == XMLStreamConstants.START_ELEMENT) {
438 parseUnknown(printWarning);
439 } else if (event == XMLStreamConstants.END_ELEMENT)
440 return;
441 }
442 }
443
444 protected final void jumpToEnd() throws XMLStreamException {
445 jumpToEnd(true);
446 }
447
448 /**
449 * Read out the common attributes and put them into current OsmPrimitive.
450 * @param current primitive to update
451 * @throws IllegalDataException if there is an error processing the underlying XML source
452 */
453 private void readCommon(PrimitiveData current) throws IllegalDataException {
454 try {
455 parseId(current, getLong("id"));
456 parseTimestamp(current, parser.getAttributeValue(null, "timestamp"));
457 parseUser(current, parser.getAttributeValue(null, "user"), parser.getAttributeValue(null, "uid"));
458 parseVisible(current, parser.getAttributeValue(null, "visible"));
459 parseVersion(current, parser.getAttributeValue(null, "version"));
460 parseAction(current, parser.getAttributeValue(null, "action"));
461 parseChangeset(current, parser.getAttributeValue(null, "changeset"));
462
463 if (options.contains(Options.SAVE_ORIGINAL_ID)) {
464 parseTag(current, "current_id", Long.toString(getLong("id")));
465 }
466 if (options.contains(Options.CONVERT_UNKNOWN_TO_TAGS)) {
467 for (int i = 0; i < parser.getAttributeCount(); i++) {
468 if (!COMMON_XML_ATTRIBUTES.contains(parser.getAttributeLocalName(i))) {
469 parseTag(current, parser.getAttributeLocalName(i), parser.getAttributeValue(i));
470 }
471 }
472 }
473 } catch (UncheckedParseException | XMLStreamException e) {
474 throw new IllegalDataException(e);
475 }
476 }
477
478 private long getLong(String name) throws XMLStreamException {
479 String value = parser.getAttributeValue(null, name);
480 try {
481 return getLong(name, value);
482 } catch (IllegalDataException e) {
483 throwException(e);
484 }
485 return 0; // should not happen
486 }
487
488 /**
489 * Exception thrown after user cancellation.
490 */
491 private static final class OsmParsingCanceledException extends XmlStreamParsingException implements ImportCancelException {
492 /**
493 * Constructs a new {@code OsmParsingCanceledException}.
494 * @param msg The error message
495 * @param location The parser location
496 */
497 OsmParsingCanceledException(String msg, Location location) {
498 super(msg, location);
499 }
500 }
501
502 @Override
503 protected DataSet doParseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException {
504 return doParseDataSet(source, progressMonitor, (ParserWorker) ir -> {
505 try {
506 setParser(XmlUtils.newSafeXMLInputFactory().createXMLStreamReader(ir));
507 parse();
508 } catch (XmlStreamParsingException | UncheckedParseException e) {
509 throw new IllegalDataException(e.getMessage(), e);
510 } catch (XMLStreamException e) {
511 String msg = e.getMessage();
512 Pattern p = Pattern.compile("Message: (.+)");
513 Matcher m = p.matcher(msg);
514 if (m.find()) {
515 msg = m.group(1);
516 }
517 if (e.getLocation() != null)
518 throw new IllegalDataException(tr("Line {0} column {1}: ",
519 e.getLocation().getLineNumber(), e.getLocation().getColumnNumber()) + msg, e);
520 else
521 throw new IllegalDataException(msg, e);
522 }
523 });
524 }
525
526 /**
527 * Parse the given input source and return the dataset.
528 *
529 * @param source the source input stream. Must not be null.
530 * @param progressMonitor the progress monitor. If null, {@link NullProgressMonitor#INSTANCE} is assumed
531 *
532 * @return the dataset with the parsed data
533 * @throws IllegalDataException if an error was found while parsing the data from the source
534 * @throws IllegalArgumentException if source is null
535 */
536 public static DataSet parseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException {
537 return parseDataSet(source, progressMonitor, (Options) null);
538 }
539
540 /**
541 * Parse the given input source and return the dataset.
542 *
543 * @param source the source input stream. Must not be null.
544 * @param progressMonitor the progress monitor. If null, {@link NullProgressMonitor#INSTANCE} is assumed
545 * @param options The options to use when parsing the dataset
546 *
547 * @return the dataset with the parsed data
548 * @throws IllegalDataException if an error was found while parsing the data from the source
549 * @throws IllegalArgumentException if source is null
550 * @since 16641
551 */
552 public static DataSet parseDataSet(InputStream source, ProgressMonitor progressMonitor, Options... options)
553 throws IllegalDataException {
554 return new OsmReader(options).doParseDataSet(source, progressMonitor);
555 }
556}
Note: See TracBrowser for help on using the repository browser.