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