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