001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs.changeset.query;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Dimension;
007import java.awt.GridBagConstraints;
008import java.awt.GridBagLayout;
009import java.awt.Insets;
010import java.awt.event.FocusAdapter;
011import java.awt.event.FocusEvent;
012import java.net.MalformedURLException;
013import java.net.URL;
014
015import javax.swing.BorderFactory;
016import javax.swing.JLabel;
017import javax.swing.JPanel;
018import javax.swing.event.DocumentEvent;
019import javax.swing.event.DocumentListener;
020import javax.swing.event.HyperlinkEvent;
021
022import org.openstreetmap.josm.Main;
023import org.openstreetmap.josm.gui.widgets.HtmlPanel;
024import org.openstreetmap.josm.gui.widgets.JosmTextField;
025import org.openstreetmap.josm.io.ChangesetQuery;
026import org.openstreetmap.josm.io.ChangesetQuery.ChangesetQueryUrlException;
027import org.openstreetmap.josm.io.OsmApi;
028import org.openstreetmap.josm.tools.ImageProvider;
029import org.openstreetmap.josm.tools.Logging;
030
031/**
032 * This panel allows to build a changeset query from an URL.
033 * @since 2689
034 */
035public class UrlBasedQueryPanel extends JPanel {
036
037    private final JosmTextField tfUrl = new JosmTextField();
038    private final JLabel lblValid = new JLabel();
039
040    /**
041     * Constructs a new {@code UrlBasedQueryPanel}.
042     */
043    public UrlBasedQueryPanel() {
044        build();
045    }
046
047    protected JPanel buildURLPanel() {
048        JPanel pnl = new JPanel(new GridBagLayout());
049        GridBagConstraints gc = new GridBagConstraints();
050        gc.weightx = 0.0;
051        gc.fill = GridBagConstraints.HORIZONTAL;
052        gc.insets = new Insets(0, 0, 0, 5);
053        pnl.add(new JLabel(tr("URL: ")), gc);
054
055        gc.gridx = 1;
056        gc.weightx = 1.0;
057        gc.fill = GridBagConstraints.HORIZONTAL;
058        pnl.add(tfUrl, gc);
059        tfUrl.getDocument().addDocumentListener(new ChangetQueryUrlValidator());
060        tfUrl.addFocusListener(
061                new FocusAdapter() {
062                    @Override
063                    public void focusGained(FocusEvent e) {
064                        tfUrl.selectAll();
065                    }
066                }
067        );
068
069        gc.gridx = 2;
070        gc.weightx = 0.0;
071        gc.fill = GridBagConstraints.HORIZONTAL;
072        pnl.add(lblValid, gc);
073        lblValid.setPreferredSize(new Dimension(20, 20));
074        return pnl;
075    }
076
077    protected JPanel buildHelpPanel() {
078        String apiUrl = OsmApi.getOsmApi().getBaseUrl();
079        HtmlPanel pnl = new HtmlPanel();
080        pnl.setText(
081                "<html><body>"
082                + tr("Please enter or paste an URL to retrieve changesets from the OSM API.")
083                + "<p><strong>" + tr("Examples") + "</strong></p>"
084                + "<ul>"
085                + "<li><a href=\""+Main.getOSMWebsite()+"/history?open=true\">"+Main.getOSMWebsite()+"/history?open=true</a></li>"
086                + "<li><a href=\""+apiUrl+"/changesets?open=true\">"+apiUrl+"/changesets?open=true</a></li>"
087                + "</ul>"
088                + tr("Note that changeset queries are currently always submitted to ''{0}'', regardless of the "
089                        + "host, port and path of the URL entered below.", apiUrl)
090                        + "</body></html>"
091        );
092        pnl.getEditorPane().addHyperlinkListener(e -> {
093                if (e.getEventType().equals(HyperlinkEvent.EventType.ACTIVATED)) {
094                    tfUrl.setText(e.getDescription());
095                    tfUrl.requestFocusInWindow();
096                }
097            });
098        return pnl;
099    }
100
101    protected final void build() {
102        setLayout(new GridBagLayout());
103        setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
104
105        GridBagConstraints gc = new GridBagConstraints();
106        gc.weightx = 1.0;
107        gc.fill = GridBagConstraints.HORIZONTAL;
108        gc.insets = new Insets(0, 0, 10, 0);
109        add(buildHelpPanel(), gc);
110
111        gc.gridy = 1;
112        gc.weightx = 1.0;
113        gc.fill = GridBagConstraints.HORIZONTAL;
114        add(buildURLPanel(), gc);
115
116        gc.gridy = 2;
117        gc.weightx = 1.0;
118        gc.weighty = 1.0;
119        gc.fill = GridBagConstraints.BOTH;
120        add(new JPanel(), gc);
121    }
122
123    protected boolean isValidChangesetQueryUrl(String text) {
124        return buildChangesetQuery(text) != null;
125    }
126
127    protected ChangesetQuery buildChangesetQuery(String text) {
128        URL url = null;
129        try {
130            url = new URL(text);
131        } catch (MalformedURLException e) {
132            return null;
133        }
134        String path = url.getPath();
135        if (path == null || !path.endsWith("/changesets"))
136            return null;
137
138        try {
139            return ChangesetQuery.buildFromUrlQuery(url.getQuery());
140        } catch (ChangesetQueryUrlException e) {
141            Logging.warn(e);
142            return null;
143        }
144    }
145
146    /**
147     * Replies the {@link ChangesetQuery} specified in this panel. null, if no valid changeset query
148     * is specified.
149     *
150     * @return the changeset query
151     */
152    public ChangesetQuery buildChangesetQuery() {
153        String value = tfUrl.getText().trim();
154        return buildChangesetQuery(value);
155    }
156
157    /**
158     * Initializes HMI for user input.
159     */
160    public void startUserInput() {
161        tfUrl.requestFocusInWindow();
162    }
163
164    /**
165     * Validates text entered in the changeset query URL field on the fly
166     */
167    class ChangetQueryUrlValidator implements DocumentListener {
168        protected String getCurrentFeedback() {
169            String fb = (String) lblValid.getClientProperty("valid");
170            return fb == null ? "none" : fb;
171        }
172
173        protected void feedbackValid() {
174            if ("valid".equals(getCurrentFeedback()))
175                return;
176            lblValid.setIcon(ImageProvider.get("dialogs", "valid"));
177            lblValid.setToolTipText(null);
178            lblValid.putClientProperty("valid", "valid");
179        }
180
181        protected void feedbackInvalid() {
182            if ("invalid".equals(getCurrentFeedback()))
183                return;
184            lblValid.setIcon(ImageProvider.get("warning-small"));
185            lblValid.setToolTipText(tr("This changeset query URL is invalid"));
186            lblValid.putClientProperty("valid", "invalid");
187        }
188
189        protected void feedbackNone() {
190            lblValid.setIcon(null);
191            lblValid.putClientProperty("valid", "none");
192        }
193
194        protected void validate() {
195            String value = tfUrl.getText();
196            if (value.trim().isEmpty()) {
197                feedbackNone();
198                return;
199            }
200            value = value.trim();
201            if (isValidChangesetQueryUrl(value)) {
202                feedbackValid();
203            } else {
204                feedbackInvalid();
205            }
206        }
207
208        @Override
209        public void changedUpdate(DocumentEvent e) {
210            validate();
211        }
212
213        @Override
214        public void insertUpdate(DocumentEvent e) {
215            validate();
216        }
217
218        @Override
219        public void removeUpdate(DocumentEvent e) {
220            validate();
221        }
222    }
223}