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; 012 013import javax.xml.parsers.ParserConfigurationException; 014 015import org.openstreetmap.josm.Main; 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.Utils; 022import org.openstreetmap.josm.tools.date.DateUtils; 023import org.xml.sax.Attributes; 024import org.xml.sax.InputSource; 025import org.xml.sax.SAXException; 026import org.xml.sax.helpers.DefaultHandler; 027 028/** 029 * Class to read Note objects from their XML representation. It can take 030 * either API style XML which starts with an "osm" tag or a planet dump 031 * style XML which starts with an "osm-notes" tag. 032 */ 033public class NoteReader { 034 035 private final InputSource inputSource; 036 private List<Note> parsedNotes; 037 038 /** 039 * Notes can be represented in two XML formats. One is returned by the API 040 * while the other is used to generate the notes dump file. The parser 041 * needs to know which one it is handling. 042 */ 043 private enum NoteParseMode { 044 API, 045 DUMP 046 } 047 048 /** 049 * SAX handler to read note information from its XML representation. 050 * Reads both API style and planet dump style formats. 051 */ 052 private class Parser extends DefaultHandler { 053 054 private NoteParseMode parseMode; 055 private final StringBuilder buffer = new StringBuilder(); 056 private Note thisNote; 057 private long commentUid; 058 private String commentUsername; 059 private Action noteAction; 060 private Date commentCreateDate; 061 private boolean commentIsNew; 062 private List<Note> notes; 063 private String commentText; 064 065 @Override 066 public void characters(char[] ch, int start, int length) throws SAXException { 067 buffer.append(ch, start, length); 068 } 069 070 @Override 071 public void startElement(String uri, String localName, String qName, Attributes attrs) throws SAXException { 072 buffer.setLength(0); 073 switch(qName) { 074 case "osm": 075 parseMode = NoteParseMode.API; 076 notes = new ArrayList<>(100); 077 return; 078 case "osm-notes": 079 parseMode = NoteParseMode.DUMP; 080 notes = new ArrayList<>(10000); 081 return; 082 } 083 084 if (parseMode == NoteParseMode.API) { 085 if ("note".equals(qName)) { 086 double lat = Double.parseDouble(attrs.getValue("lat")); 087 double lon = Double.parseDouble(attrs.getValue("lon")); 088 LatLon noteLatLon = new LatLon(lat, lon); 089 thisNote = new Note(noteLatLon); 090 } 091 return; 092 } 093 094 //The rest only applies for dump mode 095 switch(qName) { 096 case "note": 097 double lat = Double.parseDouble(attrs.getValue("lat")); 098 double lon = Double.parseDouble(attrs.getValue("lon")); 099 LatLon noteLatLon = new LatLon(lat, lon); 100 thisNote = new Note(noteLatLon); 101 thisNote.setId(Long.parseLong(attrs.getValue("id"))); 102 String closedTimeStr = attrs.getValue("closed_at"); 103 if (closedTimeStr == null) { //no closed_at means the note is still open 104 thisNote.setState(Note.State.OPEN); 105 } else { 106 thisNote.setState(Note.State.CLOSED); 107 thisNote.setClosedAt(DateUtils.fromString(closedTimeStr)); 108 } 109 thisNote.setCreatedAt(DateUtils.fromString(attrs.getValue("created_at"))); 110 break; 111 case "comment": 112 String uidStr = attrs.getValue("uid"); 113 if (uidStr == null) { 114 commentUid = 0; 115 } else { 116 commentUid = Long.parseLong(uidStr); 117 } 118 commentUsername = attrs.getValue("user"); 119 noteAction = Action.valueOf(attrs.getValue("action").toUpperCase(Locale.ENGLISH)); 120 commentCreateDate = DateUtils.fromString(attrs.getValue("timestamp")); 121 String isNew = attrs.getValue("is_new"); 122 if (isNew == null) { 123 commentIsNew = false; 124 } else { 125 commentIsNew = Boolean.parseBoolean(isNew); 126 } 127 break; 128 default: // Do nothing 129 } 130 } 131 132 @Override 133 public void endElement(String namespaceURI, String localName, String qName) { 134 if (notes != null && "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 = false; 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().toUpperCase(Locale.ENGLISH))); 166 break; 167 case "date_created": 168 thisNote.setCreatedAt(DateUtils.fromString(buffer.toString())); 169 break; 170 case "date_closed": 171 thisNote.setClosedAt(DateUtils.fromString(buffer.toString())); 172 break; 173 case "date": 174 commentCreateDate = DateUtils.fromString(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().toUpperCase(Locale.ENGLISH)); 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 parsedNotes = notes; 198 } 199 } 200 201 /** 202 * Initializes the reader with a given InputStream 203 * @param source - InputStream containing Notes XML 204 * @throws IOException if any I/O error occurs 205 */ 206 public NoteReader(InputStream source) throws IOException { 207 this.inputSource = new InputSource(source); 208 } 209 210 /** 211 * Initializes the reader with a string as a source 212 * @param source UTF-8 string containing Notes XML to parse 213 * @throws IOException if any I/O error occurs 214 */ 215 public NoteReader(String source) throws IOException { 216 this.inputSource = new InputSource(new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8))); 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 if any SAX parsing error occurs 224 * @throws IOException if any I/O error occurs 225 */ 226 public List<Note> parse() throws SAXException, IOException { 227 DefaultHandler parser = new Parser(); 228 try { 229 Utils.parseSafeSAX(inputSource, parser); 230 } catch (ParserConfigurationException e) { 231 Main.error(e); // broken SAXException chaining 232 throw new SAXException(e); 233 } 234 return parsedNotes; 235 } 236}