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