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; 015 016import org.openstreetmap.josm.data.coor.LatLon; 017import org.openstreetmap.josm.data.osm.Changeset; 018import org.openstreetmap.josm.data.osm.ChangesetDiscussionComment; 019import org.openstreetmap.josm.data.osm.User; 020import org.openstreetmap.josm.gui.progress.ProgressMonitor; 021import org.openstreetmap.josm.tools.Utils; 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" 036 * min_lon="7.380925" min_lat="46.9215164" max_lon="7.3984718" max_lat="46.9226502"> 037 * <tag k="asdfasdf" v="asdfasdf"/> 038 * <tag k="created_by" v="JOSM/1.5 (UNKNOWN de)"/> 039 * <tag k="comment" v="1234"/> 040 * </changeset> 041 * </osm> 042 * </pre> 043 * 044 */ 045public final class OsmChangesetParser { 046 private final List<Changeset> changesets; 047 048 private OsmChangesetParser() { 049 changesets = new LinkedList<>(); 050 } 051 052 /** 053 * Returns the parsed changesets. 054 * @return the parsed changesets 055 */ 056 public List<Changeset> getChangesets() { 057 return changesets; 058 } 059 060 private class Parser extends DefaultHandler { 061 private Locator locator; 062 063 @Override 064 public void setDocumentLocator(Locator locator) { 065 this.locator = locator; 066 } 067 068 protected void throwException(String msg) throws XmlParsingException { 069 throw new XmlParsingException(msg).rememberLocation(locator); 070 } 071 072 /** The current changeset */ 073 private Changeset current; 074 075 /** The current comment */ 076 private ChangesetDiscussionComment comment; 077 078 /** The current comment text */ 079 private StringBuilder text; 080 081 protected void parseChangesetAttributes(Attributes atts) throws XmlParsingException { 082 // -- id 083 String value = atts.getValue("id"); 084 if (value == null) { 085 throwException(tr("Missing mandatory attribute ''{0}''.", "id")); 086 } 087 current.setId(parseNumericAttribute(value, 1)); 088 089 // -- user / uid 090 current.setUser(createUser(atts)); 091 092 // -- created_at 093 value = atts.getValue("created_at"); 094 if (value == null) { 095 current.setCreatedAt(null); 096 } else { 097 current.setCreatedAt(DateUtils.fromString(value)); 098 } 099 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")); 112 } else if ("true".equals(value)) { 113 current.setOpen(true); 114 } else if ("false".equals(value)) { 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); 129 } catch (NumberFormatException e) { 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); 135 } catch (NumberFormatException e) { 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); 145 } catch (NumberFormatException e) { 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); 151 } catch (NumberFormatException e) { 152 throwException(tr("Illegal value for attribute ''{0}''. Got ''{1}''.", "max_lat", max_lat)); 153 } 154 current.setMax(new LatLon(maxLon, maxLat)); 155 } 156 157 // -- comments_count 158 String commentsCount = atts.getValue("comments_count"); 159 if (commentsCount != null) { 160 current.setCommentsCount(parseNumericAttribute(commentsCount, 0)); 161 } 162 } 163 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 175 private int parseNumericAttribute(String value, int minAllowed) throws XmlParsingException { 176 int att = 0; 177 try { 178 att = Integer.parseInt(value); 179 } catch (NumberFormatException e) { 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 188 @Override 189 public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { 190 switch (qName) { 191 case "osm": 192 if (atts == null) { 193 throwException(tr("Missing mandatory attribute ''{0}'' of XML element {1}.", "version", "osm")); 194 return; 195 } 196 String v = atts.getValue("version"); 197 if (v == null) { 198 throwException(tr("Missing mandatory attribute ''{0}''.", "version")); 199 } 200 if (!("0.6".equals(v))) { 201 throwException(tr("Unsupported version: {0}", v)); 202 } 203 break; 204 case "changeset": 205 current = new Changeset(); 206 parseChangesetAttributes(atts); 207 break; 208 case "tag": 209 String key = atts.getValue("k"); 210 String value = atts.getValue("v"); 211 current.put(key, value); 212 break; 213 case "discussion": 214 break; 215 case "comment": 216 parseCommentAttributes(atts); 217 break; 218 case "text": 219 text = new StringBuilder(); 220 break; 221 default: 222 throwException(tr("Undefined element ''{0}'' found in input stream. Aborting.", qName)); 223 } 224 } 225 226 @Override 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 234 public void endElement(String uri, String localName, String qName) throws SAXException { 235 if ("changeset".equals(qName)) { 236 changesets.add(current); 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; 244 } 245 } 246 247 protected User createUser(Attributes atts) throws XmlParsingException { 248 String name = atts.getValue("user"); 249 String uid = atts.getValue("uid"); 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); 258 } catch (NumberFormatException e) { 259 throwException(MessageFormat.format("Illegal value for attribute ''uid''. Got ''{0}''.", uid)); 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 272 * @throws IllegalDataException if the an error was found while parsing the data from the source 273 */ 274 @SuppressWarnings("resource") 275 public static List<Changeset> parse(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException { 276 OsmChangesetParser parser = new OsmChangesetParser(); 277 try { 278 progressMonitor.beginTask(""); 279 progressMonitor.indeterminateSubTask(tr("Parsing list of changesets...")); 280 InputSource inputSource = new InputSource(new InvalidXmlCharacterFilter(new InputStreamReader(source, StandardCharsets.UTF_8))); 281 Utils.parseSafeSAX(inputSource, parser.new Parser()); 282 return parser.getChangesets(); 283 } catch (ParserConfigurationException | SAXException e) { 284 throw new IllegalDataException(e.getMessage(), e); 285 } catch (Exception e) { 286 throw new IllegalDataException(e); 287 } finally { 288 progressMonitor.finishTask(); 289 } 290 } 291}