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