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