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.util.Date; 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.fromString(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.fromString(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 165 private void parseCommentAttributes(Attributes atts) throws XmlParsingException { 166 // -- date 167 String value = atts.getValue("date"); 168 Date date = null; 169 if (value != null) { 170 date = DateUtils.fromString(value); 171 } 172 173 comment = new ChangesetDiscussionComment(date, createUser(atts)); 174 } 175 176 private int parseNumericAttribute(String value, int minAllowed) throws XmlParsingException { 177 int att = 0; 178 try { 179 att = Integer.parseInt(value); 180 } catch (NumberFormatException e) { 181 throwException(tr("Illegal value for attribute ''{0}''. Got ''{1}''.", "id", value)); 182 } 183 if (att < minAllowed) { 184 throwException(tr("Illegal numeric value for attribute ''{0}''. Got ''{1}''.", "id", att)); 185 } 186 return att; 187 } 188 189 @Override 190 public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { 191 switch (qName) { 192 case "osm": 193 if (atts == null) { 194 throwException(tr("Missing mandatory attribute ''{0}'' of XML element {1}.", "version", "osm")); 195 return; 196 } 197 String v = atts.getValue("version"); 198 if (v == null) { 199 throwException(tr("Missing mandatory attribute ''{0}''.", "version")); 200 } 201 if (!("0.6".equals(v))) { 202 throwException(tr("Unsupported version: {0}", v)); 203 } 204 break; 205 case "changeset": 206 current = new Changeset(); 207 parseChangesetAttributes(atts); 208 break; 209 case "tag": 210 String key = atts.getValue("k"); 211 String value = atts.getValue("v"); 212 current.put(key, value); 213 break; 214 case "discussion": 215 break; 216 case "comment": 217 parseCommentAttributes(atts); 218 break; 219 case "text": 220 text = new StringBuilder(); 221 break; 222 default: 223 throwException(tr("Undefined element ''{0}'' found in input stream. Aborting.", qName)); 224 } 225 } 226 227 @Override 228 public void characters(char[] ch, int start, int length) throws SAXException { 229 if (text != null) { 230 text.append(ch, start, length); 231 } 232 } 233 234 @Override 235 public void endElement(String uri, String localName, String qName) throws SAXException { 236 if ("changeset".equals(qName)) { 237 changesets.add(current); 238 current = null; 239 } else if ("comment".equals(qName)) { 240 current.addDiscussionComment(comment); 241 comment = null; 242 } else if ("text".equals(qName)) { 243 comment.setText(text.toString()); 244 text = null; 245 } 246 } 247 248 protected User createUser(Attributes atts) throws XmlParsingException { 249 String name = atts.getValue("user"); 250 String uid = atts.getValue("uid"); 251 if (uid == null) { 252 if (name == null) 253 return null; 254 return User.createLocalUser(name); 255 } 256 try { 257 long id = Long.parseLong(uid); 258 return User.createOsmUser(id, name); 259 } catch (NumberFormatException e) { 260 throwException(MessageFormat.format("Illegal value for attribute ''uid''. Got ''{0}''.", uid)); 261 } 262 return null; 263 } 264 } 265 266 /** 267 * Parse the given input source and return the list of changesets 268 * 269 * @param source the source input stream 270 * @param progressMonitor the progress monitor 271 * 272 * @return the list of changesets 273 * @throws IllegalDataException if the an error was found while parsing the data from the source 274 */ 275 @SuppressWarnings("resource") 276 public static List<Changeset> parse(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException { 277 OsmChangesetParser parser = new OsmChangesetParser(); 278 try { 279 progressMonitor.beginTask(""); 280 progressMonitor.indeterminateSubTask(tr("Parsing list of changesets...")); 281 InputSource inputSource = new InputSource(new InvalidXmlCharacterFilter(new InputStreamReader(source, StandardCharsets.UTF_8))); 282 XmlUtils.parseSafeSAX(inputSource, parser.new Parser()); 283 return parser.getChangesets(); 284 } catch (ParserConfigurationException | SAXException e) { 285 throw new IllegalDataException(e.getMessage(), e); 286 } catch (IOException e) { 287 throw new IllegalDataException(e); 288 } finally { 289 progressMonitor.finishTask(); 290 } 291 } 292}