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