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 * &lt;osm version="0.6" generator="OpenStreetMap server"&gt;
035 *     &lt;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"&gt;
037 *         &lt;tag k="asdfasdf" v="asdfasdf"/&gt;
038 *         &lt;tag k="created_by" v="JOSM/1.5 (UNKNOWN de)"/&gt;
039 *         &lt;tag k="comment" v="1234"/&gt;
040 *     &lt;/changeset&gt;
041 * &lt;/osm&gt;
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}