001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui.oauth;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.awt.BorderLayout;
007    import java.awt.Component;
008    import java.awt.Dimension;
009    import java.awt.FlowLayout;
010    import java.awt.Font;
011    import java.awt.GridBagConstraints;
012    import java.awt.GridBagLayout;
013    import java.awt.Insets;
014    import java.awt.event.ActionEvent;
015    import java.awt.event.ComponentEvent;
016    import java.awt.event.ComponentListener;
017    import java.awt.event.ItemEvent;
018    import java.awt.event.ItemListener;
019    import java.awt.event.KeyEvent;
020    import java.awt.event.WindowAdapter;
021    import java.awt.event.WindowEvent;
022    import java.beans.PropertyChangeEvent;
023    import java.beans.PropertyChangeListener;
024    
025    import javax.swing.AbstractAction;
026    import javax.swing.BorderFactory;
027    import javax.swing.JComponent;
028    import javax.swing.JDialog;
029    import javax.swing.JLabel;
030    import javax.swing.JOptionPane;
031    import javax.swing.JPanel;
032    import javax.swing.JScrollPane;
033    import javax.swing.KeyStroke;
034    import javax.swing.UIManager;
035    import javax.swing.event.HyperlinkEvent;
036    import javax.swing.event.HyperlinkListener;
037    
038    import org.openstreetmap.josm.Main;
039    import org.openstreetmap.josm.data.CustomConfigurator;
040    import org.openstreetmap.josm.data.Preferences;
041    import org.openstreetmap.josm.data.oauth.OAuthParameters;
042    import org.openstreetmap.josm.data.oauth.OAuthToken;
043    import org.openstreetmap.josm.gui.SideButton;
044    import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
045    import org.openstreetmap.josm.gui.help.HelpUtil;
046    import org.openstreetmap.josm.gui.widgets.HtmlPanel;
047    import org.openstreetmap.josm.tools.CheckParameterUtil;
048    import org.openstreetmap.josm.tools.ImageProvider;
049    import org.openstreetmap.josm.tools.OpenBrowser;
050    import 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     */
057    public class OAuthAuthorizationWizard extends JDialog {
058        private HtmlPanel pnlMessage;
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
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
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            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 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 = new JScrollPane(new JPanel());
174            spAuthorisationProcedureUI.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
175            spAuthorisationProcedureUI.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
176            spAuthorisationProcedureUI.getVerticalScrollBar().addComponentListener(
177                    new ComponentListener() {
178                        public void componentShown(ComponentEvent e) {
179                            spAuthorisationProcedureUI.setBorder(UIManager.getBorder("ScrollPane.border"));
180                        }
181    
182                        public void componentHidden(ComponentEvent e) {
183                            spAuthorisationProcedureUI.setBorder(null);
184                        }
185    
186                        public void componentResized(ComponentEvent e) {}
187                        public void componentMoved(ComponentEvent e) {}
188                    }
189            );
190            getContentPane().add(spAuthorisationProcedureUI, BorderLayout.CENTER);
191            getContentPane().add(buildButtonRow(), BorderLayout.SOUTH);
192    
193            addWindowListener(new WindowEventHandler());
194            getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancel");
195            getRootPane().getActionMap().put("cancel", new CancelAction());
196    
197            refreshAuthorisationProcedurePanel();
198    
199            HelpUtil.setHelpContext(getRootPane(), HelpUtil.ht("/Dialog/OAuthAuthorisationWizard"));
200        }
201    
202        /**
203         * Creates the wizard.
204         *
205         * @param apiUrl the API URL. Must not be null.
206         * @throws IllegalArgumentException thrown if apiUrl is null
207         */
208        public OAuthAuthorizationWizard(String apiUrl) throws IllegalArgumentException {
209            this(Main.parent, apiUrl);
210        }
211    
212        /**
213         * Creates the wizard.
214         *
215         * @param parent the component relative to which the dialog is displayed
216         * @param apiUrl the API URL. Must not be null.
217         * @throws IllegalArgumentException thrown if apiUrl is null
218         */
219        public OAuthAuthorizationWizard(Component parent, String apiUrl) {
220            super(JOptionPane.getFrameForComponent(parent), ModalityType.DOCUMENT_MODAL);
221            CheckParameterUtil.ensureParameterNotNull(apiUrl, "apiUrl");
222            this.apiUrl = apiUrl;
223            build();
224        }
225    
226        /**
227         * Replies true if the dialog was canceled
228         *
229         * @return true if the dialog was canceled
230         */
231        public boolean isCanceled() {
232            return canceled;
233        }
234    
235        protected AbstractAuthorizationUI getCurrentAuthorisationUI() {
236            switch((AuthorizationProcedure)cbAuthorisationProcedure.getSelectedItem()) {
237            case FULLY_AUTOMATIC: return pnlFullyAutomaticAuthorisationUI;
238            case MANUALLY: return pnlManualAuthorisationUI;
239            case SEMI_AUTOMATIC: return pnlSemiAutomaticAuthorisationUI;
240            default: return null;
241            }
242        }
243    
244        /**
245         * Replies the Access Token entered using the wizard
246         *
247         * @return the access token. May be null if the wizard was canceled.
248         */
249        public OAuthToken getAccessToken() {
250            return getCurrentAuthorisationUI().getAccessToken();
251        }
252    
253        /**
254         * Replies the current OAuth parameters.
255         *
256         * @return the current OAuth parameters.
257         */
258        public OAuthParameters getOAuthParameters() {
259            return getCurrentAuthorisationUI().getOAuthParameters();
260        }
261    
262        /**
263         * Replies true if the currently selected Access Token shall be saved to
264         * the preferences.
265         *
266         * @return true if the currently selected Access Token shall be saved to
267         * the preferences
268         */
269        public boolean isSaveAccessTokenToPreferences() {
270            return getCurrentAuthorisationUI().isSaveAccessTokenToPreferences();
271        }
272    
273        /**
274         * Initializes the dialog with values from the preferences
275         *
276         */
277        public void initFromPreferences() {
278            // Copy current JOSM preferences to update API url with the one used in this wizard
279            Preferences copyPref = CustomConfigurator.clonePreferences(Main.pref);
280            copyPref.put("osm-server-url", apiUrl);
281            pnlFullyAutomaticAuthorisationUI.initFromPreferences(copyPref);
282            pnlSemiAutomaticAuthorisationUI.initFromPreferences(copyPref);
283            pnlManualAuthorisationUI.initFromPreferences(copyPref);
284        }
285    
286        @Override
287        public void setVisible(boolean visible) {
288            if (visible) {
289                new WindowGeometry(
290                        getClass().getName() + ".geometry",
291                        WindowGeometry.centerInWindow(
292                                Main.parent,
293                                new Dimension(450,540)
294                        )
295                ).applySafe(this);
296                initFromPreferences();
297            } else if (!visible && isShowing()){
298                new WindowGeometry(this).remember(getClass().getName() + ".geometry");
299            }
300            super.setVisible(visible);
301        }
302    
303        protected void setCanceled(boolean canceled) {
304            this.canceled = canceled;
305        }
306    
307        class AuthorisationProcedureChangeListener implements ItemListener {
308            public void itemStateChanged(ItemEvent arg0) {
309                refreshAuthorisationProcedurePanel();
310            }
311        }
312    
313        class CancelAction extends AbstractAction {
314            public CancelAction() {
315                putValue(NAME, tr("Cancel"));
316                putValue(SMALL_ICON, ImageProvider.get("cancel"));
317                putValue(SHORT_DESCRIPTION, tr("Close the dialog and cancel authorization"));
318            }
319    
320            public void cancel() {
321                setCanceled(true);
322                setVisible(false);
323            }
324    
325            public void actionPerformed(ActionEvent evt) {
326                cancel();
327            }
328        }
329    
330        class AcceptAccessTokenAction extends AbstractAction implements PropertyChangeListener {
331            private OAuthToken token;
332    
333            public AcceptAccessTokenAction() {
334                putValue(NAME, tr("Accept Access Token"));
335                putValue(SMALL_ICON, ImageProvider.get("ok"));
336                putValue(SHORT_DESCRIPTION, tr("Close the dialog and accept the Access Token"));
337                updateEnabledState(null);
338            }
339    
340            public void actionPerformed(ActionEvent evt) {
341                setCanceled(false);
342                setVisible(false);
343            }
344    
345            public void updateEnabledState(OAuthToken token) {
346                setEnabled(token != null);
347            }
348    
349            public void propertyChange(PropertyChangeEvent evt) {
350                if (!evt.getPropertyName().equals(AbstractAuthorizationUI.ACCESS_TOKEN_PROP))
351                    return;
352                token = (OAuthToken)evt.getNewValue();
353                updateEnabledState(token);
354            }
355        }
356    
357        class WindowEventHandler extends WindowAdapter {
358            @Override
359            public void windowClosing(WindowEvent arg0) {
360                new CancelAction().cancel();
361            }
362        }
363    
364        static class ExternalBrowserLauncher implements HyperlinkListener {
365            public void hyperlinkUpdate(HyperlinkEvent e) {
366                if (e.getEventType().equals(HyperlinkEvent.EventType.ACTIVATED)) {
367                    OpenBrowser.displayUrl(e.getDescription());
368                }
369            }
370        }
371    }