001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io;
003
004import java.io.ByteArrayInputStream;
005import java.io.IOException;
006import java.io.InputStream;
007import java.nio.charset.StandardCharsets;
008import java.util.ArrayList;
009import java.util.Date;
010import java.util.List;
011import java.util.Locale;
012import java.util.Optional;
013
014import javax.xml.parsers.ParserConfigurationException;
015
016import org.openstreetmap.josm.data.coor.LatLon;
017import org.openstreetmap.josm.data.notes.Note;
018import org.openstreetmap.josm.data.notes.NoteComment;
019import org.openstreetmap.josm.data.notes.NoteComment.Action;
020import org.openstreetmap.josm.data.osm.User;
021import org.openstreetmap.josm.tools.Logging;
022import org.openstreetmap.josm.tools.XmlUtils;
023import org.openstreetmap.josm.tools.date.DateUtils;
024import org.xml.sax.Attributes;
025import org.xml.sax.InputSource;
026import org.xml.sax.SAXException;
027import org.xml.sax.helpers.DefaultHandler;
028
029/**
030 * Class to read Note objects from their XML representation. It can take
031 * either API style XML which starts with an "osm" tag or a planet dump
032 * style XML which starts with an "osm-notes" tag.
033 */
034public class NoteReader {
035
036    private final InputSource inputSource;
037    private List<Note> parsedNotes;
038
039    /**
040     * Notes can be represented in two XML formats. One is returned by the API
041     * while the other is used to generate the notes dump file. The parser
042     * needs to know which one it is handling.
043     */
044    private enum NoteParseMode {
045        API,
046        DUMP
047    }
048
049    /**
050     * SAX handler to read note information from its XML representation.
051     * Reads both API style and planet dump style formats.
052     */
053    private class Parser extends DefaultHandler {
054
055        private NoteParseMode parseMode;
056        private final StringBuilder buffer = new StringBuilder();
057        private Note thisNote;
058        private long commentUid;
059        private String commentUsername;
060        private Action noteAction;
061        private Date commentCreateDate;
062        private boolean commentIsNew;
063        private List<Note> notes;
064        private String commentText;
065
066        @Override
067        public void characters(char[] ch, int start, int length) throws SAXException {
068            buffer.append(ch, start, length);
069        }
070
071        @Override
072        public void startElement(String uri, String localName, String qName, Attributes attrs) throws SAXException {
073            buffer.setLength(0);
074            switch(qName) {
075            case "osm":
076                parseMode = NoteParseMode.API;
077                notes = new ArrayList<>(100);
078                return;
079            case "osm-notes":
080                parseMode = NoteParseMode.DUMP;
081                notes = new ArrayList<>(10_000);
082                return;
083            }
084
085            if (parseMode == NoteParseMode.API) {
086                if ("note".equals(qName)) {
087                    double lat = Double.parseDouble(attrs.getValue("lat"));
088                    double lon = Double.parseDouble(attrs.getValue("lon"));
089                    LatLon noteLatLon = new LatLon(lat, lon);
090                    thisNote = new Note(noteLatLon);
091                }
092                return;
093            }
094
095            //The rest only applies for dump mode
096            switch(qName) {
097            case "note":
098                double lat = Double.parseDouble(attrs.getValue("lat"));
099                double lon = Double.parseDouble(attrs.getValue("lon"));
100                LatLon noteLatLon = new LatLon(lat, lon);
101                thisNote = new Note(noteLatLon);
102                thisNote.setId(Long.parseLong(attrs.getValue("id")));
103                String closedTimeStr = attrs.getValue("closed_at");
104                if (closedTimeStr == null) { //no closed_at means the note is still open
105                    thisNote.setState(Note.State.OPEN);
106                } else {
107                    thisNote.setState(Note.State.CLOSED);
108                    thisNote.setClosedAt(DateUtils.fromString(closedTimeStr));
109                }
110                thisNote.setCreatedAt(DateUtils.fromString(attrs.getValue("created_at")));
111                break;
112            case "comment":
113                commentUid = Long.parseLong(Optional.ofNullable(attrs.getValue("uid")).orElse("0"));
114                commentUsername = attrs.getValue("user");
115                noteAction = Action.valueOf(attrs.getValue("action").toUpperCase(Locale.ENGLISH));
116                commentCreateDate = DateUtils.fromString(attrs.getValue("timestamp"));
117                commentIsNew = Boolean.parseBoolean(Optional.ofNullable(attrs.getValue("is_new")).orElse("false"));
118                break;
119            default: // Do nothing
120            }
121        }
122
123        @Override
124        public void endElement(String namespaceURI, String localName, String qName) {
125            if (notes != null && "note".equals(qName)) {
126                notes.add(thisNote);
127            }
128            if ("comment".equals(qName)) {
129                User commentUser = User.createOsmUser(commentUid, commentUsername);
130                if (commentUid == 0) {
131                    commentUser = User.getAnonymous();
132                }
133                if (parseMode == NoteParseMode.API) {
134                    commentIsNew = false;
135                }
136                if (parseMode == NoteParseMode.DUMP) {
137                    commentText = buffer.toString();
138                }
139                thisNote.addComment(new NoteComment(commentCreateDate, commentUser, commentText, noteAction, commentIsNew));
140                commentUid = 0;
141                commentUsername = null;
142                commentCreateDate = null;
143                commentIsNew = false;
144                commentText = null;
145            }
146            if (parseMode == NoteParseMode.DUMP) {
147                return;
148            }
149
150            //the rest only applies to API mode
151            switch (qName) {
152            case "id":
153                thisNote.setId(Long.parseLong(buffer.toString()));
154                break;
155            case "status":
156                thisNote.setState(Note.State.valueOf(buffer.toString().toUpperCase(Locale.ENGLISH)));
157                break;
158            case "date_created":
159                thisNote.setCreatedAt(DateUtils.fromString(buffer.toString()));
160                break;
161            case "date_closed":
162                thisNote.setClosedAt(DateUtils.fromString(buffer.toString()));
163                break;
164            case "date":
165                commentCreateDate = DateUtils.fromString(buffer.toString());
166                break;
167            case "user":
168                commentUsername = buffer.toString();
169                break;
170            case "uid":
171                commentUid = Long.parseLong(buffer.toString());
172                break;
173            case "text":
174                commentText = buffer.toString();
175                buffer.setLength(0);
176                break;
177            case "action":
178                noteAction = Action.valueOf(buffer.toString().toUpperCase(Locale.ENGLISH));
179                break;
180            case "note": //nothing to do for comment or note, already handled above
181            case "comment":
182                break;
183            }
184        }
185
186        @Override
187        public void endDocument() throws SAXException {
188            parsedNotes = notes;
189        }
190    }
191
192    /**
193     * Initializes the reader with a given InputStream
194     * @param source - InputStream containing Notes XML
195     */
196    public NoteReader(InputStream source) {
197        this.inputSource = new InputSource(source);
198    }
199
200    /**
201     * Initializes the reader with a string as a source
202     * @param source UTF-8 string containing Notes XML to parse
203     */
204    public NoteReader(String source) {
205        this.inputSource = new InputSource(new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8)));
206    }
207
208    /**
209     * Parses the InputStream given to the constructor and returns
210     * the resulting Note objects
211     * @return List of Notes parsed from the input data
212     * @throws SAXException if any SAX parsing error occurs
213     * @throws IOException if any I/O error occurs
214     */
215    public List<Note> parse() throws SAXException, IOException {
216        DefaultHandler parser = new Parser();
217        try {
218            XmlUtils.parseSafeSAX(inputSource, parser);
219        } catch (ParserConfigurationException e) {
220            Logging.error(e); // broken SAXException chaining
221            throw new SAXException(e);
222        }
223        return parsedNotes;
224    }
225}