/*
* Copyright(c) 2002-2004, StarLight Systems
* All rights reserved.
*/
package com.starlight.ui.login;
import com.jgoodies.forms.builder.ButtonBarBuilder;
import com.jgoodies.forms.builder.DefaultFormBuilder;
import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.FormLayout;
import com.starlight.StringKit;
import com.starlight.ui.*;
import com.starlight.ui.validity.ChangeLabelAction;
import com.starlight.ui.validity.DisableComponentAction;
import com.starlight.ui.validity.EnableComponentAction;
import com.starlight.ui.validity.FieldValidityChecker;
import com.starlight.ui.validity.checks.HasSelectionValidityCheck;
import com.starlight.ui.validity.checks.HasTextValidityCheck;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.text.JTextComponent;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Dimension;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.prefs.Preferences;
/**
* A modal login dialog.
*
* @author reden
*/
@SuppressWarnings( { "UnusedDeclaration" } )
public class LoginFrame<T> extends JFrame {
private static final String LABEL_KEY = "LABEL";
private static final String SPINNER_KEY = "SPINNER";
private static final int MIN_WIDTH = 250;
private static final int MAX_WIDTH = 700;
private static final int MIN_HEIGHT = 300;
private static final int FIELD_COLUMNS = 13;
private LoginAuthenticator<T> authenticator;
private String username;
private T token = null;
private LoginException error = null;
private boolean canceled = false;
private boolean cancel_or_quit;
private final DefaultFormBuilder component_builder = new DefaultFormBuilder(
new FormLayout( "fill:pref, 6px, fill:pref, 0px:grow" ) );
private JLabel user_label;
private JLabel password_label;
private JTextField user_field;
private JPasswordField password_field;
private JButton ok_button;
private JButton cancel_button;
private FieldValidityChecker validator;
private ValueChangeRelay<JComponent> vcr;
private JLabel dev_mode_label;
private JCheckBox dev_mode_check;
private Thread login_thread = null;
private CardLayout card_layout = new CardLayout();
private JPanel card_panel = new JPanel( card_layout );
private WaitSpinner2 spinner = new WaitSpinner2( false );
private JLabel label = new JLabel();
private java.util.List<Option> options = new ArrayList<Option>();
private java.util.List<JComponent> components_disabled_normally =
new ArrayList<JComponent>();
private java.util.List<JComponent> components_disabled_during_login =
new ArrayList<JComponent>();
private final Preferences pref_node;
private Runnable close_handler = null;
/**
* Create the dialog.
*
* @param authenticator The authentication driver.
* @param username Default username.
* @param icon Icon to display (or null).
* @param version_string Version string to display (or null). Note, this is
* displayed as-is (i.e., "Version:" is not added to the
* front of it.
* @param cancel Whether the decline button should be labeled "Cancel"
* (true) or "Quit" (false).
* @param title The title for the frame.
*/
public LoginFrame( LoginAuthenticator<T> authenticator, String username, Icon icon,
String version_string, boolean cancel, String title ) {
super( title );
this.authenticator = authenticator;
this.username = username;
this.cancel_or_quit = cancel;
if ( authenticator == null ) throw new IllegalArgumentException(
"Authenticator cannot be null" );
pref_node = Preferences.userRoot().node( "starlight" ).node(
authenticator.getClass().getSimpleName() );
init( icon != null ? new JLabel( icon ) : null, version_string );
}
/**
* Create the dialog.
*
* @param authenticator The authentication driver.
* @param username Default username.
* @param header_component Component placed in the header of the dialog.
* @param version_string Version string to display (or null). Note, this is
* displayed as-is (i.e., "Version:" is not added to the
* front of it.
* @param cancel Whether the decline button should be labeled "Cancel"
* (true) or "Quit" (false).
* @param title The title for the frame.
*/
public LoginFrame( LoginAuthenticator<T> authenticator, String username,
JComponent header_component, String version_string, boolean cancel,
String title ) {
super( title );
this.authenticator = authenticator;
this.username = username;
this.cancel_or_quit = cancel;
if ( authenticator == null ) throw new IllegalArgumentException(
"Authenticator cannot be null" );
pref_node = Preferences.userRoot().node( "starlight" ).node(
authenticator.getClass().getSimpleName() );
init( header_component, version_string );
}
/**
* Add an option to the information provided in the login dialog.
*
* @param option_name The name of the option, this will be given as the key
* in the options map.
* @param label The String that will be shown on the label.
* @param choices The default choices for the option. Providing null
* will make a text field appear. Providing more will cause
* a combo box to be used.
* @param can_edit_choices Determines whether the user can edit the choices or must
* use one provided. Only really makes sense with a combo box.
* @param require_entry Determines whether or not something must be entered in the
* field in order to attempt login.
* @param default_value If non-null, this will be selected.
*/
public void addOption( final String option_name, String label, String[] choices,
boolean can_edit_choices, boolean require_entry, String default_value ) {
if ( choices == null ) {
addOption( option_name, label, default_value, require_entry );
return;
}
JComboBox<String> box = new JComboBox<String>( choices );
box.setName( option_name );
box.setEditable( can_edit_choices );
String initial_selection =
pref_node.get( "login_option_" + option_name, default_value );
if ( initial_selection != null ) box.setSelectedItem( initial_selection );
JLabel label_label = new JLabel( label );
options.add( new Option( option_name, require_entry, box, label_label ) );
label_label.setFont( user_label.getFont() );
if ( require_entry ) {
ChangeLabelAction.createStandardErrorLabel( validator, label_label,
new HasSelectionValidityCheck( box ) );
vcr.add( box, box );
}
component_builder.append( label_label, box );
pack();
}
/**
* Add a string option to the information provided in the login dialog.
*
* @param option_name The name of the option, this will be given as the key
* in the options map.
* @param label The String that will be shown on the label.
* @param default_value Initial value, if any.
* @param require_entry Determines whether or not something must be entered in the
* field in order to attempt login.
*/
public void addOption( String option_name, String label, String default_value,
boolean require_entry ) {
JTextField field = new JTextField( default_value );
field.setName( option_name );
String initial_value =
pref_node.get( "login_option_" + option_name, default_value );
if ( initial_value != null ) field.setText( initial_value );
JLabel label_label = new JLabel( label );
options.add( new Option( option_name, require_entry, field, label_label ) );
if ( require_entry ) {
ChangeLabelAction.createStandardErrorLabel( validator, label_label,
new HasTextValidityCheck( field ) );
vcr.add( field, field );
validator.check();
}
label_label.setFont( user_label.getFont() );
component_builder.append( label_label, field );
pack();
}
/**
* Add a boolean option to the information provided in the login dialog.
*/
public void addOption( String option_name, String label, boolean default_value ) {
JCheckBox check_box = new JCheckBox( ( String ) null );
check_box.setName( option_name );
boolean initial_value = pref_node.getBoolean( "login_option_" + option_name,
default_value );
check_box.setSelected( initial_value );
JLabel label_label = new JLabel( label );
options.add( new Option( option_name, check_box, label_label ) );
label_label.setFont( user_label.getFont() );
component_builder.append( label_label, check_box );
pack();
}
public void addOption( String label, JComponent component ) {
JLabel label_label = new JLabel( label );
options.add( new Option( label, component, label_label ) );
component_builder.append( label_label, component );
pack();
}
/**
* Add a component directly. The component will span both columns.
*
* @param disabled_normally If true, the component will be disabled
* when not attempting to log in.
* @param disabled_during_login_attempt If true, the component will be disabled
* while attempting to log in.
*/
public void addComponentDirect( JComponent component, boolean disabled_normally,
boolean disabled_during_login_attempt ) {
if ( disabled_normally ) components_disabled_normally.add( component );
if ( disabled_during_login_attempt ) {
components_disabled_during_login.add( component );
}
if ( disabled_normally ) component.setEnabled( false );
component_builder.append( component, 3 );
pack();
}
public boolean wasCanceled() {
return canceled;
}
public boolean hadError() {
return error != null;
}
public LoginException getError() {
return error;
}
public T getToken() {
return token;
}
public String getUsername() {
return username;
}
public boolean isDevMode() {
return dev_mode_check.isSelected();
}
public Object getOptionValue( String option_name ) {
for( Option option : options ) {
if ( option.name.equals( option_name ) ) {
return option.getValue();
}
}
// assert false : "Option not found: " + option_name;
return null;
}
public void setCloseHandler( Runnable handler ) {
close_handler = handler;
}
public void setCustomWaitSpinner( WaitSpinner2 spinner ) {
card_panel.remove( this.spinner );
spinner.stop(); // make sure it's stopped
this.spinner = spinner;
card_panel.add( spinner, SPINNER_KEY );
}
private void attemptLogin() {
assert SwingUtilities.isEventDispatchThread();
toggleFields( false );
card_layout.show( card_panel, SPINNER_KEY );
spinner.start();
username = user_field.getText();
login_thread = new LoginAttempt();
login_thread.start();
}
private void init( JComponent header_component, String version_string ) {
setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE );
user_field = new JTextField( username, FIELD_COLUMNS );
user_field.setName( "user" );
user_label = new JLabel( "User:" );
user_label.setLabelFor( user_field );
user_label.setDisplayedMnemonic( 'U' );
user_field.addActionListener( new ActionListener() {
public void actionPerformed( ActionEvent e ) {
if ( password_field.getPassword() == null ||
password_field.getPassword().length == 0 ) {
assert SwingUtilities.isEventDispatchThread();
password_field.requestFocus();
user_field.setSelectionStart( 0 );
user_field.setSelectionEnd( 0 );
}
else {
ok_button.doClick();
}
}
} );
new SelectOnFocusListener( user_field );
password_field = new JPasswordField( FIELD_COLUMNS );
password_field.setName( "password" );
password_label = new JLabel( "Password:" );
password_label.setLabelFor( password_field );
password_label.setDisplayedMnemonic( 'P' );
password_field.addActionListener( new ActionListener() {
public void actionPerformed( ActionEvent e ) {
ok_button.doClick();
}
} );
new SelectOnFocusListener( password_field );
ok_button = new JButton( "OK" );
ok_button.setEnabled( false );
ok_button.setMnemonic( 'O' );
ok_button.addActionListener( new ActionListener() {
public void actionPerformed( ActionEvent e ) {
attemptLogin();
}
} );
cancel_button = new JButton( cancel_or_quit ? "Cancel" : "Quit" );
cancel_button.setMnemonic( cancel_or_quit ? 'C' : 'Q' );
cancel_button.addActionListener( new ActionListener() {
public void actionPerformed( ActionEvent e ) {
if ( login_thread != null ) login_thread.interrupt();
canceled = true;
setVisible( false );
dispose();
}
} );
dev_mode_label = new JLabel( "Dev Mode:" );
dev_mode_check = new JCheckBox();
dev_mode_check.setSelected( pref_node.getBoolean( "dev_mode", false ) );
JPanel content_pane = new JPanel( new BorderLayout() );
setContentPane( content_pane );
if ( header_component != null ) {
content_pane.add( header_component, BorderLayout.NORTH );
}
else content_pane.setBorder( new EmptyBorder( 5, 0, 0, 0 ) );
component_builder.getLayout().setHonorsVisibility( false );
component_builder.append( user_label, user_field );
component_builder.append( password_label, password_field );
if ( System.getProperty( "allow_dev_mode" ) != null ) {
component_builder.append( dev_mode_label, dev_mode_check );
}
else dev_mode_check.setSelected( false );// Make sure it's disabled if not shown
JPanel component_panel = component_builder.getPanel();
component_panel.setBorder( new EmptyBorder( 24, 9, 24, 9 ) );
JPanel spacer =
new JPanel( new FormLayout( "0px:grow, fill:pref, 0px:grow", "fill:pref" ) );
CellConstraints cc = new CellConstraints();
spacer.add( component_panel, cc.xy( 2, 1 ) );
content_pane.add( spacer, BorderLayout.CENTER );
card_panel.add( label, LABEL_KEY );
card_panel.add( spinner, SPINNER_KEY );
ButtonBarBuilder builder = new ButtonBarBuilder();
builder.addGrowing( card_panel );
builder.addGlue();
builder.addButton( ok_button, cancel_button );
JPanel button_panel = builder.getPanel();
button_panel.setBorder( new EmptyBorder( 0, 12, 12, 12 ) );
content_pane.add( builder.getContainer(), BorderLayout.SOUTH );
getRootPane().setDefaultButton( ok_button );
addWindowListener( new WindowAdapter() {
public void windowOpened( WindowEvent e ) {
if ( user_field.getText() == null || user_field.getText().length() == 0 ) {
user_field.requestFocus();
}
else if ( password_field.getPassword() == null ||
password_field.getPassword().length == 0 ) {
assert SwingUtilities.isEventDispatchThread();
password_field.requestFocus();
user_field.setSelectionStart( 0 );
user_field.setSelectionEnd( 0 );
}
else {
ok_button.requestFocus();
}
}
public void windowClosing( WindowEvent e ) {
cancel_button.doClick();
}
@Override
public void windowClosed( WindowEvent e ) {
if ( close_handler != null ) close_handler.run();
}
} );
setResizable( false );
setLabel( version_string );
pack();
int width = Math.max( MIN_WIDTH, getWidth() );
if ( width > MAX_WIDTH ) {
System.err.println( "LoginDialog max width hit (" + width +
"). Width will be capped." );
width = MAX_WIDTH;
}
int height = Math.max( MIN_HEIGHT, getHeight() );
setSize( new Dimension( width, height ) );
KeyListener cancel_listener = new KeyAdapter() {
public void keyPressed( KeyEvent e ) {
if ( e.getKeyCode() == KeyEvent.VK_ESCAPE ) {
cancel_button.doClick();
}
}
};
addKeyListener( cancel_listener );
user_field.addKeyListener( cancel_listener );
password_field.addKeyListener( cancel_listener );
WindowKit.centerWindow( this );
validator = new FieldValidityChecker(
new EnableComponentAction( ok_button ),
new DisableComponentAction( ok_button ), false );
//noinspection unchecked
vcr = new ValueChangeRelay<JComponent>(
( ValueChangeRelay.ValueChangeListener<JComponent> )
validator.createVCRListener() );
ChangeLabelAction.createStandardErrorLabel( validator, user_label,
new HasTextValidityCheck( user_field ) );
vcr.add( user_field, user_field );
ChangeLabelAction.createStandardErrorLabel( validator, password_label,
new HasTextValidityCheck( password_field ) );
vcr.add( password_field, password_field );
validator.check();
}
private void toggleFields( final boolean enable ) {
// Make sure we do this in the EDT
if ( !SwingUtilities.isEventDispatchThread() ) {
SwingUtilities.invokeLater( new Runnable() {
public void run() {
toggleFields( enable );
}
} );
return;
}
user_label.setEnabled( enable );
user_field.setEnabled( enable );
password_label.setEnabled( enable );
password_field.setEnabled( enable );
ok_button.setEnabled( enable );
for ( Option option : options ) {
option.component_label.setEnabled( enable );
option.component.setEnabled( enable );
}
dev_mode_label.setEnabled( enable );
dev_mode_check.setEnabled( enable );
if ( enable ) {
for( JComponent component : components_disabled_during_login ) {
component.setEnabled( true );
}
for( JComponent component : components_disabled_normally ) {
component.setEnabled( false );
}
}
else {
for( JComponent component : components_disabled_normally ) {
component.setEnabled( true );
}
for( JComponent component : components_disabled_during_login ) {
component.setEnabled( false );
}
}
if ( enable ) {
assert SwingUtilities.isEventDispatchThread();
password_field.requestFocus();
user_field.setSelectionStart( 0 );
user_field.setSelectionEnd( 0 );
}
if ( enable ) validator.check();
}
private void setError( final LoginException error ) {
this.error = error;
SwingUtilities.invokeLater( new Runnable() {
public void run() {
String message;
if ( error.getMessage() == null ) {
message = "An unexpected error occurred while logging in:\n" + error;
}
else message = error.getMessage();
toggleFields( true );
password_field.setText( null );
ExceptionHandler.handle( LoginFrame.this, error, message );
}
} );
}
private void setLabel( final String message ) {
SwingUtilities.invokeLater( new Runnable() {
public void run() {
label.setText( message );
label.setFont( UIKit.modifyFontSize( new JLabel().getFont(), -2 ) );
card_layout.show( card_panel, LABEL_KEY );
}
} );
}
private class LoginAttempt extends Thread {
public void run() {
boolean reenable_components = true;
try {
Map<String,Object> option_map = new HashMap<String,Object>();
if ( !options.isEmpty() ) {
for ( Option option : options ) {
option_map.put( option.name, option.getValue() );
}
}
T token = authenticator.authenticate( user_field.getText(),
password_field.getPassword(), option_map,
dev_mode_check.isSelected() );
if ( isInterrupted() ) return;
// It worked! Save the token and clear errors
LoginFrame.this.token = token;
LoginFrame.this.error = null;
final JFrame me = LoginFrame.this;
reenable_components = false;
SwingUtilities.invokeLater( new Runnable() {
public void run() {
me.setVisible( false );
// Save the options
for( Option option : options ) {
String option_pref_name = "login_option_" + option.name;
if ( option.getValue() == null ) {
pref_node.remove( option_pref_name );
continue;
}
if ( option.component instanceof JComboBox ) {
pref_node.put( option_pref_name,
option.getValue().toString() );
}
else if ( option.component instanceof JCheckBox ) {
pref_node.putBoolean( option_pref_name,
( ( Boolean ) option.getValue() ).booleanValue() );
}
else if ( option.component instanceof JTextComponent ) {
pref_node.put( option_pref_name,
( ( String ) option.getValue() ) );
}
}
pref_node.putBoolean( "dev_mode", dev_mode_check.isSelected() );
me.dispose();
}
} );
}
catch( LoginException ex ) {
// ex.printStackTrace();
setError( ex );
}
catch( Exception ex ) {
// ex.printStackTrace();
String message;
if ( ex.getMessage() == null ) message = ex.toString();
else message = ex.getMessage();
setError( new LoginException( message, ex ) );
}
finally {
if ( reenable_components ) toggleFields( true );
spinner.stop();
}
}
}
private class Option {
private String name;
private boolean require_entry;
private JComponent component;
private JLabel component_label;
Option( String name, boolean require_entry, JComponent component,
JLabel component_label ) {
this.name = name;
this.require_entry = require_entry;
this.component = component;
this.component_label = component_label;
}
Option( String name, JComponent component, JLabel component_label ) {
this( name, true, component, component_label );
}
Object getValue() {
if ( component instanceof JComboBox ) {
return ( ( JComboBox ) component ).getSelectedItem();
}
else if ( component instanceof JCheckBox ) {
return Boolean.valueOf( ( ( JCheckBox ) component ).isSelected() );
}
else if ( component instanceof JTextComponent ) {
return ( ( JTextComponent ) component ).getText();
}
else {
throw new UnsupportedOperationException(
"Only combo and check boxes are currently supported." );
}
}
boolean isValueOK() {
if ( !require_entry ) return true;
if ( component instanceof JComboBox ) {
return ( ( JComboBox ) component ).getSelectedItem() != null;
}
else if ( component instanceof JTextComponent ) {
return !StringKit.isEmpty( ( ( JTextComponent ) component ).getText() );
}
return true;
}
}
}