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