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 = new Runnable() {
189                @Override
190                public void run() {
191                    if (task.isCanceled())
192                        return;
193                    Runnable r = new Runnable() {
194                        @Override
195                        public void run() {
196                            if (task.isSuccess()) {
197                                lblValid.setIcon(ImageProvider.get("dialogs", "valid"));
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                    };
207                    SwingUtilities.invokeLater(r);
208                }
209            };
210            Main.worker.submit(r);
211        }
212
213        protected final void updateEnabledState() {
214            String url = getStrippedApiUrl();
215            boolean enabled = !url.isEmpty() && !url.equals(lastTestedUrl);
216            if (enabled) {
217                lblValid.setIcon(null);
218            }
219            setEnabled(enabled);
220        }
221
222        @Override
223        public void changedUpdate(DocumentEvent arg0) {
224            updateEnabledState();
225        }
226
227        @Override
228        public void insertUpdate(DocumentEvent arg0) {
229            updateEnabledState();
230        }
231
232        @Override
233        public void removeUpdate(DocumentEvent arg0) {
234            updateEnabledState();
235        }
236    }
237
238    /**
239     * Enables or disables the API URL input.
240     * @param enabled {@code true} to enable input, {@code false} otherwise
241     */
242    public void setApiUrlInputEnabled(boolean enabled) {
243        lblApiUrl.setEnabled(enabled);
244        tfOsmServerUrl.setEnabled(enabled);
245        lblValid.setEnabled(enabled);
246        btnTest.setEnabled(enabled);
247    }
248
249    private static class ApiUrlValidator extends AbstractTextComponentValidator {
250        ApiUrlValidator(JTextComponent tc) {
251            super(tc);
252        }
253
254        @Override
255        public boolean isValid() {
256            if (getComponent().getText().trim().isEmpty())
257                return false;
258            return Utils.isValidUrl(getComponent().getText().trim());
259        }
260
261        @Override
262        public void validate() {
263            if (getComponent().getText().trim().isEmpty()) {
264                feedbackInvalid(tr("OSM API URL must not be empty. Please enter the OSM API URL."));
265                return;
266            }
267            if (!isValid()) {
268                feedbackInvalid(tr("The current value is not a valid URL"));
269            } else {
270                feedbackValid(tr("Please enter the OSM API URL."));
271            }
272        }
273    }
274
275    /**
276     * Handles changes in the default URL
277     */
278    class UseDefaultServerUrlChangeHandler implements ItemListener {
279        @Override
280        public void itemStateChanged(ItemEvent e) {
281            switch(e.getStateChange()) {
282            case ItemEvent.SELECTED:
283                setApiUrlInputEnabled(false);
284                propagator.propagate(OsmApi.DEFAULT_API_URL);
285                break;
286            case ItemEvent.DESELECTED:
287                setApiUrlInputEnabled(true);
288                valOsmServerUrl.validate();
289                tfOsmServerUrl.requestFocusInWindow();
290                propagator.propagate();
291                break;
292            default: // Do nothing
293            }
294        }
295    }
296
297    class ApiUrlPropagator extends FocusAdapter implements ActionListener {
298        protected void propagate() {
299            propagate(getStrippedApiUrl());
300        }
301
302        protected void propagate(String url) {
303            firePropertyChange(API_URL_PROP, null, url);
304        }
305
306        @Override
307        public void actionPerformed(ActionEvent e) {
308            propagate();
309        }
310
311        @Override
312        public void focusLost(FocusEvent arg0) {
313            propagate();
314        }
315    }
316}