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.JComponent;
029import javax.swing.JDialog;
030import javax.swing.JLabel;
031import javax.swing.JPanel;
032import javax.swing.JScrollPane;
033import javax.swing.KeyStroke;
034import javax.swing.UIManager;
035import javax.swing.event.HyperlinkEvent;
036import javax.swing.event.HyperlinkListener;
037import javax.swing.text.html.HTMLEditorKit;
038
039import org.openstreetmap.josm.Main;
040import org.openstreetmap.josm.data.CustomConfigurator;
041import org.openstreetmap.josm.data.Preferences;
042import org.openstreetmap.josm.data.oauth.OAuthParameters;
043import org.openstreetmap.josm.data.oauth.OAuthToken;
044import org.openstreetmap.josm.gui.SideButton;
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 SideButton(actAcceptAccessToken));
102        pnl.add(new SideButton(new CancelAction()));
103        pnl.add(new SideButton(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            putValue(SMALL_ICON, ImageProvider.get("cancel"));
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            putValue(SMALL_ICON, ImageProvider.get("ok"));
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}