[3719] | 1 | // License: GPL. For details, see LICENSE file.
|
---|
[2512] | 2 | package org.openstreetmap.josm.io;
|
---|
| 3 |
|
---|
| 4 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
| 5 |
|
---|
| 6 | import java.io.InputStream;
|
---|
| 7 | import java.io.InputStreamReader;
|
---|
[7082] | 8 | import java.nio.charset.StandardCharsets;
|
---|
[2852] | 9 | import java.text.MessageFormat;
|
---|
[7704] | 10 | import java.util.Date;
|
---|
[2512] | 11 | import java.util.LinkedList;
|
---|
| 12 | import java.util.List;
|
---|
| 13 |
|
---|
| 14 | import javax.xml.parsers.ParserConfigurationException;
|
---|
| 15 |
|
---|
| 16 | import org.openstreetmap.josm.data.coor.LatLon;
|
---|
| 17 | import org.openstreetmap.josm.data.osm.Changeset;
|
---|
[7704] | 18 | import org.openstreetmap.josm.data.osm.ChangesetDiscussionComment;
|
---|
[2512] | 19 | import org.openstreetmap.josm.data.osm.User;
|
---|
| 20 | import org.openstreetmap.josm.gui.progress.ProgressMonitor;
|
---|
[8287] | 21 | import org.openstreetmap.josm.tools.Utils;
|
---|
[6906] | 22 | import org.openstreetmap.josm.tools.XmlParsingException;
|
---|
[7299] | 23 | import org.openstreetmap.josm.tools.date.DateUtils;
|
---|
[2512] | 24 | import org.xml.sax.Attributes;
|
---|
| 25 | import org.xml.sax.InputSource;
|
---|
| 26 | import org.xml.sax.Locator;
|
---|
| 27 | import org.xml.sax.SAXException;
|
---|
| 28 | import org.xml.sax.helpers.DefaultHandler;
|
---|
| 29 |
|
---|
| 30 | /**
|
---|
| 31 | * Parser for a list of changesets, encapsulated in an OSM data set structure.
|
---|
| 32 | * Example:
|
---|
| 33 | * <pre>
|
---|
| 34 | * <osm version="0.6" generator="OpenStreetMap server">
|
---|
[8509] | 35 | * <changeset id="143" user="guggis" uid="1" created_at="2009-09-08T20:35:39Z" closed_at="2009-09-08T21:36:12Z" open="false"
|
---|
| 36 | * min_lon="7.380925" min_lat="46.9215164" max_lon="7.3984718" max_lat="46.9226502">
|
---|
[2512] | 37 | * <tag k="asdfasdf" v="asdfasdf"/>
|
---|
| 38 | * <tag k="created_by" v="JOSM/1.5 (UNKNOWN de)"/>
|
---|
| 39 | * <tag k="comment" v="1234"/>
|
---|
| 40 | * </changeset>
|
---|
| 41 | * </osm>
|
---|
| 42 | * </pre>
|
---|
| 43 | *
|
---|
| 44 | */
|
---|
[6362] | 45 | public final class OsmChangesetParser {
|
---|
[7033] | 46 | private final List<Changeset> changesets;
|
---|
[2512] | 47 |
|
---|
| 48 | private OsmChangesetParser() {
|
---|
[7005] | 49 | changesets = new LinkedList<>();
|
---|
[2512] | 50 | }
|
---|
| 51 |
|
---|
[6906] | 52 | /**
|
---|
| 53 | * Returns the parsed changesets.
|
---|
| 54 | * @return the parsed changesets
|
---|
| 55 | */
|
---|
[2512] | 56 | public List<Changeset> getChangesets() {
|
---|
| 57 | return changesets;
|
---|
| 58 | }
|
---|
| 59 |
|
---|
| 60 | private class Parser extends DefaultHandler {
|
---|
| 61 | private Locator locator;
|
---|
| 62 |
|
---|
| 63 | @Override
|
---|
| 64 | public void setDocumentLocator(Locator locator) {
|
---|
| 65 | this.locator = locator;
|
---|
| 66 | }
|
---|
| 67 |
|
---|
[6906] | 68 | protected void throwException(String msg) throws XmlParsingException {
|
---|
| 69 | throw new XmlParsingException(msg).rememberLocation(locator);
|
---|
[2512] | 70 | }
|
---|
[6906] | 71 |
|
---|
| 72 | /** The current changeset */
|
---|
[8840] | 73 | private Changeset current;
|
---|
[2512] | 74 |
|
---|
[7704] | 75 | /** The current comment */
|
---|
[8840] | 76 | private ChangesetDiscussionComment comment;
|
---|
[7704] | 77 |
|
---|
| 78 | /** The current comment text */
|
---|
[8840] | 79 | private StringBuilder text;
|
---|
[7704] | 80 |
|
---|
[8855] | 81 | protected void parseChangesetAttributes(Attributes atts) throws XmlParsingException {
|
---|
[2512] | 82 | // -- id
|
---|
| 83 | String value = atts.getValue("id");
|
---|
| 84 | if (value == null) {
|
---|
| 85 | throwException(tr("Missing mandatory attribute ''{0}''.", "id"));
|
---|
| 86 | }
|
---|
[7700] | 87 | current.setId(parseNumericAttribute(value, 1));
|
---|
[2512] | 88 |
|
---|
[7704] | 89 | // -- user / uid
|
---|
| 90 | current.setUser(createUser(atts));
|
---|
[2512] | 91 |
|
---|
| 92 | // -- created_at
|
---|
| 93 | value = atts.getValue("created_at");
|
---|
| 94 | if (value == null) {
|
---|
| 95 | current.setCreatedAt(null);
|
---|
| 96 | } else {
|
---|
| 97 | current.setCreatedAt(DateUtils.fromString(value));
|
---|
| 98 | }
|
---|
| 99 |
|
---|
| 100 | // -- closed_at
|
---|
| 101 | value = atts.getValue("closed_at");
|
---|
| 102 | if (value == null) {
|
---|
| 103 | current.setClosedAt(null);
|
---|
| 104 | } else {
|
---|
| 105 | current.setClosedAt(DateUtils.fromString(value));
|
---|
| 106 | }
|
---|
| 107 |
|
---|
| 108 | // -- open
|
---|
| 109 | value = atts.getValue("open");
|
---|
| 110 | if (value == null) {
|
---|
| 111 | throwException(tr("Missing mandatory attribute ''{0}''.", "open"));
|
---|
[7012] | 112 | } else if ("true".equals(value)) {
|
---|
[2512] | 113 | current.setOpen(true);
|
---|
[7012] | 114 | } else if ("false".equals(value)) {
|
---|
[2512] | 115 | current.setOpen(false);
|
---|
| 116 | } else {
|
---|
| 117 | throwException(tr("Illegal boolean value for attribute ''{0}''. Got ''{1}''.", "open", value));
|
---|
| 118 | }
|
---|
| 119 |
|
---|
| 120 | // -- min_lon and min_lat
|
---|
| 121 | String min_lon = atts.getValue("min_lon");
|
---|
| 122 | String min_lat = atts.getValue("min_lat");
|
---|
| 123 | String max_lon = atts.getValue("max_lon");
|
---|
| 124 | String max_lat = atts.getValue("max_lat");
|
---|
| 125 | if (min_lon != null && min_lat != null && max_lon != null && max_lat != null) {
|
---|
| 126 | double minLon = 0;
|
---|
| 127 | try {
|
---|
| 128 | minLon = Double.parseDouble(min_lon);
|
---|
[8510] | 129 | } catch (NumberFormatException e) {
|
---|
[2512] | 130 | throwException(tr("Illegal value for attribute ''{0}''. Got ''{1}''.", "min_lon", min_lon));
|
---|
| 131 | }
|
---|
| 132 | double minLat = 0;
|
---|
| 133 | try {
|
---|
| 134 | minLat = Double.parseDouble(min_lat);
|
---|
[8510] | 135 | } catch (NumberFormatException e) {
|
---|
[2512] | 136 | throwException(tr("Illegal value for attribute ''{0}''. Got ''{1}''.", "min_lat", min_lat));
|
---|
| 137 | }
|
---|
| 138 | current.setMin(new LatLon(minLat, minLon));
|
---|
| 139 |
|
---|
| 140 | // -- max_lon and max_lat
|
---|
| 141 |
|
---|
| 142 | double maxLon = 0;
|
---|
| 143 | try {
|
---|
| 144 | maxLon = Double.parseDouble(max_lon);
|
---|
[8510] | 145 | } catch (NumberFormatException e) {
|
---|
[2512] | 146 | throwException(tr("Illegal value for attribute ''{0}''. Got ''{1}''.", "max_lon", max_lon));
|
---|
| 147 | }
|
---|
| 148 | double maxLat = 0;
|
---|
| 149 | try {
|
---|
| 150 | maxLat = Double.parseDouble(max_lat);
|
---|
[8510] | 151 | } catch (NumberFormatException e) {
|
---|
[2512] | 152 | throwException(tr("Illegal value for attribute ''{0}''. Got ''{1}''.", "max_lat", max_lat));
|
---|
| 153 | }
|
---|
| 154 | current.setMax(new LatLon(maxLon, maxLat));
|
---|
| 155 | }
|
---|
[7700] | 156 |
|
---|
| 157 | // -- comments_count
|
---|
| 158 | String commentsCount = atts.getValue("comments_count");
|
---|
| 159 | if (commentsCount != null) {
|
---|
| 160 | current.setCommentsCount(parseNumericAttribute(commentsCount, 0));
|
---|
| 161 | }
|
---|
[2512] | 162 | }
|
---|
| 163 |
|
---|
[7704] | 164 | private void parseCommentAttributes(Attributes atts) throws XmlParsingException {
|
---|
| 165 | // -- date
|
---|
| 166 | String value = atts.getValue("date");
|
---|
| 167 | Date date = null;
|
---|
| 168 | if (value != null) {
|
---|
| 169 | date = DateUtils.fromString(value);
|
---|
| 170 | }
|
---|
| 171 |
|
---|
| 172 | comment = new ChangesetDiscussionComment(date, createUser(atts));
|
---|
| 173 | }
|
---|
| 174 |
|
---|
[7700] | 175 | private int parseNumericAttribute(String value, int minAllowed) throws XmlParsingException {
|
---|
| 176 | int att = 0;
|
---|
| 177 | try {
|
---|
| 178 | att = Integer.parseInt(value);
|
---|
[8510] | 179 | } catch (NumberFormatException e) {
|
---|
[7700] | 180 | throwException(tr("Illegal value for attribute ''{0}''. Got ''{1}''.", "id", value));
|
---|
| 181 | }
|
---|
| 182 | if (att < minAllowed) {
|
---|
| 183 | throwException(tr("Illegal numeric value for attribute ''{0}''. Got ''{1}''.", "id", att));
|
---|
| 184 | }
|
---|
| 185 | return att;
|
---|
| 186 | }
|
---|
| 187 |
|
---|
[6787] | 188 | @Override
|
---|
| 189 | public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
|
---|
[7012] | 190 | switch (qName) {
|
---|
| 191 | case "osm":
|
---|
[2512] | 192 | if (atts == null) {
|
---|
| 193 | throwException(tr("Missing mandatory attribute ''{0}'' of XML element {1}.", "version", "osm"));
|
---|
[7700] | 194 | return;
|
---|
[2512] | 195 | }
|
---|
| 196 | String v = atts.getValue("version");
|
---|
| 197 | if (v == null) {
|
---|
| 198 | throwException(tr("Missing mandatory attribute ''{0}''.", "version"));
|
---|
| 199 | }
|
---|
[7012] | 200 | if (!("0.6".equals(v))) {
|
---|
[2512] | 201 | throwException(tr("Unsupported version: {0}", v));
|
---|
| 202 | }
|
---|
[7012] | 203 | break;
|
---|
| 204 | case "changeset":
|
---|
[2512] | 205 | current = new Changeset();
|
---|
[8855] | 206 | parseChangesetAttributes(atts);
|
---|
[7012] | 207 | break;
|
---|
| 208 | case "tag":
|
---|
[2512] | 209 | String key = atts.getValue("k");
|
---|
| 210 | String value = atts.getValue("v");
|
---|
| 211 | current.put(key, value);
|
---|
[7012] | 212 | break;
|
---|
[7704] | 213 | case "discussion":
|
---|
| 214 | break;
|
---|
| 215 | case "comment":
|
---|
| 216 | parseCommentAttributes(atts);
|
---|
| 217 | break;
|
---|
| 218 | case "text":
|
---|
| 219 | text = new StringBuilder();
|
---|
| 220 | break;
|
---|
[7012] | 221 | default:
|
---|
[2512] | 222 | throwException(tr("Undefined element ''{0}'' found in input stream. Aborting.", qName));
|
---|
| 223 | }
|
---|
| 224 | }
|
---|
| 225 |
|
---|
| 226 | @Override
|
---|
[7704] | 227 | public void characters(char[] ch, int start, int length) throws SAXException {
|
---|
| 228 | if (text != null) {
|
---|
| 229 | text.append(ch, start, length);
|
---|
| 230 | }
|
---|
| 231 | }
|
---|
| 232 |
|
---|
| 233 | @Override
|
---|
[2512] | 234 | public void endElement(String uri, String localName, String qName) throws SAXException {
|
---|
[7012] | 235 | if ("changeset".equals(qName)) {
|
---|
[2512] | 236 | changesets.add(current);
|
---|
[7704] | 237 | current = null;
|
---|
| 238 | } else if ("comment".equals(qName)) {
|
---|
| 239 | current.addDiscussionComment(comment);
|
---|
| 240 | comment = null;
|
---|
| 241 | } else if ("text".equals(qName)) {
|
---|
| 242 | comment.setText(text.toString());
|
---|
| 243 | text = null;
|
---|
[2512] | 244 | }
|
---|
| 245 | }
|
---|
| 246 |
|
---|
[7704] | 247 | protected User createUser(Attributes atts) throws XmlParsingException {
|
---|
| 248 | String name = atts.getValue("user");
|
---|
| 249 | String uid = atts.getValue("uid");
|
---|
[2512] | 250 | if (uid == null) {
|
---|
| 251 | if (name == null)
|
---|
| 252 | return null;
|
---|
| 253 | return User.createLocalUser(name);
|
---|
| 254 | }
|
---|
| 255 | try {
|
---|
| 256 | long id = Long.parseLong(uid);
|
---|
| 257 | return User.createOsmUser(id, name);
|
---|
[8510] | 258 | } catch (NumberFormatException e) {
|
---|
[2852] | 259 | throwException(MessageFormat.format("Illegal value for attribute ''uid''. Got ''{0}''.", uid));
|
---|
[2512] | 260 | }
|
---|
| 261 | return null;
|
---|
| 262 | }
|
---|
| 263 | }
|
---|
| 264 |
|
---|
| 265 | /**
|
---|
| 266 | * Parse the given input source and return the list of changesets
|
---|
| 267 | *
|
---|
| 268 | * @param source the source input stream
|
---|
| 269 | * @param progressMonitor the progress monitor
|
---|
| 270 | *
|
---|
| 271 | * @return the list of changesets
|
---|
[8291] | 272 | * @throws IllegalDataException if the an error was found while parsing the data from the source
|
---|
[2512] | 273 | */
|
---|
[7033] | 274 | @SuppressWarnings("resource")
|
---|
[2512] | 275 | public static List<Changeset> parse(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException {
|
---|
| 276 | OsmChangesetParser parser = new OsmChangesetParser();
|
---|
| 277 | try {
|
---|
[2828] | 278 | progressMonitor.beginTask("");
|
---|
[2688] | 279 | progressMonitor.indeterminateSubTask(tr("Parsing list of changesets..."));
|
---|
[7082] | 280 | InputSource inputSource = new InputSource(new InvalidXmlCharacterFilter(new InputStreamReader(source, StandardCharsets.UTF_8)));
|
---|
[8347] | 281 | Utils.parseSafeSAX(inputSource, parser.new Parser());
|
---|
[2512] | 282 | return parser.getChangesets();
|
---|
[8510] | 283 | } catch (ParserConfigurationException | SAXException e) {
|
---|
[2512] | 284 | throw new IllegalDataException(e.getMessage(), e);
|
---|
[8510] | 285 | } catch (Exception e) {
|
---|
[2512] | 286 | throw new IllegalDataException(e);
|
---|
| 287 | } finally {
|
---|
| 288 | progressMonitor.finishTask();
|
---|
| 289 | }
|
---|
| 290 | }
|
---|
| 291 | }
|
---|