// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.io; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Optional; import javax.xml.parsers.ParserConfigurationException; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.coor.LatLon; import org.openstreetmap.josm.data.notes.Note; import org.openstreetmap.josm.data.notes.NoteComment; import org.openstreetmap.josm.data.notes.NoteComment.Action; import org.openstreetmap.josm.data.osm.User; import org.openstreetmap.josm.tools.Utils; import org.openstreetmap.josm.tools.date.DateUtils; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; /** * Class to read Note objects from their XML representation. It can take * either API style XML which starts with an "osm" tag or a planet dump * style XML which starts with an "osm-notes" tag. */ public class NoteReader { private final InputSource inputSource; private List<Note> parsedNotes; /** * Notes can be represented in two XML formats. One is returned by the API * while the other is used to generate the notes dump file. The parser * needs to know which one it is handling. */ private enum NoteParseMode { API, DUMP } /** * SAX handler to read note information from its XML representation. * Reads both API style and planet dump style formats. */ private class Parser extends DefaultHandler { private NoteParseMode parseMode; private final StringBuilder buffer = new StringBuilder(); private Note thisNote; private long commentUid; private String commentUsername; private Action noteAction; private Date commentCreateDate; private boolean commentIsNew; private List<Note> notes; private String commentText; @Override public void characters(char[] ch, int start, int length) throws SAXException { buffer.append(ch, start, length); } @Override public void startElement(String uri, String localName, String qName, Attributes attrs) throws SAXException { buffer.setLength(0); switch(qName) { case "osm": parseMode = NoteParseMode.API; notes = new ArrayList<>(100); return; case "osm-notes": parseMode = NoteParseMode.DUMP; notes = new ArrayList<>(10_000); return; } if (parseMode == NoteParseMode.API) { if ("note".equals(qName)) { double lat = Double.parseDouble(attrs.getValue("lat")); double lon = Double.parseDouble(attrs.getValue("lon")); LatLon noteLatLon = new LatLon(lat, lon); thisNote = new Note(noteLatLon); } return; } //The rest only applies for dump mode switch(qName) { case "note": double lat = Double.parseDouble(attrs.getValue("lat")); double lon = Double.parseDouble(attrs.getValue("lon")); LatLon noteLatLon = new LatLon(lat, lon); thisNote = new Note(noteLatLon); thisNote.setId(Long.parseLong(attrs.getValue("id"))); String closedTimeStr = attrs.getValue("closed_at"); if (closedTimeStr == null) { //no closed_at means the note is still open thisNote.setState(Note.State.OPEN); } else { thisNote.setState(Note.State.CLOSED); thisNote.setClosedAt(DateUtils.fromString(closedTimeStr)); } thisNote.setCreatedAt(DateUtils.fromString(attrs.getValue("created_at"))); break; case "comment": commentUid = Long.parseLong(Optional.ofNullable(attrs.getValue("uid")).orElse("0")); commentUsername = attrs.getValue("user"); noteAction = Action.valueOf(attrs.getValue("action").toUpperCase(Locale.ENGLISH)); commentCreateDate = DateUtils.fromString(attrs.getValue("timestamp")); commentIsNew = Boolean.parseBoolean(Optional.ofNullable(attrs.getValue("is_new")).orElse("false")); break; default: // Do nothing } } @Override public void endElement(String namespaceURI, String localName, String qName) { if (notes != null && "note".equals(qName)) { notes.add(thisNote); } if ("comment".equals(qName)) { User commentUser = User.createOsmUser(commentUid, commentUsername); if (commentUid == 0) { commentUser = User.getAnonymous(); } if (parseMode == NoteParseMode.API) { commentIsNew = false; } if (parseMode == NoteParseMode.DUMP) { commentText = buffer.toString(); } thisNote.addComment(new NoteComment(commentCreateDate, commentUser, commentText, noteAction, commentIsNew)); commentUid = 0; commentUsername = null; commentCreateDate = null; commentIsNew = false; commentText = null; } if (parseMode == NoteParseMode.DUMP) { return; } //the rest only applies to API mode switch (qName) { case "id": thisNote.setId(Long.parseLong(buffer.toString())); break; case "status": thisNote.setState(Note.State.valueOf(buffer.toString().toUpperCase(Locale.ENGLISH))); break; case "date_created": thisNote.setCreatedAt(DateUtils.fromString(buffer.toString())); break; case "date_closed": thisNote.setClosedAt(DateUtils.fromString(buffer.toString())); break; case "date": commentCreateDate = DateUtils.fromString(buffer.toString()); break; case "user": commentUsername = buffer.toString(); break; case "uid": commentUid = Long.parseLong(buffer.toString()); break; case "text": commentText = buffer.toString(); buffer.setLength(0); break; case "action": noteAction = Action.valueOf(buffer.toString().toUpperCase(Locale.ENGLISH)); break; case "note": //nothing to do for comment or note, already handled above case "comment": break; } } @Override public void endDocument() throws SAXException { parsedNotes = notes; } } /** * Initializes the reader with a given InputStream * @param source - InputStream containing Notes XML */ public NoteReader(InputStream source) { this.inputSource = new InputSource(source); } /** * Initializes the reader with a string as a source * @param source UTF-8 string containing Notes XML to parse */ public NoteReader(String source) { this.inputSource = new InputSource(new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8))); } /** * Parses the InputStream given to the constructor and returns * the resulting Note objects * @return List of Notes parsed from the input data * @throws SAXException if any SAX parsing error occurs * @throws IOException if any I/O error occurs */ public List<Note> parse() throws SAXException, IOException { DefaultHandler parser = new Parser(); try { Utils.parseSafeSAX(inputSource, parser); } catch (ParserConfigurationException e) { Main.error(e); // broken SAXException chaining throw new SAXException(e); } return parsedNotes; } }