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(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        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            new WindowGeometry(
316                    getClass().getName() + ".geometry",
317                    WindowGeometry.centerInWindow(
318                            Main.parent,
319                            new Dimension(450, 540)
320                    )
321            ).applySafe(this);
322            initFromPreferences();
323        } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
324            new WindowGeometry(this).remember(getClass().getName() + ".geometry");
325        }
326        super.setVisible(visible);
327    }
328
329    protected void setCanceled(boolean canceled) {
330        this.canceled = canceled;
331    }
332
333    /**
334     * Obtains an OAuth access token for the connection. Afterwards, the token is accessible via {@link OAuthAccessTokenHolder}.
335     * @param serverUrl the URL to OSM server
336     * @throws InterruptedException if we're interrupted while waiting for the event dispatching thread to finish OAuth authorization task
337     * @throws InvocationTargetException if an exception is thrown while running OAuth authorization task
338     * @since 12803
339     */
340    public static void obtainAccessToken(final URL serverUrl) throws InvocationTargetException, InterruptedException {
341        final Runnable authTask = new FutureTask<>(() -> {
342            // Concerning Utils.newDirectExecutor: Main worker cannot be used since this connection is already
343            // executed via main worker. The OAuth connections would block otherwise.
344            final OAuthAuthorizationWizard wizard = new OAuthAuthorizationWizard(
345                    Main.parent, serverUrl.toExternalForm(), Utils.newDirectExecutor());
346            wizard.showDialog();
347            return wizard;
348        });
349        // exception handling differs from implementation at GuiHelper.runInEDTAndWait()
350        if (SwingUtilities.isEventDispatchThread()) {
351            authTask.run();
352        } else {
353            SwingUtilities.invokeAndWait(authTask);
354        }
355    }
356
357    class AuthorisationProcedureChangeListener implements ItemListener {
358        @Override
359        public void itemStateChanged(ItemEvent arg0) {
360            refreshAuthorisationProcedurePanel();
361        }
362    }
363
364    class CancelAction extends AbstractAction {
365
366        /**
367         * Constructs a new {@code CancelAction}.
368         */
369        CancelAction() {
370            putValue(NAME, tr("Cancel"));
371            new ImageProvider("cancel").getResource().attachImageIcon(this);
372            putValue(SHORT_DESCRIPTION, tr("Close the dialog and cancel authorization"));
373        }
374
375        public void cancel() {
376            setCanceled(true);
377            setVisible(false);
378        }
379
380        @Override
381        public void actionPerformed(ActionEvent evt) {
382            cancel();
383        }
384    }
385
386    class AcceptAccessTokenAction extends AbstractAction implements PropertyChangeListener {
387
388        /**
389         * Constructs a new {@code AcceptAccessTokenAction}.
390         */
391        AcceptAccessTokenAction() {
392            putValue(NAME, tr("Accept Access Token"));
393            new ImageProvider("ok").getResource().attachImageIcon(this);
394            putValue(SHORT_DESCRIPTION, tr("Close the dialog and accept the Access Token"));
395            updateEnabledState(null);
396        }
397
398        @Override
399        public void actionPerformed(ActionEvent evt) {
400            setCanceled(false);
401            setVisible(false);
402        }
403
404        public final void updateEnabledState(OAuthToken token) {
405            setEnabled(token != null);
406        }
407
408        @Override
409        public void propertyChange(PropertyChangeEvent evt) {
410            if (!evt.getPropertyName().equals(AbstractAuthorizationUI.ACCESS_TOKEN_PROP))
411                return;
412            updateEnabledState((OAuthToken) evt.getNewValue());
413        }
414    }
415
416    class WindowEventHandler extends WindowAdapter {
417        @Override
418        public void windowClosing(WindowEvent e) {
419            new CancelAction().cancel();
420        }
421    }
422}