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