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