Creando un diálogo de Login con SwingX, Spring y JPA: la Vista


En la entrada anterior vimos la implementación de la capa de Servicio que será el back-end de nuestro diálogo de login. Ha llegado el momento de ver la implementación de la vista y el uso del componente JXLoginPane, para lo cual creo que es oportuno ver y mencionar las ventajas de utilizar este componente.

El componente JXLoginPane

Recordemos que al inicio de esta serie mencionábamos aspectos tales como caché de nombre de usuarios y la posibilidad de guardar contraseñas para mejorar la experiencia de usuario, la posibilidad de elegir entre una lista de servidores de autenticación en el caso que tengamos más de uno, mostrar un mensaje al usuario cuando tiene bloqueadas las mayúsculas, mostrar una animación mientras se lleva a cabo el proceso de autenticación, permitir cancelar la acción de autenticación o mostrar un mensaje de error en caso de fallo de autenticación. Pues bien, el componente JXLoginPane nos ofrece una solución out-of-the-box, configurable de acuerdo a nuestros requerimientos  y con poco trabajo de nuestro lado, siendo el único requisito mandatorio proveer una implementación de la clase abstracta LoginService. En nuestro ejemplo dicha implementación es la clase LoginServiceImp, la cual vimos en la entrada anterior.

Para comenzar podemos pensar en este componente como un panel común y corriente que podemos mostrar utilizando un contenedor de alto nivel (JFrame o JDialog):

LoginService loginService = new LoginServiceImp();
JXLoginPane loginPane = new JXLoginPane(loginService);
...
JDialog dialog = new JDialog();
...
dialog.add(loginPane);
...

De hecho la misma clase JXLoginPane provee subclases de JFrame y JDialog a medida para que sea más sencillo y poder aprovechar algunos métodos, como veremos más adelante:

LoginService loginService = new LoginServiceImp();
JXLoginPane loginPane = new JXLoginPane(loginService);
JXLoginPane.JXLoginDialog dialog = new JXLoginPane.JXLoginDialog((JFrame)null, loginPane);
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
dialog.setVisible(true);

El diálogo se verá como ilustra la siguiente imagen y por defecto muestra un mensaje al usuario indicando el bloqueo de mayúsculas, si fuera este el caso:

plain_login_pane

Para proveer caché de nombres de usuario debemos proveer una implementación de la clase abstracta UserNameStore, la cual implementamos en la capa de Servicio con el nombre UserNameStoreService:

LoginService loginService = new LoginServiceImp();
UserNameStore userNameStore = new UserNameStoreService();
JXLoginPane loginPane = new JXLoginPane(loginService);
loginPane.setUserNameStore(userNameStore);
loginPane.setSaveMode(JXLoginPane.SaveMode.USER_NAME);

Vemos que además de proveer el servicio de almacén de nombre de usuarios debemos setear la propiedad saveMode para que además de hacer uso del servicio guarde el nombre de usuario ingresado en caso que el login sea exitoso. Al proveer este mecanismo, el campo de texto para ingresar el nombre de usuario ahora será un combo box con la funcionalidad de autocompletado:

user_name_store_login_pane

Para agregar el guardado de contraseñas es necesario proveer una implementación de la clase abstracta PasswordStore y setear la propiedad saveMode con el valor PASSWORD o bien BOTH para habilitar el guardado tanto del nombre de usuario como de la contraseña:

LoginService loginService = new LoginServiceImp();
PasswordStore passwordStore = new PasswordStoreImp();
JXLoginPane loginPane = new JXLoginPane(loginService);
...
loginPane.setPasswordStore(passwordStore);
loginPane.setSaveMode(JXLoginPane.SaveMode.PASSWORD); // O JXLoginPane.SaveMode.BOTH

Ahora nuestro panel contará con un check box para que el usuario indique si quiere que la aplicación recuerde su contraseña:

password_store_login_pane

Para permitir la selección de un servidor de autenticación sólo debemos proveer una lista con los nombres de los servidores. Aquel que fue seleccionado se enviará como parámetro en el método authenticate(…) del LoginService:

LoginService loginService = new LoginServiceImp();
List<String> servers = Arrays.asList(new String[]{"localhost", "ubuntu-server"});
JXLoginPane loginPane = new JXLoginPane(loginService);
...
loginPane.setServers(servers);

En nuestro panel veremos un combo box que nos permite seleccionar nuestro servidor de autenticación:

servers_list_login_pane

Como podemos ver, el componente es altamente configurable y sólo debemos proveer lo que necesitemos. Veamos entonces la animación que nos muestra el componente mientras se procesa la autenticación en background, en el cual también se puede apreciar un botón para cancelar el inicio de sesión:

authenticating_login_pane

Finalmente en caso de fallar el proceso de autenticación, el panel mostrará un mensaje de error como el que sigue:

login_failed_message

Implementando la Vista

Ahora veremos la implementación de esta capa en la cual pondremos todas las piezas juntas. Para ello necesitaremos dos clases, una que representará la vista o ventana principal de la aplicación y otra que se encargará de mostrar el diálogo de login. En caso de ser exitoso, se mostrará la ventana principal con los datos de inicio de sesión del usuario.

LoginView.java

package com.javadeepcafe.view;

import com.javadeepcafe.services.SesionService;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.SwingWorker;
import org.jdesktop.swingx.JXLoginPane;
import org.jdesktop.swingx.auth.LoginAdapter;
import org.jdesktop.swingx.auth.LoginEvent;
import org.jdesktop.swingx.auth.LoginService;
import org.jdesktop.swingx.auth.UserNameStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author Delcio Amarillo
 */
@Component
public class LoginView {
    
    private JXLoginPane loginPane;
    
    @Autowired
    private LoginService loginService;
    
    @Autowired
    private UserNameStore userNameStore;
    
    @Autowired
    private SesionService sesionService;
    
    @Autowired
    private MainView mainView;
    
    public void createAndShowLoginDialog() {

        loginService.addLoginListener(new LoginAdapter() {
            @Override
            public void loginFailed(LoginEvent evt) {
                if (evt.getCause() != null) {
                    loginPane.setErrorMessage(evt.getCause().getMessage());
                }
            }
        });
        
        loginPane = new JXLoginPane(loginService);
        loginPane.setUserNameStore(userNameStore);
        loginPane.setSaveMode(JXLoginPane.SaveMode.USER_NAME);
        
        final JXLoginPane.JXLoginDialog dialog = new JXLoginPane.JXLoginDialog((JFrame)null, loginPane);
        dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
        dialog.setVisible(true);
        
        SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() {
            @Override
            protected Void doInBackground() throws Exception {
                if (dialog.getStatus() == JXLoginPane.Status.SUCCEEDED) {
                    String userName = loginPane.getUserName();
                    sesionService.crearNuevaSesion(userName);
                }
                return null;
            }

            @Override
            protected void done() {
                boolean succeeded = dialog.getStatus() == JXLoginPane.Status.SUCCEEDED;
                dialog.dispose();
                mainView.notifyLoginProcessResult(succeeded);
            }
        };
        worker.execute();
    }
}

MainView.java

package com.javadeepcafe.view;

import com.javadeepcafe.services.SesionService;
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.math.BigInteger;
import java.util.Date;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingWorker;
import javax.swing.UIManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author Delcio Amarillo
 */
@Component
public class MainView {
    
    private JFrame frame;
    
    @Autowired
    private LoginView loginView;
    
    @Autowired
    private SesionService sesionService;
    
    public void createAndShowGUI() {
        frame = new JFrame("Bienvenido!");
        frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                MainView.this.disposeGUI();
            }
        });
        loginView.createAndShowLoginDialog();
    }
    
    public void notifyLoginProcessResult(boolean succeeded) {
        if (succeeded) {
            frame.add(getMainPanel());
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        } else {
            frame.dispatchEvent(new WindowEvent(frame, WindowEvent.WINDOW_CLOSING));
        }
    }
    
    private JPanel getMainPanel() {
        
        BigInteger idSesion = sesionService.getSesionActiva().getId();
        Date inicioSesion = sesionService.getSesionActiva().getInicioSesion();
        BigInteger idUsuario = sesionService.getSesionActiva().getUsuario().getId();
        String nombreUsuario = sesionService.getSesionActiva().getUsuario().getNombreUsuario();
        Boolean usuarioBloqueado = sesionService.getSesionActiva().getUsuario().getBloqueado();
        
        JTextArea textArea = new JTextArea(15, 60);
        textArea.setEditable(false);
        
        textArea.append(
            String.format("Id sesión        : %s%n", idSesion)
        );
        textArea.append(
            String.format("Inicio sesión    : %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL%n", inicioSesion)
        );
        textArea.append(
            String.format("Id usuario       : %s%n", idUsuario)
        );
        textArea.append(
            String.format("Nombre de usuario: %s%n", nombreUsuario)
        );
        textArea.append(
            String.format("Usuario bloqueado: %s%n", usuarioBloqueado)
        );
        
        JButton cerrarSesionButton = new JButton(new AbstractAction("Finalizar sesión") {
            @Override
            public void actionPerformed(ActionEvent e) {
                MainView.this.disposeGUI();
            }
        });
        
        JLabel label = new JLabel();
        label.setText(
            String.format("Bienvenido de nuevo %s", nombreUsuario)
        );
        label.setFont(
            label.getFont().deriveFont(Font.ITALIC | Font.BOLD, 18)
        );
        
        JPanel controlPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING));
        controlPanel.add(cerrarSesionButton);
        
        JPanel panel = new JPanel(new BorderLayout(8,8));
        panel.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
        panel.add(label, BorderLayout.PAGE_START);
        panel.add(new JScrollPane(textArea), BorderLayout.CENTER);
        panel.add(controlPanel, BorderLayout.PAGE_END);
        return panel;
    }
    
    private void disposeGUI() {
        frame.setVisible(false);
        
        JLabel label = new JLabel("Cerrando sesión, por favor espere...");
        label.setIcon(UIManager.getIcon("OptionPane.informationIcon"));
        
        JPanel panel = new JPanel(new BorderLayout(8,8));
        panel.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
        panel.add(label);
        
        final JDialog dialog = new JDialog(frame, "Finalizando aplicación");
        dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
        dialog.setModal(false);
        dialog.add(panel);
        dialog.pack();
        dialog.setLocationRelativeTo(frame);
        dialog.setVisible(true);
        
        SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() {
            @Override
            protected Void doInBackground() throws Exception {
                sesionService.finalizarSesionActiva();
                return null;
            }
            
            @Override
            protected void done() {
                dialog.dispose();
                frame.dispose();
            }
        };
        worker.execute();
    }
}

Capturas de pantalla

login_pane

authenticating

main_view

closing_app

Configuración de Spring para soportar nuestra capa de Vista

BeansConfig.java

package com.javadeepcafe.config;

import com.javadeepcafe.view.LoginView;
import com.javadeepcafe.view.MainView;
...
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
...
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
 * @author Delcio Amarillo
 */
@Configuration
@EnableTransactionManagement
public class BeansConfig {
    
    ...
    
    @Bean
    public LoginView createLoginView() {
        return new LoginView();
    }
    
    @Bean
    public MainView createMainView() {
        return new MainView();
    }
}

Lanzando nuestra aplicación

Finalmente, veamos la clase main que inicializa nuestra aplicación.

DemoMainClass.java

package com.javadeepcafe.main;

import com.javadeepcafe.config.BeansConfig;
import com.javadeepcafe.view.MainView;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

/**
 * @author Delcio Amarillo
 */
public class DemoMainClass {

     public static void main(String[] args) {
         
         final ApplicationContext context = new AnnotationConfigApplicationContext(BeansConfig.class);
         
         SwingUtilities.invokeLater(new Runnable() {
             @Override
             public void run() {
                 try {
                     UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                 } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                     Logger.getLogger(DemoMainClass.class.getName()).log(Level.SEVERE, null, ex);
                 } finally {
                     MainView mainView = context.getBean(MainView.class);
                     mainView.createAndShowGUI();
                 }
             }
         });
    }
}

Conclusión

En esta serie vimos un ejemplo de implementación de login utilizando JPA, Spring y SwingX. Vimos la división en capas que permite que nuestra aplicación sea flexible y podamos adaptar los componentes según nuestros requerimientos. Si bien es cierto que el componente JXLoginPane pertenece al universo Swing, el diseño de las clases de back-end del mismo (LoginService, UserNameStore, PasswordStore, LoginListener) es un excelente ejemplo de división de responsabilidades orientada a la reutilización.

Código fuente

Pueden encontrar el código del ejemplo DemoLogin en JavaDeepCafe – Google Drive. El mismo tiene la estructura de un proyecto Maven, con un archivo pom.xml y una carpeta src donde se encuentra el código fuente.

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s