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