001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.preferences.server;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Font;
007import java.awt.GridBagConstraints;
008import java.awt.GridBagLayout;
009import java.awt.Insets;
010import java.awt.event.ActionEvent;
011import java.awt.event.ActionListener;
012import java.awt.event.FocusAdapter;
013import java.awt.event.FocusEvent;
014import java.awt.event.ItemEvent;
015import java.awt.event.ItemListener;
016import java.util.Arrays;
017
018import javax.swing.AbstractAction;
019import javax.swing.JButton;
020import javax.swing.JCheckBox;
021import javax.swing.JComponent;
022import javax.swing.JLabel;
023import javax.swing.JPanel;
024import javax.swing.SwingUtilities;
025import javax.swing.event.DocumentEvent;
026import javax.swing.event.DocumentListener;
027import javax.swing.text.JTextComponent;
028
029import org.openstreetmap.josm.data.preferences.ListProperty;
030import org.openstreetmap.josm.gui.MainApplication;
031import org.openstreetmap.josm.gui.help.HelpUtil;
032import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator;
033import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
034import org.openstreetmap.josm.gui.widgets.SelectAllOnFocusGainedDecorator;
035import org.openstreetmap.josm.io.OsmApi;
036import org.openstreetmap.josm.io.OsmApiInitializationException;
037import org.openstreetmap.josm.io.OsmTransferCanceledException;
038import org.openstreetmap.josm.spi.preferences.Config;
039import org.openstreetmap.josm.tools.ImageProvider;
040import org.openstreetmap.josm.tools.Logging;
041import org.openstreetmap.josm.tools.Utils;
042
043/**
044 * Component allowing input os OSM API URL.
045 */
046public class OsmApiUrlInputPanel extends JPanel {
047
048    /**
049     * OSM API URL property key.
050     */
051    public static final String API_URL_PROP = OsmApiUrlInputPanel.class.getName() + ".apiUrl";
052
053    private final JLabel lblValid = new JLabel();
054    private final JLabel lblApiUrl = new JLabel(tr("OSM Server URL:"));
055    private final HistoryComboBox tfOsmServerUrl = new HistoryComboBox();
056    private transient ApiUrlValidator valOsmServerUrl;
057    private JButton btnTest;
058    /** indicates whether to use the default OSM URL or not */
059    private JCheckBox cbUseDefaultServerUrl;
060    private final transient ListProperty SERVER_URL_HISTORY = new ListProperty("osm-server.url-history", Arrays.asList(
061            "https://api06.dev.openstreetmap.org/api", "https://master.apis.dev.openstreetmap.org/api"));
062
063    private transient ApiUrlPropagator propagator;
064
065    /**
066     * Constructs a new {@code OsmApiUrlInputPanel}.
067     */
068    public OsmApiUrlInputPanel() {
069        build();
070        HelpUtil.setHelpContext(this, HelpUtil.ht("/Preferences/Connection#ApiUrl"));
071    }
072
073    protected JComponent buildDefaultServerUrlPanel() {
074        cbUseDefaultServerUrl = new JCheckBox(
075                tr("<html>Use the default OSM server URL (<strong>{0}</strong>)</html>", Config.getUrls().getDefaultOsmApiUrl()));
076        cbUseDefaultServerUrl.addItemListener(new UseDefaultServerUrlChangeHandler());
077        cbUseDefaultServerUrl.setFont(cbUseDefaultServerUrl.getFont().deriveFont(Font.PLAIN));
078        return cbUseDefaultServerUrl;
079    }
080
081    protected final void build() {
082        setLayout(new GridBagLayout());
083        GridBagConstraints gc = new GridBagConstraints();
084
085        // the checkbox for the default UL
086        gc.fill = GridBagConstraints.HORIZONTAL;
087        gc.anchor = GridBagConstraints.NORTHWEST;
088        gc.weightx = 1.0;
089        gc.insets = new Insets(0, 0, 0, 0);
090        gc.gridwidth = 4;
091        add(buildDefaultServerUrlPanel(), gc);
092
093
094        // the input field for the URL
095        gc.gridx = 0;
096        gc.gridy = 1;
097        gc.gridwidth = 1;
098        gc.weightx = 0.0;
099        gc.insets = new Insets(0, 0, 0, 3);
100        add(lblApiUrl, gc);
101
102        gc.gridx = 1;
103        gc.weightx = 1.0;
104        add(tfOsmServerUrl, gc);
105        lblApiUrl.setLabelFor(tfOsmServerUrl);
106        SelectAllOnFocusGainedDecorator.decorate(tfOsmServerUrl.getEditorComponent());
107        valOsmServerUrl = new ApiUrlValidator(tfOsmServerUrl.getEditorComponent());
108        valOsmServerUrl.validate();
109        propagator = new ApiUrlPropagator();
110        tfOsmServerUrl.addActionListener(propagator);
111        tfOsmServerUrl.addFocusListener(propagator);
112
113        gc.gridx = 2;
114        gc.weightx = 0.0;
115        add(lblValid, gc);
116
117        gc.gridx = 3;
118        gc.weightx = 0.0;
119        ValidateApiUrlAction actTest = new ValidateApiUrlAction();
120        tfOsmServerUrl.getEditorComponent().getDocument().addDocumentListener(actTest);
121        btnTest = new JButton(actTest);
122        add(btnTest, gc);
123    }
124
125    /**
126     * Initializes the configuration panel with values from the preferences
127     */
128    public void initFromPreferences() {
129        String url = OsmApi.getOsmApi().getServerUrl();
130        tfOsmServerUrl.setPossibleItems(SERVER_URL_HISTORY.get());
131        if (Config.getUrls().getDefaultOsmApiUrl().equals(url.trim())) {
132            cbUseDefaultServerUrl.setSelected(true);
133            propagator.propagate(Config.getUrls().getDefaultOsmApiUrl());
134        } else {
135            cbUseDefaultServerUrl.setSelected(false);
136            tfOsmServerUrl.setText(url);
137            propagator.propagate(url);
138        }
139    }
140
141    /**
142     * Saves the values to the preferences
143     */
144    public void saveToPreferences() {
145        String oldUrl = OsmApi.getOsmApi().getServerUrl();
146        String hmiUrl = getStrippedApiUrl();
147        if (cbUseDefaultServerUrl.isSelected() || Config.getUrls().getDefaultOsmApiUrl().equals(hmiUrl)) {
148            Config.getPref().put("osm-server.url", null);
149        } else {
150            Config.getPref().put("osm-server.url", hmiUrl);
151            tfOsmServerUrl.addCurrentItemToHistory();
152            SERVER_URL_HISTORY.put(tfOsmServerUrl.getHistory());
153        }
154        String newUrl = OsmApi.getOsmApi().getServerUrl();
155
156        // When API URL changes, re-initialize API connection so we may adjust server-dependent settings.
157        if (!oldUrl.equals(newUrl)) {
158            try {
159                OsmApi.getOsmApi().initialize(null);
160            } catch (OsmTransferCanceledException | OsmApiInitializationException ex) {
161                Logging.warn(ex);
162            }
163        }
164    }
165
166    /**
167     * Returns the entered API URL, stripped of leading and trailing white characters.
168     * @return the entered API URL, stripped of leading and trailing white characters.
169     *         May be an empty string if nothing has been entered. In this case, it means the user wants to use {@link OsmApi#DEFAULT_API_URL}.
170     * @see Utils#strip(String)
171     * @since 6602
172     */
173    public final String getStrippedApiUrl() {
174        return Utils.strip(tfOsmServerUrl.getText());
175    }
176
177    class ValidateApiUrlAction extends AbstractAction implements DocumentListener {
178        private String lastTestedUrl;
179
180        ValidateApiUrlAction() {
181            putValue(NAME, tr("Validate"));
182            putValue(SHORT_DESCRIPTION, tr("Test the API URL"));
183            updateEnabledState();
184        }
185
186        @Override
187        public void actionPerformed(ActionEvent arg0) {
188            final String url = getStrippedApiUrl();
189            final ApiUrlTestTask task = new ApiUrlTestTask(OsmApiUrlInputPanel.this, url);
190            MainApplication.worker.submit(task);
191            Runnable r = () -> {
192                if (task.isCanceled())
193                    return;
194                Runnable r1 = () -> {
195                    if (task.isSuccess()) {
196                        lblValid.setIcon(ImageProvider.get("misc", "green_check"));
197                        lblValid.setToolTipText(tr("The API URL is valid."));
198                        lastTestedUrl = url;
199                        updateEnabledState();
200                    } else {
201                        lblValid.setIcon(ImageProvider.get("warning-small"));
202                        lblValid.setToolTipText(tr("Validation failed. The API URL seems to be invalid."));
203                    }
204                };
205                SwingUtilities.invokeLater(r1);
206            };
207            MainApplication.worker.submit(r);
208        }
209
210        protected final void updateEnabledState() {
211            String url = getStrippedApiUrl();
212            boolean enabled = !url.isEmpty() && !url.equals(lastTestedUrl);
213            if (enabled) {
214                lblValid.setIcon(null);
215            }
216            setEnabled(enabled);
217        }
218
219        @Override
220        public void changedUpdate(DocumentEvent arg0) {
221            updateEnabledState();
222        }
223
224        @Override
225        public void insertUpdate(DocumentEvent arg0) {
226            updateEnabledState();
227        }
228
229        @Override
230        public void removeUpdate(DocumentEvent arg0) {
231            updateEnabledState();
232        }
233    }
234
235    /**
236     * Enables or disables the API URL input.
237     * @param enabled {@code true} to enable input, {@code false} otherwise
238     */
239    public void setApiUrlInputEnabled(boolean enabled) {
240        lblApiUrl.setEnabled(enabled);
241        tfOsmServerUrl.setEnabled(enabled);
242        lblValid.setEnabled(enabled);
243        btnTest.setEnabled(enabled);
244    }
245
246    private static class ApiUrlValidator extends AbstractTextComponentValidator {
247        ApiUrlValidator(JTextComponent tc) {
248            super(tc);
249        }
250
251        @Override
252        public boolean isValid() {
253            if (getComponent().getText().trim().isEmpty())
254                return false;
255            return Utils.isValidUrl(getComponent().getText().trim());
256        }
257
258        @Override
259        public void validate() {
260            if (getComponent().getText().trim().isEmpty()) {
261                feedbackInvalid(tr("OSM API URL must not be empty. Please enter the OSM API URL."));
262                return;
263            }
264            if (!isValid()) {
265                feedbackInvalid(tr("The current value is not a valid URL"));
266            } else {
267                feedbackValid(tr("Please enter the OSM API URL."));
268            }
269        }
270    }
271
272    /**
273     * Handles changes in the default URL
274     */
275    class UseDefaultServerUrlChangeHandler implements ItemListener {
276        @Override
277        public void itemStateChanged(ItemEvent e) {
278            switch(e.getStateChange()) {
279            case ItemEvent.SELECTED:
280                setApiUrlInputEnabled(false);
281                propagator.propagate(Config.getUrls().getDefaultOsmApiUrl());
282                break;
283            case ItemEvent.DESELECTED:
284                setApiUrlInputEnabled(true);
285                valOsmServerUrl.validate();
286                tfOsmServerUrl.requestFocusInWindow();
287                propagator.propagate();
288                break;
289            default: // Do nothing
290            }
291        }
292    }
293
294    class ApiUrlPropagator extends FocusAdapter implements ActionListener {
295        protected void propagate() {
296            propagate(getStrippedApiUrl());
297        }
298
299        protected void propagate(String url) {
300            firePropertyChange(API_URL_PROP, null, url);
301        }
302
303        @Override
304        public void actionPerformed(ActionEvent e) {
305            propagate();
306        }
307
308        @Override
309        public void focusLost(FocusEvent arg0) {
310            propagate();
311        }
312    }
313}