Index: src/org/openstreetmap/josm/data/notes/Note.java
===================================================================
--- src/org/openstreetmap/josm/data/notes/Note.java	(revision 0)
+++ src/org/openstreetmap/josm/data/notes/Note.java	(working copy)
@@ -0,0 +1,126 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.notes;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import org.openstreetmap.josm.data.coor.LatLon;
+
+/**
+ * A map note. It always has at least one comment since a comment is required
+ * to create a note on osm.org
+ */
+public class Note {
+
+    public enum State { open, closed }
+
+    private long id;
+    private LatLon latLon;
+    private Date createdAt;
+    private Date closedAt;
+    private State state;
+    private List<NoteComment> comments = new ArrayList<NoteComment>();
+
+    /**
+     * Create a note with a given location
+     * @param latLon Geographic location of this note
+     */
+    public Note(LatLon latLon) {
+        this.latLon = latLon;
+    }
+
+    /** @return The unique OSM ID of this note */
+    public long getId() {
+    	return id;
+    }
+
+    public void setId(long id) {
+        this.id = id;
+    }
+
+    /** @return The geographic location of the note */
+    public LatLon getLatLon() {
+        return latLon;
+    }
+
+    /** @return Date that this note was submitted */
+    public Date getCreatedAt() {
+        return createdAt;
+    }
+
+    public void setCreatedAt(Date createdAt) {
+        this.createdAt = createdAt;
+    }
+
+    /** @return Date that this note was closed. Null if it is still open. */
+    public Date getClosedAt() {
+        return closedAt;
+    }
+
+    public void setClosedAt(Date closedAt) {
+        this.closedAt = closedAt;
+    }
+
+    /** @return The open or closed state of this note */
+    public State getState() {
+        return state;
+    }
+
+    public void setState(State state) {
+        this.state = state;
+    }
+
+    /** @return An ordered list of comments associated with this note */
+    public List<NoteComment> getComments() {
+        return comments;
+    }
+
+    public void addComment(NoteComment comment) {
+        this.comments.add(comment);
+    }
+
+    /**
+     * Returns the comment that was submitted by the user when creating the note
+     * @return First comment object
+     */
+    public NoteComment getFirstComment() {
+        return this.comments.get(0);
+    }
+
+    /**
+     * Copies values from a new note into an existing one. Used after a note
+     * has been updated on the server and the local copy needs refreshing.
+     * @param note New values to copy
+     */
+    public void updateWith(Note note) {
+        this.comments = note.comments;
+        this.createdAt = note.createdAt;
+        this.id = note.id;
+        this.state = note.state;
+        this.latLon = note.latLon;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + (int) (id ^ (id >>> 32));
+        return result;
+    }
+
+    /** Compares notes by OSM ID */
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        Note other = (Note) obj;
+        if (id != other.id)
+            return false;
+        return true;
+    }
+}
Index: src/org/openstreetmap/josm/data/notes/NoteComment.java
===================================================================
--- src/org/openstreetmap/josm/data/notes/NoteComment.java	(revision 0)
+++ src/org/openstreetmap/josm/data/notes/NoteComment.java	(working copy)
@@ -0,0 +1,71 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.notes;
+
+import java.util.Date;
+
+import org.openstreetmap.josm.data.osm.User;
+
+/**
+ * Represents a comment made on a note. All notes have at least on comment
+ * which is the comment the note was opened with. Comments are immutable.
+ */
+public class NoteComment {
+
+    private String text;
+    private User user;
+    private Date commentTimestamp;
+    private Action action;
+
+    //not currently used. I'm planning on using this to keep track of new actions that need to be uploaded
+    private Boolean isNew;
+
+    /**
+     * Every comment has an associated action. Some comments are just comments
+     * while others indicate the note being opened, closed or reopened
+     */
+    public enum Action {opened, closed, reopened, commented}
+
+    /**
+     * @param createDate The time at which this comment was added
+     * @param user JOSM User object of the user who created the comment
+     * @param commentText The text left by the user. Is sometimes blank
+     * @param action The action associated with this comment
+     * @param isNew Whether or not this comment is new and needs to be uploaded
+     */
+    public NoteComment(Date createDate, User user, String commentText, Action action, Boolean isNew) {
+        this.text = commentText;
+        this.user = user;
+        this.commentTimestamp = createDate;
+        this.action = action;
+        this.isNew = isNew;
+    }
+
+    /** @return Plain text of user's comment */
+    public String getText() {
+        return text;
+    }
+
+    /** @return JOSM's User object for the user who made this comment */
+    public User getUser() {
+        return user;
+    }
+
+    /** @return The time at which this comment was created */
+    public Date getCommentTimestamp() {
+        return commentTimestamp;
+    }
+
+    /** @return the action associated with this note */
+    public Action getNoteAction() {
+        return action;
+    }
+
+    public void setIsNew(Boolean isNew) {
+        this.isNew = isNew;
+    }
+
+    /** @return true if this is a new comment/action and needs to be uploaded to the API */
+    public Boolean getIsNew() {
+        return isNew;
+    }
+}
Index: src/org/openstreetmap/josm/io/NoteReader.java
===================================================================
--- src/org/openstreetmap/josm/io/NoteReader.java	(revision 0)
+++ src/org/openstreetmap/josm/io/NoteReader.java	(working copy)
@@ -0,0 +1,257 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.notes.Note;
+import org.openstreetmap.josm.data.notes.NoteComment;
+import org.openstreetmap.josm.data.notes.NoteComment.Action;
+import org.openstreetmap.josm.data.osm.User;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * Class to read Note objects from their XML representation
+ */
+public class NoteReader {
+
+    private InputSource inputSource;
+    private List<Note> parsedNotes;
+    private NoteParseMode parseMode;
+
+    /**
+     * Notes can be represented in two XML formats. One is returned by the API
+     * while the other is used to generate the notes dump file. The parser
+     * needs to know which one it is handling.
+     */
+    public enum NoteParseMode {API, DUMP}
+
+    /**
+     * Parser for the notes dump file format.
+     * It is completely different from the API XML format.
+     */
+    private class DumpParser extends DefaultHandler {
+        private StringBuffer buffer = new StringBuffer();
+        private final SimpleDateFormat ISO8601_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.ENGLISH);
+
+        private List<Note> notes = new ArrayList<Note>(100000);
+        private Note thisNote;
+
+        private Date commentCreateDate;
+        private String commentUsername;
+        private long commentUid;
+        private Action noteAction;
+        private Boolean commentIsNew;
+
+        @Override
+        public void characters(char[] ch, int start, int length) throws SAXException {
+            buffer.append(ch, start, length);
+        }
+
+        @Override
+        public void endElement(String uri, String localName, String qName) throws SAXException {
+            switch (qName) {
+                case "note":
+                    notes.add(thisNote);
+                    break;
+                case "comment":
+                    User commentUser = User.createOsmUser(commentUid, commentUsername);
+                    thisNote.addComment(new NoteComment(commentCreateDate, commentUser, buffer.toString(), noteAction, commentIsNew));
+                    commentUid = 0;
+                    commentUsername = null;
+                    commentCreateDate = null;
+                    commentIsNew = null;
+                    break;
+            }
+        }
+
+        @Override
+        public void startElement(String uri, String localName, String qName, Attributes attrs) throws SAXException {
+            buffer.setLength(0);
+            switch(qName) {
+            case "note":
+                double lat = Double.parseDouble(attrs.getValue("lat"));
+                double lon = Double.parseDouble(attrs.getValue("lon"));
+                LatLon noteLatLon = new LatLon(lat, lon);
+                thisNote = new Note(noteLatLon);
+                thisNote.setId(Long.parseLong(attrs.getValue("id")));
+                String closedTimeStr = attrs.getValue("closed_at");
+                if(closedTimeStr == null) { //no closed_at means the note is still open
+                    thisNote.setState(Note.State.open);
+                } else {
+                    thisNote.setState(Note.State.closed);
+                    thisNote.setClosedAt(parseDate(ISO8601_FORMAT, closedTimeStr));
+                }
+                thisNote.setCreatedAt(parseDate(ISO8601_FORMAT, attrs.getValue("created_at")));
+                break;
+            case "comment":
+                String uidStr = attrs.getValue("uid");
+                if(uidStr == null) {
+                    commentUid = 0;
+                } else {
+                    commentUid = Long.parseLong(uidStr);
+                }
+                commentUsername = attrs.getValue("user");
+                noteAction = Action.valueOf(attrs.getValue("action"));
+                commentCreateDate = parseDate(ISO8601_FORMAT, attrs.getValue("timestamp"));
+                String isNew = attrs.getValue("is_new");
+                if(isNew == null) {
+                    commentIsNew = false;
+                } else {
+                    commentIsNew = Boolean.valueOf(isNew);
+                }
+                break;
+            }
+        }
+
+        @Override
+        public void endDocument() throws SAXException  {
+            Main.info("parsed notes: " + notes.size());
+            parsedNotes = notes;
+        }
+    }
+
+    private class ApiParser extends DefaultHandler {
+
+        private StringBuffer accumulator = new StringBuffer();
+        private final SimpleDateFormat NOTE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z", Locale.ENGLISH);
+
+        private List<Note> notes = new ArrayList<Note>();
+        private Note thisNote;
+
+        private Date commentCreateDate;
+        private String commentUsername;
+        private long commentUid;
+        private String commentText;
+        private Action commentAction;
+
+        @Override
+        public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
+            accumulator.setLength(0);
+            if ("note".equals(qName)) {
+                double lat = Double.parseDouble(atts.getValue("lat"));
+                double lon = Double.parseDouble(atts.getValue("lon"));
+                LatLon noteLatLon = new LatLon(lat, lon);
+                thisNote = new Note(noteLatLon);
+            }
+        }
+
+        @Override
+        public void characters(char[] ch, int start, int length) {
+            accumulator.append(ch, start, length);
+        }
+
+        @Override
+        public void endElement(String namespaceURI, String localName, String qName) {
+            switch (qName) {
+            case "id":
+                thisNote.setId(Long.parseLong(accumulator.toString()));
+                break;
+            case "status":
+                thisNote.setState(Note.State.valueOf(accumulator.toString()));
+                break;
+            case "date_created":
+                thisNote.setCreatedAt(parseDate(NOTE_DATE_FORMAT, accumulator.toString()));
+                break;
+            case "note":
+                notes.add(thisNote);
+                break;
+            case "date":
+                commentCreateDate = parseDate(NOTE_DATE_FORMAT, accumulator.toString());
+                break;
+            case "user":
+                commentUsername = accumulator.toString();
+                break;
+            case "uid":
+                commentUid = Long.parseLong(accumulator.toString());
+                break;
+            case "text":
+                commentText = accumulator.toString();
+                break;
+            case "comment":
+                User commentUser = User.createOsmUser(commentUid, commentUsername);
+                thisNote.addComment(new NoteComment(commentCreateDate, commentUser, commentText, commentAction, false));
+                commentUid = 0;
+                commentUsername = null;
+                commentCreateDate = null;
+                commentText = null;
+                break;
+            case "action":
+                commentAction = Action.valueOf(accumulator.toString());
+                break;
+            }
+        }
+
+        @Override
+        public void endDocument() throws SAXException  {
+            Main.info("parsed notes: " + notes.size());
+            parsedNotes = notes;
+        }
+    }
+
+    /**
+     * Convenience method to handle the date parsing try/catch. Will return null if
+     * there is a parsing exception. This means whatever generated this XML is in error
+     * and there isn't anything we can do about it.
+     * @param dateStr - String to parse
+     * @return Parsed date, null if parsing fails
+     */
+    private Date parseDate(SimpleDateFormat sdf, String dateStr) {
+        try {
+            return sdf.parse(dateStr);
+        } catch(ParseException e) {
+            Main.error("error parsing date in note parser");
+            return null;
+        }
+    }
+
+    /**
+     * Initializes the reader with a given InputStream
+     * @param source - InputStream containing Notes XML
+     * @param parseMode - Indicate if we are parsing API or dump file style XML
+     * @throws IOException
+     */
+    public NoteReader(InputStream source, NoteParseMode parseMode) throws IOException {
+        this.inputSource = new InputSource(source);
+        this.parseMode = parseMode;
+    }
+
+    /**
+     * Parses the InputStream given to the constructor and returns
+     * the resulting Note objects
+     * @return List of Notes parsed from the input data
+     * @throws SAXException
+     * @throws IOException
+     */
+    public List<Note> parse() throws SAXException, IOException {
+        DefaultHandler parser;
+        if(parseMode == NoteParseMode.DUMP) {
+            parser = new DumpParser();
+        } else {
+            parser = new ApiParser();
+        }
+        try {
+            SAXParserFactory factory = SAXParserFactory.newInstance();
+            factory.setNamespaceAware(true);
+            factory.newSAXParser().parse(inputSource, parser);
+        } catch (ParserConfigurationException e) {
+            Main.error(e); // broken SAXException chaining
+            throw new SAXException(e);
+        }
+        return parsedNotes;
+    }
+}
