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; 013 014import javax.xml.parsers.ParserConfigurationException; 015 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.Logging; 022import org.openstreetmap.josm.tools.XmlUtils; 023import org.openstreetmap.josm.tools.date.DateUtils; 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 final 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 { 045 API, 046 DUMP 047 } 048 049 /** 050 * SAX handler to read note information from its XML representation. 051 * Reads both API style and planet dump style formats. 052 */ 053 private class Parser extends DefaultHandler { 054 055 private NoteParseMode parseMode; 056 private final StringBuilder buffer = new StringBuilder(); 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 private 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<>(100); 078 return; 079 case "osm-notes": 080 parseMode = NoteParseMode.DUMP; 081 notes = new ArrayList<>(10_000); 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(DateUtils.fromString(closedTimeStr)); 109 } 110 thisNote.setCreatedAt(DateUtils.fromString(attrs.getValue("created_at"))); 111 break; 112 case "comment": 113 commentUid = Long.parseLong(Optional.ofNullable(attrs.getValue("uid")).orElse("0")); 114 commentUsername = attrs.getValue("user"); 115 noteAction = Action.valueOf(attrs.getValue("action").toUpperCase(Locale.ENGLISH)); 116 commentCreateDate = DateUtils.fromString(attrs.getValue("timestamp")); 117 commentIsNew = Boolean.parseBoolean(Optional.ofNullable(attrs.getValue("is_new")).orElse("false")); 118 break; 119 default: // Do nothing 120 } 121 } 122 123 @Override 124 public void endElement(String namespaceURI, String localName, String qName) { 125 if (notes != null && "note".equals(qName)) { 126 notes.add(thisNote); 127 } 128 if ("comment".equals(qName)) { 129 User commentUser = User.createOsmUser(commentUid, commentUsername); 130 if (commentUid == 0) { 131 commentUser = User.getAnonymous(); 132 } 133 if (parseMode == NoteParseMode.API) { 134 commentIsNew = false; 135 } 136 if (parseMode == NoteParseMode.DUMP) { 137 commentText = buffer.toString(); 138 } 139 thisNote.addComment(new NoteComment(commentCreateDate, commentUser, commentText, noteAction, commentIsNew)); 140 commentUid = 0; 141 commentUsername = null; 142 commentCreateDate = null; 143 commentIsNew = false; 144 commentText = null; 145 } 146 if (parseMode == NoteParseMode.DUMP) { 147 return; 148 } 149 150 //the rest only applies to API mode 151 switch (qName) { 152 case "id": 153 thisNote.setId(Long.parseLong(buffer.toString())); 154 break; 155 case "status": 156 thisNote.setState(Note.State.valueOf(buffer.toString().toUpperCase(Locale.ENGLISH))); 157 break; 158 case "date_created": 159 thisNote.setCreatedAt(DateUtils.fromString(buffer.toString())); 160 break; 161 case "date_closed": 162 thisNote.setClosedAt(DateUtils.fromString(buffer.toString())); 163 break; 164 case "date": 165 commentCreateDate = DateUtils.fromString(buffer.toString()); 166 break; 167 case "user": 168 commentUsername = buffer.toString(); 169 break; 170 case "uid": 171 commentUid = Long.parseLong(buffer.toString()); 172 break; 173 case "text": 174 commentText = buffer.toString(); 175 buffer.setLength(0); 176 break; 177 case "action": 178 noteAction = Action.valueOf(buffer.toString().toUpperCase(Locale.ENGLISH)); 179 break; 180 case "note": //nothing to do for comment or note, already handled above 181 case "comment": 182 break; 183 } 184 } 185 186 @Override 187 public void endDocument() throws SAXException { 188 parsedNotes = notes; 189 } 190 } 191 192 /** 193 * Initializes the reader with a given InputStream 194 * @param source - InputStream containing Notes XML 195 */ 196 public NoteReader(InputStream source) { 197 this.inputSource = new InputSource(source); 198 } 199 200 /** 201 * Initializes the reader with a string as a source 202 * @param source UTF-8 string containing Notes XML to parse 203 */ 204 public NoteReader(String source) { 205 this.inputSource = new InputSource(new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8))); 206 } 207 208 /** 209 * Parses the InputStream given to the constructor and returns 210 * the resulting Note objects 211 * @return List of Notes parsed from the input data 212 * @throws SAXException if any SAX parsing error occurs 213 * @throws IOException if any I/O error occurs 214 */ 215 public List<Note> parse() throws SAXException, IOException { 216 DefaultHandler parser = new Parser(); 217 try { 218 XmlUtils.parseSafeSAX(inputSource, parser); 219 } catch (ParserConfigurationException e) { 220 Logging.error(e); // broken SAXException chaining 221 throw new SAXException(e); 222 } 223 return parsedNotes; 224 } 225}