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