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.JCheckBox; 020import javax.swing.JComponent; 021import javax.swing.JLabel; 022import javax.swing.JPanel; 023import javax.swing.SwingUtilities; 024import javax.swing.event.DocumentEvent; 025import javax.swing.event.DocumentListener; 026import javax.swing.text.JTextComponent; 027 028import org.openstreetmap.josm.Main; 029import org.openstreetmap.josm.data.preferences.CollectionProperty; 030import org.openstreetmap.josm.gui.SideButton; 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 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 /** 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 SideButton(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}