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.GridBagLayout;
008import java.awt.event.ActionEvent;
009import java.awt.event.ActionListener;
010import java.awt.event.FocusAdapter;
011import java.awt.event.FocusEvent;
012import java.awt.event.ItemEvent;
013import java.awt.event.ItemListener;
014import java.util.Arrays;
015
016import javax.swing.AbstractAction;
017import javax.swing.JButton;
018import javax.swing.JCheckBox;
019import javax.swing.JComponent;
020import javax.swing.JLabel;
021import javax.swing.JPanel;
022import javax.swing.SwingUtilities;
023import javax.swing.event.DocumentEvent;
024import javax.swing.event.DocumentListener;
025import javax.swing.text.JTextComponent;
026
027import org.openstreetmap.josm.data.preferences.ListProperty;
028import org.openstreetmap.josm.gui.MainApplication;
029import org.openstreetmap.josm.gui.help.HelpUtil;
030import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator;
031import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
032import org.openstreetmap.josm.gui.widgets.SelectAllOnFocusGainedDecorator;
033import org.openstreetmap.josm.io.OsmApi;
034import org.openstreetmap.josm.io.OsmApiInitializationException;
035import org.openstreetmap.josm.io.OsmTransferCanceledException;
036import org.openstreetmap.josm.spi.preferences.Config;
037import org.openstreetmap.josm.spi.preferences.IUrls;
038import org.openstreetmap.josm.tools.GBC;
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
084        // the checkbox for the default UL
085        add(buildDefaultServerUrlPanel(), GBC.eop().fill(GBC.HORIZONTAL));
086
087        // the input field for the URL
088        add(lblApiUrl, GBC.std().insets(0, 0, 3, 0));
089        add(tfOsmServerUrl, GBC.std().fill(GBC.HORIZONTAL).insets(0, 0, 3, 0));
090        lblApiUrl.setLabelFor(tfOsmServerUrl);
091        SelectAllOnFocusGainedDecorator.decorate(tfOsmServerUrl.getEditorComponent());
092        valOsmServerUrl = new ApiUrlValidator(tfOsmServerUrl.getEditorComponent());
093        valOsmServerUrl.validate();
094        propagator = new ApiUrlPropagator();
095        tfOsmServerUrl.addActionListener(propagator);
096        tfOsmServerUrl.addFocusListener(propagator);
097
098        add(lblValid, GBC.std().insets(0, 0, 3, 0));
099
100        ValidateApiUrlAction actTest = new ValidateApiUrlAction();
101        tfOsmServerUrl.getEditorComponent().getDocument().addDocumentListener(actTest);
102        btnTest = new JButton(actTest);
103        add(btnTest, GBC.eop());
104    }
105
106    /**
107     * Initializes the configuration panel with values from the preferences
108     */
109    public void initFromPreferences() {
110        String url = OsmApi.getOsmApi().getServerUrl();
111        tfOsmServerUrl.getModel().prefs().load(SERVER_URL_HISTORY);
112        if (Config.getUrls().getDefaultOsmApiUrl().equals(url.trim())) {
113            cbUseDefaultServerUrl.setSelected(true);
114            propagator.propagate(Config.getUrls().getDefaultOsmApiUrl());
115        } else {
116            cbUseDefaultServerUrl.setSelected(false);
117            tfOsmServerUrl.setText(url);
118            propagator.propagate(url);
119        }
120    }
121
122    /**
123     * Saves the values to the preferences
124     */
125    public void saveToPreferences() {
126        String oldUrl = OsmApi.getOsmApi().getServerUrl();
127        String hmiUrl = getStrippedApiUrl();
128        if (cbUseDefaultServerUrl.isSelected() || Config.getUrls().getDefaultOsmApiUrl().equals(hmiUrl)) {
129            Config.getPref().put("osm-server.url", null);
130        } else {
131            Config.getPref().put("osm-server.url", hmiUrl);
132            tfOsmServerUrl.addCurrentItemToHistory();
133            tfOsmServerUrl.getModel().prefs().save(SERVER_URL_HISTORY);
134        }
135        String newUrl = OsmApi.getOsmApi().getServerUrl();
136
137        // When API URL changes, re-initialize API connection so we may adjust server-dependent settings.
138        if (!oldUrl.equals(newUrl)) {
139            try {
140                OsmApi.getOsmApi().initialize(null);
141            } catch (OsmTransferCanceledException | OsmApiInitializationException ex) {
142                Logging.warn(ex);
143            }
144        }
145    }
146
147    /**
148     * Returns the entered API URL, stripped of leading and trailing white characters.
149     * @return the entered API URL, stripped of leading and trailing white characters. May be an empty string
150     *         if nothing has been entered. In this case, it means the user wants to use {@link IUrls#getDefaultOsmApiUrl}.
151     * @see Utils#strip(String)
152     * @since 6602
153     */
154    public final String getStrippedApiUrl() {
155        return Utils.strip(tfOsmServerUrl.getText());
156    }
157
158    class ValidateApiUrlAction extends AbstractAction implements DocumentListener {
159        private String lastTestedUrl;
160
161        ValidateApiUrlAction() {
162            putValue(NAME, tr("Validate"));
163            putValue(SHORT_DESCRIPTION, tr("Test the API URL"));
164            updateEnabledState();
165        }
166
167        @Override
168        public void actionPerformed(ActionEvent arg0) {
169            final String url = getStrippedApiUrl();
170            final ApiUrlTestTask task = new ApiUrlTestTask(OsmApiUrlInputPanel.this, url);
171            MainApplication.worker.submit(task);
172            Runnable r = () -> {
173                if (task.isCanceled())
174                    return;
175                Runnable r1 = () -> {
176                    if (task.isSuccess()) {
177                        lblValid.setIcon(ImageProvider.get("misc", "green_check"));
178                        lblValid.setToolTipText(tr("The API URL is valid."));
179                        lastTestedUrl = url;
180                        updateEnabledState();
181                    } else {
182                        lblValid.setIcon(ImageProvider.get("warning-small"));
183                        lblValid.setToolTipText(tr("Validation failed. The API URL seems to be invalid."));
184                    }
185                };
186                SwingUtilities.invokeLater(r1);
187            };
188            MainApplication.worker.submit(r);
189        }
190
191        protected final void updateEnabledState() {
192            String url = getStrippedApiUrl();
193            boolean enabled = !url.isEmpty() && !url.equals(lastTestedUrl);
194            if (enabled) {
195                lblValid.setIcon(null);
196            }
197            setEnabled(enabled);
198        }
199
200        @Override
201        public void changedUpdate(DocumentEvent arg0) {
202            updateEnabledState();
203        }
204
205        @Override
206        public void insertUpdate(DocumentEvent arg0) {
207            updateEnabledState();
208        }
209
210        @Override
211        public void removeUpdate(DocumentEvent arg0) {
212            updateEnabledState();
213        }
214    }
215
216    /**
217     * Enables or disables the API URL input.
218     * @param enabled {@code true} to enable input, {@code false} otherwise
219     */
220    public void setApiUrlInputEnabled(boolean enabled) {
221        lblApiUrl.setEnabled(enabled);
222        tfOsmServerUrl.setEnabled(enabled);
223        lblValid.setEnabled(enabled);
224        btnTest.setEnabled(enabled);
225    }
226
227    private static class ApiUrlValidator extends AbstractTextComponentValidator {
228        ApiUrlValidator(JTextComponent tc) {
229            super(tc);
230        }
231
232        @Override
233        public boolean isValid() {
234            if (getComponent().getText().trim().isEmpty())
235                return false;
236            return Utils.isValidUrl(getComponent().getText().trim());
237        }
238
239        @Override
240        public void validate() {
241            if (getComponent().getText().trim().isEmpty()) {
242                feedbackInvalid(tr("OSM API URL must not be empty. Please enter the OSM API URL."));
243                return;
244            }
245            if (!isValid()) {
246                feedbackInvalid(tr("The current value is not a valid URL"));
247            } else {
248                feedbackValid(tr("Please enter the OSM API URL."));
249            }
250        }
251    }
252
253    /**
254     * Handles changes in the default URL
255     */
256    class UseDefaultServerUrlChangeHandler implements ItemListener {
257        @Override
258        public void itemStateChanged(ItemEvent e) {
259            switch(e.getStateChange()) {
260            case ItemEvent.SELECTED:
261                setApiUrlInputEnabled(false);
262                propagator.propagate(Config.getUrls().getDefaultOsmApiUrl());
263                break;
264            case ItemEvent.DESELECTED:
265                setApiUrlInputEnabled(true);
266                valOsmServerUrl.validate();
267                tfOsmServerUrl.requestFocusInWindow();
268                propagator.propagate();
269                break;
270            default: // Do nothing
271            }
272        }
273    }
274
275    class ApiUrlPropagator extends FocusAdapter implements ActionListener {
276        protected void propagate() {
277            propagate(getStrippedApiUrl());
278        }
279
280        protected void propagate(String url) {
281            firePropertyChange(API_URL_PROP, null, url);
282        }
283
284        @Override
285        public void actionPerformed(ActionEvent e) {
286            propagate();
287        }
288
289        @Override
290        public void focusLost(FocusEvent arg0) {
291            propagate();
292        }
293    }
294}