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