/* Copyright (c) 2013 Boundless and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Distribution License v1.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/org/documents/edl-v10.html * * Contributors: * Victor Olaya (Boundless) - initial implementation */ package org.locationtech.geogig.osm.internal.history; import static com.google.common.base.Optional.fromNullable; import static com.google.common.base.Preconditions.checkArgument; import static javax.xml.stream.XMLStreamConstants.END_DOCUMENT; import static javax.xml.stream.XMLStreamConstants.END_ELEMENT; import static javax.xml.stream.XMLStreamConstants.START_ELEMENT; import static org.locationtech.geogig.osm.internal.history.ParsingUtils.parseDateTime; import java.io.InputStream; import java.util.Iterator; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import com.google.common.collect.AbstractIterator; import com.google.common.collect.ImmutableSet; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.Point; /** * Example changeset download: * * <pre> * <code> * <?xml version="1.0" encoding="UTF-8"?> * <osmChange version="0.6" generator="Example" copyright="GeoGig contributors" attribution="http://geogig.org" license="http://geogig.org"> * <create> * <node id="1234" lat="50.3" lon="16.0" changeset="1" user="fred" uid="1" visible="true" timestamp="2014-09-04T03:30:00Z" version="1"> * <tag k="name" v="Tag"/> * <tag k="amenity" v="restaurant"/> * </node> * </create> * </osmChange> * </code> * </pre> */ class ChangesetContentsScanner { private static ImmutableSet<String> CHANGE_TAGS = ImmutableSet.of( Change.Type.create.toString(), Change.Type.modify.toString(), Change.Type.delete.toString()); private static ImmutableSet<String> PRIMITIVE_TAGS = ImmutableSet.of("node", "way", "relation"); private static final GeometryFactory GEOMFACT = new GeometryFactory(); public Iterator<Change> parse(InputStream changesetDownloadStream) throws XMLStreamException { final XMLStreamReader reader; reader = XMLInputFactory.newFactory().createXMLStreamReader(changesetDownloadStream, "UTF-8"); // position reader at first change, if any reader.nextTag(); reader.require(START_ELEMENT, null, "osmChange"); Iterator<Change> iterator = new AbstractIterator<Change>() { @Override protected Change computeNext() { Change next; try { if (findNextChange(reader)) { next = parseChange(reader); } else { return super.endOfData(); } } catch (XMLStreamException e) { System.err.println("Error parsing change, ignoring and continuing " + "with next change if possible. " + e.getMessage()); next = computeNext(); } return next; } }; return iterator; } private boolean findNextChange(XMLStreamReader reader) throws XMLStreamException { int eventType = reader.getEventType(); do { if (eventType == START_ELEMENT) { String tag = reader.getLocalName(); if (CHANGE_TAGS.contains(tag)) { return true; } } reader.next(); eventType = reader.getEventType(); } while (eventType != END_DOCUMENT); return false; } /** * Example changeset: * * <pre> * <code> * <?xml version="1.0" encoding="UTF-8"?> * <osmChange version="0.6" generator="OpenStreetMap server" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/"> * <create> * <relation id="4386" visible="true" timestamp="2009-11-21T09:02:09Z" user="jttt" uid="48" version="1" changeset="1864"> * <tag k="type" v="aoeua"/> * </relation> * </create> * <modify> * <relation id="4385" visible="true" timestamp="2009-11-21T09:05:32Z" user="jttt" uid="48" version="2" changeset="1864"> * <member type="relation" ref="4384" role=""/> * <tag k="type" v="parent"/> * </relation> * </modify> * <delete> * <way id="49391" visible="false" timestamp="2009-11-21T09:05:32Z" user="jttt" uid="48" version="3" changeset="1864"/> * </delete> * <delete> * <node id="1082496" changeset="1864" user="jttt" uid="48" visible="false" timestamp="2009-11-21T09:05:57Z" version="2"/> * </delete> * <create> * <way id="49393" visible="true" timestamp="2009-11-21T09:12:53Z" user="jttt" uid="48" version="1" changeset="1864"> * <nd ref="1082500"/> * <nd ref="1082501"/> * <nd ref="1082502"/> * <nd ref="1082503"/> * </way> * </create> * <modify> * <relation id="4394" visible="true" timestamp="2009-11-21T09:21:31Z" user="jttt" uid="48" version="2" changeset="1864"> * <member type="relation" ref="4393" role=""/> * <member type="relation" ref="4395" role=""/> * <member type="relation" ref="4396" role=""/> * <tag k="eouaoeu" v="oeueaoeu"/> * </relation> * </modify> <create> * <relation id="4390" visible="true" timestamp="2009-11-21T09:12:53Z" user="jttt" uid="48" version="1" changeset="1864"> * <tag k="type" v="aeou"/> * </relation> * </create> * <create> * <relation id="4391" visible="true" timestamp="2009-11-21T09:12:53Z" user="jttt" uid="48" version="1" changeset="1864"> * <tag k="type" v="aoeuau"/> * </relation> * </create> * <create> * <relation id="4392" visible="true" timestamp="2009-11-21T09:13:55Z" user="jttt" uid="48" version="1" changeset="1864"> * <tag k="type" v="aeou"/> * </relation> * </create> * <create> * <relation id="4393" visible="true" timestamp="2009-11-21T09:18:06Z" user="jttt" uid="48" version="1" changeset="1864"> * <member type="way" ref="49393" role=""/> * <tag k="type" v="aeua"/> * </relation> * </create> * <create> * <relation id="4394" visible="true" timestamp="2009-11-21T09:18:49Z" user="jttt" uid="48" version="1" changeset="1864"> * <member type="relation" ref="4393" role=""/> * <tag k="eouaoeu" v="oeueaoeu"/> * </relation> * </create> * </osmChange> * </code> * </pre> */ private Change parseChange(XMLStreamReader reader) throws XMLStreamException { reader.require(START_ELEMENT, null, null); final String changeName = reader.getLocalName(); checkArgument(CHANGE_TAGS.contains(changeName)); final Change.Type type = Change.Type.valueOf(reader.getLocalName()); reader.nextTag(); reader.require(START_ELEMENT, null, null); final String primitiveName = reader.getLocalName(); checkArgument(PRIMITIVE_TAGS.contains(primitiveName)); Primitive primitive = parsePrimitive(reader); reader.require(END_ELEMENT, null, primitiveName); reader.nextTag(); reader.require(END_ELEMENT, null, changeName); Change change = new Change(type, primitive); return change; } Primitive parsePrimitive(XMLStreamReader reader) throws XMLStreamException { reader.require(START_ELEMENT, null, null); final String primitiveName = reader.getLocalName(); checkArgument(PRIMITIVE_TAGS.contains(primitiveName)); Primitive primitive = inferrPrimitive(reader); primitive.setId(Long.valueOf(reader.getAttributeValue(null, "id"))); primitive.setVisible(Boolean.valueOf(reader.getAttributeValue(null, "visible"))); primitive.setTimestamp(parseDateTime(reader.getAttributeValue(null, "timestamp"))); primitive.setUserName(reader.getAttributeValue(null, "user")); Long uid = Long.valueOf(fromNullable(reader.getAttributeValue(null, "uid")).or("-1")); primitive.setUserId(uid); Integer version = Integer.valueOf(fromNullable(reader.getAttributeValue(null, "version")) .or("1")); primitive.setVersion(version); primitive.setChangesetId(Long.valueOf(reader.getAttributeValue(null, "changeset"))); if (primitive instanceof Node) { Node node = (Node) primitive; String lat = reader.getAttributeValue(null, "lat"); String lon = reader.getAttributeValue(null, "lon"); // may be null in case of a delete change if (lat != null && lon != null) { double x = Double.valueOf(lon); double y = Double.valueOf(lat); Point location = GEOMFACT.createPoint(new Coordinate(x, y)); node.setLocation(location); } parseNodeContents(node, reader); } else if (primitive instanceof Way) { Way way = (Way) primitive; parseWayContents(way, reader); } else { Relation relation = (Relation) primitive; parseRelationContents(relation, reader); } reader.require(END_ELEMENT, null, primitiveName); return primitive; } /** * @param node * @param reader * @throws XMLStreamException */ private void parseNodeContents(Node node, XMLStreamReader reader) throws XMLStreamException { while (true) { int tag = reader.next(); if (tag == END_ELEMENT) { String tagName = reader.getLocalName(); if (tagName.equals("node")) { break; } } else if (tag == START_ELEMENT) { String tagName = reader.getLocalName(); if ("tag".equals(tagName)) { parseTag(node, reader); reader.require(END_ELEMENT, null, "tag"); } } else if (tag == END_DOCUMENT) { throw new IllegalStateException("premature end of document"); } } reader.require(END_ELEMENT, null, "node"); } /** * @param way * @param reader * @throws XMLStreamException */ private void parseWayContents(Way way, XMLStreamReader reader) throws XMLStreamException { reader.require(START_ELEMENT, null, "way"); while (true) { int tag = reader.next(); if (tag == END_ELEMENT) { String tagName = reader.getLocalName(); if (tagName.equals("way")) { break; } } else if (tag == START_ELEMENT) { String tagName = reader.getLocalName(); if ("tag".equals(tagName)) { parseTag(way, reader); reader.require(END_ELEMENT, null, "tag"); } else if ("nd".equals(tagName)) { long nodeRef = Long.valueOf(reader.getAttributeValue(null, "ref")); reader.nextTag(); reader.require(END_ELEMENT, null, "nd"); way.addNode(nodeRef); } } else if (tag == END_DOCUMENT) { throw new IllegalStateException("premature end of document"); } } reader.require(END_ELEMENT, null, "way"); } /** * @param relation * @param reader * @throws XMLStreamException */ private void parseRelationContents(Relation relation, XMLStreamReader reader) throws XMLStreamException { reader.require(START_ELEMENT, null, "relation"); while (true) { int tag = reader.next(); if (tag == END_ELEMENT) { String tagName = reader.getLocalName(); if (tagName.equals("relation")) { break; } } else if (tag == START_ELEMENT) { String tagName = reader.getLocalName(); if ("tag".equals(tagName)) { parseTag(relation, reader); reader.require(END_ELEMENT, null, "tag"); } else if ("member".equals(tagName)) { String type = reader.getAttributeValue(null, "type"); long ref = Long.valueOf(reader.getAttributeValue(null, "ref")); String role = reader.getAttributeValue(null, "role"); if ("".equals(role)) { role = null; } reader.nextTag(); reader.require(END_ELEMENT, null, "member"); Relation.Member member = new Relation.Member(type, ref, role); relation.addMember(member); } } else if (tag == END_DOCUMENT) { throw new IllegalStateException("premature end of document"); } } reader.require(END_ELEMENT, null, "relation"); } private void parseTag(Primitive primitive, XMLStreamReader reader) throws XMLStreamException { reader.require(START_ELEMENT, null, "tag"); String key = reader.getAttributeValue(null, "k"); String value = reader.getAttributeValue(null, "v"); primitive.getTags().put(key, value); reader.nextTag(); reader.require(END_ELEMENT, null, "tag"); } /** * @param reader * @return */ private Primitive inferrPrimitive(XMLStreamReader reader) { final String primitiveName = reader.getLocalName(); if ("node".equals(primitiveName)) { return new Node(); } else if ("way".equals(primitiveName)) { return new Way(); } else if ("relation".equals(primitiveName)) { return new Relation(); } throw new IllegalArgumentException("Unknown primitive tag: " + primitiveName); } }