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.util.concurrent.Executor; 024 025import javax.swing.AbstractAction; 026import javax.swing.BorderFactory; 027import javax.swing.JButton; 028import javax.swing.JDialog; 029import javax.swing.JLabel; 030import javax.swing.JPanel; 031import javax.swing.JScrollPane; 032import javax.swing.UIManager; 033import javax.swing.event.HyperlinkEvent; 034import javax.swing.event.HyperlinkListener; 035import javax.swing.text.html.HTMLEditorKit; 036 037import org.openstreetmap.josm.Main; 038import org.openstreetmap.josm.data.CustomConfigurator; 039import org.openstreetmap.josm.data.Preferences; 040import org.openstreetmap.josm.data.oauth.OAuthParameters; 041import org.openstreetmap.josm.data.oauth.OAuthToken; 042import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction; 043import org.openstreetmap.josm.gui.help.HelpUtil; 044import org.openstreetmap.josm.gui.preferences.server.OAuthAccessTokenHolder; 045import org.openstreetmap.josm.gui.util.GuiHelper; 046import org.openstreetmap.josm.gui.widgets.HtmlPanel; 047import org.openstreetmap.josm.io.OsmApi; 048import org.openstreetmap.josm.tools.CheckParameterUtil; 049import org.openstreetmap.josm.tools.ImageProvider; 050import org.openstreetmap.josm.tools.InputMapUtils; 051import org.openstreetmap.josm.tools.OpenBrowser; 052import org.openstreetmap.josm.tools.UserCancelException; 053import org.openstreetmap.josm.tools.WindowGeometry; 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.getEditorPane().addHyperlinkListener(new ExternalBrowserLauncher()); 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(600, 420)); 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 // Copy current JOSM preferences to update API url with the one used in this wizard 308 Preferences copyPref = CustomConfigurator.clonePreferences(Main.pref); 309 copyPref.put("osm-server.url", apiUrl); 310 pnlFullyAutomaticAuthorisationUI.initFromPreferences(copyPref); 311 pnlSemiAutomaticAuthorisationUI.initFromPreferences(copyPref); 312 pnlManualAuthorisationUI.initFromPreferences(copyPref); 313 } 314 315 @Override 316 public void setVisible(boolean visible) { 317 if (visible) { 318 new WindowGeometry( 319 getClass().getName() + ".geometry", 320 WindowGeometry.centerInWindow( 321 Main.parent, 322 new Dimension(450, 540) 323 ) 324 ).applySafe(this); 325 initFromPreferences(); 326 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775 327 new WindowGeometry(this).remember(getClass().getName() + ".geometry"); 328 } 329 super.setVisible(visible); 330 } 331 332 protected void setCanceled(boolean canceled) { 333 this.canceled = canceled; 334 } 335 336 class AuthorisationProcedureChangeListener implements ItemListener { 337 @Override 338 public void itemStateChanged(ItemEvent arg0) { 339 refreshAuthorisationProcedurePanel(); 340 } 341 } 342 343 class CancelAction extends AbstractAction { 344 345 /** 346 * Constructs a new {@code CancelAction}. 347 */ 348 CancelAction() { 349 putValue(NAME, tr("Cancel")); 350 new ImageProvider("cancel").getResource().attachImageIcon(this); 351 putValue(SHORT_DESCRIPTION, tr("Close the dialog and cancel authorization")); 352 } 353 354 public void cancel() { 355 setCanceled(true); 356 setVisible(false); 357 } 358 359 @Override 360 public void actionPerformed(ActionEvent evt) { 361 cancel(); 362 } 363 } 364 365 class AcceptAccessTokenAction extends AbstractAction implements PropertyChangeListener { 366 367 /** 368 * Constructs a new {@code AcceptAccessTokenAction}. 369 */ 370 AcceptAccessTokenAction() { 371 putValue(NAME, tr("Accept Access Token")); 372 new ImageProvider("ok").getResource().attachImageIcon(this); 373 putValue(SHORT_DESCRIPTION, tr("Close the dialog and accept the Access Token")); 374 updateEnabledState(null); 375 } 376 377 @Override 378 public void actionPerformed(ActionEvent evt) { 379 setCanceled(false); 380 setVisible(false); 381 } 382 383 public final void updateEnabledState(OAuthToken token) { 384 setEnabled(token != null); 385 } 386 387 @Override 388 public void propertyChange(PropertyChangeEvent evt) { 389 if (!evt.getPropertyName().equals(AbstractAuthorizationUI.ACCESS_TOKEN_PROP)) 390 return; 391 updateEnabledState((OAuthToken) evt.getNewValue()); 392 } 393 } 394 395 class WindowEventHandler extends WindowAdapter { 396 @Override 397 public void windowClosing(WindowEvent e) { 398 new CancelAction().cancel(); 399 } 400 } 401 402 static class ExternalBrowserLauncher implements HyperlinkListener { 403 @Override 404 public void hyperlinkUpdate(HyperlinkEvent e) { 405 if (e.getEventType().equals(HyperlinkEvent.EventType.ACTIVATED)) { 406 OpenBrowser.displayUrl(e.getDescription()); 407 } 408 } 409 } 410}