001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.io; 003 004import java.io.IOException; 005import java.io.InputStream; 006import java.text.ParseException; 007import java.text.SimpleDateFormat; 008import java.util.ArrayList; 009import java.util.Date; 010import java.util.List; 011import java.util.Locale; 012 013import javax.xml.parsers.ParserConfigurationException; 014import javax.xml.parsers.SAXParserFactory; 015 016import org.openstreetmap.josm.Main; 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.xml.sax.Attributes; 023import org.xml.sax.InputSource; 024import org.xml.sax.SAXException; 025import org.xml.sax.helpers.DefaultHandler; 026 027/** 028 * Class to read Note objects from their XML representation. It can take 029 * either API style XML which starts with an "osm" tag or a planet dump 030 * style XML which starts with an "osm-notes" tag. 031 */ 032public class NoteReader { 033 034 private InputSource inputSource; 035 private List<Note> parsedNotes; 036 037 /** 038 * Notes can be represented in two XML formats. One is returned by the API 039 * while the other is used to generate the notes dump file. The parser 040 * needs to know which one it is handling. 041 */ 042 private enum NoteParseMode {API, DUMP} 043 044 /** 045 * SAX handler to read note information from its XML representation. 046 * Reads both API style and planet dump style formats. 047 */ 048 private class Parser extends DefaultHandler { 049 050 private final SimpleDateFormat ISO8601_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.ENGLISH); 051 private final SimpleDateFormat NOTE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z", Locale.ENGLISH); 052 053 private NoteParseMode parseMode; 054 private StringBuffer buffer = new StringBuffer(); 055 private Note thisNote; 056 private long commentUid; 057 private String commentUsername; 058 private Action noteAction; 059 private Date commentCreateDate; 060 private Boolean commentIsNew; 061 private List<Note> notes; 062 String commentText; 063 064 @Override 065 public void characters(char[] ch, int start, int length) throws SAXException { 066 buffer.append(ch, start, length); 067 } 068 069 @Override 070 public void startElement(String uri, String localName, String qName, Attributes attrs) throws SAXException { 071 buffer.setLength(0); 072 switch(qName) { 073 case "osm": 074 parseMode = NoteParseMode.API; 075 notes = new ArrayList<Note>(100); 076 return; 077 case "osm-notes": 078 parseMode = NoteParseMode.DUMP; 079 notes = new ArrayList<Note>(10000); 080 return; 081 } 082 083 if (parseMode == NoteParseMode.API) { 084 if("note".equals(qName)) { 085 double lat = Double.parseDouble(attrs.getValue("lat")); 086 double lon = Double.parseDouble(attrs.getValue("lon")); 087 LatLon noteLatLon = new LatLon(lat, lon); 088 thisNote = new Note(noteLatLon); 089 } 090 return; 091 } 092 093 //The rest only applies for dump mode 094 switch(qName) { 095 case "note": 096 double lat = Double.parseDouble(attrs.getValue("lat")); 097 double lon = Double.parseDouble(attrs.getValue("lon")); 098 LatLon noteLatLon = new LatLon(lat, lon); 099 thisNote = new Note(noteLatLon); 100 thisNote.setId(Long.parseLong(attrs.getValue("id"))); 101 String closedTimeStr = attrs.getValue("closed_at"); 102 if(closedTimeStr == null) { //no closed_at means the note is still open 103 thisNote.setState(Note.State.open); 104 } else { 105 thisNote.setState(Note.State.closed); 106 thisNote.setClosedAt(parseDate(ISO8601_FORMAT, closedTimeStr)); 107 } 108 thisNote.setCreatedAt(parseDate(ISO8601_FORMAT, attrs.getValue("created_at"))); 109 break; 110 case "comment": 111 String uidStr = attrs.getValue("uid"); 112 if(uidStr == null) { 113 commentUid = 0; 114 } else { 115 commentUid = Long.parseLong(uidStr); 116 } 117 commentUsername = attrs.getValue("user"); 118 noteAction = Action.valueOf(attrs.getValue("action")); 119 commentCreateDate = parseDate(ISO8601_FORMAT, attrs.getValue("timestamp")); 120 String isNew = attrs.getValue("is_new"); 121 if(isNew == null) { 122 commentIsNew = false; 123 } else { 124 commentIsNew = Boolean.valueOf(isNew); 125 } 126 break; 127 } 128 } 129 130 @Override 131 public void endElement(String namespaceURI, String localName, String qName) { 132 if("note".equals(qName)) { 133 notes.add(thisNote); 134 } 135 if("comment".equals(qName)) { 136 User commentUser = User.createOsmUser(commentUid, commentUsername); 137 if(parseMode == NoteParseMode.API) { 138 commentIsNew = false; 139 } 140 if(parseMode == NoteParseMode.DUMP) { 141 commentText = buffer.toString(); 142 } 143 thisNote.addComment(new NoteComment(commentCreateDate, commentUser, commentText, noteAction, commentIsNew)); 144 commentUid = 0; 145 commentUsername = null; 146 commentCreateDate = null; 147 commentIsNew = null; 148 commentText = null; 149 } 150 if(parseMode == NoteParseMode.DUMP) { 151 return; 152 } 153 154 //the rest only applies to API mode 155 switch (qName) { 156 case "id": 157 thisNote.setId(Long.parseLong(buffer.toString())); 158 break; 159 case "status": 160 thisNote.setState(Note.State.valueOf(buffer.toString())); 161 break; 162 case "date_created": 163 thisNote.setCreatedAt(parseDate(NOTE_DATE_FORMAT, buffer.toString())); 164 break; 165 case "date": 166 commentCreateDate = parseDate(NOTE_DATE_FORMAT, buffer.toString()); 167 break; 168 case "user": 169 commentUsername = buffer.toString(); 170 break; 171 case "uid": 172 commentUid = Long.parseLong(buffer.toString()); 173 break; 174 case "text": 175 commentText = buffer.toString(); 176 buffer.setLength(0); 177 break; 178 case "action": 179 noteAction = Action.valueOf(buffer.toString()); 180 break; 181 case "note": //nothing to do for comment or note, already handled above 182 case "comment": 183 break; 184 } 185 } 186 187 @Override 188 public void endDocument() throws SAXException { 189 Main.info("parsed notes: " + notes.size()); 190 parsedNotes = notes; 191 } 192 193 /** 194 * Convenience method to handle the date parsing try/catch. Will return null if 195 * there is a parsing exception. This means whatever generated this XML is in error 196 * and there isn't anything we can do about it. 197 * @param dateStr - String to parse 198 * @return Parsed date, null if parsing fails 199 */ 200 private Date parseDate(SimpleDateFormat sdf, String dateStr) { 201 try { 202 return sdf.parse(dateStr); 203 } catch(ParseException e) { 204 Main.error("error parsing date in note parser"); 205 return null; 206 } 207 } 208 } 209 210 /** 211 * Initializes the reader with a given InputStream 212 * @param source - InputStream containing Notes XML 213 * @throws IOException 214 */ 215 public NoteReader(InputStream source) throws IOException { 216 this.inputSource = new InputSource(source); 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 224 * @throws IOException 225 */ 226 public List<Note> parse() throws SAXException, IOException { 227 DefaultHandler parser = new Parser(); 228 try { 229 SAXParserFactory factory = SAXParserFactory.newInstance(); 230 factory.setNamespaceAware(true); 231 factory.newSAXParser().parse(inputSource, parser); 232 } catch (ParserConfigurationException e) { 233 Main.error(e); // broken SAXException chaining 234 throw new SAXException(e); 235 } 236 return parsedNotes; 237 } 238}