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