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.text.ParseException;
009import java.text.SimpleDateFormat;
010import java.util.ArrayList;
011import java.util.Date;
012import java.util.List;
013import java.util.Locale;
014
015import javax.xml.parsers.ParserConfigurationException;
016import javax.xml.parsers.SAXParserFactory;
017
018import org.openstreetmap.josm.Main;
019import org.openstreetmap.josm.data.coor.LatLon;
020import org.openstreetmap.josm.data.notes.Note;
021import org.openstreetmap.josm.data.notes.NoteComment;
022import org.openstreetmap.josm.data.notes.NoteComment.Action;
023import org.openstreetmap.josm.data.osm.User;
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 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 {API, DUMP}
045
046    /**
047     * SAX handler to read note information from its XML representation.
048     * Reads both API style and planet dump style formats.
049     */
050    private class Parser extends DefaultHandler {
051
052        private final SimpleDateFormat ISO8601_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.ENGLISH);
053        private final SimpleDateFormat NOTE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z", Locale.ENGLISH);
054
055        private NoteParseMode parseMode;
056        private StringBuffer buffer = new StringBuffer();
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        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<Note>(100);
078                return;
079            case "osm-notes":
080                parseMode = NoteParseMode.DUMP;
081                notes = new ArrayList<Note>(10000);
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(parseDate(ISO8601_FORMAT, closedTimeStr));
109                }
110                thisNote.setCreatedAt(parseDate(ISO8601_FORMAT, attrs.getValue("created_at")));
111                break;
112            case "comment":
113                String uidStr = attrs.getValue("uid");
114                if(uidStr == null) {
115                    commentUid = 0;
116                } else {
117                    commentUid = Long.parseLong(uidStr);
118                }
119                commentUsername = attrs.getValue("user");
120                noteAction = Action.valueOf(attrs.getValue("action"));
121                commentCreateDate = parseDate(ISO8601_FORMAT, attrs.getValue("timestamp"));
122                String isNew = attrs.getValue("is_new");
123                if(isNew == null) {
124                    commentIsNew = false;
125                } else {
126                    commentIsNew = Boolean.valueOf(isNew);
127                }
128                break;
129            }
130        }
131
132        @Override
133        public void endElement(String namespaceURI, String localName, String qName) {
134            if("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 = null;
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()));
166                break;
167            case "date_created":
168                thisNote.setCreatedAt(parseDate(NOTE_DATE_FORMAT, buffer.toString()));
169                break;
170            case "date_closed":
171                thisNote.setClosedAt(parseDate(NOTE_DATE_FORMAT, buffer.toString()));
172                break;
173            case "date":
174                commentCreateDate = parseDate(NOTE_DATE_FORMAT, 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());
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            Main.info("parsed notes: " + notes.size());
198            parsedNotes = notes;
199        }
200
201        /**
202         * Convenience method to handle the date parsing try/catch. Will return null if
203         * there is a parsing exception. This means whatever generated this XML is in error
204         * and there isn't anything we can do about it.
205         * @param dateStr - String to parse
206         * @return Parsed date, null if parsing fails
207         */
208        private Date parseDate(SimpleDateFormat sdf, String dateStr) {
209            try {
210                return sdf.parse(dateStr);
211            } catch(ParseException e) {
212                Main.error("error parsing date in note parser");
213                return null;
214            }
215        }
216    }
217
218    /**
219     * Initializes the reader with a given InputStream
220     * @param source - InputStream containing Notes XML
221     * @throws IOException
222     */
223    public NoteReader(InputStream source) throws IOException {
224        this.inputSource = new InputSource(source);
225    }
226
227    /**
228     * Initializes the reader with a string as a source
229     * @param source UTF-8 string containing Notes XML to parse
230     * @throws IOException
231     */
232    public NoteReader(String source) throws IOException {
233        this.inputSource = new InputSource(new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8)));
234    }
235
236    /**
237     * Parses the InputStream given to the constructor and returns
238     * the resulting Note objects
239     * @return List of Notes parsed from the input data
240     * @throws SAXException
241     * @throws IOException
242     */
243    public List<Note> parse() throws SAXException, IOException {
244        DefaultHandler parser = new Parser();
245        try {
246            SAXParserFactory factory = SAXParserFactory.newInstance();
247            factory.setNamespaceAware(true);
248            factory.newSAXParser().parse(inputSource, parser);
249        } catch (ParserConfigurationException e) {
250            Main.error(e); // broken SAXException chaining
251            throw new SAXException(e);
252        }
253        return parsedNotes;
254    }
255}