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