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