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.text.ParseException; 009import java.text.SimpleDateFormat; 010import java.util.ArrayList; 011import java.util.Date; 012import java.util.List; 013import java.util.Locale; 014 015import javax.xml.parsers.ParserConfigurationException; 016import javax.xml.parsers.SAXParserFactory; 017 018import org.openstreetmap.josm.Main; 019import org.openstreetmap.josm.data.coor.LatLon; 020import org.openstreetmap.josm.data.notes.Note; 021import org.openstreetmap.josm.data.notes.NoteComment; 022import org.openstreetmap.josm.data.notes.NoteComment.Action; 023import org.openstreetmap.josm.data.osm.User; 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 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 {API, DUMP} 045 046 /** 047 * SAX handler to read note information from its XML representation. 048 * Reads both API style and planet dump style formats. 049 */ 050 private class Parser extends DefaultHandler { 051 052 private final SimpleDateFormat ISO8601_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.ENGLISH); 053 private final SimpleDateFormat NOTE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z", Locale.ENGLISH); 054 055 private NoteParseMode parseMode; 056 private StringBuffer buffer = new StringBuffer(); 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 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<Note>(100); 078 return; 079 case "osm-notes": 080 parseMode = NoteParseMode.DUMP; 081 notes = new ArrayList<Note>(10000); 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(parseDate(ISO8601_FORMAT, closedTimeStr)); 109 } 110 thisNote.setCreatedAt(parseDate(ISO8601_FORMAT, attrs.getValue("created_at"))); 111 break; 112 case "comment": 113 String uidStr = attrs.getValue("uid"); 114 if(uidStr == null) { 115 commentUid = 0; 116 } else { 117 commentUid = Long.parseLong(uidStr); 118 } 119 commentUsername = attrs.getValue("user"); 120 noteAction = Action.valueOf(attrs.getValue("action")); 121 commentCreateDate = parseDate(ISO8601_FORMAT, attrs.getValue("timestamp")); 122 String isNew = attrs.getValue("is_new"); 123 if(isNew == null) { 124 commentIsNew = false; 125 } else { 126 commentIsNew = Boolean.valueOf(isNew); 127 } 128 break; 129 } 130 } 131 132 @Override 133 public void endElement(String namespaceURI, String localName, String qName) { 134 if("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 = null; 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())); 166 break; 167 case "date_created": 168 thisNote.setCreatedAt(parseDate(NOTE_DATE_FORMAT, buffer.toString())); 169 break; 170 case "date_closed": 171 thisNote.setClosedAt(parseDate(NOTE_DATE_FORMAT, buffer.toString())); 172 break; 173 case "date": 174 commentCreateDate = parseDate(NOTE_DATE_FORMAT, 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()); 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 Main.info("parsed notes: " + notes.size()); 198 parsedNotes = notes; 199 } 200 201 /** 202 * Convenience method to handle the date parsing try/catch. Will return null if 203 * there is a parsing exception. This means whatever generated this XML is in error 204 * and there isn't anything we can do about it. 205 * @param dateStr - String to parse 206 * @return Parsed date, null if parsing fails 207 */ 208 private Date parseDate(SimpleDateFormat sdf, String dateStr) { 209 try { 210 return sdf.parse(dateStr); 211 } catch(ParseException e) { 212 Main.error("error parsing date in note parser"); 213 return null; 214 } 215 } 216 } 217 218 /** 219 * Initializes the reader with a given InputStream 220 * @param source - InputStream containing Notes XML 221 * @throws IOException 222 */ 223 public NoteReader(InputStream source) throws IOException { 224 this.inputSource = new InputSource(source); 225 } 226 227 /** 228 * Initializes the reader with a string as a source 229 * @param source UTF-8 string containing Notes XML to parse 230 * @throws IOException 231 */ 232 public NoteReader(String source) throws IOException { 233 this.inputSource = new InputSource(new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8))); 234 } 235 236 /** 237 * Parses the InputStream given to the constructor and returns 238 * the resulting Note objects 239 * @return List of Notes parsed from the input data 240 * @throws SAXException 241 * @throws IOException 242 */ 243 public List<Note> parse() throws SAXException, IOException { 244 DefaultHandler parser = new Parser(); 245 try { 246 SAXParserFactory factory = SAXParserFactory.newInstance(); 247 factory.setNamespaceAware(true); 248 factory.newSAXParser().parse(inputSource, parser); 249 } catch (ParserConfigurationException e) { 250 Main.error(e); // broken SAXException chaining 251 throw new SAXException(e); 252 } 253 return parsedNotes; 254 } 255}