001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.io; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.io.IOException; 007import java.io.InputStream; 008import java.io.InputStreamReader; 009import java.nio.charset.StandardCharsets; 010import java.text.MessageFormat; 011import java.time.Instant; 012import java.util.LinkedList; 013import java.util.List; 014 015import javax.xml.parsers.ParserConfigurationException; 016 017import org.openstreetmap.josm.data.coor.LatLon; 018import org.openstreetmap.josm.data.osm.Changeset; 019import org.openstreetmap.josm.data.osm.ChangesetDiscussionComment; 020import org.openstreetmap.josm.data.osm.User; 021import org.openstreetmap.josm.gui.progress.ProgressMonitor; 022import org.openstreetmap.josm.tools.XmlParsingException; 023import org.openstreetmap.josm.tools.XmlUtils; 024import org.openstreetmap.josm.tools.date.DateUtils; 025import org.xml.sax.Attributes; 026import org.xml.sax.InputSource; 027import org.xml.sax.Locator; 028import org.xml.sax.SAXException; 029import org.xml.sax.helpers.DefaultHandler; 030 031/** 032 * Parser for a list of changesets, encapsulated in an OSM data set structure. 033 * Example: 034 * <pre> 035 * <osm version="0.6" generator="OpenStreetMap server"> 036 * <changeset id="143" user="guggis" uid="1" created_at="2009-09-08T20:35:39Z" closed_at="2009-09-08T21:36:12Z" open="false" 037 * min_lon="7.380925" min_lat="46.9215164" max_lon="7.3984718" max_lat="46.9226502"> 038 * <tag k="asdfasdf" v="asdfasdf"/> 039 * <tag k="created_by" v="JOSM/1.5 (UNKNOWN de)"/> 040 * <tag k="comment" v="1234"/> 041 * </changeset> 042 * </osm> 043 * </pre> 044 * 045 */ 046public final class OsmChangesetParser { 047 private final List<Changeset> changesets; 048 049 private OsmChangesetParser() { 050 changesets = new LinkedList<>(); 051 } 052 053 /** 054 * Returns the parsed changesets. 055 * @return the parsed changesets 056 */ 057 public List<Changeset> getChangesets() { 058 return changesets; 059 } 060 061 private class Parser extends DefaultHandler { 062 private Locator locator; 063 064 @Override 065 public void setDocumentLocator(Locator locator) { 066 this.locator = locator; 067 } 068 069 protected void throwException(String msg) throws XmlParsingException { 070 throw new XmlParsingException(msg).rememberLocation(locator); 071 } 072 073 /** The current changeset */ 074 private Changeset current; 075 076 /** The current comment */ 077 private ChangesetDiscussionComment comment; 078 079 /** The current comment text */ 080 private StringBuilder text; 081 082 protected void parseChangesetAttributes(Attributes atts) throws XmlParsingException { 083 // -- id 084 String value = atts.getValue("id"); 085 if (value == null) { 086 throwException(tr("Missing mandatory attribute ''{0}''.", "id")); 087 } 088 current.setId(parseNumericAttribute(value, 1)); 089 090 // -- user / uid 091 current.setUser(createUser(atts)); 092 093 // -- created_at 094 value = atts.getValue("created_at"); 095 if (value == null) { 096 current.setCreatedAt(null); 097 } else { 098 current.setCreatedAt(DateUtils.parseInstant(value)); 099 } 100 101 // -- closed_at 102 value = atts.getValue("closed_at"); 103 if (value == null) { 104 current.setClosedAt(null); 105 } else { 106 current.setClosedAt(DateUtils.parseInstant(value)); 107 } 108 109 // -- open 110 value = atts.getValue("open"); 111 if (value == null) { 112 throwException(tr("Missing mandatory attribute ''{0}''.", "open")); 113 } else if ("true".equals(value)) { 114 current.setOpen(true); 115 } else if ("false".equals(value)) { 116 current.setOpen(false); 117 } else { 118 throwException(tr("Illegal boolean value for attribute ''{0}''. Got ''{1}''.", "open", value)); 119 } 120 121 // -- min_lon and min_lat 122 String minLonStr = atts.getValue("min_lon"); 123 String minLatStr = atts.getValue("min_lat"); 124 String maxLonStr = atts.getValue("max_lon"); 125 String maxLatStr = atts.getValue("max_lat"); 126 if (minLonStr != null && minLatStr != null && maxLonStr != null && maxLatStr != null) { 127 double minLon = 0; 128 try { 129 minLon = Double.parseDouble(minLonStr); 130 } catch (NumberFormatException e) { 131 throwException(tr("Illegal value for attribute ''{0}''. Got ''{1}''.", "min_lon", minLonStr)); 132 } 133 double minLat = 0; 134 try { 135 minLat = Double.parseDouble(minLatStr); 136 } catch (NumberFormatException e) { 137 throwException(tr("Illegal value for attribute ''{0}''. Got ''{1}''.", "min_lat", minLatStr)); 138 } 139 current.setMin(new LatLon(minLat, minLon)); 140 141 // -- max_lon and max_lat 142 143 double maxLon = 0; 144 try { 145 maxLon = Double.parseDouble(maxLonStr); 146 } catch (NumberFormatException e) { 147 throwException(tr("Illegal value for attribute ''{0}''. Got ''{1}''.", "max_lon", maxLonStr)); 148 } 149 double maxLat = 0; 150 try { 151 maxLat = Double.parseDouble(maxLatStr); 152 } catch (NumberFormatException e) { 153 throwException(tr("Illegal value for attribute ''{0}''. Got ''{1}''.", "max_lat", maxLatStr)); 154 } 155 current.setMax(new LatLon(maxLat, maxLon)); 156 } 157 158 // -- comments_count 159 String commentsCount = atts.getValue("comments_count"); 160 if (commentsCount != null) { 161 current.setCommentsCount(parseNumericAttribute(commentsCount, 0)); 162 } 163 164 // -- changes_count 165 String changesCount = atts.getValue("changes_count"); 166 if (changesCount != null) { 167 current.setChangesCount(parseNumericAttribute(changesCount, 0)); 168 } 169 } 170 171 private void parseCommentAttributes(Attributes atts) throws XmlParsingException { 172 // -- date 173 String value = atts.getValue("date"); 174 Instant date = value != null ? DateUtils.parseInstant(value) : null; 175 comment = new ChangesetDiscussionComment(date, createUser(atts)); 176 } 177 178 private int parseNumericAttribute(String value, int minAllowed) throws XmlParsingException { 179 int att = 0; 180 try { 181 att = Integer.parseInt(value); 182 } catch (NumberFormatException e) { 183 throwException(tr("Illegal value for attribute ''{0}''. Got ''{1}''.", "id", value)); 184 } 185 if (att < minAllowed) { 186 throwException(tr("Illegal numeric value for attribute ''{0}''. Got ''{1}''.", "id", att)); 187 } 188 return att; 189 } 190 191 @Override 192 public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { 193 switch (qName) { 194 case "osm": 195 if (atts == null) { 196 throwException(tr("Missing mandatory attribute ''{0}'' of XML element {1}.", "version", "osm")); 197 return; 198 } 199 String v = atts.getValue("version"); 200 if (v == null) { 201 throwException(tr("Missing mandatory attribute ''{0}''.", "version")); 202 } 203 if (!"0.6".equals(v)) { 204 throwException(tr("Unsupported version: {0}", v)); 205 } 206 break; 207 case "changeset": 208 current = new Changeset(); 209 parseChangesetAttributes(atts); 210 break; 211 case "tag": 212 String key = atts.getValue("k"); 213 String value = atts.getValue("v"); 214 current.put(key, value); 215 break; 216 case "discussion": 217 break; 218 case "comment": 219 parseCommentAttributes(atts); 220 break; 221 case "text": 222 text = new StringBuilder(); 223 break; 224 default: 225 throwException(tr("Undefined element ''{0}'' found in input stream. Aborting.", qName)); 226 } 227 } 228 229 @Override 230 public void characters(char[] ch, int start, int length) throws SAXException { 231 if (text != null) { 232 text.append(ch, start, length); 233 } 234 } 235 236 @Override 237 public void endElement(String uri, String localName, String qName) throws SAXException { 238 if ("changeset".equals(qName)) { 239 changesets.add(current); 240 current = null; 241 } else if ("comment".equals(qName)) { 242 current.addDiscussionComment(comment); 243 comment = null; 244 } else if ("text".equals(qName)) { 245 comment.setText(text.toString()); 246 text = null; 247 } 248 } 249 250 protected User createUser(Attributes atts) throws XmlParsingException { 251 String name = atts.getValue("user"); 252 String uid = atts.getValue("uid"); 253 if (uid == null) { 254 if (name == null) 255 return null; 256 return User.createLocalUser(name); 257 } 258 try { 259 long id = Long.parseLong(uid); 260 return User.createOsmUser(id, name); 261 } catch (NumberFormatException e) { 262 throwException(MessageFormat.format("Illegal value for attribute ''uid''. Got ''{0}''.", uid)); 263 } 264 return null; 265 } 266 } 267 268 /** 269 * Parse the given input source and return the list of changesets 270 * 271 * @param source the source input stream 272 * @param progressMonitor the progress monitor 273 * 274 * @return the list of changesets 275 * @throws IllegalDataException if the an error was found while parsing the data from the source 276 */ 277 @SuppressWarnings("resource") 278 public static List<Changeset> parse(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException { 279 OsmChangesetParser parser = new OsmChangesetParser(); 280 try { 281 progressMonitor.beginTask(""); 282 progressMonitor.indeterminateSubTask(tr("Parsing list of changesets...")); 283 InputSource inputSource = new InputSource(new InvalidXmlCharacterFilter(new InputStreamReader(source, StandardCharsets.UTF_8))); 284 XmlUtils.parseSafeSAX(inputSource, parser.new Parser()); 285 return parser.getChangesets(); 286 } catch (ParserConfigurationException | SAXException e) { 287 throw new IllegalDataException(e.getMessage(), e); 288 } catch (IOException e) { 289 throw new IllegalDataException(e); 290 } finally { 291 progressMonitor.finishTask(); 292 } 293 } 294}