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