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