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

Last change on this file since 2377 was 2377, checked in by Gubaer, 15 years ago

fixed #3443: Illegal value for attribute 'version' when loading OSM file

  • Property svn:eol-style set to native
File size: 23.3 KB
Line 
1package org.openstreetmap.josm.io;
2
3import static org.openstreetmap.josm.tools.I18n.tr;
4
5import java.io.InputStream;
6import java.io.InputStreamReader;
7import java.util.ArrayList;
8import java.util.Collection;
9import java.util.Date;
10import java.util.HashMap;
11import java.util.LinkedList;
12import java.util.List;
13import java.util.Map;
14import java.util.logging.Logger;
15
16import javax.xml.parsers.ParserConfigurationException;
17import javax.xml.parsers.SAXParserFactory;
18
19import org.openstreetmap.josm.data.Bounds;
20import org.openstreetmap.josm.data.coor.LatLon;
21import org.openstreetmap.josm.data.osm.DataSet;
22import org.openstreetmap.josm.data.osm.DataSource;
23import org.openstreetmap.josm.data.osm.Node;
24import org.openstreetmap.josm.data.osm.OsmPrimitive;
25import org.openstreetmap.josm.data.osm.Relation;
26import org.openstreetmap.josm.data.osm.RelationMember;
27import org.openstreetmap.josm.data.osm.User;
28import org.openstreetmap.josm.data.osm.Way;
29import org.openstreetmap.josm.gui.progress.ProgressMonitor;
30import org.openstreetmap.josm.tools.DateUtils;
31import org.xml.sax.Attributes;
32import org.xml.sax.InputSource;
33import org.xml.sax.Locator;
34import org.xml.sax.SAXException;
35import org.xml.sax.helpers.DefaultHandler;
36
37/**
38 * Parser for the Osm Api. Read from an input stream and construct a dataset out of it.
39 *
40 */
41public class OsmReader {
42 static private final Logger logger = Logger.getLogger(OsmReader.class.getName());
43
44 /**
45 * The dataset to add parsed objects to.
46 */
47 private DataSet ds = new DataSet();
48
49 /**
50 * Replies the parsed data set
51 *
52 * @return the parsed data set
53 */
54 public DataSet getDataSet() {
55 return ds;
56 }
57
58 /** the map from external ids to read OsmPrimitives. External ids are
59 * longs too, but in contrast to internal ids negative values are used
60 * to identify primitives unknown to the OSM server
61 *
62 * The keys are strings composed as follows
63 * <ul>
64 * <li>"n" + id for nodes</li>
65 * <li>"w" + id for nodes</li>
66 * <li>"r" + id for nodes</li>
67 * </ul>
68 */
69 private Map<String, OsmPrimitive> externalIdMap = new HashMap<String, OsmPrimitive>();
70
71
72 /**
73 * constructor (for private use only)
74 *
75 * @see #parseDataSet(InputStream, DataSet, ProgressMonitor)
76 * @see #parseDataSetOsm(InputStream, DataSet, ProgressMonitor)
77 */
78 private OsmReader() {
79 externalIdMap = new HashMap<String, OsmPrimitive>();
80 }
81
82 private static class OsmPrimitiveData {
83 public long id = 0;
84 public boolean modified = false;
85 public boolean deleted = false;
86 public Date timestamp = new Date();
87 public User user = null;
88 public boolean visible = true;
89 public int version = 0;
90 public LatLon latlon = new LatLon(0,0);
91 private OsmPrimitive primitive;
92
93 public void copyTo(OsmPrimitive osm) {
94 // id < 0 possible if read from a file
95 if (id <= 0) {
96 osm.clearOsmId();
97 } else {
98 osm.setOsmId(id, version);
99 }
100 osm.setDeleted(deleted);
101 osm.setModified(modified | deleted);
102 osm.setTimestamp(timestamp);
103 osm.setUser(user);
104 osm.setVisible(visible);
105 osm.mappaintStyle = null;
106 }
107
108 public Node createNode() {
109 Node node = new Node();
110 node.setCoor(latlon);
111 copyTo(node);
112 primitive = node;
113 return node;
114 }
115
116 public Way createWay() {
117 Way way = new Way();
118 copyTo(way);
119 primitive = way;
120 return way;
121 }
122 public Relation createRelation() {
123 Relation relation= new Relation();
124 copyTo(relation);
125 primitive = relation;
126 return relation;
127 }
128
129 public void rememberTag(String key, String value) {
130 primitive.put(key, value);
131 }
132 }
133
134 /**
135 * Used as a temporary storage for relation members, before they
136 * are resolved into pointers to real objects.
137 */
138 private static class RelationMemberData {
139 public String type;
140 public long id;
141 public String role;
142 }
143
144 /**
145 * Data structure for the remaining way objects
146 */
147 private Map<Long, Collection<Long>> ways = new HashMap<Long, Collection<Long>>();
148
149 /**
150 * Data structure for relation objects
151 */
152 private Map<Long, Collection<RelationMemberData>> relations = new HashMap<Long, Collection<RelationMemberData>>();
153
154 private class Parser extends DefaultHandler {
155 private Locator locator;
156
157 @Override
158 public void setDocumentLocator(Locator locator) {
159 this.locator = locator;
160 }
161
162 protected void throwException(String msg) throws OsmDataParsingException{
163 throw new OsmDataParsingException(msg).rememberLocation(locator);
164 }
165 /**
166 * The current osm primitive to be read.
167 */
168 private OsmPrimitiveData current;
169 private String generator;
170
171 protected void fixLegacyVersion(Attributes atts) throws SAXException {
172 if (ds.version.equals("0.6") && atts.getValue("version") == null) {
173 throwException(
174 tr("Mandatory attribute ''version'' missing for object with id {0}.", current.id)
175 );
176 } else if (ds.version.equals("0.5") && atts.getValue("version") == null) {
177 // legacy mode. 0.5 data might not have version attribute. Init with
178 // default value 1
179 //
180 current.version = 1;
181 }
182 }
183
184 @Override public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
185 if (qName.equals("osm")) {
186 if (atts == null) {
187 throwException(tr("Missing mandatory attribute ''{0}'' of XML element {1}.", "version", "osm"));
188 }
189 String v = atts.getValue("version");
190 if (v == null) {
191 throwException(tr("Missing mandatory attribute ''{0}''.", "version"));
192 }
193 if (!(v.equals("0.5") || v.equals("0.6"))) {
194 throwException(tr("Unsupported version: {0}", v));
195 }
196 // save generator attribute for later use when creating DataSource objects
197 generator = atts.getValue("generator");
198 ds.version = v;
199
200 } else if (qName.equals("bounds")) {
201 // new style bounds.
202 String minlon = atts.getValue("minlon");
203 String minlat = atts.getValue("minlat");
204 String maxlon = atts.getValue("maxlon");
205 String maxlat = atts.getValue("maxlat");
206 String origin = atts.getValue("origin");
207 if (minlon != null && maxlon != null && minlat != null && maxlat != null) {
208 if (origin == null) {
209 origin = generator;
210 }
211 Bounds bounds = new Bounds(
212 new LatLon(Double.parseDouble(minlat), Double.parseDouble(minlon)),
213 new LatLon(Double.parseDouble(maxlat), Double.parseDouble(maxlon)));
214 DataSource src = new DataSource(bounds, origin);
215 ds.dataSources.add(src);
216 } else {
217 throwException(tr(
218 "Missing manadatory attributes on element ''bounds''. Got minlon=''{0}'',minlat=''{1}'',maxlon=''{3}'',maxlat=''{4}'', origin=''{5}''.",
219 minlon, minlat, maxlon, maxlat, origin
220 ));
221 }
222
223 // ---- PARSING NODES AND WAYS ----
224
225 } else if (qName.equals("node")) {
226 current = new OsmPrimitiveData();
227 current.latlon = new LatLon(getDouble(atts, "lat"), getDouble(atts, "lon"));
228 readCommon(atts, current);
229 fixLegacyVersion(atts);
230 Node n = current.createNode();
231 externalIdMap.put("n"+current.id, n);
232 } else if (qName.equals("way")) {
233 current = new OsmPrimitiveData();
234 readCommon(atts, current);
235 fixLegacyVersion(atts);
236 Way w = current.createWay();
237 externalIdMap.put("w"+current.id, w);
238 ways.put(current.id, new ArrayList<Long>());
239 } else if (qName.equals("nd")) {
240 Collection<Long> list = ways.get(current.id);
241 if (list == null) {
242 throwException(
243 tr("Found XML element <nd> not as direct child of element <way>.")
244 );
245 }
246 if (atts.getValue("ref") == null) {
247 throwException(
248 tr("Missing mandatory attribute ''{0}'' on <nd> of way {1}.", "ref", current.id)
249 );
250 }
251 long id = getLong(atts, "ref");
252 if (id == 0) {
253 throwException(
254 tr("Illegal value of attribute ''ref'' of element <nd>. Got {0}.", id)
255 );
256 }
257 list.add(id);
258
259 // ---- PARSING RELATIONS ----
260
261 } else if (qName.equals("relation")) {
262 current = new OsmPrimitiveData();
263 readCommon(atts, current);
264 fixLegacyVersion(atts);
265 Relation r = current.createRelation();
266 externalIdMap.put("r"+current.id, r );
267 relations.put(current.id, new LinkedList<RelationMemberData>());
268 } else if (qName.equals("member")) {
269 Collection<RelationMemberData> list = relations.get(current.id);
270 if (list == null) {
271 throwException(
272 tr("Found XML element <member> not as direct child of element <relation>.")
273 );
274 }
275 RelationMemberData emd = new RelationMemberData();
276 String value = atts.getValue("ref");
277 if (value == null) {
278 throwException(tr("Missing attribute ''ref'' on member in relation {0}.",current.id));
279 }
280 try {
281 emd.id = Long.parseLong(value);
282 } catch(NumberFormatException e) {
283 throwException(tr("Illegal value for attribute ''ref'' on member in relation {0}. Got {1}", Long.toString(current.id),value));
284 }
285 value = atts.getValue("type");
286 if (value == null) {
287 throwException(tr("Missing attribute ''type'' on member {0} in relation {1}.", Long.toString(emd.id), Long.toString(current.id)));
288 }
289 if (! (value.equals("way") || value.equals("node") || value.equals("relation"))) {
290 throwException(tr("Illegal value for attribute ''type'' on member {0} in relation {1}. Got {2}.", Long.toString(emd.id), Long.toString(current.id), value));
291 }
292 emd.type= value;
293 value = atts.getValue("role");
294 emd.role = value;
295
296 if (emd.id == 0) {
297 throwException(tr("Incomplete <member> specification with ref=0"));
298 }
299
300 list.add(emd);
301
302 // ---- PARSING TAGS (applicable to all objects) ----
303
304 } else if (qName.equals("tag")) {
305 String key = atts.getValue("k");
306 String value = atts.getValue("v");
307 current.rememberTag(key, value);
308 } else {
309 System.out.println(tr("Undefined element ''{0}'' found in input stream. Skipping.", qName));
310 }
311 }
312
313 private double getDouble(Attributes atts, String value) {
314 return Double.parseDouble(atts.getValue(value));
315 }
316
317 private User createUser(String uid, String name) throws SAXException {
318 if (uid == null) {
319 if (name == null)
320 return null;
321 return User.createLocalUser(name);
322 }
323 try {
324 long id = Long.parseLong(uid);
325 return User.createOsmUser(id, name);
326 } catch(NumberFormatException e) {
327 throwException(tr("Illegal value for attribute ''uid''. Got ''{0}''.", uid));
328 }
329 return null;
330 }
331 /**
332 * Read out the common attributes from atts and put them into this.current.
333 */
334 void readCommon(Attributes atts, OsmPrimitiveData current) throws SAXException {
335 current.id = getLong(atts, "id");
336 if (current.id == 0) {
337 throwException(tr("Illegal object with ID=0."));
338 }
339
340 String time = atts.getValue("timestamp");
341 if (time != null && time.length() != 0) {
342 current.timestamp = DateUtils.fromString(time);
343 }
344
345 // user attribute added in 0.4 API
346 String user = atts.getValue("user");
347 // uid attribute added in 0.6 API
348 String uid = atts.getValue("uid");
349 current.user = createUser(uid, user);
350
351 // visible attribute added in 0.4 API
352 String visible = atts.getValue("visible");
353 if (visible != null) {
354 current.visible = Boolean.parseBoolean(visible);
355 }
356
357 String version = atts.getValue("version");
358 current.version = 0;
359 if (version != null) {
360 try {
361 current.version = Integer.parseInt(version);
362 } catch(NumberFormatException e) {
363 throwException(tr("Illegal value for attribute ''version'' on OSM primitive with ID {0}. Got {1}.", Long.toString(current.id), version));
364 }
365 if (ds.version.equals("0.6")){
366 if (current.version <= 0 && current.id > 0) {
367 throwException(tr("Illegal value for attribute ''version'' on OSM primitive with ID {0}. Got {1}.", Long.toString(current.id), version));
368 } else if (current.version < 0 && current.id <=0) {
369 System.out.println(tr("WARNING: Normalizing value of attribute ''version'' of element {0} to {2}, API version is ''{3}''. Got {1}.", current.id, current.version, 0, "0.6"));
370 current.version = 0;
371 }
372 } else if (ds.version.equals("0.5")) {
373 if (current.version <= 0 && current.id > 0) {
374 System.out.println(tr("WARNING: Normalizing value of attribute ''version'' of element {0} to {2}, API version is ''{3}''. Got {1}.", current.id, current.version, 1, "0.5"));
375 current.version = 1;
376 } else if (current.version < 0 && current.id <=0) {
377 System.out.println(tr("WARNING: Normalizing value of attribute ''version'' of element {0} to {2}, API version is ''{3}''. Got {1}.", current.id, current.version, 0, "0.5"));
378 current.version = 0;
379 }
380 } else {
381 // should not happen. API version has been checked before
382 throwException(tr("Unknown or unsupported API version. Got {0}.", ds.version));
383 }
384 } else {
385 // version expected for OSM primitives with an id assigned by the server (id > 0), since API 0.6
386 //
387 if (current.id > 0 && ds.version != null && ds.version.equals("0.6")) {
388 throwException(tr("Missing attribute ''version'' on OSM primitive with ID {0}.", Long.toString(current.id)));
389 }
390 }
391
392 String action = atts.getValue("action");
393 if (action == null)
394 return;
395 if (action.equals("delete")) {
396 current.deleted = true;
397 } else if (action.startsWith("modify")) {
398 current.modified = true;
399 }
400 }
401
402 private long getLong(Attributes atts, String name) throws SAXException {
403 String value = atts.getValue(name);
404 if (value == null) {
405 throwException(tr("Missing required attribute ''{0}''.",name));
406 }
407 try {
408 return Long.parseLong(value);
409 } catch(NumberFormatException e) {
410 throwException(tr("Illegal long value for attribute ''{0}''. Got ''{1}''.",name, value));
411 }
412 return 0; // should not happen
413 }
414 }
415
416
417 /**
418 * Processes the ways after parsing. Rebuilds the list of nodes of each way and
419 * adds the way to the dataset
420 *
421 * @throws IllegalDataException thrown if a data integrity problem is detected
422 */
423 protected void processWaysAfterParsing() throws IllegalDataException{
424 for (Long externalWayId: ways.keySet()) {
425 Way w = (Way)externalIdMap.get("w" + externalWayId);
426 boolean incomplete = false;
427 List<Node> wayNodes = new ArrayList<Node>();
428 for (long id : ways.get(externalWayId)) {
429 Node n = (Node)externalIdMap.get("n" +id);
430 if (n == null) {
431 if (id <= 0)
432 throw new IllegalDataException (
433 tr(
434 "Way with external ID ''{0}'' includes missing node with external ID ''{1}''.",
435 externalWayId,
436 id
437 )
438 );
439 n = new Node(id);
440 n.incomplete = true;
441 incomplete = true;
442 }
443 wayNodes.add(n);
444 }
445 w.setNodes(wayNodes);
446 if (incomplete) {
447 logger.warning(tr("Marked way {0} with {1} nodes incomplete because at least one node was missing in the " +
448 "loaded data and is therefore incomplete too.", externalWayId, w.getNodesCount()));
449 w.incomplete = true;
450 ds.addPrimitive(w);
451 } else {
452 w.incomplete = false;
453 ds.addPrimitive(w);
454 }
455 }
456 }
457
458 /**
459 * Processes the parsed nodes after parsing. Just adds them to
460 * the dataset
461 *
462 */
463 protected void processNodesAfterParsing() {
464 for (OsmPrimitive primitive: externalIdMap.values()) {
465 if (primitive instanceof Node) {
466 this.ds.addPrimitive(primitive);
467 }
468 }
469 }
470
471 /**
472 * Completes the parsed relations with its members.
473 *
474 * @throws IllegalDataException thrown if a data integrity problem is detected, i.e. if a
475 * relation member refers to a local primitive which wasn't available in the data
476 *
477 */
478 private void processRelationsAfterParsing() throws IllegalDataException {
479 for (Long externalRelationId : relations.keySet()) {
480 Relation relation = (Relation) externalIdMap.get("r" +externalRelationId);
481 List<RelationMember> relationMembers = new ArrayList<RelationMember>();
482 for (RelationMemberData rm : relations.get(externalRelationId)) {
483 OsmPrimitive primitive = null;
484
485 // lookup the member from the map of already created primitives
486 //
487 if (rm.type.equals("node")) {
488 primitive = externalIdMap.get("n" + rm.id);
489 } else if (rm.type.equals("way")) {
490 primitive = externalIdMap.get("w" + rm.id);
491 } else if (rm.type.equals("relation")) {
492 primitive = externalIdMap.get("r" + rm.id);
493 } else
494 throw new IllegalDataException(
495 tr("Unknown relation member type ''{0}'' in relation with external id ''{1}''.", rm.type,externalRelationId)
496 );
497
498 if (primitive == null) {
499 if (rm.id <= 0)
500 // relation member refers to a primitive with a negative id which was not
501 // found in the data. This is always a data integrity problem and we abort
502 // with an exception
503 //
504 throw new IllegalDataException(
505 tr(
506 "Relation with external id ''{0}'' refers to a missing primitive with external id ''{1}''.",
507 externalRelationId,
508 rm.id
509 )
510 );
511
512 // member refers to OSM primitive which was not present in the parsed data
513 // -> create a new incomplete primitive and add it to the dataset
514 //
515 if (rm.type.equals("node")) {
516 primitive = new Node(rm.id);
517 } else if (rm.type.equals("way")) {
518 primitive = new Way(rm.id);
519 } else if (rm.type.equals("relation")) {
520 primitive = new Relation(rm.id);
521 } else {
522 // can't happen, we've been testing for valid member types
523 // at the beginning of this method
524 //
525 }
526 ds.addPrimitive(primitive);
527 }
528 relationMembers.add(new RelationMember(rm.role, primitive));
529 }
530 relation.setMembers(relationMembers);
531 ds.addPrimitive(relation);
532 }
533 }
534
535 /**
536 * Parse the given input source and return the dataset.
537 *
538 * @param source the source input stream
539 * @param progressMonitor the progress monitor
540 *
541 * @return the dataset with the parsed data
542 * @throws IllegalDataException thrown if the an error was found while parsing the data from the source
543 */
544 public static DataSet parseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException {
545 OsmReader reader = new OsmReader();
546 try {
547 progressMonitor.beginTask(tr("Prepare OSM data...", 2));
548 progressMonitor.subTask(tr("Parsing OSM data..."));
549 InputSource inputSource = new InputSource(new InputStreamReader(source, "UTF-8"));
550 SAXParserFactory.newInstance().newSAXParser().parse(inputSource, reader.new Parser());
551 progressMonitor.worked(1);
552
553 progressMonitor.subTask(tr("Preparing data set..."));
554 reader.processNodesAfterParsing();
555 reader.processWaysAfterParsing();
556 reader.processRelationsAfterParsing();
557 progressMonitor.worked(1);
558 return reader.getDataSet();
559 } catch(IllegalDataException e) {
560 throw e;
561 } catch(ParserConfigurationException e) {
562 throw new IllegalDataException(e.getMessage(), e);
563 } catch(SAXException e) {
564 throw new IllegalDataException(e.getMessage(), e);
565 } catch(Exception e) {
566 throw new IllegalDataException(e);
567 } finally {
568 progressMonitor.finishTask();
569 }
570 }
571}
Note: See TracBrowser for help on using the repository browser.