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