Index: trunk/src/org/openstreetmap/josm/gui/widgets/AbstractTextComponentValidator.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/widgets/AbstractTextComponentValidator.java	(revision 2688)
+++ trunk/src/org/openstreetmap/josm/gui/widgets/AbstractTextComponentValidator.java	(revision 2688)
@@ -0,0 +1,155 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.widgets;
+
+import java.awt.Color;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+
+import javax.swing.BorderFactory;
+import javax.swing.JTextField;
+import javax.swing.UIManager;
+import javax.swing.border.Border;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.text.JTextComponent;
+
+import org.openstreetmap.josm.tools.CheckParameterUtil;
+
+/**
+ * This is an abstract class for a validator on a text component.
+ * 
+ * Subclasses implement {@see #validate()}. {@see #validate()} is invoked whenever
+ * <ul>
+ *   <li>the content of the text component changes (the validator is a {@see DocumentListener})</li>
+ *   <li>the text component loses focus (the validator is a {@see FocusListener})</li>
+ *   <li>the text component is a {@see JTextField} and an {@see ActionEvent} is detected</li>
+ * </ul>
+ * 
+ * 
+ */
+public abstract class AbstractTextComponentValidator implements ActionListener, FocusListener, DocumentListener, PropertyChangeListener{
+    static final private Border ERROR_BORDER = BorderFactory.createLineBorder(Color.RED, 1);
+    static final private Color ERROR_BACKGROUND =  new Color(255,224,224);
+
+    private JTextComponent tc;
+    /** remembers whether the content of the text component is currently valid or not; null means,
+     * we don't know yet
+     */
+    private Boolean valid = null;
+
+    protected void feedbackInvalid(String msg) {
+        if (valid == null || valid) {
+            // only provide feedback if the validity has changed. This avoids
+            // unnecessary UI updates.
+            tc.setBorder(ERROR_BORDER);
+            tc.setBackground(ERROR_BACKGROUND);
+            tc.setToolTipText(msg);
+            valid = false;
+        }
+    }
+
+    protected void feedbackDisabled() {
+        feedbackValid(null);
+    }
+
+    protected void feedbackValid(String msg) {
+        if (valid == null || !valid) {
+            // only provide feedback if the validity has changed. This avoids
+            // unnecessary UI updates.
+            tc.setBorder(UIManager.getBorder("TextField.border"));
+            tc.setBackground(UIManager.getColor("TextField.background"));
+            tc.setToolTipText(msg == null ? "" : msg);
+            valid = true;
+        }
+    }
+
+    /**
+     * Replies the decorated text component
+     * 
+     * @return the decorated text component
+     */
+    public JTextComponent getComponent() {
+        return tc;
+    }
+
+    /**
+     * Creates the validator and weires it to the text component <code>tc</code>.
+     * 
+     * @param tc the text component. Must not be null.
+     * @throws IllegalArgumentException thrown if tc is null
+     */
+    public AbstractTextComponentValidator(JTextComponent tc) throws IllegalArgumentException {
+        CheckParameterUtil.ensureParameterNotNull(tc, "tc");
+        this.tc = tc;
+        tc.addFocusListener(this);
+        tc.getDocument().addDocumentListener(this);
+        if (tc instanceof JTextField) {
+            JTextField tf = (JTextField)tc;
+            tf.addActionListener(this);
+        }
+        tc.addPropertyChangeListener("enabled", this);
+    }
+
+    /**
+     * Implement in subclasses to validate the content of the text component.
+     * 
+     */
+    public abstract void validate();
+
+    /**
+     * Replies true if the current content of the decorated text component is valid;
+     * false otherwise
+     * 
+     * @return true if the current content of the decorated text component is valid
+     */
+    public abstract boolean isValid();
+
+    /* -------------------------------------------------------------------------------- */
+    /* interface FocusListener                                                          */
+    /* -------------------------------------------------------------------------------- */
+    public void focusGained(FocusEvent arg0) {}
+
+    public void focusLost(FocusEvent arg0) {
+        validate();
+    }
+
+    /* -------------------------------------------------------------------------------- */
+    /* interface ActionListener                                                         */
+    /* -------------------------------------------------------------------------------- */
+    public void actionPerformed(ActionEvent arg0) {
+        validate();
+    }
+
+    /* -------------------------------------------------------------------------------- */
+    /* interface DocumentListener                                                       */
+    /* -------------------------------------------------------------------------------- */
+    public void changedUpdate(DocumentEvent arg0) {
+        validate();
+    }
+
+    public void insertUpdate(DocumentEvent arg0) {
+        validate();
+    }
+
+    public void removeUpdate(DocumentEvent arg0) {
+        validate();
+    }
+
+    /* -------------------------------------------------------------------------------- */
+    /* interface PropertyChangeListener                                                 */
+    /* -------------------------------------------------------------------------------- */
+    public void propertyChange(PropertyChangeEvent evt) {
+        if (evt.getPropertyName().equals("enabled")) {
+            boolean enabled = (Boolean)evt.getNewValue();
+            if (enabled) {
+                validate();
+            } else {
+                feedbackDisabled();
+            }
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/widgets/BoundingBoxSelectionPanel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/widgets/BoundingBoxSelectionPanel.java	(revision 2688)
+++ trunk/src/org/openstreetmap/josm/gui/widgets/BoundingBoxSelectionPanel.java	(revision 2688)
@@ -0,0 +1,277 @@
+// License: GPL. Copyright 2007 by Immanuel Scholz and others
+package org.openstreetmap.josm.gui.widgets;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.Toolkit;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.FlavorEvent;
+import java.awt.datatransfer.FlavorListener;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.awt.event.ActionEvent;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+
+import javax.swing.AbstractAction;
+import javax.swing.BorderFactory;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.JTextField;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.text.JTextComponent;
+
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.coor.CoordinateFormat;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.gui.JMultilineLabel;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.OsmUrlToBounds;
+
+/**
+ *
+ *
+ */
+public class BoundingBoxSelectionPanel extends JPanel {
+
+    private JTextField[] tfLatLon = null;
+    private final JTextField tfOsmUrl = new JTextField();
+
+    protected void buildInputFields() {
+        tfLatLon = new JTextField[4];
+        for(int i=0; i< 4; i++) {
+            tfLatLon[i] = new JTextField(11);
+            tfLatLon[i].setMinimumSize(new Dimension(100,new JTextField().getMinimumSize().height));
+            SelectAllOnFocusGainedDecorator.decorate(tfLatLon[i]);
+        }
+        LatitudeValidator.decorate(tfLatLon[0]);
+        LatitudeValidator.decorate(tfLatLon[2]);
+        LongitudeValidator.decorate(tfLatLon[1]);
+        LongitudeValidator.decorate(tfLatLon[3]);
+    }
+
+    protected void build() {
+        buildInputFields();
+        setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+        setLayout(new GridBagLayout());
+        tfOsmUrl.getDocument().addDocumentListener(new OsmUrlRefresher());
+
+        // select content on receiving focus. this seems to be the default in the
+        // windows look+feel but not for others. needs invokeLater to avoid strange
+        // side effects that will cancel out the newly made selection otherwise.
+        tfOsmUrl.addFocusListener(new SelectAllOnFocusGainedDecorator());
+
+        add(new JLabel(tr("Min. latitude")), GBC.std().insets(0,0,3,5));
+        add(tfLatLon[0], GBC.std().insets(0,0,3,5));
+        add(new JLabel(tr("Min. longitude")), GBC.std().insets(0,0,3,5));
+        add(tfLatLon[1], GBC.eol());
+        add(new JLabel(tr("Max. latitude")), GBC.std().insets(0,0,3,5));
+        add(tfLatLon[2], GBC.std().insets(0,0,3,5));
+        add(new JLabel(tr("Max. longitude")), GBC.std().insets(0,0,3,5));
+        add(tfLatLon[3], GBC.eol());
+
+        GridBagConstraints gc = new GridBagConstraints();
+        gc.gridx = 0;
+        gc.gridy = 2;
+        gc.gridwidth = 4;
+        gc.fill = GridBagConstraints.HORIZONTAL;
+        gc.weightx = 1.0;
+        gc.insets = new Insets(10,0,0,3);
+        add(new JMultilineLabel(tr("URL from www.openstreetmap.org (you can paste a download URL here to specify a bounding box)")), gc);
+
+        gc.gridy = 3;
+        gc.insets = new Insets(3,0,0,3);
+        add(tfOsmUrl, gc);
+        tfOsmUrl.addMouseListener(new PopupMenuLauncher() {
+            @Override
+            public void launch(MouseEvent e) {
+                OsmUrlPopup popup = new OsmUrlPopup();
+                popup.show(tfOsmUrl, e.getX(), e.getY());
+            }
+        });
+    }
+
+    public BoundingBoxSelectionPanel() {
+        build();
+    }
+
+    public void setBoundingBox(Bounds area) {
+        updateBboxFields(area);
+    }
+
+    public Bounds getBoundingBox() {
+        double minlon, minlat, maxlon,maxlat;
+        try {
+            minlon = Double.parseDouble(tfLatLon[0].getText().trim());
+            minlat = Double.parseDouble(tfLatLon[1].getText().trim());
+            maxlon = Double.parseDouble(tfLatLon[2].getText().trim());
+            maxlat = Double.parseDouble(tfLatLon[3].getText().trim());
+        } catch(NumberFormatException e) {
+            return null;
+        }
+        if (!LatLon.isValidLon(minlon) || !LatLon.isValidLon(maxlon)
+                || !LatLon.isValidLat(minlat) || ! LatLon.isValidLat(maxlat))
+            return null;
+        if (minlon > maxlon)
+            return null;
+        if (minlat > maxlat)
+            return null;
+        return new Bounds(minlon,minlat,maxlon,maxlat);
+    }
+
+    private boolean parseURL() {
+        Bounds b = OsmUrlToBounds.parse(tfOsmUrl.getText());
+        if(b == null) return false;
+        updateBboxFields(b);
+        return true;
+    }
+
+    private void updateBboxFields(Bounds area) {
+        if (area == null) return;
+        tfLatLon[0].setText(area.getMin().latToString(CoordinateFormat.DECIMAL_DEGREES));
+        tfLatLon[1].setText(area.getMin().lonToString(CoordinateFormat.DECIMAL_DEGREES));
+        tfLatLon[2].setText(area.getMax().latToString(CoordinateFormat.DECIMAL_DEGREES));
+        tfLatLon[3].setText(area.getMax().lonToString(CoordinateFormat.DECIMAL_DEGREES));
+    }
+
+    static private class LatitudeValidator extends AbstractTextComponentValidator {
+
+        public static void decorate(JTextComponent tc) {
+            new LatitudeValidator(tc);
+        }
+
+        public LatitudeValidator(JTextComponent tc) {
+            super(tc);
+        }
+
+        @Override
+        public void validate() {
+            double value = 0;
+            try {
+                value = Double.parseDouble(getComponent().getText());
+            } catch(NumberFormatException ex) {
+                feedbackInvalid(tr("The string ''{0}'' isn''t a valid double value.", getComponent().getText()));
+                return;
+            }
+            if (!LatLon.isValidLat(value)) {
+                feedbackInvalid(tr("Value for latitude in range [-90,90] required.", getComponent().getText()));
+                return;
+            }
+            feedbackValid("");
+        }
+
+        @Override
+        public boolean isValid() {
+            double value = 0;
+            try {
+                value = Double.parseDouble(getComponent().getText());
+            } catch(NumberFormatException ex) {
+                return false;
+            }
+            if (!LatLon.isValidLat(value))
+                return false;
+            return true;
+        }
+    }
+
+    static private class LongitudeValidator extends AbstractTextComponentValidator{
+
+        public static void decorate(JTextComponent tc) {
+            new LongitudeValidator(tc);
+        }
+
+        public LongitudeValidator(JTextComponent tc) {
+            super(tc);
+        }
+
+        @Override
+        public void validate() {
+            double value = 0;
+            try {
+                value = Double.parseDouble(getComponent().getText());
+            } catch(NumberFormatException ex) {
+                feedbackInvalid(tr("The string ''{0}'' isn''t a valid double value.", getComponent().getText()));
+                return;
+            }
+            if (!LatLon.isValidLon(value)) {
+                feedbackInvalid(tr("Value for longitude in range [-180,180] required.", getComponent().getText()));
+                return;
+            }
+            feedbackValid("");
+        }
+
+        @Override
+        public boolean isValid() {
+            double value = 0;
+            try {
+                value = Double.parseDouble(getComponent().getText());
+            } catch(NumberFormatException ex) {
+                return false;
+            }
+            if (!LatLon.isValidLon(value))
+                return false;
+            return true;
+        }
+    }
+
+    class OsmUrlRefresher implements DocumentListener {
+        public void changedUpdate(DocumentEvent e) { parseURL(); }
+        public void insertUpdate(DocumentEvent e) { parseURL(); }
+        public void removeUpdate(DocumentEvent e) { parseURL(); }
+    }
+
+    class PasteUrlAction extends AbstractAction implements FlavorListener {
+
+        public PasteUrlAction() {
+            putValue(NAME, tr("Paste"));
+            putValue(SMALL_ICON, ImageProvider.get("paste"));
+            putValue(SHORT_DESCRIPTION, tr("Paste URL from clipboard"));
+            Toolkit.getDefaultToolkit().getSystemClipboard().addFlavorListener(this);
+        }
+
+        protected String getClipboardContent() {
+            Transferable t = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null);
+            try {
+                if (t != null && t.isDataFlavorSupported(DataFlavor.stringFlavor)) {
+                    String text = (String)t.getTransferData(DataFlavor.stringFlavor);
+                    return text;
+                }
+            } catch (UnsupportedFlavorException ex) {
+                ex.printStackTrace();
+                return null;
+            } catch (IOException ex) {
+                ex.printStackTrace();
+                return null;
+            }
+            return null;
+        }
+
+        public void actionPerformed(ActionEvent e) {
+            String content = getClipboardContent();
+            if (content != null) {
+                tfOsmUrl.setText(content);
+            }
+        }
+
+        protected void updateEnabledState() {
+            setEnabled(getClipboardContent() != null);
+        }
+
+        public void flavorsChanged(FlavorEvent e) {
+            updateEnabledState();
+        }
+    }
+
+    class OsmUrlPopup extends JPopupMenu {
+        public OsmUrlPopup() {
+            add(new PasteUrlAction());
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/widgets/HtmlPanel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/widgets/HtmlPanel.java	(revision 2688)
+++ trunk/src/org/openstreetmap/josm/gui/widgets/HtmlPanel.java	(revision 2688)
@@ -0,0 +1,84 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.widgets;
+
+import java.awt.BorderLayout;
+import java.awt.Font;
+import java.text.MessageFormat;
+
+import javax.swing.JEditorPane;
+import javax.swing.JPanel;
+import javax.swing.UIManager;
+import javax.swing.text.html.HTMLEditorKit;
+import javax.swing.text.html.StyleSheet;
+
+/**
+ * This panel can be used to display larger larger sections of formatted text in
+ * HTML.
+ * 
+ * It displays HTML text in the same font as {@see JLabel}. Hyperlinks are rendered in
+ * blue and they are underlined. There is also a CSS rule for the HTML tag &lt;strong&gt;
+ * configured.
+ * 
+ */
+public class HtmlPanel extends JPanel {
+    private JEditorPane jepMessage;
+
+    protected void build() {
+        setLayout(new BorderLayout());
+        jepMessage = new JEditorPane("text/html", "");
+        jepMessage.setOpaque(false);
+        jepMessage.setEditable(false);
+        Font f = UIManager.getFont("Label.font");
+        StyleSheet ss = new StyleSheet();
+        String rule = MessageFormat.format(
+                "font-family: ''{0}'';font-size: {1,number}pt; font-weight: {2}; font-style: {3}",
+                f.getName(),
+                f.getSize(),
+                f.isBold() ? "bold" : "normal",
+                        f.isItalic() ? "italic" : "normal"
+        );
+        rule = "body {" + rule + "}";
+        rule = MessageFormat.format(
+                "font-family: ''{0}'';font-size: {1,number}pt; font-weight: {2}; font-style: {3}",
+                f.getName(),
+                f.getSize(),
+                "bold",
+                f.isItalic() ? "italic" : "normal"
+        );
+        rule = "strong {" + rule + "}";
+        ss.addRule(rule);
+        ss.addRule("a {text-decoration: underline; color: blue}");
+        ss.addRule("ul {margin-left: 1cm; list-style-type: disc}");
+        HTMLEditorKit kit = new HTMLEditorKit();
+        kit.setStyleSheet(ss);
+        jepMessage.setEditorKit(kit);
+
+        add(jepMessage, BorderLayout.CENTER);
+    }
+
+    public HtmlPanel() {
+        build();
+    }
+
+    /**
+     * Replies the editor pane used internally to render the HTML text.
+     * 
+     * @return the editor pane used internally to render the HTML text.
+     */
+    public JEditorPane getEditorPane() {
+        return jepMessage;
+    }
+
+    /**
+     * Sets the current text to display. <code>text</code> is a html fragment.
+     * If null, empty string is assumed.
+     * 
+     * @param text the text to display
+     */
+    public void setText(String text) {
+        if (text == null) {
+            text = "";
+        }
+        jepMessage.setText(text);
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/widgets/PopupMenuLauncher.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/widgets/PopupMenuLauncher.java	(revision 2688)
+++ trunk/src/org/openstreetmap/josm/gui/widgets/PopupMenuLauncher.java	(revision 2688)
@@ -0,0 +1,45 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.widgets;
+
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+
+import javax.swing.JPopupMenu;
+
+public class PopupMenuLauncher extends MouseAdapter {
+    private JPopupMenu menu;
+
+    public PopupMenuLauncher() {
+        menu = null;
+    }
+    public PopupMenuLauncher(JPopupMenu menu) {
+        this.menu = menu;
+    }
+
+    @Override
+    public void mousePressed(MouseEvent e) {
+        if (e.isPopupTrigger()) {
+            launch(e);
+        }
+    }
+
+    @Override
+    public void mouseClicked(MouseEvent e) {
+        if (e.isPopupTrigger()) {
+            launch(e);
+        }
+    }
+
+    @Override
+    public void mouseReleased(MouseEvent e) {
+        if (e.isPopupTrigger()) {
+            launch(e);
+        }
+    }
+
+    public void launch(MouseEvent evt) {
+        if (menu != null) {
+            menu.show(evt.getComponent(), evt.getX(),evt.getY());
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/widgets/SelectAllOnFocusGainedDecorator.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/widgets/SelectAllOnFocusGainedDecorator.java	(revision 2688)
+++ trunk/src/org/openstreetmap/josm/gui/widgets/SelectAllOnFocusGainedDecorator.java	(revision 2688)
@@ -0,0 +1,25 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.widgets;
+
+import java.awt.Component;
+import java.awt.event.FocusAdapter;
+import java.awt.event.FocusEvent;
+
+import javax.swing.text.JTextComponent;
+
+public class SelectAllOnFocusGainedDecorator extends FocusAdapter{
+
+    public static void decorate(JTextComponent tc) {
+        if (tc == null) return;
+        tc.addFocusListener(new SelectAllOnFocusGainedDecorator());
+    }
+
+    @Override
+    public void focusGained(FocusEvent e) {
+        Component c = e.getComponent();
+        if (c instanceof JTextComponent) {
+            JTextComponent tc = (JTextComponent)c;
+            tc.selectAll();
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/io/ChangesetQuery.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/ChangesetQuery.java	(revision 2687)
+++ trunk/src/org/openstreetmap/josm/io/ChangesetQuery.java	(revision 2688)
@@ -2,46 +2,195 @@
 package org.openstreetmap.josm.io;
 
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
 import java.util.Date;
-
-import org.openstreetmap.josm.data.coor.CoordinateFormat;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.data.coor.LatLon;
-import org.openstreetmap.josm.tools.DateUtils;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
+import org.openstreetmap.josm.tools.CheckParameterUtil;
 
 public class ChangesetQuery {
-    private Long user = null;
-    private LatLon min = null;
-    private LatLon max = null;
+
+    /**
+     * Replies a changeset query object from the query part of a OSM API URL for querying
+     * changesets.
+     * 
+     * @param query the query part
+     * @return the query object
+     * @throws ChangesetQueryUrlException thrown if query doesn't consist of valid query parameters
+     * 
+     */
+    static public ChangesetQuery buildFromUrlQuery(String query) throws ChangesetQueryUrlException{
+        return new ChangesetQueryUrlParser().parse(query);
+    }
+
+    /** the user id this query is restricted to. null, if no restriction to a user id applies */
+    private Integer uid = null;
+    /** the user name this query is restricted to. null, if no restriction to a user name applies */
+    private String userName = null;
+    /** the bounding box this query is restricted to. null, if no restriction to a bounding box applies */
+    private Bounds bounds = null;
+
     private Date closedAfter = null;
     private Date createdBefore = null;
+    /** indicates whether only open changesets are queried. null, if no restrictions regarding open changesets apply */
     private Boolean open = null;
+    /** indicates whether only closed changesets are queried. null, if no restrictions regarding open changesets apply */
     private Boolean closed = null;
 
     public ChangesetQuery() {}
 
-    public ChangesetQuery forUser(long uid) {
+    /**
+     * Restricts the query to changesets owned by the user with id <code>uid</code>.
+     * 
+     * @param uid the uid of the user. >0 expected.
+     * @return the query object with the applied restriction
+     * @throws IllegalArgumentException thrown if uid <= 0
+     * @see #forUser(String)
+     */
+    public ChangesetQuery forUser(int uid) throws IllegalArgumentException{
         if (uid <= 0)
             throw new IllegalArgumentException(tr("Parameter ''{0}'' > 0 expected. Got ''{1}''.", "uid", uid));
-        this.user = uid;
-        return this;
-    }
-
-    public ChangesetQuery inBbox(double minLon, double minLat, double maxLon, double maxLat) {
+        this.uid = uid;
+        this.userName = null;
+        return this;
+    }
+
+    /**
+     * Restricts the query to changesets owned by the user with user name <code>username</code>.
+     * 
+     * Caveat: for historical reasons the username might not be unique! It is recommended to use
+     * {@see #forUser(int)} to restrict the query to a specific user.
+     * 
+     * @param username the username. Must not be null.
+     * @return the query object with the applied restriction
+     * @throws IllegalArgumentException thrown if username is null.
+     * @see #forUser(int)
+     */
+    public ChangesetQuery forUser(String username) {
+        CheckParameterUtil.ensureParameterNotNull(username, "username");
+        this.userName = username;
+        this.uid = 0;
+        return this;
+    }
+
+    /**
+     * Replies true if this query is restricted to user whom we only know the user name
+     * for.
+     * 
+     * @return true if this query is restricted to user whom we only know the user name
+     * for
+     */
+    public boolean isRestrictedToPartiallyIdentifiedUser() {
+        return userName != null;
+    }
+
+    /**
+     * Replies the user name which this query is restricted to. null, if this query isn't
+     * restricted to a user name, i.e. if {@see #isRestrictedToPartiallyIdentifiedUser()} is false.
+     * 
+     * @return the user name which this query is restricted to
+     */
+    public String getUserName() {
+        return userName;
+    }
+
+    /**
+     * Replies true if this query is restricted to user whom know the user id for.
+     * 
+     * @return true if this query is restricted to user whom know the user id for
+     */
+    public boolean isRestrictedToFullyIdentifiedUser() {
+        return uid > 0;
+    }
+
+    /**
+     * Replies a query which is restricted to a bounding box.
+     * 
+     * @param minLon  min longitude of the bounding box. Valid longitude value expected.
+     * @param minLat  min latitude of the bounding box. Valid latitude value expected.
+     * @param maxLon  max longitude of the bounding box. Valid longitude value expected.
+     * @param maxLat  max latitude of the bounding box.  Valid latitude value expected.
+     * 
+     * @return the restricted changeset query
+     * @throws IllegalArgumentException thrown if either of the parameters isn't a valid longitude or
+     * latitude value
+     */
+    public ChangesetQuery inBbox(double minLon, double minLat, double maxLon, double maxLat) throws IllegalArgumentException{
+        if (!LatLon.isValidLon(minLon))
+            throw new IllegalArgumentException(tr("Illegal longitude value for parameter ''{0}'', got {1}", "minLon", minLon));
+        if (!LatLon.isValidLon(maxLon))
+            throw new IllegalArgumentException(tr("Illegal longitude value for parameter ''{0}'', got {1}", "maxLon", maxLon));
+        if (!LatLon.isValidLat(minLat))
+            throw new IllegalArgumentException(tr("Illegal latitude value for parameter ''{0}'', got {1}", "minLat", minLat));
+        if (!LatLon.isValidLat(maxLat))
+            throw new IllegalArgumentException(tr("Illegal longitude value for parameter ''{0}'', got {1}", "maxLat", maxLat));
+
         return inBbox(new LatLon(minLon, minLat), new LatLon(maxLon, maxLat));
     }
 
+    /**
+     * Replies a query which is restricted to a bounding box.
+     * 
+     * @param min the min lat/lon coordinates of the bounding box. Must not be null.
+     * @param max the max lat/lon coordiantes of the bounding box. Must not be null.
+     * 
+     * @return the restricted changeset query
+     * @throws IllegalArgumentException thrown if min is null
+     * @throws IllegalArgumentException thrown if max is null
+     */
     public ChangesetQuery inBbox(LatLon min, LatLon max) {
-        this.min = min;
-        this.max = max;
-        return this;
-    }
-
-    public ChangesetQuery closedAfter(Date d) {
+        CheckParameterUtil.ensureParameterNotNull(min, "min");
+        CheckParameterUtil.ensureParameterNotNull(max, "max");
+        this.bounds  = new Bounds(min,max);
+        return this;
+    }
+
+    /**
+     *  Replies a query which is restricted to a bounding box given by <code>bbox</code>.
+     * 
+     * @param bbox the bounding box. Must not be null.
+     * @return the changeset query
+     * @throws IllegalArgumentException thrown if bbox is null.
+     */
+    public ChangesetQuery inBbox(Bounds bbox) throws IllegalArgumentException {
+        CheckParameterUtil.ensureParameterNotNull(bbox, "bbox");
+        this.bounds = bbox;
+        return this;
+    }
+
+    /**
+     * Restricts the result to changesets which have been closed after the date given by <code>d</code>.
+     * <code>d</code> d is a date relative to the current time zone.
+     * 
+     * @param d the date . Must not be null.
+     * @return the restricted changeset query
+     * @throws IllegalArgumentException thrown if d is null
+     */
+    public ChangesetQuery closedAfter(Date d) throws IllegalArgumentException{
+        CheckParameterUtil.ensureParameterNotNull(d, "d");
         this.closedAfter = d;
         return this;
     }
 
-    public ChangesetQuery between(Date closedAfter, Date createdBefore ) {
+    /**
+     * Restricts the result to changesets which have been closed after <code>closedAfter</code> and which
+     * habe been created before <code>createdBefore</code>. Both dates are expressed relative to the current
+     * time zone.
+     * 
+     * @param closedAfter only reply changesets closed after this date. Must not be null.
+     * @param createdBefore only reply changesets created before this date. Must not be null.
+     * @return the restricted changeset query
+     * @throws IllegalArgumentException thrown if closedAfter is null
+     * @throws IllegalArgumentException thrown if createdBefore is null
+     */
+    public ChangesetQuery closedAfterAndCreatedBefore(Date closedAfter, Date createdBefore ) throws IllegalArgumentException{
+        CheckParameterUtil.ensureParameterNotNull(closedAfter, "closedAfter");
+        CheckParameterUtil.ensureParameterNotNull(createdBefore, "createdBefore");
         this.closedAfter = closedAfter;
         this.createdBefore = createdBefore;
@@ -49,32 +198,45 @@
     }
 
-    public ChangesetQuery beingOpen() {
-        this.open =  true;
-        this.closed = null;
-        return this;
-    }
-
-    public ChangesetQuery beingClosed() {
-        this.open =  null;
-        this.closed = true;
-        return this;
-    }
-
+    /**
+     * Restricts the result to changesets which are or aren't open, depending on the value of
+     * <code>isOpen</code>
+     * 
+     * @param isOpen whether changesets should or should not be open
+     * @return the restricted changeset query
+     */
+    public ChangesetQuery beingOpen(boolean isOpen) {
+        this.open =  isOpen;
+        return this;
+    }
+
+    /**
+     * Restricts the result to changesets which are or aren't closed, depending on the value of
+     * <code>isClosed</code>
+     * 
+     * @param isClosed whether changesets should or should not be open
+     * @return the restricted changeset query
+     */
+    public ChangesetQuery beingClosed(boolean isClosed) {
+        this.closed = isClosed;
+        return this;
+    }
+
+    /**
+     * Replies the query string to be used in a query URL for the OSM API.
+     * 
+     * @return the query string
+     */
     public String getQueryString() {
         StringBuffer sb = new StringBuffer();
-        if (user != null) {
-            sb.append("user").append("=").append(user);
-        }
-        if (min!=null && max != null) {
+        if (uid != null) {
+            sb.append("user").append("=").append(uid);
+        } else if (userName != null) {
+            sb.append("display_name").append("=").append(userName);
+        }
+        if (bounds != null) {
             if (sb.length() > 0) {
                 sb.append("&");
             }
-            sb.append("min_lon").append("=").append(min.lonToString(CoordinateFormat.DECIMAL_DEGREES));
-            sb.append("&");
-            sb.append("min_lat").append("=").append(min.latToString(CoordinateFormat.DECIMAL_DEGREES));
-            sb.append("&");
-            sb.append("max_lon").append("=").append(max.lonToString(CoordinateFormat.DECIMAL_DEGREES));
-            sb.append("&");
-            sb.append("max_lat").append("=").append(max.latToString(CoordinateFormat.DECIMAL_DEGREES));
+            sb.append("bbox=").append(bounds.encodeAsString(","));
         }
         if (closedAfter != null && createdBefore != null) {
@@ -82,11 +244,13 @@
                 sb.append("&");
             }
-            sb.append("time").append("=").append(DateUtils.fromDate(closedAfter))
-            .append(",").append(DateUtils.fromDate(createdBefore));
+            SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz");
+            sb.append("time").append("=").append(df.format(closedAfter));
+            sb.append(",").append(df.format(createdBefore));
         } else if (closedAfter != null) {
             if (sb.length() > 0) {
                 sb.append("&");
             }
-            sb.append("time").append("=").append(DateUtils.fromDate(closedAfter));
+            SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz");
+            sb.append("time").append("=").append(df.format(closedAfter));
         }
 
@@ -95,12 +259,176 @@
                 sb.append("&");
             }
-            sb.append("open=true");
+            sb.append("open=").append(Boolean.toString(open));
         } else if (closed != null) {
             if (sb.length() > 0) {
                 sb.append("&");
             }
-            sb.append("closed=true");
+            sb.append("closed=").append(Boolean.toString(closed));
         }
         return sb.toString();
     }
+
+
+    public static class ChangesetQueryUrlException extends Exception {
+
+        public ChangesetQueryUrlException() {
+            super();
+        }
+
+        public ChangesetQueryUrlException(String arg0, Throwable arg1) {
+            super(arg0, arg1);
+        }
+
+        public ChangesetQueryUrlException(String arg0) {
+            super(arg0);
+        }
+
+        public ChangesetQueryUrlException(Throwable arg0) {
+            super(arg0);
+        }
+    }
+
+    public static class ChangesetQueryUrlParser {
+        protected int parseUid(String value) throws ChangesetQueryUrlException {
+            if (value == null || value.trim().equals(""))
+                throw new ChangesetQueryUrlException(tr("Unexpected value for ''{0}'' in changeset query url, got {1}", "uid",value));
+            int id;
+            try {
+                id = Integer.parseInt(value);
+                if (id <= 0)
+                    throw new ChangesetQueryUrlException(tr("Unexpected value for ''{0}'' in changeset query url, got {1}", "uid",value));
+            } catch(NumberFormatException e) {
+                throw new ChangesetQueryUrlException(tr("Unexpected value for ''{0}'' in changeset query url, got {1}", "uid",value));
+            }
+            return id;
+        }
+
+        protected boolean parseOpen(String value) throws ChangesetQueryUrlException {
+            if (value == null || value.trim().equals(""))
+                throw new ChangesetQueryUrlException(tr("Unexpected value for ''{0}'' in changeset query url, got {1}", "open",value));
+            if (value.equals("true"))
+                return true;
+            else if (value.equals("false"))
+                return false;
+            else
+                throw new ChangesetQueryUrlException(tr("Unexpected value for ''{0}'' in changeset query url, got {1}", "open",value));
+        }
+
+        protected boolean parseBoolean(String value, String parameter) throws ChangesetQueryUrlException {
+            if (value == null || value.trim().equals(""))
+                throw new ChangesetQueryUrlException(tr("Unexpected value for ''{0}'' in changeset query url, got {1}", parameter,value));
+            if (value.equals("true"))
+                return true;
+            else if (value.equals("false"))
+                return false;
+            else
+                throw new ChangesetQueryUrlException(tr("Unexpected value for ''{0}'' in changeset query url, got {1}", parameter,value));
+        }
+
+        protected Date parseDate(String value, String parameter) throws ChangesetQueryUrlException {
+            if (value == null || value.trim().equals(""))
+                throw new ChangesetQueryUrlException(tr("Unexpected value for ''{0}'' in changeset query url, got {1}", parameter,value));
+            if (value.endsWith("Z")) {
+                // OSM API generates date strings we time zone abbreviation "Z" which Java SimpleDateFormat
+                // doesn't understand. Convert into GMT time zone before parsing.
+                //
+                value = value.substring(0,value.length() - 1) + "GMT+00:00";
+            }
+            DateFormat formatter = new SimpleDateFormat("MM/dd/yy");
+            formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz");
+            try {
+                return formatter.parse(value);
+            } catch(ParseException e) {
+                throw new ChangesetQueryUrlException(tr("Unexpected value for ''{0}'' in changeset query url, got {1}", parameter,value));
+            }
+        }
+
+        protected Date[] parseTime(String value) throws ChangesetQueryUrlException {
+            String[] dates = value.split(",");
+            if (dates == null || dates.length == 0 || dates.length > 2)
+                throw new ChangesetQueryUrlException(tr("Unexpected value for ''{0}'' in changeset query url, got {1}", "time", value));
+            if (dates.length == 1)
+                return new Date[]{parseDate(dates[0], "time")};
+            else if (dates.length == 2)
+                return new Date[]{parseDate(dates[0], "time"),parseDate(dates[1], "time")};
+            return null;
+        }
+
+        protected ChangesetQuery crateFromMap(Map<String,String> queryParams) throws ChangesetQueryUrlException {
+            ChangesetQuery csQuery = new ChangesetQuery();
+
+            for (String k: queryParams.keySet()) {
+                if (k.equals("uid")) {
+                    if (queryParams.containsKey("display_name"))
+                        throw new ChangesetQueryUrlException(tr("Can''t create a changeset query including both the query parameters ''uid'' and ''display_name''"));
+                    csQuery.forUser(parseUid(queryParams.get("uid")));
+                } else if (k.equals("display_name")) {
+                    if (queryParams.containsKey("uid"))
+                        throw new ChangesetQueryUrlException(tr("Can''t create a changeset query including both the query parameters ''uid'' and ''display_name''"));
+                    csQuery.forUser(queryParams.get("display_name"));
+                } else if (k.equals("open")) {
+                    boolean b = parseBoolean(queryParams.get(k), "open");
+                    csQuery.beingOpen(b);
+                } else if (k.equals("closed")) {
+                    boolean b = parseBoolean(queryParams.get(k), "closed");
+                    csQuery.beingClosed(b);
+                } else if (k.equals("time")) {
+                    Date[] dates = parseTime(queryParams.get(k));
+                    switch(dates.length) {
+                    case 1:
+                        csQuery.closedAfter(dates[0]);
+                        break;
+                    case 2:
+                        csQuery.closedAfterAndCreatedBefore(dates[0], dates[1]);
+                        break;
+                    }
+                } else if (k.equals("bbox")) {
+                    try {
+                        csQuery.inBbox(new Bounds(queryParams.get(k), ","));
+                    } catch(IllegalArgumentException e) {
+                        throw new ChangesetQueryUrlException(e);
+                    }
+                } else
+                    throw new ChangesetQueryUrlException(tr("Unsupported parameter ''{0}'' in changeset query string",k ));
+            }
+            return csQuery;
+        }
+
+        protected Map<String,String> createMapFromQueryString(String query) {
+            Map<String,String> queryParams  = new HashMap<String, String>();
+            String[] keyValuePairs = query.split("&");
+            for (String keyValuePair: keyValuePairs) {
+                String[] kv = keyValuePair.split("=");
+                queryParams.put(kv[0], kv[1]);
+            }
+            return queryParams;
+        }
+
+        /**
+         * Parses the changeset query given as URL query parameters and replies a
+         * {@see ChangesetQuery}
+         * 
+         * <code>query</code> is the query part of a API url for querying changesets,
+         * see <a href="http://wiki.openstreetmap.org/wiki/API_v0.6#Query:_GET_.2Fapi.2F0.6.2Fchangesets">OSM API</a>.
+         * 
+         * Example for an query string:<br>
+         * <pre>
+         *    uid=1234&open=true
+         * </pre>
+         * 
+         * @param query the query string. If null, an empty query (identical to a query for all changesets) is
+         * assumed
+         * @return the changeset query
+         * @throws ChangesetQueryUrlException if the query string doesn't represent a legal query for changesets
+         */
+        public ChangesetQuery parse(String query) throws  ChangesetQueryUrlException{
+            if (query == null)
+                return new ChangesetQuery();
+            query = query.trim();
+            if (query.equals(""))
+                return new ChangesetQuery();
+            Map<String,String> queryParams  = createMapFromQueryString(query);
+            return crateFromMap(queryParams);
+        }
+    }
 }
Index: trunk/src/org/openstreetmap/josm/io/OsmChangesetContentParser.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/OsmChangesetContentParser.java	(revision 2688)
+++ trunk/src/org/openstreetmap/josm/io/OsmChangesetContentParser.java	(revision 2688)
@@ -0,0 +1,333 @@
+// License: GPL. Copyright 2007 by Immanuel Scholz and others
+package org.openstreetmap.josm.io;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringReader;
+import java.io.UnsupportedEncodingException;
+import java.util.Date;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.ChangesetDataSet;
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.openstreetmap.josm.data.osm.ChangesetDataSet.ChangesetModificationType;
+import org.openstreetmap.josm.data.osm.history.HistoryNode;
+import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive;
+import org.openstreetmap.josm.data.osm.history.HistoryRelation;
+import org.openstreetmap.josm.data.osm.history.HistoryWay;
+import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.tools.CheckParameterUtil;
+import org.openstreetmap.josm.tools.DateUtils;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * Parser for OSM changeset content.
+ *
+ */
+public class OsmChangesetContentParser {
+
+    private InputSource source;
+    private ChangesetDataSet data;
+
+    private class Parser extends DefaultHandler {
+
+        /** the current primitive to be read */
+        private HistoryOsmPrimitive currentPrimitive;
+        /** the current change modification type */
+        private ChangesetDataSet.ChangesetModificationType currentModificationType;
+
+        private Locator locator;
+
+        @Override
+        public void setDocumentLocator(Locator locator) {
+            this.locator = locator;
+        }
+
+        protected void throwException(String message) throws OsmDataParsingException {
+            throw new OsmDataParsingException(
+                    message
+            ).rememberLocation(locator);
+        }
+
+        protected void throwException(Exception e) throws OsmDataParsingException {
+            throw new OsmDataParsingException(
+                    e
+            ).rememberLocation(locator);
+        }
+
+        protected long getMandatoryAttributeLong(Attributes attr, String name) throws SAXException{
+            String v = attr.getValue(name);
+            if (v == null) {
+                throwException(tr("Missing mandatory attribute ''{0}''.", name));
+            }
+            Long l = 0l;
+            try {
+                l = Long.parseLong(v);
+            } catch(NumberFormatException e) {
+                throwException(tr("Illegal value for mandatory attribute ''{0}'' of type long. Got ''{1}''.", name, v));
+            }
+            if (l < 0) {
+                throwException(tr("Illegal value for mandatory attribute ''{0}'' of type long (>=0). Got ''{1}''.", name, v));
+            }
+            return l;
+        }
+
+        protected long getAttributeLong(Attributes attr, String name, long defaultValue) throws SAXException{
+            String v = attr.getValue(name);
+            if (v == null)
+                return defaultValue;
+            Long l = 0l;
+            try {
+                l = Long.parseLong(v);
+            } catch(NumberFormatException e) {
+                throwException(tr("Illegal value for mandatory attribute ''{0}'' of type long. Got ''{1}''.", name, v));
+            }
+            if (l < 0) {
+                throwException(tr("Illegal value for mandatory attribute ''{0}'' of type long (>=0). Got ''{1}''.", name, v));
+            }
+            return l;
+        }
+
+        protected Double getMandatoryAttributeDouble(Attributes attr, String name) throws SAXException{
+            String v = attr.getValue(name);
+            if (v == null) {
+                throwException(tr("Missing mandatory attribute ''{0}''.", name));
+            }
+            double d = 0.0;
+            try {
+                d = Double.parseDouble(v);
+            } catch(NumberFormatException e) {
+                throwException(tr("Illegal value for mandatory attribute ''{0}'' of type double. Got ''{1}''.", name, v));
+            }
+            return d;
+        }
+
+        protected String getMandatoryAttributeString(Attributes attr, String name) throws SAXException{
+            String v = attr.getValue(name);
+            if (v == null) {
+                throwException(tr("Missing mandatory attribute ''{0}''.", name));
+            }
+            return v;
+        }
+
+        protected String getAttributeString(Attributes attr, String name, String defaultValue) {
+            String v = attr.getValue(name);
+            if (v == null)
+                return defaultValue;
+            return v;
+        }
+
+        protected boolean getMandatoryAttributeBoolean(Attributes attr, String name) throws SAXException{
+            String v = attr.getValue(name);
+            if (v == null) {
+                throwException(tr("Missing mandatory attribute ''{0}''.", name));
+            }
+            if (v.equals("true")) return true;
+            if (v.equals("false")) return false;
+            throwException(tr("Illegal value for mandatory attribute ''{0}'' of type boolean. Got ''{1}''.", name, v));
+            // not reached
+            return false;
+        }
+
+        protected  HistoryOsmPrimitive createPrimitive(Attributes atts, OsmPrimitiveType type) throws SAXException {
+            long id = getMandatoryAttributeLong(atts,"id");
+            long version = getMandatoryAttributeLong(atts,"version");
+            long changesetId = getMandatoryAttributeLong(atts,"changeset");
+            boolean visible= getMandatoryAttributeBoolean(atts, "visible");
+            long uid = getAttributeLong(atts, "uid",-1);
+            String user = getAttributeString(atts, "user", tr("<anonymous>"));
+            String v = getMandatoryAttributeString(atts, "timestamp");
+            Date timestamp = DateUtils.fromString(v);
+            HistoryOsmPrimitive primitive = null;
+            if (type.equals(OsmPrimitiveType.NODE)) {
+                double lat = getMandatoryAttributeDouble(atts, "lat");
+                double lon = getMandatoryAttributeDouble(atts, "lon");
+                primitive = new HistoryNode(
+                        id,version,visible,user,uid,changesetId,timestamp, new LatLon(lat,lon)
+                );
+
+            } else if (type.equals(OsmPrimitiveType.WAY)) {
+                primitive = new HistoryWay(
+                        id,version,visible,user,uid,changesetId,timestamp
+                );
+            }if (type.equals(OsmPrimitiveType.RELATION)) {
+                primitive = new HistoryRelation(
+                        id,version,visible,user,uid,changesetId,timestamp
+                );
+            }
+            return primitive;
+        }
+
+        protected void startNode(Attributes atts) throws SAXException {
+            currentPrimitive= createPrimitive(atts, OsmPrimitiveType.NODE);
+        }
+
+        protected void startWay(Attributes atts) throws SAXException {
+            currentPrimitive= createPrimitive(atts, OsmPrimitiveType.WAY);
+        }
+        protected void startRelation(Attributes atts) throws SAXException {
+            currentPrimitive= createPrimitive(atts, OsmPrimitiveType.RELATION);
+        }
+
+        protected void handleTag(Attributes atts) throws SAXException {
+            String key= getMandatoryAttributeString(atts, "k");
+            String value= getMandatoryAttributeString(atts, "v");
+            currentPrimitive.put(key,value);
+        }
+
+        protected void handleNodeReference(Attributes atts) throws SAXException {
+            long ref = getMandatoryAttributeLong(atts, "ref");
+            ((HistoryWay)currentPrimitive).addNode(ref);
+        }
+
+        protected void handleMember(Attributes atts) throws SAXException {
+            long ref = getMandatoryAttributeLong(atts, "ref");
+            String v = getMandatoryAttributeString(atts, "type");
+            OsmPrimitiveType type = null;
+            try {
+                type = OsmPrimitiveType.fromApiTypeName(v);
+            } catch(IllegalArgumentException e) {
+                throwException(tr("Illegal value for mandatory attribute ''{0}'' of type OsmPrimitiveType. Got ''{1}''.", "type", v));
+            }
+            String role = getMandatoryAttributeString(atts, "role");
+            org.openstreetmap.josm.data.osm.history.RelationMember member = new org.openstreetmap.josm.data.osm.history.RelationMember(role, type,ref);
+            ((HistoryRelation)currentPrimitive).addMember(member);
+        }
+
+        @Override public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
+            if (qName.equals("node")) {
+                startNode(atts);
+            } else if (qName.equals("way")) {
+                startWay(atts);
+            } else if (qName.equals("relation")) {
+                startRelation(atts);
+            } else if (qName.equals("tag")) {
+                handleTag(atts);
+            } else if (qName.equals("nd")) {
+                handleNodeReference(atts);
+            } else if (qName.equals("member")) {
+                handleMember(atts);
+            } else if (qName.equals("osmChange")) {
+                // do nothing
+            } else if (qName.equals("create")) {
+                currentModificationType = ChangesetModificationType.CREATED;
+            } else if (qName.equals("modify")) {
+                currentModificationType = ChangesetModificationType.UPDATED;
+            } else if (qName.equals("delete")) {
+                currentModificationType = ChangesetModificationType.DELETED;
+            } else {
+                System.err.println(tr("Warning: unsupported start element ''{0}'' in changeset content at position ({1},{2}). Skipping.", qName, locator.getLineNumber(), locator.getColumnNumber()));
+            }
+        }
+
+        @Override
+        public void endElement(String uri, String localName, String qName) throws SAXException {
+            if (qName.equals("node")
+                    || qName.equals("way")
+                    || qName.equals("relation")) {
+                if (currentModificationType == null) {
+                    throwException(tr("Illegal document structure. Found node, way, or relation outside of ''create'', ''modify'', or ''delete''."));
+                }
+                data.put(currentPrimitive, currentModificationType);
+            } else if (qName.equals("osmChange")) {
+                // do nothing
+            } else if (qName.equals("create")) {
+                currentModificationType = null;
+            } else if (qName.equals("modify")) {
+                currentModificationType = null;
+            } else if (qName.equals("delete")) {
+                currentModificationType = null;
+            } else if (qName.equals("tag")) {
+                // do nothing
+            } else if (qName.equals("nd")) {
+                // do nothing
+            } else if (qName.equals("member")) {
+                // do nothing
+            } else {
+                System.err.println(tr("Warning: unsupported end element ''{0}'' in changeset content at position ({1},{2}). Skipping.", qName, locator.getLineNumber(), locator.getColumnNumber()));
+            }
+        }
+
+        @Override
+        public void error(SAXParseException e) throws SAXException {
+            throwException(e);
+        }
+
+        @Override
+        public void fatalError(SAXParseException e) throws SAXException {
+            throwException(e);
+        }
+    }
+
+    /**
+     * Create a parser
+     * 
+     * @param source the input stream with the changeset content as XML document. Must not be null.
+     * @throws IllegalArgumentException thrown if source is null.
+     */
+    public OsmChangesetContentParser(InputStream source) throws UnsupportedEncodingException {
+        CheckParameterUtil.ensureParameterNotNull(source, "source");
+        this.source = new InputSource(new InputStreamReader(source, "UTF-8"));
+        data = new ChangesetDataSet();
+    }
+
+    public OsmChangesetContentParser(String source) {
+        CheckParameterUtil.ensureParameterNotNull(source, "source");
+        this.source = new InputSource(new StringReader(source));
+        data = new ChangesetDataSet();
+    }
+
+    /**
+     * Parses the content
+     * 
+     * @param progressMonitor the progress monitor. Set to {@see NullProgressMonitor#INSTANCE}
+     * if null
+     * @return the parsed data
+     * @throws OsmDataParsingException thrown if something went wrong. Check for chained
+     * exceptions.
+     */
+    public ChangesetDataSet parse(ProgressMonitor progressMonitor) throws OsmDataParsingException {
+        if (progressMonitor == null) {
+            progressMonitor = NullProgressMonitor.INSTANCE;
+        }
+        try {
+            progressMonitor.beginTask(tr(""));
+            progressMonitor.indeterminateSubTask(tr("Parsing changeset content ..."));
+            SAXParserFactory.newInstance().newSAXParser().parse(source, new Parser());
+        } catch(OsmDataParsingException e){
+            throw e;
+        } catch (ParserConfigurationException e) {
+            throw new OsmDataParsingException(e);
+        } catch(SAXException e) {
+            throw new OsmDataParsingException(e);
+        } catch(IOException e) {
+            throw new OsmDataParsingException(e);
+        } finally {
+            progressMonitor.finishTask();
+        }
+        return data;
+    }
+
+    /**
+     * Parses the content from the input source
+     * 
+     * @return the parsed data
+     * @throws OsmDataParsingException thrown if something went wrong. Check for chained
+     * exceptions.
+     */
+    public ChangesetDataSet parse() throws OsmDataParsingException {
+        return parse(null);
+    }
+}
Index: trunk/src/org/openstreetmap/josm/io/OsmChangesetParser.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/OsmChangesetParser.java	(revision 2687)
+++ trunk/src/org/openstreetmap/josm/io/OsmChangesetParser.java	(revision 2688)
@@ -213,5 +213,6 @@
         OsmChangesetParser parser = new OsmChangesetParser();
         try {
-            progressMonitor.beginTask(tr("Parsing list of changesets...", 1));
+            progressMonitor.beginTask(tr(""));
+            progressMonitor.indeterminateSubTask(tr("Parsing list of changesets..."));
             InputSource inputSource = new InputSource(new InputStreamReader(source, "UTF-8"));
             SAXParserFactory.newInstance().newSAXParser().parse(inputSource, parser.new Parser());
Index: trunk/src/org/openstreetmap/josm/io/OsmServerChangesetReader.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/OsmServerChangesetReader.java	(revision 2687)
+++ trunk/src/org/openstreetmap/josm/io/OsmServerChangesetReader.java	(revision 2688)
@@ -6,4 +6,5 @@
 
 import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -13,4 +14,5 @@
 
 import org.openstreetmap.josm.data.osm.Changeset;
+import org.openstreetmap.josm.data.osm.ChangesetDataSet;
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
@@ -140,5 +142,5 @@
                 if (in == null)
                     return null;
-                monitor.indeterminateSubTask(tr("({0}/{1}) Downloading changeset {0} ...", i,ids.size(), id));
+                monitor.indeterminateSubTask(tr("({0}/{1}) Downloading changeset {2} ...", i,ids.size(), id));
                 List<Changeset> changesets = OsmChangesetParser.parse(in, monitor.createSubTaskMonitor(1, true));
                 if (changesets == null || changesets.isEmpty()) {
@@ -159,13 +161,36 @@
 
     /**
-     * not implemented yet
+     * Downloads the content of a changeset
      *
-     * @param id
-     * @param monitor
-     * @return
-     * @throws OsmTransferException
+     * @param id the changset id. >0 required.
+     * @param monitor the progress monitor. {@see NullProgressMonitor#INSTANCE} assumed if null.
+     * @return the changset content
+     * @throws IllegalArgumentException thrown if id <= 0
+     * @throws OsmTransferException thrown if something went wrong
      */
-    public Changeset downloadChangeset(long id, ProgressMonitor monitor) throws OsmTransferException {
-        return null;
+    public ChangesetDataSet downloadChangeset(int id, ProgressMonitor monitor) throws IllegalArgumentException, OsmTransferException {
+        if (id <= 0)
+            throw new IllegalArgumentException(tr("Expected value of type integer > 0 for parameter ''{0}'', got {1}", "id", id));
+        if (monitor == null) {
+            monitor = NullProgressMonitor.INSTANCE;
+        }
+        try {
+            monitor.beginTask(tr("Downloading changeset content"));
+            StringBuffer sb = new StringBuffer();
+            sb.append("changeset/").append(id).append("/download");
+            InputStream in = getInputStream(sb.toString(), monitor.createSubTaskMonitor(1, true));
+            if (in == null)
+                return null;
+            monitor.setCustomText(tr("Downloading content for changeset {0} ...", id));
+            OsmChangesetContentParser parser = new OsmChangesetContentParser(in);
+            ChangesetDataSet ds = parser.parse(monitor.createSubTaskMonitor(1, true));
+            return ds;
+        } catch(UnsupportedEncodingException e) {
+            throw new OsmTransferException(e);
+        } catch(OsmDataParsingException e) {
+            throw new OsmTransferException(e);
+        } finally {
+            monitor.finishTask();
+        }
     }
 }
Index: trunk/src/org/openstreetmap/josm/io/OsmServerUserInfoReader.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/OsmServerUserInfoReader.java	(revision 2687)
+++ trunk/src/org/openstreetmap/josm/io/OsmServerUserInfoReader.java	(revision 2688)
@@ -53,5 +53,5 @@
                 throw new OsmDataParsingException(tr("Missing attribute ''{0}'' on XML tag ''{1}''.", "id", "user"));
             try {
-                userInfo.setId(Long.parseLong(v));
+                userInfo.setId(Integer.parseInt(v));
             } catch(NumberFormatException e) {
                 throw new OsmDataParsingException(tr("Illegal value for attribute ''{0}'' on XML tag ''{1}''. Got {2}.", "id", "user", v));
Index: trunk/src/org/openstreetmap/josm/tools/WindowGeometry.java
===================================================================
--- trunk/src/org/openstreetmap/josm/tools/WindowGeometry.java	(revision 2687)
+++ trunk/src/org/openstreetmap/josm/tools/WindowGeometry.java	(revision 2688)
@@ -6,5 +6,4 @@
 import java.awt.Component;
 import java.awt.Dimension;
-import java.awt.Frame;
 import java.awt.Point;
 import java.awt.Toolkit;
@@ -12,6 +11,4 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-
-import javax.swing.JOptionPane;
 
 import org.openstreetmap.josm.Main;
@@ -40,13 +37,19 @@
 
     /**
-     * Replies a window geometry object for a window which a specific size which is centered
-     * relative to a parent window
-     *
-     * @param parent the parent window
+     * Replies a window geometry object for a window with a specific size which is centered
+     * relative to the parent window of a reference component.
+     *
+     * @param reference the reference component.
      * @param extent the size
      * @return the geometry object
      */
-    static public WindowGeometry centerInWindow(Component parent, Dimension extent) {
-        Frame parentWindow = JOptionPane.getFrameForComponent(parent);
+    static public WindowGeometry centerInWindow(Component reference, Dimension extent) {
+        Window parentWindow = null;
+        while(reference != null && ! (reference instanceof Window) ) {
+            reference = reference.getParent();
+        }
+        if (reference == null || ! (reference instanceof Window))
+            return new WindowGeometry(new Point(0,0), extent);
+        parentWindow = (Window)reference;
         Point topLeft = new Point(
                 Math.max(0, (parentWindow.getSize().width - extent.width) /2),
@@ -156,5 +159,5 @@
             initFromPreferences(preferenceKey);
         } catch(WindowGeometryException e) {
-//            System.out.println(tr("Warning: Failed to restore window geometry from key ''{0}''. Falling back to default geometry. Details: {1}", preferenceKey, e.getMessage()));
+            //            System.out.println(tr("Warning: Failed to restore window geometry from key ''{0}''. Falling back to default geometry. Details: {1}", preferenceKey, e.getMessage()));
             initFromWindowGeometry(defaultGeometry);
         }
