001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.oauth; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.BorderLayout; 007import java.awt.Component; 008import java.awt.Dimension; 009import java.awt.FlowLayout; 010import java.awt.Font; 011import java.awt.GridBagConstraints; 012import java.awt.GridBagLayout; 013import java.awt.Insets; 014import java.awt.event.ActionEvent; 015import java.awt.event.ComponentEvent; 016import java.awt.event.ComponentListener; 017import java.awt.event.ItemEvent; 018import java.awt.event.ItemListener; 019import java.awt.event.KeyEvent; 020import java.awt.event.WindowAdapter; 021import java.awt.event.WindowEvent; 022import java.beans.PropertyChangeEvent; 023import java.beans.PropertyChangeListener; 024import java.util.concurrent.Executor; 025 026import javax.swing.AbstractAction; 027import javax.swing.BorderFactory; 028import javax.swing.JComponent; 029import javax.swing.JDialog; 030import javax.swing.JLabel; 031import javax.swing.JOptionPane; 032import javax.swing.JPanel; 033import javax.swing.JScrollPane; 034import javax.swing.KeyStroke; 035import javax.swing.UIManager; 036import javax.swing.event.HyperlinkEvent; 037import javax.swing.event.HyperlinkListener; 038import javax.swing.text.html.HTMLEditorKit; 039 040import org.openstreetmap.josm.Main; 041import org.openstreetmap.josm.data.CustomConfigurator; 042import org.openstreetmap.josm.data.Preferences; 043import org.openstreetmap.josm.data.oauth.OAuthParameters; 044import org.openstreetmap.josm.data.oauth.OAuthToken; 045import org.openstreetmap.josm.gui.SideButton; 046import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction; 047import org.openstreetmap.josm.gui.help.HelpUtil; 048import org.openstreetmap.josm.gui.preferences.server.OAuthAccessTokenHolder; 049import org.openstreetmap.josm.gui.util.GuiHelper; 050import org.openstreetmap.josm.gui.widgets.HtmlPanel; 051import org.openstreetmap.josm.io.OsmApi; 052import org.openstreetmap.josm.tools.CheckParameterUtil; 053import org.openstreetmap.josm.tools.ImageProvider; 054import org.openstreetmap.josm.tools.OpenBrowser; 055import org.openstreetmap.josm.tools.UserCancelException; 056import org.openstreetmap.josm.tools.WindowGeometry; 057 058/** 059 * This wizard walks the user to the necessary steps to retrieve an OAuth Access Token which 060 * allows JOSM to access the OSM API on the users behalf. 061 * 062 */ 063public class OAuthAuthorizationWizard extends JDialog { 064 private boolean canceled; 065 private final String apiUrl; 066 067 private AuthorizationProcedureComboBox cbAuthorisationProcedure; 068 private FullyAutomaticAuthorizationUI pnlFullyAutomaticAuthorisationUI; 069 private SemiAutomaticAuthorizationUI pnlSemiAutomaticAuthorisationUI; 070 private ManualAuthorizationUI pnlManualAuthorisationUI; 071 private JScrollPane spAuthorisationProcedureUI; 072 private final Executor executor; 073 074 /** 075 * Launches the wizard, {@link OAuthAccessTokenHolder#setAccessToken(OAuthToken) sets the token} 076 * and {@link OAuthAccessTokenHolder#setSaveToPreferences(boolean) saves to preferences}. 077 * @throws UserCancelException if user cancels the operation 078 */ 079 public void showDialog() throws UserCancelException { 080 setVisible(true); 081 if (isCanceled()) { 082 throw new UserCancelException(); 083 } 084 OAuthAccessTokenHolder holder = OAuthAccessTokenHolder.getInstance(); 085 holder.setAccessToken(getAccessToken()); 086 holder.setSaveToPreferences(isSaveAccessTokenToPreferences()); 087 } 088 089 /** 090 * Builds the row with the action buttons 091 * 092 * @return panel with buttons 093 */ 094 protected JPanel buildButtonRow() { 095 JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER)); 096 097 AcceptAccessTokenAction actAcceptAccessToken = new AcceptAccessTokenAction(); 098 pnlFullyAutomaticAuthorisationUI.addPropertyChangeListener(actAcceptAccessToken); 099 pnlSemiAutomaticAuthorisationUI.addPropertyChangeListener(actAcceptAccessToken); 100 pnlManualAuthorisationUI.addPropertyChangeListener(actAcceptAccessToken); 101 102 pnl.add(new SideButton(actAcceptAccessToken)); 103 pnl.add(new SideButton(new CancelAction())); 104 pnl.add(new SideButton(new ContextSensitiveHelpAction(HelpUtil.ht("/Dialog/OAuthAuthorisationWizard")))); 105 106 return pnl; 107 } 108 109 /** 110 * Builds the panel with general information in the header 111 * 112 * @return panel with information display 113 */ 114 protected JPanel buildHeaderInfoPanel() { 115 JPanel pnl = new JPanel(new GridBagLayout()); 116 pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 117 GridBagConstraints gc = new GridBagConstraints(); 118 119 // the oauth logo in the header 120 gc.anchor = GridBagConstraints.NORTHWEST; 121 gc.fill = GridBagConstraints.HORIZONTAL; 122 gc.weightx = 1.0; 123 gc.gridwidth = 2; 124 ImageProvider logoProv = new ImageProvider("oauth", "oauth-logo").setMaxHeight(100); 125 JLabel lbl = new JLabel(logoProv.get()); 126 lbl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 127 lbl.setOpaque(true); 128 pnl.add(lbl, gc); 129 130 // OAuth in a nutshell ... 131 gc.gridy = 1; 132 gc.insets = new Insets(5, 0, 0, 5); 133 HtmlPanel pnlMessage = new HtmlPanel(); 134 pnlMessage.setText("<html><body>" 135 + tr("With OAuth you grant JOSM the right to upload map data and GPS tracks " 136 + "on your behalf (<a href=\"{0}\">more info...</a>).", "http://oauth.net/") 137 + "</body></html>" 138 ); 139 pnlMessage.getEditorPane().addHyperlinkListener(new ExternalBrowserLauncher()); 140 pnl.add(pnlMessage, gc); 141 142 // the authorisation procedure 143 gc.gridy = 2; 144 gc.gridwidth = 1; 145 gc.weightx = 0.0; 146 lbl = new JLabel(tr("Please select an authorization procedure: ")); 147 lbl.setFont(lbl.getFont().deriveFont(Font.PLAIN)); 148 pnl.add(lbl, gc); 149 150 gc.gridx = 1; 151 gc.gridwidth = 1; 152 gc.weightx = 1.0; 153 pnl.add(cbAuthorisationProcedure = new AuthorizationProcedureComboBox(), gc); 154 cbAuthorisationProcedure.addItemListener(new AuthorisationProcedureChangeListener()); 155 lbl.setLabelFor(cbAuthorisationProcedure); 156 157 if (!OsmApi.DEFAULT_API_URL.equals(apiUrl)) { 158 gc.gridy = 3; 159 gc.gridwidth = 2; 160 gc.gridx = 0; 161 final HtmlPanel pnlWarning = new HtmlPanel(); 162 final HTMLEditorKit kit = (HTMLEditorKit) pnlWarning.getEditorPane().getEditorKit(); 163 kit.getStyleSheet().addRule(".warning-body {" 164 + "background-color:rgb(253,255,221);padding: 10pt; " 165 + "border-color:rgb(128,128,128);border-style: solid;border-width: 1px;}"); 166 kit.getStyleSheet().addRule("ol {margin-left: 1cm}"); 167 pnlWarning.setText("<html><body>" 168 + "<p class=\"warning-body\">" 169 + tr("<strong>Warning:</strong> Since you are using not the default OSM API, " + 170 "make sure to set an OAuth consumer key and secret in the <i>Advanced OAuth parameters</i>.") 171 + "</p>" 172 + "</body></html>"); 173 pnl.add(pnlWarning, gc); 174 } 175 176 return pnl; 177 } 178 179 /** 180 * Refreshes the view of the authorisation panel, depending on the authorisation procedure 181 * currently selected 182 */ 183 protected void refreshAuthorisationProcedurePanel() { 184 AuthorizationProcedure procedure = (AuthorizationProcedure) cbAuthorisationProcedure.getSelectedItem(); 185 switch(procedure) { 186 case FULLY_AUTOMATIC: 187 spAuthorisationProcedureUI.getViewport().setView(pnlFullyAutomaticAuthorisationUI); 188 pnlFullyAutomaticAuthorisationUI.revalidate(); 189 break; 190 case SEMI_AUTOMATIC: 191 spAuthorisationProcedureUI.getViewport().setView(pnlSemiAutomaticAuthorisationUI); 192 pnlSemiAutomaticAuthorisationUI.revalidate(); 193 break; 194 case MANUALLY: 195 spAuthorisationProcedureUI.getViewport().setView(pnlManualAuthorisationUI); 196 pnlManualAuthorisationUI.revalidate(); 197 break; 198 } 199 validate(); 200 repaint(); 201 } 202 203 /** 204 * builds the UI 205 */ 206 protected final void build() { 207 getContentPane().setLayout(new BorderLayout()); 208 getContentPane().add(buildHeaderInfoPanel(), BorderLayout.NORTH); 209 210 setTitle(tr("Get an Access Token for ''{0}''", apiUrl)); 211 this.setMinimumSize(new Dimension(600, 420)); 212 213 pnlFullyAutomaticAuthorisationUI = new FullyAutomaticAuthorizationUI(apiUrl, executor); 214 pnlSemiAutomaticAuthorisationUI = new SemiAutomaticAuthorizationUI(apiUrl, executor); 215 pnlManualAuthorisationUI = new ManualAuthorizationUI(apiUrl, executor); 216 217 spAuthorisationProcedureUI = GuiHelper.embedInVerticalScrollPane(new JPanel()); 218 spAuthorisationProcedureUI.getVerticalScrollBar().addComponentListener( 219 new ComponentListener() { 220 @Override 221 public void componentShown(ComponentEvent e) { 222 spAuthorisationProcedureUI.setBorder(UIManager.getBorder("ScrollPane.border")); 223 } 224 225 @Override 226 public void componentHidden(ComponentEvent e) { 227 spAuthorisationProcedureUI.setBorder(null); 228 } 229 230 @Override 231 public void componentResized(ComponentEvent e) {} 232 233 @Override 234 public void componentMoved(ComponentEvent e) {} 235 } 236 ); 237 getContentPane().add(spAuthorisationProcedureUI, BorderLayout.CENTER); 238 getContentPane().add(buildButtonRow(), BorderLayout.SOUTH); 239 240 addWindowListener(new WindowEventHandler()); 241 getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancel"); 242 getRootPane().getActionMap().put("cancel", new CancelAction()); 243 244 refreshAuthorisationProcedurePanel(); 245 246 HelpUtil.setHelpContext(getRootPane(), HelpUtil.ht("/Dialog/OAuthAuthorisationWizard")); 247 } 248 249 /** 250 * Creates the wizard. 251 * 252 * @param parent the component relative to which the dialog is displayed 253 * @param apiUrl the API URL. Must not be null. 254 * @param executor the executor used for running the HTTP requests for the authorization 255 * @throws IllegalArgumentException if apiUrl is null 256 */ 257 public OAuthAuthorizationWizard(Component parent, String apiUrl, Executor executor) { 258 super(JOptionPane.getFrameForComponent(parent), ModalityType.DOCUMENT_MODAL); 259 CheckParameterUtil.ensureParameterNotNull(apiUrl, "apiUrl"); 260 this.apiUrl = apiUrl; 261 this.executor = executor; 262 build(); 263 } 264 265 /** 266 * Replies true if the dialog was canceled 267 * 268 * @return true if the dialog was canceled 269 */ 270 public boolean isCanceled() { 271 return canceled; 272 } 273 274 protected AbstractAuthorizationUI getCurrentAuthorisationUI() { 275 switch((AuthorizationProcedure) cbAuthorisationProcedure.getSelectedItem()) { 276 case FULLY_AUTOMATIC: return pnlFullyAutomaticAuthorisationUI; 277 case MANUALLY: return pnlManualAuthorisationUI; 278 case SEMI_AUTOMATIC: return pnlSemiAutomaticAuthorisationUI; 279 default: return null; 280 } 281 } 282 283 /** 284 * Replies the Access Token entered using the wizard 285 * 286 * @return the access token. May be null if the wizard was canceled. 287 */ 288 public OAuthToken getAccessToken() { 289 return getCurrentAuthorisationUI().getAccessToken(); 290 } 291 292 /** 293 * Replies the current OAuth parameters. 294 * 295 * @return the current OAuth parameters. 296 */ 297 public OAuthParameters getOAuthParameters() { 298 return getCurrentAuthorisationUI().getOAuthParameters(); 299 } 300 301 /** 302 * Replies true if the currently selected Access Token shall be saved to 303 * the preferences. 304 * 305 * @return true if the currently selected Access Token shall be saved to 306 * the preferences 307 */ 308 public boolean isSaveAccessTokenToPreferences() { 309 return getCurrentAuthorisationUI().isSaveAccessTokenToPreferences(); 310 } 311 312 /** 313 * Initializes the dialog with values from the preferences 314 * 315 */ 316 public void initFromPreferences() { 317 // Copy current JOSM preferences to update API url with the one used in this wizard 318 Preferences copyPref = CustomConfigurator.clonePreferences(Main.pref); 319 copyPref.put("osm-server.url", apiUrl); 320 pnlFullyAutomaticAuthorisationUI.initFromPreferences(copyPref); 321 pnlSemiAutomaticAuthorisationUI.initFromPreferences(copyPref); 322 pnlManualAuthorisationUI.initFromPreferences(copyPref); 323 } 324 325 @Override 326 public void setVisible(boolean visible) { 327 if (visible) { 328 new WindowGeometry( 329 getClass().getName() + ".geometry", 330 WindowGeometry.centerInWindow( 331 Main.parent, 332 new Dimension(450, 540) 333 ) 334 ).applySafe(this); 335 initFromPreferences(); 336 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775 337 new WindowGeometry(this).remember(getClass().getName() + ".geometry"); 338 } 339 super.setVisible(visible); 340 } 341 342 protected void setCanceled(boolean canceled) { 343 this.canceled = canceled; 344 } 345 346 class AuthorisationProcedureChangeListener implements ItemListener { 347 @Override 348 public void itemStateChanged(ItemEvent arg0) { 349 refreshAuthorisationProcedurePanel(); 350 } 351 } 352 353 class CancelAction extends AbstractAction { 354 355 /** 356 * Constructs a new {@code CancelAction}. 357 */ 358 CancelAction() { 359 putValue(NAME, tr("Cancel")); 360 putValue(SMALL_ICON, ImageProvider.get("cancel")); 361 putValue(SHORT_DESCRIPTION, tr("Close the dialog and cancel authorization")); 362 } 363 364 public void cancel() { 365 setCanceled(true); 366 setVisible(false); 367 } 368 369 @Override 370 public void actionPerformed(ActionEvent evt) { 371 cancel(); 372 } 373 } 374 375 class AcceptAccessTokenAction extends AbstractAction implements PropertyChangeListener { 376 377 /** 378 * Constructs a new {@code AcceptAccessTokenAction}. 379 */ 380 AcceptAccessTokenAction() { 381 putValue(NAME, tr("Accept Access Token")); 382 putValue(SMALL_ICON, ImageProvider.get("ok")); 383 putValue(SHORT_DESCRIPTION, tr("Close the dialog and accept the Access Token")); 384 updateEnabledState(null); 385 } 386 387 @Override 388 public void actionPerformed(ActionEvent evt) { 389 setCanceled(false); 390 setVisible(false); 391 } 392 393 public final void updateEnabledState(OAuthToken token) { 394 setEnabled(token != null); 395 } 396 397 @Override 398 public void propertyChange(PropertyChangeEvent evt) { 399 if (!evt.getPropertyName().equals(AbstractAuthorizationUI.ACCESS_TOKEN_PROP)) 400 return; 401 updateEnabledState((OAuthToken) evt.getNewValue()); 402 } 403 } 404 405 class WindowEventHandler extends WindowAdapter { 406 @Override 407 public void windowClosing(WindowEvent e) { 408 new CancelAction().cancel(); 409 } 410 } 411 412 static class ExternalBrowserLauncher implements HyperlinkListener { 413 @Override 414 public void hyperlinkUpdate(HyperlinkEvent e) { 415 if (e.getEventType().equals(HyperlinkEvent.EventType.ACTIVATED)) { 416 OpenBrowser.displayUrl(e.getDescription()); 417 } 418 } 419 } 420}