GUI-based pseudo-random password generator. Modification of [this app](https://codereview.stackexchange.com/questions/66986/small-password-generator-app-with-gui), improved according to suggestions from Code Review users. Additional feature: calculation of time to crack password on average desktop PC, which is determined as time to go through all possible combinations of characters. 

Ideas for further improvement?

![enter image description here](https://i.stack.imgur.com/I130V.png)

**Application.java** - entry point

```
package com.singularityfx.passgen;

import java.awt.EventQueue;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.logging.Logger;

import javax.swing.UIManager;
import javax.swing.UIManager.LookAndFeelInfo;

import com.singularityfx.utils.ExceptionReporter;

/**
 * Application entry point.
 * Responsible for configuration of locale and look & feel, and for loading
 * the GUI.
 *
 */
public class Application {
    private static final String DEFAULT_LANGUAGE = "en";
    private static final String DEFAULT_COUNTRY = "US";
    private static Logger log = Logger.getLogger(Application.class.getName());
    /**
     * GUI messages for locale specified in <code>resources/Locale.properties</code>
     */
    static ResourceBundle messages;
    static Locale locale;
    /**
     * Loads GUI messages for locale specified in 
     * <code>resources/Locale.properties</code> from 
     * <code>resources/MessagesBundle_&lt;language&gt;_&lt;country&gt;.properties</code>
     */
    private static void loadMessages() {
        Properties localeProperties = new Properties();
        InputStream in = Application.class.getResourceAsStream("/Locale.properties");
        try {
            localeProperties.load(in);
            locale = new Locale(
                    localeProperties.getProperty("language", DEFAULT_LANGUAGE),
                    localeProperties.getProperty("country", DEFAULT_COUNTRY));
        } catch (IOException e) {
            log.severe(e.toString());
            locale = new Locale(DEFAULT_LANGUAGE, DEFAULT_COUNTRY);
            new ExceptionReporter(e, false);
        }
        messages = ResourceBundle.getBundle("MessagesBundle", locale);
    }
    /**
     * Creates Event Dispatch Thread and loads GUI
     */
    private static void loadGUI() {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                new GUI().createAndShowGUI();
            }
        });
    }
    /**
     * Checks if Nimbus look & feel is available. If yes - sets Nimbus as  
     * current look & feel. Otherwise default look & feel is used.
     */
    private static void setLookAndFeel() {
        try {
            for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
                if ("Nimbus".equals(info.getName())) {
                    UIManager.setLookAndFeel(info.getClassName());
                    break;
                }
            }
        } catch (Exception e) {
            StringBuilder msg = new StringBuilder();
            msg.append(e.toString()).append(". ")
                .append("Default look & feel will be used");
            log.info(msg.toString());
        }
    }
    /**
     * Entry point.
     */
    public static void main(String[] args) {
        loadMessages();
        setLookAndFeel();
        loadGUI();
    }
}

```

**GUI.java**

```
package com.singularityfx.passgen;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.MessageFormat;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JFormattedTextField;
import javax.swing.JOptionPane;
import javax.swing.JFormattedTextField.AbstractFormatter;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.JTextArea;
import javax.swing.JTextField;

/**
 * Represents Graphical User Interface. Creates and configures GUI components.
 * Includes ActionListener to launch password generation process.
 */
@SuppressWarnings("serial")
public class GUI extends JPanel implements ActionListener {
    private static final int FRAME_MIN_WIDTH = 320;
    private static final int FRAME_MIN_HEIGHT = 0;
    private static final int BORDER_WIDTH = 5;
    private static final int DEFAULT_NUMBER_OF_CHARACTERS = 12;
    private static final int DAYS_IN_MONTH = 30;
    private static final BigDecimal SECOND = BigDecimal.valueOf(1);
    private static final BigDecimal MINUTE = BigDecimal.valueOf(60);
    private static final BigDecimal HOUR = BigDecimal.valueOf(3600);
    private static final BigDecimal DAY = BigDecimal.valueOf(3600 * 24);
    private static final BigDecimal MONTH = BigDecimal.valueOf(3600 * 24 * DAYS_IN_MONTH);
    private static final BigDecimal YEAR = BigDecimal.valueOf(3600 * 24 * 365);
    private PassGen passwordGenerator;
    private JLabel lblNumberOfChars = 
            new JLabel(Application.messages.getString("lblNumberOfChars"));
    private JFormattedTextField txtNumberOfChars = new JFormattedTextField();
    private JCheckBox useSpecial = new JCheckBox(
            Application.messages.getString("useSpecial"));
    private JLabel lblPW =
            new JLabel(Application.messages.getString("lblPW"));
    private JTextArea txtPassword = new JTextArea();
    private JButton btnGenerate = 
            new JButton(Application.messages.getString("btnGenerate"));
    private JTextArea txtInfo = new JTextArea(3, 0);
    /**
     * Verifies if the number of characters field contains a valid positive
     * integer. Otherwise displays a message and forbids to remove focus from
     * the field. The check is done upon an attempt to remove focus from the
     * field.
     */
    private InputVerifier inputVerifier = new InputVerifier() {
        @Override
        public boolean verify(JComponent input) {
            JFormattedTextField ftf = (JFormattedTextField)input;
            AbstractFormatter formatter = ftf.getFormatter();
            if (formatter != null) {
                String text = ftf.getText();
                try {
                    int number = Integer.parseInt(text);
                    return (number > 0);
                } catch (NumberFormatException e) {
                    return false;
                }
            }
            return true;
        }
        @Override
        public boolean shouldYieldFocus(JComponent input) {
            boolean shouldYieldFocus = verify(input);
            if (!shouldYieldFocus) {
                JOptionPane.showMessageDialog(
                        GUI.this, 
                        Application.messages.getString("invalidNumber"));
            }
            return shouldYieldFocus;
        }
    };

    public GUI() {
        setupPanel();
        addNumberOfCharactersField();
        addEmptySpace();
        addUseSpecialCheckbox();
        addEmptySpace();
        addGenerateButton();
        addEmptySpace();
        addHorizontalLine();
        addEmptySpace();
        addPasswordField();
        addEmptySpace();
        addTxtInfo();
        addEmptySpace();
    }

    private void setupPanel() {
        this.setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
        this.setBorder(BorderFactory.createEmptyBorder(
                BORDER_WIDTH, BORDER_WIDTH, BORDER_WIDTH, BORDER_WIDTH));
    }

    private void addNumberOfCharactersField() {
        lblNumberOfChars.setAlignmentX(CENTER_ALIGNMENT);
        this.add(lblNumberOfChars);
        txtNumberOfChars.setAlignmentX(CENTER_ALIGNMENT);
        txtNumberOfChars.setHorizontalAlignment(JTextField.LEFT);
        txtNumberOfChars.setValue(DEFAULT_NUMBER_OF_CHARACTERS);
        txtNumberOfChars.setInputVerifier(inputVerifier);
        txtNumberOfChars.addActionListener(this);
        this.add(txtNumberOfChars);
    }

    private void addUseSpecialCheckbox() {
        useSpecial.setAlignmentX(CENTER_ALIGNMENT);
        this.add(useSpecial);
    }

    private void addGenerateButton() {
        btnGenerate.setAlignmentX(CENTER_ALIGNMENT);
        btnGenerate.addActionListener(this);
        this.add(btnGenerate);
    }

    private void addPasswordField() {
        lblPW.setAlignmentX(CENTER_ALIGNMENT);
        this.add(lblPW);
        txtPassword.setAlignmentX(CENTER_ALIGNMENT);
        this.add(txtPassword);
    }

    private void addTxtInfo() {
        txtInfo.setAlignmentX(CENTER_ALIGNMENT);
        txtInfo.setLineWrap(true);
        txtInfo.setWrapStyleWord(true);
        txtInfo.setForeground(Color.GRAY);
        this.add(txtInfo);
    }

    private void addEmptySpace() {
        this.add(Box.createRigidArea(new Dimension(0, BORDER_WIDTH)));
    }

    private void addHorizontalLine() {
        this.add(new JSeparator());
    }

    public void createAndShowGUI() {
        JFrame frame = new JFrame(Application.messages.getString("windowTitle"));
        frame.setMinimumSize(new Dimension(FRAME_MIN_WIDTH, FRAME_MIN_HEIGHT));
        frame.add(this);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    /**
     * Listens to ActionEvent which is a signal to start password generation 
     * and displaying. Calls {@link #displayPw()} to do the actual job.
     * @see #displayPw()
     */
    @Override
    public void actionPerformed(ActionEvent evt) {
        if ((evt.getSource() == btnGenerate)
           || (evt.getSource() == txtNumberOfChars)) {
            displayPw();
        }
    }

    /**
     * Generates the password using {@link PassGen} and displays it in the GUI.
     * Displays approximate time to crack password on average desktop PC.
     */
    private void displayPw() {
        txtPassword.setText("");
        passwordGenerator = new PassGen(useSpecial.isSelected());
        int numberOfChars = 0;
        numberOfChars = (int) txtNumberOfChars.getValue();
        char[] pw = passwordGenerator.generate(numberOfChars);
        /* The following loop is used in order to avoid creation of additional
         * String, as String with password information cannot be manually 
         * removed from memory thus increasing security risk.
         */
        for (int i = 0; i < pw.length; i++) {
            txtPassword.append(Character.toString(pw[i]));
            pw[i] = 0; // Fill array with zeroes to remove password information
                       // from memory
        }
        txtInfo.setText(formTimeToCrackMessage());
    }

    private String formTimeToCrackMessage() {
        BigDecimal timeToCrack = passwordGenerator
                .calculateTimeToCrack((int)txtNumberOfChars.getValue());
        String numberOfUnits = null;
        String messageKey = "timeToCrack";

        if (timeToCrack.compareTo(SECOND) < 0) {
            numberOfUnits = String.format(
                    Application.locale,
                    "%1$.8f",
                    timeToCrack.doubleValue()
                    );
            messageKey += "Seconds";
        } else if (timeToCrack.compareTo(MINUTE) < 0) {
            messageKey += "Seconds";
        } else if (timeToCrack.compareTo(HOUR) < 0) {
            timeToCrack = timeToCrack.divide(MINUTE, 0, RoundingMode.HALF_DOWN);
            messageKey += "Minutes";
        } else if (timeToCrack.compareTo(DAY) < 0) {
            timeToCrack = timeToCrack.divide(HOUR, 0, RoundingMode.HALF_DOWN);
            messageKey += "Hours";
        } else if (timeToCrack.compareTo(MONTH) < 0) {
            timeToCrack = timeToCrack.divide(DAY, 0, RoundingMode.HALF_DOWN);
            messageKey += "Days";
        } else if (timeToCrack.compareTo(YEAR) < 0) {
            timeToCrack = timeToCrack.divide(MONTH, 0, RoundingMode.HALF_DOWN);
            messageKey += "Months";
        } else {
            timeToCrack = timeToCrack.divide(YEAR, 0, RoundingMode.HALF_DOWN);
            messageKey += "Years";
        }

        if (numberOfUnits == null) {
            numberOfUnits = timeToCrack.toBigInteger().toString();
        }

        return MessageFormat.format(
                Application.messages.getString(messageKey),
                numberOfUnits
                );
    }
}

```

**PassGen.java** - class responsible for generating the password

```
package com.singularityfx.passgen;

import java.math.BigDecimal;
import java.security.SecureRandom;
import java.util.Random;

/**
 * Pseudo-random password generator.
 * Generated password can be of any desired length and may consist of
 * alphanumerical characters only or alphanumerical characters plus special
 * symbols.
 */
public class PassGen {
    private static final String LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
    private static final String UPPERCASE = LOWERCASE.toUpperCase();
    private static final String DIGITS = "0123456789";
    private static final String SPECIAL = " `~!@#$%^&*()_-+={}[]|:;'<>,.?/\"\\";
    /**
     * Number of calculations per second used to determine time to crack the
     * password
     */
    private static final BigDecimal CALCULATIONS_PER_SECOND = 
            BigDecimal.valueOf(4_000_000_000L);
    /**
     * Characters that can be used in generated passwords 
     */
    private char[] characters;
    private Random random = new SecureRandom();
    /**
     * Constructor. Creates set of characters that can be used in generated 
     * passwords.
     * @param  useSpecial  if true, generated password will consist of
     * alphanumeric and special characters, otherwise alphanumeric only
     * @see #characters
     */
    public PassGen(boolean useSpecial) {
        StringBuilder sb = new StringBuilder();
        sb.append(LOWERCASE).append(UPPERCASE).append(DIGITS);
        if (useSpecial) {
            sb.append(SPECIAL);
        }
        characters = sb.toString().toCharArray();
    }

    /**
     * Generates the password of specified length from the characters contained
     * in {@link #characters}.
     * @param  length  length of password
     * @return         generated password of specified length
     */
    public char[] generate(int length) {
        char[] pw = new char[length];
        for (int i = 0; i < length; i++) {
            pw[i] = characters[random.nextInt(characters.length)];
        }
        return pw;
    }

    /**
     * Calculates approximate time to crack (brute force) the password using
     * an average desktop PC, which is determined as time to go through all 
     * possible combinations of characters. 
     * @param length   length of password
     * @return         time to crack password in seconds
     */
    public BigDecimal calculateTimeToCrack(int length) {
        BigDecimal alphabetSize = BigDecimal.valueOf(characters.length);
        BigDecimal possibleCombinations = alphabetSize.pow(length);
        return possibleCombinations
                .divide(CALCULATIONS_PER_SECOND);
    }
}

```

**ExceptionReporter.java** - utility class for displaying the critical error messages to the user

```
package com.singularityfx.utils;

import javax.swing.JOptionPane;

/**
 * Utility class used to display exception and additional optional custom 
 * message in a dialog window.
 *
 */
public class ExceptionReporter {
    private Exception ex;
    private boolean closeApp;
    private String customMessage;
    /**
     * Platform-specific line separator
     */
    private String ls = System.getProperty("line.separator");
    /**
     * Constructor used when custom error message is not required
     * @param ex       Exception to be reported
     * @param closeApp Flag indicating if application shall be closed after exception is reported
     */
    public ExceptionReporter(Exception ex, boolean closeApp) {
        this(ex, closeApp, null);
    }
    /**
     * Constructor used when custom error message is required
     * @param ex             Exception to be reported
     * @param closeApp       Flag indicating if application shall be closed after exception is reported
     * @param customMessage  Custom message to be displayed along with exception information
     */
    public ExceptionReporter(Exception ex, boolean closeApp, String customMessage) {
        this.ex = ex;
        this.closeApp = closeApp;
        this.customMessage = customMessage;
        report();
    }
    /**
     * Reports exception, then closes the application if necessary 
     * Exception is reported (as Exception.toString()) with additional custom 
     * error message (if supplied during construction of ExceptionReporter 
     * object) to the user via a dialog window.
     */
    private void report() {
        StringBuilder message = new StringBuilder();
        if (customMessage != null) { 
            message.append(customMessage)
                .append(ls)
                .append(ls);
        }
        if (ex != null) {
            message.append(ex.toString());
        }
        JOptionPane.showMessageDialog(null, message.toString(),
                "", JOptionPane.ERROR_MESSAGE);
        if (closeApp) {
            System.exit(1);
        }
    }
}

```

**MessagesBundleTest.java** - tests internationalization resource bundles for completeness

```
package com.singularityfx.passgen;

import java.util.Arrays;
import java.util.Collection;
import java.util.Locale;
import java.util.ResourceBundle;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

/**
 * Checks if messages bundles for all supported locales contains all 
 * required messages.
 * @author SingularityFX
 *
 */
@RunWith(Parameterized.class)
public class MessagesBundleTest {
    /**
     * List of required messages
     */
    private String[] messageKeys = {
        "windowTitle",
        "lblNumberOfChars",
        "lblPW",
        "btnGenerate",
        "invalidNumber",
        "useSpecial",
        "timeToCrackSeconds",
        "timeToCrackMinutes",
        "timeToCrackHours",
        "timeToCrackDays",
        "timeToCrackMonths",
        "timeToCrackYears",
    };
    private ResourceBundle messages;

    public MessagesBundleTest(String language, String country) {
        Locale locale = new Locale(language, country);
        this.messages = ResourceBundle.getBundle("MessagesBundle", locale); 
    }

    /**
     * @return representation of supported locales
     */
    @SuppressWarnings("rawtypes")
    @Parameterized.Parameters
    public static Collection locales() {
        return Arrays.asList(new Object[][] {
                {"en", "US"},
                {"ru", "RU"}        
        });
    }

    /**
     * Will fail with MissingResourceException if any of the message keys
     * are missing in the messages bundle.
     */
    @Test
    public void testMessagesAvailable() {
        for (String messageKey : messageKeys) {
            messages.getString(messageKey);
        }
    }

}

```

**PassGenTest.java** - tests `PassGen.calculateTimeToCrack()` method

```
package com.singularityfx.passgen;

import static org.junit.Assert.*;

import java.math.BigDecimal;
import org.junit.Test;

public class PassGenTest {
    private static final int NUMBER_OF_CHARACTERS = 12;
    private static final BigDecimal TIME_TO_CRACK_ALPHANUM =
            new BigDecimal("806566690599.474955264");
    private static final BigDecimal TIME_TO_CRACK_SPECIAL =
            new BigDecimal("135090021915659.24072265625");
    private static PassGen passwordGenerator;

    @Test
    public void testCalculateTimeToCrackAlphanumerical() {
        passwordGenerator = new PassGen(false);
        assertEquals(
                TIME_TO_CRACK_ALPHANUM, 
                passwordGenerator.calculateTimeToCrack(NUMBER_OF_CHARACTERS)
                );
    }

    @Test
    public void testCalculateTimeToCrackSpecial() {
        passwordGenerator = new PassGen(true);
        assertEquals(
                TIME_TO_CRACK_SPECIAL,
                passwordGenerator.calculateTimeToCrack(NUMBER_OF_CHARACTERS)
                );
    }
}

```

**MessagesBundle\_en\_US.properties** - localized messages

> 
> 
> ```none
> windowTitle = Password Generator
> lblNumberOfChars = Enter number of characters
> lblPW = Your generated password:
> btnGenerate = Generate password
> invalidNumber = Please enter a positive integer
> useSpecial = Use special symbols?
> timeToCrackSeconds = Time to crack password on average desktop PC is {0} second(s)
> timeToCrackMinutes = Time to crack password on average desktop PC is {0} minute(s)
> timeToCrackHours = Time to crack password on average desktop PC is {0} hour(s)
> timeToCrackDays = Time to crack password on average desktop PC is {0} day(s)
> timeToCrackMonths = Time to crack password on average desktop PC is {0} month(s)
> timeToCrackYears = Time to crack password on average desktop PC is {0} year(s)
> 
> ```
> 
>