001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io;
003
004import java.io.IOException;
005import java.io.InputStream;
006import java.text.ParseException;
007import java.text.SimpleDateFormat;
008import java.util.ArrayList;
009import java.util.Date;
010import java.util.List;
011import java.util.Locale;
012
013import javax.xml.parsers.ParserConfigurationException;
014import javax.xml.parsers.SAXParserFactory;
015
016import org.openstreetmap.josm.Main;
017import org.openstreetmap.josm.data.coor.LatLon;
018import org.openstreetmap.josm.data.notes.Note;
019import org.openstreetmap.josm.data.notes.NoteComment;
020import org.openstreetmap.josm.data.notes.NoteComment.Action;
021import org.openstreetmap.josm.data.osm.User;
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 {API, DUMP}
043
044    /**
045     * SAX handler to read note information from its XML representation.
046     * Reads both API style and planet dump style formats.
047     */
048    private class Parser extends DefaultHandler {
049
050        private final SimpleDateFormat ISO8601_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.ENGLISH);
051        private final SimpleDateFormat NOTE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z", Locale.ENGLISH);
052
053        private NoteParseMode parseMode;
054        private StringBuffer buffer = new StringBuffer();
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        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(parseDate(ISO8601_FORMAT, closedTimeStr));
107                }
108                thisNote.setCreatedAt(parseDate(ISO8601_FORMAT, 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 = parseDate(ISO8601_FORMAT, attrs.getValue("timestamp"));
120                String isNew = attrs.getValue("is_new");
121                if(isNew == null) {
122                    commentIsNew = false;
123                } else {
124                    commentIsNew = Boolean.valueOf(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(parseMode == NoteParseMode.API) {
138                    commentIsNew = false;
139                }
140                if(parseMode == NoteParseMode.DUMP) {
141                    commentText = buffer.toString();
142                }
143                thisNote.addComment(new NoteComment(commentCreateDate, commentUser, commentText, noteAction, commentIsNew));
144                commentUid = 0;
145                commentUsername = null;
146                commentCreateDate = null;
147                commentIsNew = null;
148                commentText = null;
149            }
150            if(parseMode == NoteParseMode.DUMP) {
151                return;
152            }
153
154            //the rest only applies to API mode
155            switch (qName) {
156            case "id":
157                thisNote.setId(Long.parseLong(buffer.toString()));
158                break;
159            case "status":
160                thisNote.setState(Note.State.valueOf(buffer.toString()));
161                break;
162            case "date_created":
163                thisNote.setCreatedAt(parseDate(NOTE_DATE_FORMAT, buffer.toString()));
164                break;
165            case "date":
166                commentCreateDate = parseDate(NOTE_DATE_FORMAT, buffer.toString());
167                break;
168            case "user":
169                commentUsername = buffer.toString();
170                break;
171            case "uid":
172                commentUid = Long.parseLong(buffer.toString());
173                break;
174            case "text":
175                commentText = buffer.toString();
176                buffer.setLength(0);
177                break;
178            case "action":
179                noteAction = Action.valueOf(buffer.toString());
180                break;
181            case "note": //nothing to do for comment or note, already handled above
182            case "comment":
183                break;
184            }
185        }
186
187        @Override
188        public void endDocument() throws SAXException  {
189            Main.info("parsed notes: " + notes.size());
190            parsedNotes = notes;
191        }
192
193        /**
194         * Convenience method to handle the date parsing try/catch. Will return null if
195         * there is a parsing exception. This means whatever generated this XML is in error
196         * and there isn't anything we can do about it.
197         * @param dateStr - String to parse
198         * @return Parsed date, null if parsing fails
199         */
200        private Date parseDate(SimpleDateFormat sdf, String dateStr) {
201            try {
202                return sdf.parse(dateStr);
203            } catch(ParseException e) {
204                Main.error("error parsing date in note parser");
205                return null;
206            }
207        }
208    }
209
210    /**
211     * Initializes the reader with a given InputStream
212     * @param source - InputStream containing Notes XML
213     * @throws IOException
214     */
215    public NoteReader(InputStream source) throws IOException {
216        this.inputSource = new InputSource(source);
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
224     * @throws IOException
225     */
226    public List<Note> parse() throws SAXException, IOException {
227        DefaultHandler parser = new Parser();
228        try {
229            SAXParserFactory factory = SAXParserFactory.newInstance();
230            factory.setNamespaceAware(true);
231            factory.newSAXParser().parse(inputSource, parser);
232        } catch (ParserConfigurationException e) {
233            Main.error(e); // broken SAXException chaining
234            throw new SAXException(e);
235        }
236        return parsedNotes;
237    }
238}