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