001/*
002 * $Id: JXLoginPane.java 4147 2012-02-01 17:13:24Z kschaefe $
003 *
004 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
005 * Santa Clara, California 95054, U.S.A. All rights reserved.
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015 * Lesser General Public License for more details.
016 *
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020 */
021package org.jdesktop.swingx;
022
023import java.awt.BorderLayout;
024import java.awt.CardLayout;
025import java.awt.Component;
026import java.awt.ComponentOrientation;
027import java.awt.Container;
028import java.awt.Cursor;
029import java.awt.Dialog;
030import java.awt.Dimension;
031import java.awt.EventQueue;
032import java.awt.FlowLayout;
033import java.awt.Font;
034import java.awt.Frame;
035import java.awt.GridBagConstraints;
036import java.awt.GridBagLayout;
037import java.awt.GridLayout;
038import java.awt.Image;
039import java.awt.Insets;
040import java.awt.LayoutManager;
041import java.awt.Window;
042import java.awt.event.ActionEvent;
043import java.awt.event.ActionListener;
044import java.awt.event.ItemEvent;
045import java.awt.event.ItemListener;
046import java.awt.event.KeyAdapter;
047import java.awt.event.KeyEvent;
048import java.awt.event.WindowAdapter;
049import java.beans.PropertyChangeEvent;
050import java.beans.PropertyChangeListener;
051import java.util.ArrayList;
052import java.util.Collections;
053import java.util.List;
054import java.util.Locale;
055import java.util.logging.Level;
056import java.util.logging.Logger;
057
058import javax.swing.AbstractListModel;
059import javax.swing.Action;
060import javax.swing.BorderFactory;
061import javax.swing.Box;
062import javax.swing.BoxLayout;
063import javax.swing.ComboBoxModel;
064import javax.swing.DefaultComboBoxModel;
065import javax.swing.JButton;
066import javax.swing.JCheckBox;
067import javax.swing.JComboBox;
068import javax.swing.JComponent;
069import javax.swing.JDialog;
070import javax.swing.JFrame;
071import javax.swing.JLabel;
072import javax.swing.JPanel;
073import javax.swing.JPasswordField;
074import javax.swing.JProgressBar;
075import javax.swing.JTextField;
076import javax.swing.KeyStroke;
077import javax.swing.SwingConstants;
078import javax.swing.SwingUtilities;
079import javax.swing.UIManager;
080import javax.swing.WindowConstants;
081import javax.swing.border.EmptyBorder;
082import javax.swing.plaf.basic.BasicHTML;
083import javax.swing.text.View;
084
085import org.jdesktop.beans.JavaBean;
086import org.jdesktop.swingx.action.AbstractActionExt;
087import org.jdesktop.swingx.auth.DefaultUserNameStore;
088import org.jdesktop.swingx.auth.LoginAdapter;
089import org.jdesktop.swingx.auth.LoginEvent;
090import org.jdesktop.swingx.auth.LoginListener;
091import org.jdesktop.swingx.auth.LoginService;
092import org.jdesktop.swingx.auth.PasswordStore;
093import org.jdesktop.swingx.auth.UserNameStore;
094import org.jdesktop.swingx.autocomplete.AutoCompleteDecorator;
095import org.jdesktop.swingx.painter.MattePainter;
096import org.jdesktop.swingx.plaf.LoginPaneAddon;
097import org.jdesktop.swingx.plaf.LoginPaneUI;
098import org.jdesktop.swingx.plaf.LookAndFeelAddons;
099import org.jdesktop.swingx.plaf.UIManagerExt;
100import org.jdesktop.swingx.plaf.basic.CapsLockSupport;
101import org.jdesktop.swingx.util.WindowUtils;
102
103/**
104 *  <p>JXLoginPane is a specialized JPanel that implements a Login dialog with
105 *  support for saving passwords supplied for future use in a secure
106 *  manner. <strong>LoginService</strong> is invoked to perform authentication
107 *  and optional <strong>PasswordStore</strong> can be provided to store the user
108 *  login information.</p>
109 *
110 *  <p> In order to perform the authentication, <strong>JXLoginPane</strong>
111 *  calls the <code>authenticate</code> method of the <strong>LoginService
112 *  </strong>. In order to perform the persistence of the password,
113 *  <strong>JXLoginPane</strong> calls the put method of the
114 *  <strong>PasswordStore</strong> object that is supplied. If
115 *  the <strong>PasswordStore</strong> is <code>null</code>, then the password
116 *  is not saved. Similarly, if a <strong>PasswordStore</strong> is
117 *  supplied and the password is null, then the <strong>PasswordStore</strong>
118 *  will be queried for the password using the <code>get</code> method.
119 *
120 *  Example:
121 *  <code><pre>
122 *         final JXLoginPane panel = new JXLoginPane(new LoginService() {
123 *                      public boolean authenticate(String name, char[] password,
124 *                                      String server) throws Exception {
125 *                              // perform authentication and return true on success.
126 *                              return false;
127 *                      }});
128 *      final JFrame frame = JXLoginPane.showLoginFrame(panel);
129 * </pre></code>
130 *
131 * @author Bino George
132 * @author Shai Almog
133 * @author rbair
134 * @author Karl Schaefer
135 * @author rah003
136 * @author Jonathan Giles
137 */
138@JavaBean
139public class JXLoginPane extends JXPanel {
140
141    /**
142     * The Logger
143     */
144    private static final Logger LOG = Logger.getLogger(JXLoginPane.class.getName());
145    /**
146     * Comment for <code>serialVersionUID</code>
147     */
148    private static final long serialVersionUID = 3544949969896288564L;
149    /**
150     * UI Class ID
151     */
152    public final static String uiClassID = "LoginPaneUI";
153    /**
154     * Action key for an Action in the ActionMap that initiates the Login
155     * procedure
156     */
157    public static final String LOGIN_ACTION_COMMAND = "login";
158    /**
159     * Action key for an Action in the ActionMap that cancels the Login
160     * procedure
161     */
162    public static final String CANCEL_LOGIN_ACTION_COMMAND = "cancel-login";
163    /**
164     * The JXLoginPane can attempt to save certain user information such as
165     * the username, password, or both to their respective stores.
166     * This type specifies what type of save should be performed.
167     */
168    public static enum SaveMode {NONE, USER_NAME, PASSWORD, BOTH}
169    /**
170     * Returns the status of the login process
171     */
172    public enum Status {NOT_STARTED, IN_PROGRESS, FAILED, CANCELLED, SUCCEEDED}
173    /**
174     * Used as a prefix when pulling data out of UIManager for i18n
175     */
176    private static String CLASS_NAME = JXLoginPane.class.getSimpleName();
177
178    /**
179     * The current login status for this panel
180     */
181    private Status status = Status.NOT_STARTED;
182    /**
183     * An optional banner at the top of the panel
184     */
185    private JXImagePanel banner;
186    /**
187     * Text that should appear on the banner
188     */
189    private String bannerText;
190    /**
191     * Custom label allowing the developer to display some message to the user
192     */
193    private JLabel messageLabel;
194    /**
195     * Shows an error message such as "user name or password incorrect" or
196     * "could not contact server" or something like that if something
197     * goes wrong
198     */
199    private JXLabel errorMessageLabel;
200    /**
201     * A Panel containing all of the input fields, check boxes, etc necessary
202     * for the user to do their job. The items on this panel change whenever
203     * the SaveMode changes, so this panel must be recreated at runtime if the
204     * SaveMode changes. Thus, I must maintain this reference so I can remove
205     * this panel from the content panel at runtime.
206     */
207    private JXPanel loginPanel;
208    /**
209     * The panel on which the input fields, messageLabel, and errorMessageLabel
210     * are placed. While the login thread is running, this panel is removed
211     * from the dialog and replaced by the progressPanel
212     */
213    private JXPanel contentPanel;
214    /**
215     * This is the area in which the name field is placed. That way it can toggle on the fly
216     * between text field and a combo box depending on the situation, and have a simple
217     * way to get the user name
218     */
219    private NameComponent namePanel;
220    /**
221     * The password field presented allowing the user to enter their password
222     */
223    private JPasswordField passwordField;
224    /**
225     * A combo box presenting the user with a list of servers to which they
226     * may log in. This is an optional feature, which is only enabled if
227     * the List of servers supplied to the JXLoginPane has a length greater
228     * than 1.
229     */
230    private JComboBox serverCombo;
231    /**
232     * Check box presented if a PasswordStore is used, allowing the user to decide whether to
233     * save their password
234     */
235    private JCheckBox saveCB;
236    
237    /**
238     * Label displayed whenever caps lock is on.
239     */
240    private JLabel capsOn;
241    /**
242     * A special panel that displays a progress bar and cancel button, and
243     * which notify the user of the login process, and allow them to cancel
244     * that process.
245     */
246    private JXPanel progressPanel;
247    /**
248     * A JLabel on the progressPanel that is used for informing the user
249     * of the status of the login procedure (logging in..., canceling login...)
250     */
251    private JLabel progressMessageLabel;
252    /**
253     * The LoginService to use. This must be specified for the login dialog to operate.
254     * If no LoginService is defined, a default login service is used that simply
255     * allows all users access. This is useful for demos or prototypes where a proper login
256     * server is not available.
257     */
258    private LoginService loginService;
259    /**
260     * Optional: a PasswordStore to use for storing and retrieving passwords for a specific
261     * user.
262     */
263    private PasswordStore passwordStore;
264    /**
265     * Optional: a UserNameStore to use for storing user names and retrieving them
266     */
267    private UserNameStore userNameStore;
268    /**
269     * A list of servers where each server is represented by a String. If the
270     * list of Servers is greater than 1, then a combo box will be presented to
271     * the user to choose from. If any servers are specified, the selected one
272     * (or the only one if servers.size() == 1) will be passed to the LoginService
273     */
274    private List<String> servers;
275    /**
276     *  Whether to save password or username or both.
277     */
278    private SaveMode saveMode;
279    /**
280     * Tracks the cursor at the time that authentication was started, and restores to that
281     * cursor after authentication ends, or is canceled;
282     */
283    private Cursor oldCursor;
284    
285    private boolean namePanelEnabled = true;
286
287    /**
288     * The default login listener used by this panel.
289     */
290    private LoginListener defaultLoginListener;
291
292    /**
293     * Login/cancel control pane;
294     */
295    private JXBtnPanel buttonPanel;
296    
297    /**
298     * Card pane holding user/pwd fields view and the progress view.
299     */
300    private JPanel contentCardPane;
301    private boolean isErrorMessageSet;
302
303    /**
304     * Creates a default JXLoginPane instance
305     */
306    static {
307        LookAndFeelAddons.contribute(new LoginPaneAddon());
308    }
309
310    /**
311     * Populates UIDefaults with the localizable Strings we will use
312     * in the Login panel.
313     */
314    private void reinitLocales(Locale l) {
315        // PENDING: JW - use the locale given as parameter
316        // as this probably (?) should be called before super.setLocale
317        setBannerText(UIManagerExt.getString(CLASS_NAME + ".bannerString", getLocale()));
318        banner.setImage(createLoginBanner());
319        if (!isErrorMessageSet) {
320            errorMessageLabel.setText(UIManager.getString(CLASS_NAME + ".errorMessage", getLocale()));
321        }
322        progressMessageLabel.setText(UIManagerExt.getString(CLASS_NAME + ".pleaseWait", getLocale()));
323        recreateLoginPanel();
324        Window w = SwingUtilities.getWindowAncestor(this);
325        if (w instanceof JXLoginFrame) {
326            JXLoginFrame f = (JXLoginFrame) w;
327            f.setTitle(UIManagerExt.getString(CLASS_NAME + ".titleString", getLocale()));
328            if (buttonPanel != null) {
329                buttonPanel.getOk().setText(UIManagerExt.getString(CLASS_NAME + ".loginString", getLocale()));
330                buttonPanel.getCancel().setText(UIManagerExt.getString(CLASS_NAME + ".cancelString", getLocale()));
331            }
332        }
333        JLabel lbl = (JLabel) passwordField.getClientProperty("labeledBy");
334        if (lbl != null) {
335            lbl.setText(UIManagerExt.getString(CLASS_NAME + ".passwordString", getLocale()));
336        }
337        lbl = (JLabel) namePanel.getComponent().getClientProperty("labeledBy");
338        if (lbl != null) {
339            lbl.setText(UIManagerExt.getString(CLASS_NAME + ".nameString", getLocale()));
340        }
341        if (serverCombo != null) {
342            lbl = (JLabel) serverCombo.getClientProperty("labeledBy");
343            if (lbl != null) {
344                lbl.setText(UIManagerExt.getString(CLASS_NAME + ".serverString", getLocale()));
345            }
346        }
347        saveCB.setText(UIManagerExt.getString(CLASS_NAME + ".rememberPasswordString", getLocale()));
348        // by default, caps is initialized in off state - i.e. without warning. Setting to
349        // whitespace preserves formatting of the panel.
350        capsOn.setText(isCapsLockOn() ? UIManagerExt.getString(CLASS_NAME + ".capsOnWarning", getLocale()) : " ");
351
352        getActionMap().get(LOGIN_ACTION_COMMAND).putValue(Action.NAME, UIManagerExt.getString(CLASS_NAME + ".loginString", getLocale()));
353        getActionMap().get(CANCEL_LOGIN_ACTION_COMMAND).putValue(Action.NAME, UIManagerExt.getString(CLASS_NAME + ".cancelString", getLocale()));
354
355    }
356
357    //--------------------------------------------------------- Constructors
358    /**
359     * Create a {@code JXLoginPane} that always accepts the user, never stores
360     * passwords or user ids, and has no target servers.
361     * <p>
362     * This constructor should <i>NOT</i> be used in a real application. It is
363     * provided for compliance to the bean specification and for use with visual
364     * editors.
365     */
366    public JXLoginPane() {
367        this(null);
368    }
369
370    /**
371     * Create a {@code JXLoginPane} with the specified {@code LoginService}
372     * that does not store user ids or passwords and has no target servers.
373     *
374     * @param service
375     *            the {@code LoginService} to use for logging in
376     */
377    public JXLoginPane(LoginService service) {
378        this(service, null, null);
379    }
380
381    /**
382     * Create a {@code JXLoginPane} with the specified {@code LoginService},
383     * {@code PasswordStore}, and {@code UserNameStore}, but without a server
384     * list.
385     * <p>
386     * If you do not want to store passwords or user ids, those parameters can
387     * be {@code null}. {@code SaveMode} is autoconfigured from passed in store
388     * parameters.
389     *
390     * @param service
391     *            the {@code LoginService} to use for logging in
392     * @param passwordStore
393     *            the {@code PasswordStore} to use for storing password
394     *            information
395     * @param userStore
396     *            the {@code UserNameStore} to use for storing user information
397     */
398    public JXLoginPane(LoginService service, PasswordStore passwordStore, UserNameStore userStore) {
399        this(service, passwordStore, userStore, null);
400    }
401
402    /**
403     * Create a {@code JXLoginPane} with the specified {@code LoginService},
404     * {@code PasswordStore}, {@code UserNameStore}, and server list.
405     * <p>
406     * If you do not want to store passwords or user ids, those parameters can
407     * be {@code null}. {@code SaveMode} is autoconfigured from passed in store
408     * parameters.
409     * <p>
410     * Setting the server list to {@code null} will unset all of the servers.
411     * The server list is guaranteed to be non-{@code null}.
412     *
413     * @param service
414     *            the {@code LoginService} to use for logging in
415     * @param passwordStore
416     *            the {@code PasswordStore} to use for storing password
417     *            information
418     * @param userStore
419     *            the {@code UserNameStore} to use for storing user information
420     * @param servers
421     *            a list of servers to authenticate against
422     */
423    public JXLoginPane(LoginService service, PasswordStore passwordStore, UserNameStore userStore, List<String> servers) {
424        setLoginService(service);
425        setPasswordStore(passwordStore);
426        setUserNameStore(userStore);
427        setServers(servers);
428
429
430        //create the login and cancel actions, and add them to the action map
431        getActionMap().put(LOGIN_ACTION_COMMAND, createLoginAction());
432        getActionMap().put(CANCEL_LOGIN_ACTION_COMMAND, createCancelAction());
433
434        //initialize the save mode
435        if (passwordStore != null && userStore != null) {
436            saveMode = SaveMode.BOTH;
437        } else if (passwordStore != null) {
438            saveMode = SaveMode.PASSWORD;
439        } else if (userStore != null) {
440            saveMode = SaveMode.USER_NAME;
441        } else {
442            saveMode = SaveMode.NONE;
443        }
444
445        // #732 set all internal components opacity to false in order to allow top level (frame's content pane) background painter to have any effect.
446        setOpaque(false);
447        CapsLockSupport.getInstance().addPropertyChangeListener("capsLockEnabled", new PropertyChangeListener() {
448            @Override
449            public void propertyChange(PropertyChangeEvent evt) {
450                if (capsOn != null) {
451                    if (Boolean.TRUE.equals(evt.getNewValue())) {
452                        capsOn.setText(UIManagerExt.getString(CLASS_NAME + ".capsOnWarning", getLocale()));
453                    } else {
454                        capsOn.setText(" ");
455                    }
456                }
457            }
458        });
459        initComponents();
460    }
461
462    /**
463     * Gets current state of the caps lock as seen by the login panel. The state seen by the login
464     * panel and therefore returned by this method can be delayed in comparison to the real caps
465     * lock state and displayed by the keyboard light. This is usually the case when component or
466     * its text fields are not focused.
467     *
468     * @return True when caps lock is on, false otherwise. Returns always false when
469     * <code>isCapsLockDetectionSupported()</code> returns false.
470     */
471    public boolean isCapsLockOn() {
472        return CapsLockSupport.getInstance().isCapsLockEnabled();
473    }
474
475    //------------------------------------------------------------- UI Logic
476
477    /**
478     * {@inheritDoc}
479     */
480    @Override
481    public LoginPaneUI getUI() {
482        return (LoginPaneUI) super.getUI();
483    }
484
485    /**
486     * Sets the look and feel (L&F) object that renders this component.
487     *
488     * @param ui the LoginPaneUI L&F object
489     * @see javax.swing.UIDefaults#getUI
490     */
491    public void setUI(LoginPaneUI ui) {
492        // initialized here due to implicit updateUI call from JPanel
493        if (banner == null) {
494            banner = new JXImagePanel();
495        }
496        if (errorMessageLabel == null) {
497            errorMessageLabel = new JXLabel(UIManagerExt.getString(CLASS_NAME + ".errorMessage", getLocale()));
498        }
499        super.setUI(ui);
500        banner.setImage(createLoginBanner());
501    }
502
503    /**
504     * Notification from the <code>UIManager</code> that the L&F has changed.
505     * Replaces the current UI object with the latest version from the
506     * <code>UIManager</code>.
507     *
508     * @see javax.swing.JComponent#updateUI
509     */
510    @Override
511    public void updateUI() {
512        setUI((LoginPaneUI) LookAndFeelAddons.getUI(this, LoginPaneUI.class));
513    }
514
515    /**
516     * Returns the name of the L&F class that renders this component.
517     *
518     * @return the string {@link #uiClassID}
519     * @see javax.swing.JComponent#getUIClassID
520     * @see javax.swing.UIDefaults#getUI
521     */
522    @Override
523    public String getUIClassID() {
524        return uiClassID;
525    }
526
527    /**
528     * Recreates the login panel, and replaces the current one with the new one
529     */
530    protected void recreateLoginPanel() {
531        JXPanel old = loginPanel;
532        loginPanel = createLoginPanel();
533        loginPanel.setBorder(BorderFactory.createEmptyBorder(0, 36, 7, 11));
534        contentPanel.remove(old);
535        contentPanel.add(loginPanel, 1);
536    }
537
538    /**
539     * Creates and returns a new LoginPanel, based on the SaveMode state of
540     * the login panel. Whenever the SaveMode changes, the panel is recreated.
541     * I do this rather than hiding/showing components, due to a cleaner
542     * implementation (no invisible components, components are not sharing
543     * locations in the LayoutManager, etc).
544     */
545    private JXPanel createLoginPanel() {
546        JXPanel loginPanel = new JXPanel();
547        
548        JPasswordField oldPwd = passwordField;
549        //create the password component
550        passwordField = new JPasswordField("", 15);
551        JLabel passwordLabel = new JLabel(UIManagerExt.getString(CLASS_NAME + ".passwordString", getLocale()));
552        passwordLabel.setLabelFor(passwordField);
553        if (oldPwd != null) {
554            passwordField.setText(new String(oldPwd.getPassword()));
555        }
556
557        NameComponent oldPanel = namePanel;
558        //create the NameComponent
559        if (saveMode == SaveMode.NONE) {
560            namePanel = new SimpleNamePanel();
561        } else {
562            namePanel = new ComboNamePanel();
563        }
564        if (oldPanel != null) {
565            // need to reset here otherwise value will get lost during LAF change as panel gets recreated.
566            namePanel.setUserName(oldPanel.getUserName());
567            namePanel.setEnabled(oldPanel.isEnabled());
568            namePanel.setEditable(oldPanel.isEditable());
569        } else {
570            namePanel.setEnabled(namePanelEnabled);
571            namePanel.setEditable(namePanelEnabled);
572        }
573        JLabel nameLabel = new JLabel(UIManagerExt.getString(CLASS_NAME + ".nameString", getLocale()));
574        nameLabel.setLabelFor(namePanel.getComponent());
575
576        //create the server combo box if necessary
577        JLabel serverLabel = new JLabel(UIManagerExt.getString(CLASS_NAME + ".serverString", getLocale()));
578        if (servers.size() > 1) {
579            serverCombo = new JComboBox(servers.toArray());
580            serverLabel.setLabelFor(serverCombo);
581        } else {
582            serverCombo = null;
583        }
584
585        //create the save check box. By default, it is not selected
586        saveCB = new JCheckBox(UIManagerExt.getString(CLASS_NAME + ".rememberPasswordString", getLocale()));
587        saveCB.setIconTextGap(10);
588        //TODO should get this from preferences!!! And, it should be based on the user
589        saveCB.setSelected(false); 
590        //determine whether to show/hide the save check box based on the SaveMode
591        saveCB.setVisible(saveMode == SaveMode.PASSWORD || saveMode == SaveMode.BOTH);
592        saveCB.setOpaque(false);
593
594        capsOn = new JLabel();
595        capsOn.setText(isCapsLockOn() ? UIManagerExt.getString(CLASS_NAME + ".capsOnWarning", getLocale()) : " ");
596
597        int lShift = 3;// lShift is used to align all other components with the checkbox
598        GridLayout grid = new GridLayout(2,1);
599        grid.setVgap(5);
600        JPanel fields = new JPanel(grid);
601        fields.setOpaque(false);
602        fields.add(namePanel.getComponent());
603        fields.add(passwordField);
604
605        loginPanel.setLayout(new GridBagLayout());
606        GridBagConstraints gridBagConstraints = new GridBagConstraints();
607        gridBagConstraints.gridx = 0;
608        gridBagConstraints.gridy = 0;
609        gridBagConstraints.anchor = GridBagConstraints.LINE_START;
610        gridBagConstraints.insets = new Insets(4, lShift, 5, 11);
611        loginPanel.add(nameLabel, gridBagConstraints);
612
613        gridBagConstraints = new GridBagConstraints();
614        gridBagConstraints.gridx = 1;
615        gridBagConstraints.gridy = 0;
616        gridBagConstraints.gridwidth = 1;
617        gridBagConstraints.gridheight = 2;
618        gridBagConstraints.anchor = GridBagConstraints.LINE_START;
619        gridBagConstraints.fill = GridBagConstraints.BOTH;
620        gridBagConstraints.weightx = 1.0;
621        gridBagConstraints.insets = new Insets(0, 0, 5, 0);
622        loginPanel.add(fields, gridBagConstraints);
623
624        gridBagConstraints = new GridBagConstraints();
625        gridBagConstraints.gridx = 0;
626        gridBagConstraints.gridy = 1;
627        gridBagConstraints.anchor = GridBagConstraints.LINE_START;
628        gridBagConstraints.insets = new Insets(5, lShift, 5, 11);
629        loginPanel.add(passwordLabel, gridBagConstraints);
630        
631        if (serverCombo != null) {
632            gridBagConstraints = new GridBagConstraints();
633            gridBagConstraints.gridx = 0;
634            gridBagConstraints.gridy = 2;
635            gridBagConstraints.anchor = GridBagConstraints.LINE_START;
636            gridBagConstraints.insets = new Insets(0, lShift, 5, 11);
637            loginPanel.add(serverLabel, gridBagConstraints);
638
639            gridBagConstraints = new GridBagConstraints();
640            gridBagConstraints.gridx = 1;
641            gridBagConstraints.gridy = 2;
642            gridBagConstraints.gridwidth = 1;
643            gridBagConstraints.anchor = GridBagConstraints.LINE_START;
644            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;
645            gridBagConstraints.weightx = 1.0;
646            gridBagConstraints.insets = new Insets(0, 0, 5, 0);
647            loginPanel.add(serverCombo, gridBagConstraints);
648            
649            gridBagConstraints = new GridBagConstraints();
650            gridBagConstraints.gridx = 0;
651            gridBagConstraints.gridy = 3;
652            gridBagConstraints.gridwidth = 2;
653            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;
654            gridBagConstraints.anchor = GridBagConstraints.LINE_START;
655            gridBagConstraints.weightx = 1.0;
656            gridBagConstraints.insets = new Insets(0, 0, 4, 0);
657            loginPanel.add(saveCB, gridBagConstraints);
658
659            gridBagConstraints = new GridBagConstraints();
660            gridBagConstraints.gridx = 0;
661            gridBagConstraints.gridy = 4;
662            gridBagConstraints.gridwidth = 2;
663            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;
664            gridBagConstraints.anchor = GridBagConstraints.LINE_START;
665            gridBagConstraints.weightx = 1.0;
666            gridBagConstraints.insets = new Insets(0, lShift, 0, 11);
667            loginPanel.add(capsOn, gridBagConstraints);
668        } else {
669            gridBagConstraints = new GridBagConstraints();
670            gridBagConstraints.gridx = 0;
671            gridBagConstraints.gridy = 2;
672            gridBagConstraints.gridwidth = 2;
673            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;
674            gridBagConstraints.anchor = GridBagConstraints.LINE_START;
675            gridBagConstraints.weightx = 1.0;
676            gridBagConstraints.insets = new Insets(0, 0, 4, 0);
677            loginPanel.add(saveCB, gridBagConstraints);
678
679            gridBagConstraints = new GridBagConstraints();
680            gridBagConstraints.gridx = 0;
681            gridBagConstraints.gridy = 3;
682            gridBagConstraints.gridwidth = 2;
683            gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;
684            gridBagConstraints.anchor = GridBagConstraints.LINE_START;
685            gridBagConstraints.weightx = 1.0;
686            gridBagConstraints.insets = new Insets(0, lShift, 0, 11);
687            loginPanel.add(capsOn, gridBagConstraints);
688        }
689        loginPanel.setOpaque(false);
690        return loginPanel;
691    }
692
693    /**
694     * This method adds functionality to support bidi languages within this
695     * component
696     */
697    @Override
698    public void setComponentOrientation(ComponentOrientation orient) {
699        // this if is used to avoid needless creations of the image
700        if(orient != super.getComponentOrientation()) {
701            super.setComponentOrientation(orient);
702            banner.setImage(createLoginBanner());
703            progressPanel.applyComponentOrientation(orient);
704        }
705    }
706
707    /**
708     * Create all of the UI components for the login panel
709     */
710    private void initComponents() {
711        //create the default banner
712        banner.setImage(createLoginBanner());
713
714        //create the default label
715        messageLabel = new JLabel(" ");
716        messageLabel.setOpaque(false);
717        messageLabel.setFont(messageLabel.getFont().deriveFont(Font.BOLD));
718
719        //create the main components
720        loginPanel = createLoginPanel();
721
722        //create the message and hyperlink and hide them
723        errorMessageLabel.setIcon(UIManager.getIcon(CLASS_NAME + ".errorIcon", getLocale()));
724        errorMessageLabel.setVerticalTextPosition(SwingConstants.TOP);
725        errorMessageLabel.setLineWrap(true);
726        errorMessageLabel.setPaintBorderInsets(false);
727        errorMessageLabel.setBackgroundPainter(new MattePainter(UIManager.getColor(CLASS_NAME + ".errorBackground", getLocale()), true));
728        errorMessageLabel.setMaxLineSpan(320);
729        errorMessageLabel.setVisible(false);
730
731        //aggregate the optional message label, content, and error label into
732        //the contentPanel
733        contentPanel = new JXPanel(new LoginPaneLayout());
734        contentPanel.setOpaque(false);
735        messageLabel.setBorder(BorderFactory.createEmptyBorder(12, 12, 7, 11));
736        contentPanel.add(messageLabel);
737        loginPanel.setBorder(BorderFactory.createEmptyBorder(0, 36, 7, 11));
738        contentPanel.add(loginPanel);
739        errorMessageLabel.setBorder(UIManager.getBorder(CLASS_NAME + ".errorBorder", getLocale()));
740        contentPanel.add(errorMessageLabel);
741
742        //create the progress panel
743        progressPanel = new JXPanel(new GridBagLayout());
744        progressPanel.setOpaque(false);
745        progressMessageLabel = new JLabel(UIManagerExt.getString(CLASS_NAME + ".pleaseWait", getLocale()));
746        progressMessageLabel.setFont(UIManager.getFont(CLASS_NAME +".pleaseWaitFont", getLocale()));
747        JProgressBar pb = new JProgressBar();
748        pb.setIndeterminate(true);
749        JButton cancelButton = new JButton(getActionMap().get(CANCEL_LOGIN_ACTION_COMMAND));
750        progressPanel.add(progressMessageLabel, new GridBagConstraints(0, 0, 2, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(12, 12, 11, 11), 0, 0));
751        progressPanel.add(pb, new GridBagConstraints(0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(0, 24, 11, 7), 0, 0));
752        progressPanel.add(cancelButton, new GridBagConstraints(1, 1, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 11, 11), 0, 0));
753
754        //layout the panel
755        setLayout(new BorderLayout());
756        add(banner, BorderLayout.NORTH);
757        contentCardPane = new JPanel(new CardLayout());
758        contentCardPane.setOpaque(false);
759        contentCardPane.add(contentPanel, "0");
760        contentCardPane.add(progressPanel, "1");
761        add(contentCardPane, BorderLayout.CENTER);
762
763    }
764
765    private final class LoginPaneLayout extends VerticalLayout implements LayoutManager {
766        @Override
767        public Dimension preferredLayoutSize(Container parent) {
768            Insets insets = parent.getInsets();
769            Dimension pref = new Dimension(0, 0);
770            int gap = getGap();
771            for (int i = 0, c = parent.getComponentCount(); i < c; i++) {
772              Component m = parent.getComponent(i);
773              if (m.isVisible()) {
774                Dimension componentPreferredSize = m.getPreferredSize();
775                // swingx-917 - don't let jlabel to force width due to long text
776                if (m instanceof JLabel) {
777                    View view = (View) ((JLabel)m).getClientProperty(BasicHTML.propertyKey);
778                    if (view != null) {
779                        view.setSize(pref.width, m.getHeight());
780                        // get fresh preferred size since we have forced new size on label
781                        componentPreferredSize = m.getPreferredSize();
782                    }
783                } else {
784                    pref.width = Math.max(pref.width, componentPreferredSize.width);
785                }
786                pref.height += componentPreferredSize.height + gap;
787              }
788            }
789
790            pref.width += insets.left + insets.right;
791            pref.height += insets.top + insets.bottom;
792
793            return pref;
794          }
795    }
796
797    /**
798     * Create and return an image to use for the Banner. This may be overridden
799     * to return any image you like
800     */
801    protected Image createLoginBanner() {
802        return getUI() == null ? null : getUI().getBanner();
803    }
804
805    /**
806     * Create and return an Action for logging in
807     */
808    protected Action createLoginAction() {
809        return new LoginAction(this);
810    }
811
812    /**
813     * Create and return an Action for canceling login
814     */
815    protected Action createCancelAction() {
816        return new CancelAction(this);
817    }
818
819    //------------------------------------------------------ Bean Properties
820    //REMEMBER: when adding new methods, they need to fire property change events!!!
821    /**
822     * @return Returns the saveMode.
823     */
824    public SaveMode getSaveMode() {
825        return saveMode;
826    }
827
828    /**
829     * The save mode indicates whether the "save" password is checked by default. This method
830     * makes no difference if the passwordStore is null.
831     *
832     * @param saveMode The saveMode to set either SAVE_NONE, SAVE_PASSWORD or SAVE_USERNAME
833     */
834    public void setSaveMode(SaveMode saveMode) {
835        if (this.saveMode != saveMode) {
836            SaveMode oldMode = getSaveMode();
837            this.saveMode = saveMode;
838            recreateLoginPanel();
839            firePropertyChange("saveMode", oldMode, getSaveMode());
840        }
841    }
842    
843    public boolean isRememberPassword() {
844        return saveCB.isVisible() && saveCB.isSelected();
845    }
846
847    /**
848     * @return the List of servers
849     */
850    public List<String> getServers() {
851        return Collections.unmodifiableList(servers);
852    }
853
854    /**
855     * Sets the list of servers. See the servers field javadoc for more info.
856     */
857    public void setServers(List<String> servers) {
858        //only at startup
859        if (this.servers == null) {
860            this.servers = servers == null ? new ArrayList<String>() : servers;
861        } else if (this.servers != servers) {
862            List<String> old = getServers();
863            this.servers = servers == null ? new ArrayList<String>() : servers;
864            recreateLoginPanel();
865            firePropertyChange("servers", old, getServers());
866        }
867    }
868
869    private LoginListener getDefaultLoginListener() {
870        if (defaultLoginListener == null) {
871            defaultLoginListener = new LoginListenerImpl();
872        }
873
874        return defaultLoginListener;
875    }
876
877    /**
878     * Sets the {@code LoginService} for this panel. Setting the login service
879     * to {@code null} will actually set the service to use
880     * {@code NullLoginService}.
881     *
882     * @param service
883     *            the service to set. If {@code service == null}, then a
884     *            {@code NullLoginService} is used.
885     */
886    public void setLoginService(LoginService service) {
887        LoginService oldService = getLoginService();
888        LoginService newService = service == null ? new NullLoginService() : service;
889
890        //newService is guaranteed to be nonnull
891        if (!newService.equals(oldService)) {
892            if (oldService != null) {
893                oldService.removeLoginListener(getDefaultLoginListener());
894            }
895
896            loginService = newService;
897            this.loginService.addLoginListener(getDefaultLoginListener());
898
899            firePropertyChange("loginService", oldService, getLoginService());
900        }
901    }
902
903    /**
904     * Gets the <strong>LoginService</strong> for this panel.
905     *
906     * @return service service
907     */
908    public LoginService getLoginService() {
909        return loginService;
910    }
911
912    /**
913     * Sets the <strong>PasswordStore</strong> for this panel.
914     *
915     * @param store PasswordStore
916     */
917    public void setPasswordStore(PasswordStore store) {
918        PasswordStore oldStore = getPasswordStore();
919        PasswordStore newStore = store == null ? new NullPasswordStore() : store;
920
921        //newStore is guaranteed to be nonnull
922        if (!newStore.equals(oldStore)) {
923            passwordStore = newStore;
924
925            firePropertyChange("passwordStore", oldStore, getPasswordStore());
926        }
927    }
928
929    /**
930     * Gets the {@code UserNameStore} for this panel.
931     *
932     * @return the {@code UserNameStore}
933     */
934    public UserNameStore getUserNameStore() {
935        return userNameStore;
936    }
937
938    /**
939     * Sets the user name store for this panel.
940     * @param store
941     */
942    public void setUserNameStore(UserNameStore store) {
943        UserNameStore oldStore = getUserNameStore();
944        UserNameStore newStore = store == null ? new DefaultUserNameStore() : store;
945
946        //newStore is guaranteed to be nonnull
947        if (!newStore.equals(oldStore)) {
948            userNameStore = newStore;
949
950            firePropertyChange("userNameStore", oldStore, getUserNameStore());
951        }
952    }
953
954    /**
955     * Gets the <strong>PasswordStore</strong> for this panel.
956     *
957     * @return store PasswordStore
958     */
959    public PasswordStore getPasswordStore() {
960        return passwordStore;
961    }
962
963    /**
964     * Sets the <strong>User name</strong> for this panel.
965     *
966     * @param username User name
967     */
968    public void setUserName(String username) {
969        if (namePanel != null) {
970            String old = getUserName();
971            namePanel.setUserName(username);
972            firePropertyChange("userName", old, getUserName());
973        }
974    }
975
976    /**
977     * Enables or disables <strong>User name</strong> for this panel.
978     *
979     * @param enabled 
980     */
981    public void setUserNameEnabled(boolean enabled) {
982        boolean old = isUserNameEnabled();
983        this.namePanelEnabled = enabled;
984        if (namePanel != null) {
985            namePanel.setEnabled(enabled);
986            namePanel.setEditable(enabled);
987        }
988        firePropertyChange("userNameEnabled", old, isUserNameEnabled());
989    }
990    
991    /**
992     * Gets current state of the user name field. Field can be either disabled (false) for editing or enabled (true).
993     * @return True when user name field is enabled and editable, false otherwise.
994     */
995    public boolean isUserNameEnabled() {
996        return this.namePanelEnabled;
997    }
998
999    /**
1000     * Gets the <strong>User name</strong> for this panel.
1001     * @return the user name
1002     */
1003    public String getUserName() {
1004        return namePanel == null ? null : namePanel.getUserName();
1005    }
1006
1007    /**
1008     * Sets the <strong>Password</strong> for this panel.
1009     *
1010     * @param password Password
1011     */
1012    public void setPassword(char[] password) {
1013        passwordField.setText(new String(password));
1014    }
1015
1016    /**
1017     * Gets the <strong>Password</strong> for this panel.
1018     *
1019     * @return password Password
1020     */
1021    public char[] getPassword() {
1022        return passwordField.getPassword();
1023    }
1024
1025    /**
1026     * Return the image used as the banner
1027     */
1028    public Image getBanner() {
1029        return banner.getImage();
1030    }
1031
1032    /**
1033     * Set the image to use for the banner. If the {@code img} is {@code null},
1034     * then no image will be displayed.
1035     *
1036     * @param img
1037     *            the image to display
1038     */
1039    public void setBanner(Image img) {
1040        // we do not expose the ImagePanel, so we will produce property change
1041        // events here
1042        Image oldImage = getBanner();
1043
1044        if (oldImage != img) {
1045            banner.setImage(img);
1046            firePropertyChange("banner", oldImage, getBanner());
1047        }
1048    }
1049
1050    /**
1051     * Set the text to use when creating the banner. If a custom banner image is
1052     * specified, then this is ignored. If {@code text} is {@code null}, then
1053     * no text is displayed.
1054     *
1055     * @param text
1056     *            the text to display
1057     */
1058    public void setBannerText(String text) {
1059        if (text == null) {
1060            text = "";
1061        }
1062
1063        if (!text.equals(this.bannerText)) {
1064            String oldText = this.bannerText;
1065            this.bannerText = text;
1066            //fix the login banner
1067            this.banner.setImage(createLoginBanner());
1068            firePropertyChange("bannerText", oldText, text);
1069        }
1070    }
1071
1072    /**
1073     * Returns text used when creating the banner
1074     */
1075    public String getBannerText() {
1076        return bannerText;
1077    }
1078
1079    /**
1080     * Returns the custom message for this login panel
1081     */
1082    public String getMessage() {
1083        return messageLabel.getText();
1084    }
1085
1086    /**
1087     * Sets a custom message for this login panel
1088     */
1089    public void setMessage(String message) {
1090        String old = messageLabel.getText();
1091        messageLabel.setText(message);
1092        firePropertyChange("message", old, messageLabel.getText());
1093    }
1094
1095    /**
1096     * Returns the error message for this login panel
1097     */
1098    public String getErrorMessage() {
1099        return errorMessageLabel.getText();
1100    }
1101
1102    /**
1103     * Sets the error message for this login panel
1104     */
1105    public void setErrorMessage(String errorMessage) {
1106        isErrorMessageSet = true;
1107        String old = errorMessageLabel.getText();
1108        errorMessageLabel.setText(errorMessage);
1109        firePropertyChange("errorMessage", old, errorMessageLabel.getText());
1110    }
1111
1112    /**
1113     * Returns the panel's status
1114     */
1115    public Status getStatus() {
1116        return status;
1117    }
1118
1119    /**
1120     * Change the status
1121     */
1122    protected void setStatus(Status newStatus) {
1123        if (status != newStatus) {
1124            Status oldStatus = status;
1125            status = newStatus;
1126            firePropertyChange("status", oldStatus, newStatus);
1127        }
1128    }
1129
1130    @Override
1131    public void setLocale(Locale l) {
1132        super.setLocale(l);
1133        reinitLocales(l);
1134    }
1135    //-------------------------------------------------------------- Methods
1136
1137    /**
1138     * Initiates the login procedure. This method is called internally by
1139     * the LoginAction. This method handles cursor management, and actually
1140     * calling the LoginService's startAuthentication method. Method will return
1141     * immediately if asynchronous login is enabled or will block until
1142     * authentication finishes if <code>getSynchronous()</code> returns true.
1143     */
1144    protected void startLogin() {
1145        oldCursor = getCursor();
1146        try {
1147            setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
1148            progressMessageLabel.setText(UIManagerExt.getString(CLASS_NAME + ".pleaseWait", getLocale()));
1149            String name = getUserName();
1150            char[] password = getPassword();
1151            String server = servers.size() == 1 ? servers.get(0) : serverCombo == null ? null : (String)serverCombo.getSelectedItem();
1152            
1153            loginService.startAuthentication(name, password, server);
1154        } catch(Exception ex) {
1155        //The status is set via the loginService listener, so no need to set
1156        //the status here. Just log the error.
1157        LOG.log(Level.WARNING, "Authentication exception while logging in", ex);
1158        } finally {
1159            setCursor(oldCursor);
1160        }
1161    }
1162
1163    /**
1164     * Cancels the login procedure. Handles cursor management and interfacing
1165     * with the LoginService's cancelAuthentication method. Calling this method
1166     * has an effect only when authentication is still in progress (i.e. after
1167     * previous call to <code>startAuthentications()</code> and only when
1168     * authentication is performed asynchronously (<code>getSynchronous()</code>
1169     * returns false).
1170     */
1171    protected void cancelLogin() {
1172        progressMessageLabel.setText(UIManagerExt.getString(CLASS_NAME + ".cancelWait", getLocale()));
1173        getActionMap().get(CANCEL_LOGIN_ACTION_COMMAND).setEnabled(false);
1174        loginService.cancelAuthentication();
1175        setCursor(oldCursor);
1176    }
1177
1178    /**
1179     * Puts the password into the password store. If password store is not set, method will do
1180     * nothing.
1181     */
1182    protected void savePassword() {
1183        if (saveCB.isSelected()
1184            && (saveMode == SaveMode.BOTH || saveMode == SaveMode.PASSWORD)
1185            && passwordStore != null) {
1186            passwordStore.set(getUserName(),getLoginService().getServer(),getPassword());
1187        }
1188    }
1189
1190    //--------------------------------------------- Listener Implementations
1191    /*
1192
1193     For Login (initiated in LoginAction):
1194        0) set the status
1195        1) Immediately disable the login action
1196        2) Immediately disable the close action (part of enclosing window)
1197        3) initialize the progress pane
1198          a) enable the cancel login action
1199          b) set the message text
1200        4) hide the content pane, show the progress pane
1201
1202     When cancelling (initiated in CancelAction):
1203         0) set the status
1204         1) Disable the cancel login action
1205         2) Change the message text on the progress pane
1206
1207     When cancel finishes (handled in LoginListener):
1208         0) set the status
1209         1) hide the progress pane, show the content pane
1210         2) enable the close action (part of enclosing window)
1211         3) enable the login action
1212
1213     When login fails (handled in LoginListener):
1214         0) set the status
1215         1) hide the progress pane, show the content pane
1216         2) enable the close action (part of enclosing window)
1217         3) enable the login action
1218         4) Show the error message
1219         5) resize the window (part of enclosing window)
1220
1221     When login succeeds (handled in LoginListener):
1222         0) set the status
1223         1) close the dialog/frame (part of enclosing window)
1224     */
1225    /**
1226     * Listener class to track state in the LoginService
1227     */
1228    protected class LoginListenerImpl extends LoginAdapter {
1229        @Override
1230        public void loginSucceeded(LoginEvent source) {
1231            //save the user names and passwords
1232            String userName = namePanel.getUserName();
1233            if ((getSaveMode() == SaveMode.USER_NAME || getSaveMode() == SaveMode.BOTH)
1234                    && userName != null && !userName.trim().equals("")) {
1235                userNameStore.addUserName(userName);
1236                userNameStore.saveUserNames();
1237            }
1238            
1239            // if the user and/or password store knows of this user, 
1240            // and the checkbox is unchecked, we remove them, otherwise
1241            // we save the password
1242            if (saveCB.isSelected()) {
1243                savePassword();
1244            } else {
1245                // remove the password from the password store
1246                if (passwordStore != null) {
1247                    passwordStore.removeUserPassword(userName);
1248                }
1249            }
1250            
1251            setStatus(Status.SUCCEEDED);
1252        }
1253
1254        @Override
1255        public void loginStarted(LoginEvent source) {
1256            assert EventQueue.isDispatchThread();
1257            getActionMap().get(LOGIN_ACTION_COMMAND).setEnabled(false);
1258            getActionMap().get(CANCEL_LOGIN_ACTION_COMMAND).setEnabled(true);
1259//            remove(contentPanel);
1260//            add(progressPanel, BorderLayout.CENTER);
1261            ((CardLayout) contentCardPane.getLayout()).last(contentCardPane);
1262            revalidate();
1263            repaint();
1264            setStatus(Status.IN_PROGRESS);
1265        }
1266
1267        @Override
1268        public void loginFailed(LoginEvent source) {
1269            assert EventQueue.isDispatchThread();
1270//            remove(progressPanel);
1271//            add(contentPanel, BorderLayout.CENTER);
1272            ((CardLayout) contentCardPane.getLayout()).first(contentCardPane);
1273            getActionMap().get(LOGIN_ACTION_COMMAND).setEnabled(true);
1274            errorMessageLabel.setVisible(true);
1275            revalidate();
1276            repaint();
1277            setStatus(Status.FAILED);
1278        }
1279
1280        @Override
1281        public void loginCanceled(LoginEvent source) {
1282            assert EventQueue.isDispatchThread();
1283//            remove(progressPanel);
1284//            add(contentPanel, BorderLayout.CENTER);
1285            ((CardLayout) contentCardPane.getLayout()).first(contentCardPane);
1286            getActionMap().get(LOGIN_ACTION_COMMAND).setEnabled(true);
1287            errorMessageLabel.setVisible(false);
1288            revalidate();
1289            repaint();
1290            setStatus(Status.CANCELLED);
1291        }
1292    }
1293
1294    //---------------------------------------------- Default Implementations
1295    /**
1296     * Action that initiates a login procedure. Delegates to JXLoginPane.startLogin
1297     */
1298    private static final class LoginAction extends AbstractActionExt {
1299        private static final long serialVersionUID = 7256761187925982485L;
1300        private JXLoginPane panel;
1301        public LoginAction(JXLoginPane p) {
1302            super(UIManagerExt.getString(CLASS_NAME + ".loginString", p.getLocale()), LOGIN_ACTION_COMMAND);
1303            this.panel = p;
1304        }
1305        @Override
1306        public void actionPerformed(ActionEvent e) {
1307            panel.startLogin();
1308        }
1309        @Override
1310        public void itemStateChanged(ItemEvent e) {}
1311    }
1312
1313    /**
1314     * Action that cancels the login procedure.
1315     */
1316    private static final class CancelAction extends AbstractActionExt {
1317        private static final long serialVersionUID = 4040029973355439229L;
1318        private JXLoginPane panel;
1319        public CancelAction(JXLoginPane p) {
1320            super(UIManagerExt.getString(CLASS_NAME + ".cancelLogin", p.getLocale()), CANCEL_LOGIN_ACTION_COMMAND);
1321            this.panel = p;
1322            this.setEnabled(false);
1323        }
1324        @Override
1325        public void actionPerformed(ActionEvent e) {
1326            panel.cancelLogin();
1327        }
1328        @Override
1329        public void itemStateChanged(ItemEvent e) {}
1330    }
1331
1332    /**
1333     * Simple login service that allows everybody to login. This is useful in demos and allows
1334     * us to avoid having to check for LoginService being null
1335     */
1336    private static final class NullLoginService extends LoginService {
1337        @Override
1338        public boolean authenticate(String name, char[] password, String server) throws Exception {
1339            return true;
1340        }
1341
1342        @Override
1343        public boolean equals(Object obj) {
1344            return obj instanceof NullLoginService;
1345        }
1346
1347        @Override
1348        public int hashCode() {
1349            return 7;
1350        }
1351    }
1352
1353    /**
1354     * Simple PasswordStore that does not remember passwords
1355     */
1356    private static final class NullPasswordStore extends PasswordStore {
1357        @Override
1358        public boolean set(String username, String server, char[] password) {
1359            //null op
1360            return false;
1361        }
1362        
1363        @Override
1364        public char[] get(String username, String server) {
1365            return new char[0];
1366        }
1367        
1368        @Override
1369        public void removeUserPassword(String username) {
1370            return;
1371        }
1372
1373        @Override
1374        public boolean equals(Object obj) {
1375            return obj instanceof NullPasswordStore;
1376        }
1377
1378        @Override
1379        public int hashCode() {
1380            return 7;
1381        }
1382    }
1383
1384    //--------------------------------- Default NamePanel Implementations
1385    private static interface NameComponent {
1386        public String getUserName();
1387        public boolean isEnabled();
1388        public boolean isEditable();
1389        public void setEditable(boolean enabled);
1390        public void setEnabled(boolean enabled);
1391        public void setUserName(String userName);
1392        public JComponent getComponent();
1393    }
1394    
1395    private void updatePassword(final String username) {
1396        String password = "";
1397        if (username != null) {
1398                char[] pw = passwordStore.get(username, null);
1399                password = pw == null ? "" : new String(pw);
1400                
1401                // if the userstore has this username, we should change the 
1402                // 'remember me' checkbox to be selected. Unselecting this will
1403                // result in the user being 'forgotten'.
1404                saveCB.setSelected(userNameStore.containsUserName(username));
1405        }
1406        
1407        passwordField.setText(password);
1408    }
1409
1410    /**
1411     * If a UserNameStore is not used, then this text field is presented allowing the user
1412     * to simply enter their user name
1413     */
1414    private final class SimpleNamePanel extends JTextField implements NameComponent {
1415        private static final long serialVersionUID = 6513437813612641002L;
1416
1417        public SimpleNamePanel() {
1418            super("", 15);
1419            
1420            // auto-complete based on the users input
1421            // AutoCompleteDecorator.decorate(this, Arrays.asList(userNameStore.getUserNames()), false);
1422
1423            // listen to text input, and offer password suggestion based on current
1424            // text
1425            if (passwordStore != null && passwordField!=null) {
1426                addKeyListener(new KeyAdapter() {
1427                    @Override
1428                    public void keyReleased(KeyEvent e) {
1429                        updatePassword(getText());
1430                    }
1431                });
1432            }
1433        }
1434        
1435        @Override
1436        public String getUserName() {
1437            return getText();
1438        }
1439        @Override
1440        public void setUserName(String userName) {
1441            setText(userName);
1442        }
1443        @Override
1444        public JComponent getComponent() {
1445            return this;
1446        }
1447    }
1448    
1449    /**
1450     * If a UserNameStore is used, then this combo box is presented allowing the user
1451     * to select a previous login name, or type in a new login name
1452     */
1453    private final class ComboNamePanel extends JComboBox implements NameComponent {
1454        private static final long serialVersionUID = 2511649075486103959L;
1455
1456        public ComboNamePanel() {
1457            super();
1458            setModel(new NameComboBoxModel());
1459            setEditable(true);
1460            
1461            // auto-complete based on the users input
1462            AutoCompleteDecorator.decorate(this);
1463
1464            // listen to selection or text input, and offer password suggestion based on current
1465            // text
1466            if (passwordStore != null && passwordField!=null) {
1467                final JTextField textfield = (JTextField) getEditor().getEditorComponent();
1468                textfield.addKeyListener(new KeyAdapter() {
1469                    @Override
1470                    public void keyReleased(KeyEvent e) {
1471                        updatePassword(textfield.getText());
1472                    }
1473                });
1474                
1475                super.addItemListener(new ItemListener() {
1476                    @Override
1477                public void itemStateChanged(ItemEvent e) {
1478                        updatePassword((String)getSelectedItem());
1479                    }
1480                });
1481            }
1482        }
1483        
1484        @Override
1485        public String getUserName() {
1486            Object item = getModel().getSelectedItem();
1487            return item == null ? null : item.toString();
1488        }
1489        @Override
1490        public void setUserName(String userName) {
1491            getModel().setSelectedItem(userName);
1492        }
1493        public void setUserNames(String[] names) {
1494            setModel(new DefaultComboBoxModel(names));
1495        }
1496        @Override
1497        public JComponent getComponent() {
1498            return this;
1499        }
1500        
1501        private final class NameComboBoxModel extends AbstractListModel implements ComboBoxModel {
1502            private static final long serialVersionUID = 7097674687536018633L;
1503            private Object selectedItem;
1504            @Override
1505            public void setSelectedItem(Object anItem) {
1506                selectedItem = anItem;
1507                fireContentsChanged(this, -1, -1);
1508            }
1509            @Override
1510            public Object getSelectedItem() {
1511                return selectedItem;
1512            }
1513            @Override
1514            public Object getElementAt(int index) {
1515                if (index == -1) {
1516                    return null;
1517                }
1518                
1519                return userNameStore.getUserNames()[index];
1520            }
1521            @Override
1522            public int getSize() {
1523                return userNameStore.getUserNames().length;
1524            }
1525        }
1526    }
1527
1528    //------------------------------------------ Static Construction Methods
1529    /**
1530     * Shows a login dialog. This method blocks.
1531     * @return The status of the login operation
1532     */
1533    public static Status showLoginDialog(Component parent, LoginService svc) {
1534        return showLoginDialog(parent, svc, null, null);
1535    }
1536
1537    /**
1538     * Shows a login dialog. This method blocks.
1539     * @return The status of the login operation
1540     */
1541    public static Status showLoginDialog(Component parent, LoginService svc, PasswordStore ps, UserNameStore us) {
1542        return showLoginDialog(parent, svc, ps, us, null);
1543    }
1544
1545    /**
1546     * Shows a login dialog. This method blocks.
1547     * @return The status of the login operation
1548     */
1549    public static Status showLoginDialog(Component parent, LoginService svc, PasswordStore ps, UserNameStore us, List<String> servers) {
1550        JXLoginPane panel = new JXLoginPane(svc, ps, us, servers);
1551        return showLoginDialog(parent, panel);
1552    }
1553
1554    /**
1555     * Shows a login dialog. This method blocks.
1556     * @return The status of the login operation
1557     */
1558    public static Status showLoginDialog(Component parent, JXLoginPane panel) {
1559        Window w = WindowUtils.findWindow(parent);
1560        JXLoginDialog dlg =  null;
1561        if (w == null) {
1562            dlg = new JXLoginDialog((Frame)null, panel);
1563        } else if (w instanceof Dialog) {
1564            dlg = new JXLoginDialog((Dialog)w, panel);
1565        } else if (w instanceof Frame) {
1566            dlg = new JXLoginDialog((Frame)w, panel);
1567        } else {
1568            throw new AssertionError("Shouldn't be able to happen");
1569        }
1570        dlg.setVisible(true);
1571        return dlg.getStatus();
1572    }
1573
1574    /**
1575     * Shows a login frame. A JFrame is not modal, and thus does not block
1576     */
1577    public static JXLoginFrame showLoginFrame(LoginService svc) {
1578        return showLoginFrame(svc, null, null);
1579    }
1580
1581    /**
1582     */
1583    public static JXLoginFrame showLoginFrame(LoginService svc, PasswordStore ps, UserNameStore us) {
1584        return showLoginFrame(svc, ps, us, null);
1585    }
1586
1587    /**
1588     */
1589    public static JXLoginFrame showLoginFrame(LoginService svc, PasswordStore ps, UserNameStore us, List<String> servers) {
1590        JXLoginPane panel = new JXLoginPane(svc, ps, us, servers);
1591        return showLoginFrame(panel);
1592    }
1593
1594    /**
1595     */
1596    public static JXLoginFrame showLoginFrame(JXLoginPane panel) {
1597        return new JXLoginFrame(panel);
1598    }
1599
1600    public static final class JXLoginDialog extends JDialog {
1601        private static final long serialVersionUID = -3185639594267828103L;
1602        private JXLoginPane panel;
1603
1604        public JXLoginDialog(Frame parent, JXLoginPane p) {
1605            super(parent, true);
1606            init(p);
1607        }
1608
1609        public JXLoginDialog(Dialog parent, JXLoginPane p) {
1610            super(parent, true);
1611            init(p);
1612        }
1613
1614        protected void init(JXLoginPane p) {
1615            setTitle(UIManagerExt.getString(CLASS_NAME + ".titleString", getLocale()));
1616            this.panel = p;
1617            initWindow(this, panel);
1618        }
1619    
1620        public JXLoginPane.Status getStatus() {
1621            return panel.getStatus();
1622        }
1623    }
1624
1625    public static final class JXLoginFrame extends JXFrame {
1626        private static final long serialVersionUID = -9016407314342050807L;
1627        private JXLoginPane panel;
1628
1629        public JXLoginFrame(JXLoginPane p) {
1630            super(UIManagerExt.getString(CLASS_NAME + ".titleString", p.getLocale()));
1631            JXPanel cp = new JXPanel();
1632            cp.setOpaque(true);
1633            setContentPane(cp);
1634            this.panel = p;
1635            initWindow(this, panel);
1636        }
1637
1638        @Override
1639        public JXPanel getContentPane() {
1640            return (JXPanel) super.getContentPane();
1641        }
1642
1643        public JXLoginPane.Status getStatus() {
1644            return panel.getStatus();
1645        }
1646
1647        public JXLoginPane getPanel() {
1648            return panel;
1649        }
1650    }
1651
1652    /**
1653     * Utility method for initializing a Window for displaying a LoginDialog.
1654     * This is particularly useful because the differences between JFrame and
1655     * JDialog are so minor.
1656     *
1657     * Note: This method is package private for use by JXLoginDialog (proper,
1658     * not JXLoginPane.JXLoginDialog). Change to private if JXLoginDialog is
1659     * removed.
1660     */
1661    static void initWindow(final Window w, final JXLoginPane panel) {
1662        w.setLayout(new BorderLayout());
1663        w.add(panel, BorderLayout.CENTER);
1664        JButton okButton = new JButton(panel.getActionMap().get(LOGIN_ACTION_COMMAND));
1665        final JButton cancelButton = new JButton(
1666                UIManagerExt.getString(CLASS_NAME + ".cancelString", panel.getLocale()));
1667        cancelButton.addActionListener(new ActionListener() {
1668            @Override
1669            public void actionPerformed(ActionEvent e) {
1670                //change panel status to canceled!
1671                panel.status = JXLoginPane.Status.CANCELLED;
1672                w.setVisible(false);
1673                w.dispose();
1674            }
1675        });
1676        panel.addPropertyChangeListener("status", new PropertyChangeListener() {
1677            @Override
1678            public void propertyChange(PropertyChangeEvent evt) {
1679                JXLoginPane.Status status = (JXLoginPane.Status)evt.getNewValue();
1680                switch (status) {
1681                    case NOT_STARTED:
1682                        break;
1683                    case IN_PROGRESS:
1684                        cancelButton.setEnabled(false);
1685                        break;
1686                    case CANCELLED:
1687                        cancelButton.setEnabled(true);
1688                        w.pack();
1689                        break;
1690                    case FAILED:
1691                        cancelButton.setEnabled(true);
1692                        panel.passwordField.requestFocusInWindow();
1693                        w.pack();
1694                        break;
1695                    case SUCCEEDED:
1696                        w.setVisible(false);
1697                        w.dispose();
1698                }
1699                for (PropertyChangeListener l : w.getPropertyChangeListeners("status")) {
1700                    PropertyChangeEvent pce = new PropertyChangeEvent(w, "status", evt.getOldValue(), evt.getNewValue());
1701                    l.propertyChange(pce);
1702                }
1703            }
1704        });
1705        // FIX for #663 - commented out two lines below. Not sure why they were here in a first place.
1706        // cancelButton.setText(UIManager.getString(CLASS_NAME + ".cancelString"));
1707        // okButton.setText(UIManager.getString(CLASS_NAME + ".loginString"));
1708        JXBtnPanel buttonPanel = new JXBtnPanel(okButton, cancelButton);
1709        buttonPanel.setOpaque(false);
1710        panel.setButtonPanel(buttonPanel);
1711        JXPanel controls = new JXPanel(new FlowLayout(FlowLayout.RIGHT));
1712        controls.setOpaque(false);
1713        new BoxLayout(controls, BoxLayout.X_AXIS);
1714        controls.add(Box.createHorizontalGlue());
1715        controls.add(buttonPanel);
1716        w.add(controls, BorderLayout.SOUTH);
1717        w.addWindowListener(new WindowAdapter() {
1718            @Override
1719            public void windowClosing(java.awt.event.WindowEvent e) {
1720                panel.cancelLogin();
1721            }
1722        });
1723
1724        if (w instanceof JFrame) {
1725            final JFrame f = (JFrame)w;
1726            f.getRootPane().setDefaultButton(okButton);
1727            f.setResizable(false);
1728            f.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
1729            KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
1730            ActionListener closeAction = new ActionListener() {
1731                @Override
1732                public void actionPerformed(ActionEvent e) {
1733                    f.setVisible(false);
1734                    f.dispose();
1735                }
1736            };
1737            f.getRootPane().registerKeyboardAction(closeAction, ks, JComponent.WHEN_IN_FOCUSED_WINDOW);
1738        } else if (w instanceof JDialog) {
1739            final JDialog d = (JDialog)w;
1740            d.getRootPane().setDefaultButton(okButton);
1741            d.setResizable(false);
1742            KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
1743            ActionListener closeAction = new ActionListener() {
1744                @Override
1745                public void actionPerformed(ActionEvent e) {
1746                    d.setVisible(false);
1747                }
1748            };
1749            d.getRootPane().registerKeyboardAction(closeAction, ks, JComponent.WHEN_IN_FOCUSED_WINDOW);
1750        }
1751        w.pack();
1752        w.setLocation(WindowUtils.getPointForCentering(w));
1753    }
1754
1755    private void setButtonPanel(JXBtnPanel buttonPanel) {
1756        this.buttonPanel = buttonPanel;
1757    }
1758    
1759    private static class JXBtnPanel extends JXPanel {
1760        private static final long serialVersionUID = 4136611099721189372L;
1761        private JButton cancel;
1762        private JButton ok;
1763
1764        public JXBtnPanel(JButton okButton, JButton cancelButton) {
1765            GridLayout layout = new GridLayout(1,2);
1766            layout.setHgap(5);
1767            setLayout(layout);
1768            this.ok = okButton;
1769            this.cancel = cancelButton;
1770            add(okButton);
1771            add(cancelButton);
1772            setBorder(new EmptyBorder(0,0,7,11));
1773        }
1774
1775        /**
1776         * @return the cancel button.
1777         */
1778        public JButton getCancel() {
1779            return cancel;
1780        }
1781
1782        /**
1783         * @return the ok button.
1784         */
1785        public JButton getOk() {
1786            return ok;
1787        }
1788
1789    }
1790}