Login dialog window won't dispose completely

偶尔善良 提交于 2019-12-11 03:25:01

问题


I've got a dialog window for logging in a user to a main program in Java. I've recently discovered, however, that if the user clicks the "Cancel" button or the window's native Close button, the program still runs, even if the login window itself has been disposed of. I have to force quit it. My instincts tell me that it has to do with the LoginService that is created as part of creating a JXLoginPane. Please have a look at my (well-documented) code below and give me your thoughts:

package info.chrismcgee.sky;

import info.chrismcgee.beans.Login;
import info.chrismcgee.dbutil.LoginConnectionManager;
import info.chrismcgee.login.PasswordHash;
import info.chrismcgee.tables.LoginManager;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.EmptyBorder;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jdesktop.swingx.JXLoginPane;
import org.jdesktop.swingx.auth.LoginService;

public class LoginDialog extends JDialog {

    // For logging!
    static final Logger log = LogManager.getLogger(LoginDialog.class.getName());

    /**
     * Serialize, to keep Eclipse from throwing a warning message.
     */
    private static final long serialVersionUID = 52954843540592996L;
    private final JPanel contentPanel = new JPanel(); // default.
    // The login pane is a field because it needs to be called
    // by the "OK" button later.
    private JXLoginPane loginPane;
    // User bean is a field because two inner methods need to access it,
    // and it would be cheaper to only create one bean.
    private Login bean;

    /**
     * Launch the application.
     * Unedited.
     */
    public static void main(String[] args) {

        log.entry("main (LoginDialog)");

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {

                log.entry("run (LoginDialog)");

                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                    LoginDialog dialog = new LoginDialog();
                    dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
                    dialog.setVisible(true);
                } catch (Exception e) {
                    log.error("Error running the LoginDialog", e);
                }

                log.exit("run (LoginDialog)");
            }
        });

        log.exit("main (LoginDialog)");
    }

    /**
     * Create the dialog.
     */
    public LoginDialog() {
        setBounds(100, 100, 450, 300);
        getContentPane().setLayout(new BorderLayout()); // default.
        contentPanel.setLayout(new FlowLayout()); // default.
        contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); // default.
        getContentPane().add(contentPanel, BorderLayout.CENTER); // default.
        {
            // Create a JXLoginPane using a LoginService
            // to handle the authentication.
            loginPane = new JXLoginPane(new LoginService() {

                // `authenticate` *must* be overridden.
                // We will not be using the "server" string, however.
                @Override
                public boolean authenticate(String name, char[] password, String server)
                        throws Exception {

                    log.entry("authenticate (LoginDialog)");

                    // With the username entered by the user, get the user information
                    // from the database and store it in a Login bean.
                    bean = LoginManager.getRow(name);

                    // If the user does not exist in the database, the bean will be null.
                    if (bean != null)
                        // Use `PasswordHash`'s `validatePassword` static method
                        // to see if the entered password (after being hashed)
                        // matches the hashed password stored in the bean.
                        // Returns `true` if it matches, `false` if it doesn't.
                        return log.exit(PasswordHash.validatePassword(password, bean.getHashPass()));
                    else
                        // If the user doesn't exist in the database, just return `false`,
                        // as if the password was wrong. This way, the user isn't alerted
                        // as to which of the two pieces of credentials is wrong.
                        return log.exit(false);
                }
            });
            // Add the login pane to the main content panel.
            contentPanel.add(loginPane);
        }
        {
            // Create the button pane for the bottom of the dialog.
            JPanel buttonPane = new JPanel();
            buttonPane.setLayout(new FlowLayout(FlowLayout.RIGHT)); // default.
            getContentPane().add(buttonPane, BorderLayout.SOUTH); // default.
            {
                // Create the "OK" button plus its action listener.
                JButton okButton = new JButton("OK");
                okButton.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent evt) {

                        log.entry("OK button pressed. (LoginDialog)");

                        // Several of these will throw exceptions,
                        // so it's in a `try-catch` block.
                        try {
                            // This `if` statement is what calls the `authenticate`
                            // method to see if the credentials match the database.
                            if (loginPane.getLoginService().authenticate(
                                    loginPane.getUserName().toLowerCase(),
                                    loginPane.getPassword(), null))
                            {
                                // If the credentials are in order, close the connection
                                // to the `logins` database, since they won't be needed anymore.
                                LoginConnectionManager.getInstance().close();
                                // Also close the login window; it won't be needed anymore, either.
                                Window window = SwingUtilities.windowForComponent(contentPanel);
                                window.dispose();

                                log.trace("Running Scheduling with access level of " + bean.getAccessLevel());
                                // And finally run the main `Scheduling.java` program,
                                // passing to it the user's access level.
                                String[] args = {Integer.toString(bean.getAccessLevel())};
                                Scheduling.main(args);
                            }
                            else
                            {
                                // If the login credentials fail, let the user know generically.
                                JOptionPane.showMessageDialog(null, "Incorrect username or password.", "Bad Username or Password", JOptionPane.ERROR_MESSAGE);
                            }
                        } catch (Exception e) {
                            log.error("Exception when hitting the 'OK' button.", e);
                        }

                        log.exit("OK button done.");
                    }
                });
                okButton.setActionCommand("OK"); // default.
                // Add the "OK" button the button pane.
                buttonPane.add(okButton); // default.
                getRootPane().setDefaultButton(okButton); // default.
            }
            {
                // Create the "Cancel" button plus its listener.
                JButton cancelButton = new JButton("Cancel");
                cancelButton.setActionCommand("Cancel"); // default.
                cancelButton.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent e) {

                        log.entry("CANCEL button pressed. (LoginDialog)");

                        // Just close the connection to the database,
                        LoginConnectionManager.getInstance().close();
                        // and close the login window.
                        Window window = SwingUtilities.windowForComponent((Component) e.getSource());
                        window.dispose();

//                      System.exit(0);
                        log.exit("CANCEL button done.");
                    }
                });
                // Add the "Cancel" button to the button pane.
                buttonPane.add(cancelButton); // default.
            }
        }
    }

}

I assume that the LoginService created a thread when the dialog was created. What is the proper way to end this thread, if that's the case? And if it's not the case, what's going on and how can I fix it?

10/01/14 2:42pm EDIT: I've taken dic19's advice and simplified things. Greatly. I've created a barebones dialog now using his recommendation of just sticking with the built-in JXLoginDialog. It doesn't have all the methods that are needed for actually logging into my main program, but here's that simplified dialog, which should be enough to see if the program continues running after the "Cancel" button is pressed:

package info.chrismcgee.sky;

import info.chrismcgee.beans.Login;
import info.chrismcgee.login.PasswordHash;
import info.chrismcgee.tables.LoginManager;

import javax.swing.JDialog;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jdesktop.swingx.JXLoginPane;
import org.jdesktop.swingx.auth.LoginService;

public class LoginDialog {

    static final Logger log = LogManager.getLogger(LoginDialogOriginal.class.getName());
    private static Login bean;
    private static LoginService loginService = new LoginService() {

        @Override
        public boolean authenticate(String name, char[] password, String server)
                throws Exception {

            log.entry("authenticate (LoginDialogOriginal)");

            bean = LoginManager.getRow(name);

            if (bean != null)
                return log.exit(PasswordHash.validatePassword(password, bean.getHashPass()));
            else
                return log.exit(false);
        }
    };

    /**
     * Launch the application.
     */
    public static void main(String[] args) {
        try {
            JXLoginPane loginPane = new JXLoginPane(loginService);
            JXLoginPane.JXLoginDialog dialog = new JXLoginPane.JXLoginDialog(new JDialog(), loginPane);
            dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
            dialog.setVisible(true);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

I've removed comments to tighten up the code just a bit more.

Sadly, while clicking "Cancel" does dispose of the dialog, the program is still in memory and I have to force-quit it, as before. Perhaps there's a thread or something going on in PasswordHash? I more or less just lifted the code from CrackStation's post for that.

10/02/14 3:32pm EDIT: I've tried updating that simplified program, based on dic19's recommendation of creating a JFrame that is then either shown or disposed of based on the success of the login dialog. I'm afraid this still does not change the program hanging around after the user clicks on "Cancel". It's still in memory and still needs to be force-quitted. Here's the updated code with dic19's modifications:

package info.chrismcgee.sky;

import java.awt.event.WindowEvent;

import info.chrismcgee.beans.Login;
import info.chrismcgee.login.PasswordHash;
import info.chrismcgee.tables.LoginManager;

import javax.swing.JDialog;
import javax.swing.JFrame;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jdesktop.swingx.JXLoginPane;
import org.jdesktop.swingx.auth.LoginService;

public class LoginDialog {

    static final Logger log = LogManager.getLogger(LoginDialogOriginal.class.getName());
    private static Login bean;
    private static LoginService loginService = new LoginService() {

        @Override
        public boolean authenticate(String name, char[] password, String server)
                throws Exception {

            log.entry("authenticate (LoginDialogOriginal)");

            bean = LoginManager.getRow(name);

            if (bean != null)
                return log.exit(PasswordHash.validatePassword(password, bean.getHashPass()));
            else
                return log.exit(false);
        }
    };

    /**
     * Launch the application.
     */
    public static void main(String[] args) {
        try {
            JFrame frame = new JFrame("Welcome!"); // A non-visible JFrame which acts as the parent frame.
            frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
            JXLoginPane loginPane = new JXLoginPane(loginService);
            JXLoginPane.JXLoginDialog dialog = new JXLoginPane.JXLoginDialog(frame, loginPane);
            dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
            dialog.setVisible(true);

            if (dialog.getStatus() != JXLoginPane.Status.SUCCEEDED)
                frame.dispatchEvent(new WindowEvent(frame, WindowEvent.WINDOW_CLOSING));
            else
                frame.setVisible(true);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

Again, I've removed comments for brevity. Any idea why this is still causing the program to hang around in memory after clicking "Cancel"?


回答1:


frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);  // To quit the whole GUI.

or

dialog.dispose(); // After user clicks cancel.

But as the cancel button is not explicitly created by you, you cannot call dialog.dispose() directly. BUT you still can. with 3 extra lines of code. to call dialog.dispose(), add a window listener(extend java.awt.event.WindowAdapter) on your dialog. and override the windowClosing() method. and write dialog.dispose() in its body.

Snippet:

dialog.addWindowListener(

    new WindowAdapter() {

        @Override
        public void windowClosing(WindowEvent event) {

            dialog.dispose();
        }
    }
);  



回答2:


The login/authentication framework around JXLoginPane is intended to do the interaction between LoginService and login pane asynchronously in order to animate the view while the login process is being done in a background thread.

That being told and despite authenticate(username, password, server) method is public, you should never call it explicitely. You should use startAuthentication(username, password, server) and do the validation using an apropriate LoginListener instead.

okButton.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent evt) {
        ...
        loginPane.getLoginService().startAuthentication(
                                loginPane.getUserName().toLowerCase(),
                                loginPane.getPassword(), null);
        ...
    }
});

On the other hand cancel button should cancel (suprise) the login process by calling cancelAuthentitacion() method. Otherwise this process still running until be done (if ever happens) and your application won't exit, which is actually the symptom you're experiencing. So, translated to code:

cancelButton.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        ...
        loginPane.getLoginService().cancelAuthentication();
        ...
    }
});

Don't reinvent the wheel...

Please note that you actually have no need to do all these stuff. You can just use JXLoginDialog as exemplified in this answer and avoid pretty much all start/cancel implementation. In a snippet:

    LoginService loginService = new LoginServiceImp(); // your login service implementation

    JXLoginPane loginPane = new JXLoginPane(loginService);

    JXLoginPane.JXLoginDialog dialog = new JXLoginPane.JXLoginDialog(parentFrame, loginPane);
    dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
    dialog.setVisible(true);

Finally if you don't like JXLoginDialog you can access the default login pane's login/cancel actions through its action map:

    JXLoginPane loginPane = new JXLoginPane(loginService);

    ActionMap map = loginPane.getActionMap();
    Action loginAction = map.get(JXLoginPane.LOGIN_ACTION_COMMAND);
    Action cancelAction = map.get(JXLoginPane.CANCEL_LOGIN_ACTION_COMMAND);

    JButton okButton = new JButton(loginAction);
    JButton cancelButton = new JButton(cancelAction);

And then place these buttons wherever you want in your custom dialog. All this process is also explained in jxloginpane tag wiki.


Edit

After 10 minutes testing your updated code (10/01/14 2:42pm EDIT) I have realized this:

    JXLoginPane.JXLoginDialog dialog = new JXLoginPane.JXLoginDialog(new JDialog(), loginPane);

Now your application is not ending because of the dialog's parent still "alive". You should dispose both dialogs: login dialog and its parent.

However IMHO it would be better to have a non visible JFrame as parent of the login dialog and check the dialog's status after it is closed (pretty much like we do with JOptionPane) : if status is SUCCEEDED then show the frame, otherwise dispose the frame too:

    JFrame frame = new JFrame("Welcome!");
    frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    ...
    JXLoginPane.JXLoginDialog dialog = new JXLoginPane.JXLoginDialog(frame, loginPane);
    dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
    dialog.setVisible(true);

    if (dialog.getStatus() != JXLoginPane.Status.SUCCEEDED) {
        frame.dispatchEvent(new WindowEvent(frame, WindowEvent.WINDOW_CLOSING));
    } else {
        // make frame visible here
    }



回答3:


Final code:

package info.chrismcgee.sky;

import info.chrismcgee.beans.Login;
import info.chrismcgee.dbutil.LoginConnectionManager;
import info.chrismcgee.login.PasswordHash;
import info.chrismcgee.tables.LoginManager;

import java.awt.EventQueue;
import java.awt.event.WindowEvent;
import java.text.ChoiceFormat;
import java.text.Format;
import java.text.MessageFormat;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.ResourceBundle;

import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.UIManager;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jdesktop.swingx.JXLoginPane;
import org.jdesktop.swingx.auth.LoginAdapter;
import org.jdesktop.swingx.auth.LoginEvent;
import org.jdesktop.swingx.auth.LoginListener;
import org.jdesktop.swingx.auth.LoginService;

public class LoginDialog {

    // FIELD LIST
    // For logging!
    static final Logger log = LogManager.getLogger(LoginDialogOriginal.class.getName());
    // A non-visible JFrame which acts as the parent frame.
    private JFrame frame;
    // A simple int to track number of failure attempts.
    private int failedAttemptsCount = 1;
    // User bean is a field because two inner methods need to access it,
    // and it would be cheaper to only create one bean.
    private Login bean;

    private String failureMessage(Locale currentLocale, int attemptNumber)
    {
        // Because the message text must be translated,
        // isolate it in a ResourceBundle.
        ResourceBundle bundle = ResourceBundle.getBundle("info.chrismcgee.components.ChoiceBundle", currentLocale);

        // Create a Message Formatter and set its locale.
        MessageFormat messageForm = new MessageFormat("");
        messageForm.setLocale(currentLocale);

        // For the upcoming ChoiceFormatter, set two arrays.
        // The first array denotes the range of numbers possible.
        double[] attemptLimits = {0, 1, 2};
        // The second array maps to the first, setting the variable names in the Choice.
        String[] attemptStrings = {
                bundle.getString("noAttempts"),
                bundle.getString("oneAttempt"),
                bundle.getString("multipleAttempts")
        };

        // Now finally create the ChoiceFormat, which maps the arrays together.
        ChoiceFormat choiceForm = new ChoiceFormat(attemptLimits, attemptStrings);

        // Retreive the message pattern from the bundle,
        // applying it to the MessageFormat object.
        String pattern = bundle.getString("pattern");
        messageForm.applyPattern(pattern);

        // Now assign the ChoiceFormat object to the MessageFormat object.
        Format[] formats = {choiceForm, NumberFormat.getInstance()};
        messageForm.setFormats(formats);

        // Now that everything is set up, let's prepare the message arguments.
        Object[] messageArguments = {new Integer(attemptNumber), new Integer(attemptNumber)};
        String result = messageForm.format(messageArguments);

        log.debug("Result of the failureMessage method is:");
        log.debug(result);

        // And return the message.
        return result;
    }

    /**
     * The main method that is called from main. It handles all of the work
     * of creating a login dialog and showing it.
     */
    private void showLoginDialog()
    {
        // Attempt to set the UI to match the current OS.
        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (Exception err) {
            log.error("Exception thrown when attempting to set the look and feel for the current OS.", err);
        }

        // Initialize the invisible Frame and set its default close behavior.
        frame = new JFrame("Welcome!");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        // Login pane for the Login dialog.
        JXLoginPane loginPane = new JXLoginPane();
        // Login listener that tracks failure attempts, successes, and canceled events.
        LoginListener loginListener = new LoginAdapter() {
            // The error message to display to the user.
            String message;

            /* (non-Javadoc)
             * @see org.jdesktop.swingx.auth.LoginAdapter#loginCanceled(org.jdesktop.swingx.auth.LoginEvent)
             */
            @Override
            public void loginCanceled(LoginEvent source) {
                // Close the connection to the `logins` database, since it won't be needed anymore.
                LoginConnectionManager.getInstance().close();
                // And close out this dialog.
                frame.dispose();
            }

            /* (non-Javadoc)
             * @see org.jdesktop.swingx.auth.LoginAdapter#loginFailed(org.jdesktop.swingx.auth.LoginEvent)
             */
            @Override
            public void loginFailed(LoginEvent source) {
                if (failedAttemptsCount < 4)
                    message = failureMessage(Locale.US, (4 - failedAttemptsCount++));
                else
                {
                    // Close the connection to the `logins` database, since it won't be needed anymore.
                    LoginConnectionManager.getInstance().close();
                    frame.dispose();
                }

                loginPane.setErrorMessage(message);
            }

            /* (non-Javadoc)
             * @see org.jdesktop.swingx.auth.LoginAdapter#loginSucceeded(org.jdesktop.swingx.auth.LoginEvent)
             */
            @Override
            public void loginSucceeded(LoginEvent source) {
                // If the credentials are in order, close the connection
                // to the `logins` database, since it won't be needed anymore.
                LoginConnectionManager.getInstance().close();
                // Also close the login window; it won't be needed anymore, either.
                frame.dispose();

                log.trace("Running Scheduling with access level of " + bean.getAccessLevel());
                // And finally run the main `Scheduling.java` program,
                // passing to it the user's access level.
                String[] args = {Integer.toString(bean.getAccessLevel())};
                Scheduling.main(args);
            }
        };

        // The login service which will have the actual validation logic.
        LoginService loginService = new LoginService() {

            // `authenticate` *must* be overridden.
            // We will not be using the "server" string, however.
            @Override
            public boolean authenticate(String name, char[] password, String server)
                    throws Exception {

                log.entry("authenticate (LoginDialog)");

                // With the username entered by the user, get the user information
                // from the database and store it in a Login bean.
                bean = LoginManager.getRow(name);

                // If the user does not exist in the database, the bean will be null.
                if (bean != null)
                    // Use `PasswordHash`'s `validatePassword` static method
                    // to see if the entered password (after being hashed)
                    // matches the hashed password stored in the bean.
                    // Returns `true` if it matches, `false` if it doesn't.
                    return log.exit(PasswordHash.validatePassword(password, bean.getHashPass()));
                else
                    // If the user doesn't exist in the database, just return `false`,
                    // as if the password was wrong. This way, the user isn't alerted
                    // as to which of the two pieces of credentials is wrong.
                    return log.exit(false);
            }
        };

        loginService.addLoginListener(loginListener);
        loginPane.setLoginService(loginService);

        JXLoginPane.JXLoginDialog dialog = new JXLoginPane.JXLoginDialog(frame, loginPane);
        dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
        dialog.setVisible(true);

        // If the loginPane was cancelled or closed or otherwise did not succeed,
        // then the main JFrame needs to be disposed to exit the application.
        if (dialog.getStatus() != JXLoginPane.Status.SUCCEEDED)
            frame.dispatchEvent(new WindowEvent(frame, WindowEvent.WINDOW_CLOSING));
        else
            frame.dispose();
    }

    /**
     * Launch the application.
     */
    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                new LoginDialog().showLoginDialog();
            }

        });
    }

}

It's a bit long, yes, but that's because I decided to go the extra mile and add in a custom failure message that can be adapted to the current locale and handle plurals. Again, a big thanks to both @dic19 and @Aditya for their help in getting this working—and closing properly! ;~)

Feel free to add comments to this if you want to suggest better ways of addressing anything in here. It could be helpful to both myself and others!



来源:https://stackoverflow.com/questions/26145425/login-dialog-window-wont-dispose-completely

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!