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.Color;
008import java.awt.FlowLayout;
009import java.awt.Font;
010import java.awt.GridBagConstraints;
011import java.awt.GridBagLayout;
012import java.awt.Insets;
013import java.awt.event.ActionEvent;
014import java.awt.event.ItemEvent;
015import java.util.concurrent.Executor;
016
017import javax.swing.AbstractAction;
018import javax.swing.BorderFactory;
019import javax.swing.JButton;
020import javax.swing.JCheckBox;
021import javax.swing.JLabel;
022import javax.swing.JPanel;
023
024import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
025import org.openstreetmap.josm.data.oauth.OAuthToken;
026import org.openstreetmap.josm.gui.util.GuiHelper;
027import org.openstreetmap.josm.gui.widgets.HtmlPanel;
028import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
029import org.openstreetmap.josm.gui.widgets.JosmTextField;
030import org.openstreetmap.josm.tools.ImageProvider;
031import org.openstreetmap.josm.tools.OpenBrowser;
032
033/**
034 * This is the UI for running a semic-automic authorisation procedure.
035 *
036 * In contrast to the fully-automatic procedure the user is dispatched to an
037 * external browser for login and authorisation.
038 *
039 * @since 2746
040 */
041public class SemiAutomaticAuthorizationUI extends AbstractAuthorizationUI {
042    private final AccessTokenInfoPanel pnlAccessTokenInfo = new AccessTokenInfoPanel();
043    private transient OAuthToken requestToken;
044
045    private RetrieveRequestTokenPanel pnlRetrieveRequestToken;
046    private RetrieveAccessTokenPanel pnlRetrieveAccessToken;
047    private ShowAccessTokenPanel pnlShowAccessToken;
048    private final transient Executor executor;
049
050    /**
051     * build the UI
052     */
053    protected final void build() {
054        setLayout(new BorderLayout());
055        setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
056        pnlRetrieveRequestToken = new RetrieveRequestTokenPanel();
057        pnlRetrieveAccessToken = new RetrieveAccessTokenPanel();
058        pnlShowAccessToken = new ShowAccessTokenPanel();
059        add(pnlRetrieveRequestToken, BorderLayout.CENTER);
060    }
061
062    /**
063     * Constructs a new {@code SemiAutomaticAuthorizationUI} for the given API URL.
064     * @param apiUrl The OSM API URL
065     * @param executor the executor used for running the HTTP requests for the authorization
066     * @since 5422
067     */
068    public SemiAutomaticAuthorizationUI(String apiUrl, Executor executor) {
069        super(apiUrl);
070        this.executor = executor;
071        build();
072    }
073
074    @Override
075    public boolean isSaveAccessTokenToPreferences() {
076        return pnlAccessTokenInfo.isSaveToPreferences();
077    }
078
079    protected void transitionToRetrieveAccessToken() {
080        OsmOAuthAuthorizationClient client = new OsmOAuthAuthorizationClient(
081                getAdvancedPropertiesPanel().getAdvancedParameters()
082        );
083        String authoriseUrl = client.getAuthoriseUrl(requestToken);
084        OpenBrowser.displayUrl(authoriseUrl);
085
086        removeAll();
087        pnlRetrieveAccessToken.setAuthoriseUrl(authoriseUrl);
088        add(pnlRetrieveAccessToken, BorderLayout.CENTER);
089        pnlRetrieveAccessToken.invalidate();
090        validate();
091        repaint();
092    }
093
094    protected void transitionToRetrieveRequestToken() {
095        requestToken = null;
096        setAccessToken(null);
097        removeAll();
098        add(pnlRetrieveRequestToken, BorderLayout.CENTER);
099        pnlRetrieveRequestToken.invalidate();
100        validate();
101        repaint();
102    }
103
104    protected void transitionToShowAccessToken() {
105        removeAll();
106        add(pnlShowAccessToken, BorderLayout.CENTER);
107        pnlShowAccessToken.invalidate();
108        validate();
109        repaint();
110        pnlShowAccessToken.setAccessToken(getAccessToken());
111    }
112
113    static class StepLabel extends JLabel {
114        StepLabel(String text) {
115            super(text);
116            setFont(getFont().deriveFont(16f));
117        }
118    }
119
120    /**
121     * This is the panel displayed in the first step of the semi-automatic authorisation process.
122     */
123    private class RetrieveRequestTokenPanel extends JPanel {
124
125        /**
126         * Constructs a new {@code RetrieveRequestTokenPanel}.
127         */
128        RetrieveRequestTokenPanel() {
129            build();
130        }
131
132        protected JPanel buildAdvancedParametersPanel() {
133            JPanel pnl = new JPanel(new GridBagLayout());
134            GridBagConstraints gc = new GridBagConstraints();
135
136            gc.anchor = GridBagConstraints.NORTHWEST;
137            gc.fill = GridBagConstraints.HORIZONTAL;
138            gc.weightx = 0.0;
139            gc.insets = new Insets(0, 0, 0, 3);
140            JCheckBox cbShowAdvancedParameters = new JCheckBox();
141            pnl.add(cbShowAdvancedParameters, gc);
142            cbShowAdvancedParameters.setSelected(false);
143            cbShowAdvancedParameters.addItemListener(
144                    evt -> getAdvancedPropertiesPanel().setVisible(evt.getStateChange() == ItemEvent.SELECTED)
145            );
146
147            gc.gridx = 1;
148            gc.weightx = 1.0;
149            JMultilineLabel lbl = new JMultilineLabel(tr("Display Advanced OAuth Parameters"));
150            lbl.setFont(lbl.getFont().deriveFont(Font.PLAIN));
151            pnl.add(lbl, gc);
152
153            gc.gridy = 1;
154            gc.gridx = 1;
155            gc.insets = new Insets(3, 0, 3, 0);
156            gc.fill = GridBagConstraints.BOTH;
157            gc.weightx = 1.0;
158            gc.weighty = 1.0;
159            pnl.add(getAdvancedPropertiesPanel(), gc);
160            getAdvancedPropertiesPanel().setBorder(
161                    BorderFactory.createCompoundBorder(
162                            BorderFactory.createLineBorder(Color.GRAY, 1),
163                            BorderFactory.createEmptyBorder(3, 3, 3, 3)
164                    )
165            );
166            getAdvancedPropertiesPanel().setVisible(false);
167            return pnl;
168        }
169
170        protected JPanel buildCommandPanel() {
171            JPanel pnl = new JPanel(new GridBagLayout());
172            GridBagConstraints gc = new GridBagConstraints();
173
174            gc.anchor = GridBagConstraints.NORTHWEST;
175            gc.fill = GridBagConstraints.BOTH;
176            gc.weightx = 1.0;
177            gc.weighty = 1.0;
178            gc.insets = new Insets(0, 0, 0, 3);
179
180
181            HtmlPanel h = new HtmlPanel();
182            h.setText(tr("<html>"
183                    + "Please click on <strong>{0}</strong> to retrieve an OAuth Request Token from "
184                    + "''{1}''.</html>",
185                    tr("Retrieve Request Token"),
186                    getAdvancedPropertiesPanel().getAdvancedParameters().getRequestTokenUrl()
187            ));
188            pnl.add(h, gc);
189
190            JPanel pnl1 = new JPanel(new FlowLayout(FlowLayout.LEFT));
191            pnl1.add(new JButton(new RetrieveRequestTokenAction()));
192            gc.fill = GridBagConstraints.HORIZONTAL;
193            gc.weightx = 1.0;
194            gc.gridy = 1;
195            pnl.add(pnl1, gc);
196            return pnl;
197
198        }
199
200        protected final void build() {
201            setLayout(new BorderLayout(0, 5));
202            add(new StepLabel(tr("<html>Step 1/3: Retrieve an OAuth Request Token</html>")), BorderLayout.NORTH);
203            add(buildAdvancedParametersPanel(), BorderLayout.CENTER);
204            add(buildCommandPanel(), BorderLayout.SOUTH);
205        }
206    }
207
208    /**
209     * This is the panel displayed in the second step of the semi-automatic authorization process.
210     */
211    private class RetrieveAccessTokenPanel extends JPanel {
212
213        private final JosmTextField tfAuthoriseUrl = new JosmTextField(null, null, 0, false);
214
215        /**
216         * Constructs a new {@code RetrieveAccessTokenPanel}.
217         */
218        RetrieveAccessTokenPanel() {
219            build();
220        }
221
222        protected JPanel buildTitlePanel() {
223            JPanel pnl = new JPanel(new BorderLayout());
224            pnl.add(new StepLabel(tr("<html>Step 2/3: Authorize and retrieve an Access Token</html>")), BorderLayout.CENTER);
225            return pnl;
226        }
227
228        protected JPanel buildContentPanel() {
229            JPanel pnl = new JPanel(new GridBagLayout());
230            GridBagConstraints gc = new GridBagConstraints();
231
232            gc.anchor = GridBagConstraints.NORTHWEST;
233            gc.fill = GridBagConstraints.HORIZONTAL;
234            gc.weightx = 1.0;
235            gc.gridwidth = 2;
236            HtmlPanel html = new HtmlPanel();
237            html.setText(tr("<html>"
238                    + "JOSM successfully retrieved a Request Token. "
239                    + "JOSM is now launching an authorization page in an external browser. "
240                    + "Please login with your OSM username and password and follow the instructions "
241                    + "to authorize the Request Token. Then switch back to this dialog and click on "
242                    + "<strong>{0}</strong><br><br>"
243                    + "If launching the external browser fails you can copy the following authorize URL "
244                    + "and paste it into the address field of your browser.</html>",
245                    tr("Request Access Token")
246            ));
247            pnl.add(html, gc);
248
249            gc.gridx = 0;
250            gc.gridy = 1;
251            gc.weightx = 0.0;
252            gc.gridwidth = 1;
253            pnl.add(new JLabel(tr("Authorize URL:")), gc);
254
255            gc.gridx = 1;
256            gc.weightx = 1.0;
257            pnl.add(tfAuthoriseUrl, gc);
258            tfAuthoriseUrl.setEditable(false);
259
260            return pnl;
261        }
262
263        protected JPanel buildActionPanel() {
264            JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT));
265            pnl.add(new JButton(new BackAction()));
266            pnl.add(new JButton(new RetrieveAccessTokenAction()));
267            return pnl;
268        }
269
270        protected final void build() {
271            setLayout(new BorderLayout());
272            add(buildTitlePanel(), BorderLayout.NORTH);
273            add(buildContentPanel(), BorderLayout.CENTER);
274            add(buildActionPanel(), BorderLayout.SOUTH);
275        }
276
277        public void setAuthoriseUrl(String url) {
278            tfAuthoriseUrl.setText(url);
279        }
280
281        /**
282         * Action to go back to step 1 in the process
283         */
284        class BackAction extends AbstractAction {
285            BackAction() {
286                putValue(NAME, tr("Back"));
287                putValue(SHORT_DESCRIPTION, tr("Go back to step 1/3"));
288                new ImageProvider("dialogs", "previous").getResource().attachImageIcon(this);
289            }
290
291            @Override
292            public void actionPerformed(ActionEvent arg0) {
293                transitionToRetrieveRequestToken();
294            }
295        }
296    }
297
298    /**
299     * Displays the retrieved Access Token in step 3.
300     */
301    class ShowAccessTokenPanel extends JPanel {
302
303        /**
304         * Constructs a new {@code ShowAccessTokenPanel}.
305         */
306        ShowAccessTokenPanel() {
307            build();
308        }
309
310        protected JPanel buildTitlePanel() {
311            JPanel pnl = new JPanel(new BorderLayout());
312            pnl.add(new StepLabel(tr("<html>Step 3/3: Successfully retrieved an Access Token</html>")), BorderLayout.CENTER);
313            return pnl;
314        }
315
316        protected JPanel buildContentPanel() {
317            JPanel pnl = new JPanel(new GridBagLayout());
318            GridBagConstraints gc = new GridBagConstraints();
319
320            gc.anchor = GridBagConstraints.NORTHWEST;
321            gc.fill = GridBagConstraints.HORIZONTAL;
322            gc.weightx = 1.0;
323            HtmlPanel html = new HtmlPanel();
324            html.setText(tr("<html>"
325                    + "JOSM has successfully retrieved an Access Token. "
326                    + "You can now accept this token. JOSM will use it in the future for authentication "
327                    + "and authorization to the OSM server.<br><br>"
328                    + "The access token is: </html>"
329            ));
330            pnl.add(html, gc);
331
332            gc.gridx = 0;
333            gc.gridy = 1;
334            gc.weightx = 1.0;
335            gc.gridwidth = 1;
336            pnl.add(pnlAccessTokenInfo, gc);
337            pnlAccessTokenInfo.setSaveToPreferences(
338                    OAuthAccessTokenHolder.getInstance().isSaveToPreferences()
339            );
340            return pnl;
341        }
342
343        protected JPanel buildActionPanel() {
344            JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT));
345            pnl.add(new JButton(new RestartAction()));
346            pnl.add(new JButton(new TestAccessTokenAction()));
347            return pnl;
348        }
349
350        protected final void build() {
351            setLayout(new BorderLayout());
352            add(buildTitlePanel(), BorderLayout.NORTH);
353            add(buildContentPanel(), BorderLayout.CENTER);
354            add(buildActionPanel(), BorderLayout.SOUTH);
355        }
356
357        /**
358         * Action to go back to step 1 in the process
359         */
360        class RestartAction extends AbstractAction {
361            RestartAction() {
362                putValue(NAME, tr("Restart"));
363                putValue(SHORT_DESCRIPTION, tr("Go back to step 1/3"));
364                new ImageProvider("dialogs", "previous").getResource().attachImageIcon(this);
365            }
366
367            @Override
368            public void actionPerformed(ActionEvent arg0) {
369                transitionToRetrieveRequestToken();
370            }
371        }
372
373        public void setAccessToken(OAuthToken accessToken) {
374            pnlAccessTokenInfo.setAccessToken(accessToken);
375        }
376    }
377
378    /**
379     * Action for retrieving a request token
380     */
381    class RetrieveRequestTokenAction extends AbstractAction {
382
383        RetrieveRequestTokenAction() {
384            putValue(NAME, tr("Retrieve Request Token"));
385            new ImageProvider("oauth", "oauth-small").getResource().attachImageIcon(this);
386            putValue(SHORT_DESCRIPTION, tr("Click to retrieve a Request Token"));
387        }
388
389        @Override
390        public void actionPerformed(ActionEvent evt) {
391            final RetrieveRequestTokenTask task = new RetrieveRequestTokenTask(
392                    SemiAutomaticAuthorizationUI.this,
393                    getAdvancedPropertiesPanel().getAdvancedParameters()
394            );
395            executor.execute(task);
396            Runnable r = () -> {
397                if (task.isCanceled()) return;
398                if (task.getRequestToken() == null) return;
399                requestToken = task.getRequestToken();
400                GuiHelper.runInEDT(SemiAutomaticAuthorizationUI.this::transitionToRetrieveAccessToken);
401            };
402            executor.execute(r);
403        }
404    }
405
406    /**
407     * Action for retrieving an Access Token
408     */
409    class RetrieveAccessTokenAction extends AbstractAction {
410
411        RetrieveAccessTokenAction() {
412            putValue(NAME, tr("Retrieve Access Token"));
413            new ImageProvider("oauth", "oauth-small").getResource().attachImageIcon(this);
414            putValue(SHORT_DESCRIPTION, tr("Click to retrieve an Access Token"));
415        }
416
417        @Override
418        public void actionPerformed(ActionEvent evt) {
419            final RetrieveAccessTokenTask task = new RetrieveAccessTokenTask(
420                    SemiAutomaticAuthorizationUI.this,
421                    getAdvancedPropertiesPanel().getAdvancedParameters(),
422                    requestToken
423            );
424            executor.execute(task);
425            Runnable r = () -> {
426                if (task.isCanceled()) return;
427                if (task.getAccessToken() == null) return;
428                GuiHelper.runInEDT(() -> {
429                    setAccessToken(task.getAccessToken());
430                    transitionToShowAccessToken();
431                });
432            };
433            executor.execute(r);
434        }
435    }
436
437    /**
438     * Action for testing an Access Token
439     */
440    class TestAccessTokenAction extends AbstractAction {
441
442        TestAccessTokenAction() {
443            putValue(NAME, tr("Test Access Token"));
444            new ImageProvider("oauth", "oauth-small").getResource().attachImageIcon(this);
445            putValue(SHORT_DESCRIPTION, tr("Click to test the Access Token"));
446        }
447
448        @Override
449        public void actionPerformed(ActionEvent evt) {
450            TestAccessTokenTask task = new TestAccessTokenTask(
451                    SemiAutomaticAuthorizationUI.this,
452                    getApiUrl(),
453                    getAdvancedPropertiesPanel().getAdvancedParameters(),
454                    getAccessToken()
455            );
456            executor.execute(task);
457        }
458    }
459}