This is a GUI-based random password generator, which supports internationalization.
It consists of 6 small classes:
Main classes:
Application
- entry pointGUI
- self-explanatoryPassGen
- class responsible for generating the passwordExceptionReporter
- utility class for displaying the critical error messages to the user
Test classes:
MessagesBundleTest
- tests internationalization resource bundles for completenessPassGenTest
- ensures that alphabet used for password generation is correct
Plus internationalization resource bundles which are not included here.
Please review on generally accepted rules / conventions / best practices and how can it be improved.
This is what the app looks like on Windows:
Application.java
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.
* @author SingularityFX
*
*/
public class Application {
private static Logger log = Logger.getLogger(Application.class.getName());
/**
* GUI messages for locale specified in <code>resources/Locale.properties</code>
*/
static ResourceBundle messages;
/**
* Loads GUI messages for locale specified in
* <code>resources/Locale.properties</code> from
* <code>resources/MessagesBundle_<language>_<country>.properties</code>
*/
private static void loadMessages() {
Properties localeProp = new Properties();
InputStream in = Application.class.getResourceAsStream("/Locale.properties");
try {
localeProp.load(in);
} catch (IOException e) {
log.severe(e.toString());
new ExceptionReporter(e, true);
}
Locale locale = new Locale(
localeProp.getProperty("language", "en"),
localeProp.getProperty("country", "US"));
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.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import com.singularityfx.utils.ExceptionReporter;
/**
* Represents Graphical User Interface. Creates and configures GUI components.
* Includes ActionListener to launch password generation process.
* @author SingularityFX
*/
@SuppressWarnings("serial")
public class GUI extends JPanel implements ActionListener {
private static final int FRAME_MIN_WIDTH = 300;
private static final int FRAME_MIN_HEIGHT = 0;
private final int BORDER_WIDTH = 5;
private JLabel lblNumberOfChars =
new JLabel(Application.messages.getString("lblNumberOfChars"));
/**
* Text field used to indicate number of characters in the generated password
*/
private JTextField txtNumberOfChars = new JTextField("8");
private JLabel lblPW =
new JLabel(Application.messages.getString("lblPW"));
/**
* Text area where generated password is displayed
*/
private JTextArea txtPW = new JTextArea();
/**
* Button launching password generation
*/
private JButton btnGenerate =
new JButton(Application.messages.getString("btnGenerate"));
public GUI() {
// Set up panel
this.setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
this.setBorder(BorderFactory.createEmptyBorder(
BORDER_WIDTH, BORDER_WIDTH, BORDER_WIDTH, BORDER_WIDTH));
// Set up lblNumberOfChars and add it to panel
lblNumberOfChars.setAlignmentX(CENTER_ALIGNMENT);
this.add(lblNumberOfChars);
// Set up txtNumberOfChars and add it to panel
txtNumberOfChars.setAlignmentX(CENTER_ALIGNMENT);
txtNumberOfChars.setHorizontalAlignment(JTextField.LEFT);
this.add(txtNumberOfChars);
// Add empty space
this.add(Box.createRigidArea(new Dimension(0, BORDER_WIDTH)));
// Set up lblPW and add it to panel
lblPW.setAlignmentX(CENTER_ALIGNMENT);
this.add(lblPW);
// Set up txtPW and add it to panel
txtPW.setAlignmentX(CENTER_ALIGNMENT);
this.add(txtPW);
// Add empty space
this.add(Box.createRigidArea(new Dimension(0, BORDER_WIDTH)));
// Set up btnGenerate and add it to panel
btnGenerate.setAlignmentX(CENTER_ALIGNMENT);
btnGenerate.addActionListener(this);
this.add(btnGenerate);
}
/**
* Launches GUI.
* Creates and configures JFrame, adds panel (represented by this class).
*/
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) {
displayPw();
}
}
/**
* Generates the password using {@link PassGen} and displays it in the GUI.
*/
private void displayPw() {
txtPW.setText("");
PassGen pg = new PassGen();
int numberOfChars = 0;
try {
numberOfChars = Integer.parseInt(txtNumberOfChars.getText());
} catch (NumberFormatException ex) {
new ExceptionReporter(null, false,
Application.messages.getString("numberFormatException"));
}
char[] pw = pg.generate(numberOfChars);
for (int i = 0; i < pw.length; i++) {
txtPW.append(Character.toString(pw[i]));
pw[i] = 0;
}
}
}
PassGen.java
package com.singularityfx.passgen;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* Pseudo-random password generator.
* Generated password can be of any desired length and will consist of
* alphanumerical characters only.
* @author SingularityFX
*
*/
public class PassGen {
/**
* List of characters that can be used in generated passwords
*/
private List<Character> alphabet = new ArrayList<Character>();
private Random random = new Random();
/**
* Start index for ASCII numerical symbols range
*/
private int asciiNumbersStart = 48;
/**
* End index for ASCII numerical symbols range
*/
private int asciiNumbersEnd = 57;
/**
* Start index for ASCII lowercase latin alphabetical symbols range
*/
private int asciiLowercaseStart = 97;
/**
* End index for ASCII lowercase latin alphabetical symbols range
*/
private int asciiLowercaseEnd = 122;
/**
* Start index for ASCII uppercase latin alphabetical symbols range
*/
private int asciiUppercaseStart = 65;
/**
* End index for ASCII uppercase latin alphabetical symbols range
*/
private int asciiUppercaseEnd = 90;
private int defaultPasswordLength = 12;
/**
* Sole constructor. Calls {@link #fillAlphabet(int, int)} for all
* applicable ASCII ranges.
* @see #fillAlphabet(int, int)
*/
public PassGen() {
fillAlphabet(asciiNumbersStart, asciiNumbersEnd);
fillAlphabet(asciiLowercaseStart, asciiLowercaseEnd);
fillAlphabet(asciiUppercaseStart, asciiUppercaseEnd);
}
/**
* Adds the specified range of ASCII table to the alphabet that can be
* used in password generation (including both startIndex and endIndex)
* @param startIndex start index of the range (inclusive)
* @param endIndex end index of the range (inclusive)
*/
private void fillAlphabet(int startIndex, int endIndex) {
for (int i = startIndex; i <= endIndex; i++) {
alphabet.add((char)i);
}
}
List<Character> getAlphabet() { // package access for unit testing purposes
return this.alphabet;
}
/**
* Generates the password of default length from the characters contained
* in {@link #alphabet}.
* @return generated password of default length
*/
public char[] generate() {
return generate(defaultPasswordLength);
}
/**
* Generates the password of specified length from the characters contained
* in {@link #alphabet}.
* @param length length of password to be generated
* @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] = alphabet.get(random.nextInt(alphabet.size()));
}
return pw;
}
}
ExceptionReporter.java
package com.singularityfx.utils;
import javax.swing.JOptionPane;
/**
* Utility class used to display unhandled exceptions.
* Displays unhandled exception to user in a dialog window.
* Shall be invoked from the <code>catch</code> clause
* @author SingularityFX
*
*/
public class ExceptionReporter {
/**
* Exception to be reported
*/
private Exception ex;
/**
* Flag indicating if application shall be closed after exception is reported
*/
private boolean closeApp;
/**
* Optional custom error message to be displayed to user
*/
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 {@link #ex}
* @param closeApp {@link #closeApp}
*/
public ExceptionReporter(Exception ex, boolean closeApp) {
this.ex = ex;
this.closeApp = closeApp;
report();
}
/**
* Constructor used when custom error message is required
* @param ex {@link #ex}
* @param closeApp {@link #closeApp}
* @param customMessage {@link #customMessage}
*/
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 (if supplied during construction of ExceptionReporter object) to
* the user via dialog window.
* Closes the application if {@link #closeApp} is <code>true</true>
*/
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(),
"Error", JOptionPane.ERROR_MESSAGE);
if (closeApp) {
System.exit(1);
}
}
}
MessagesBundleTest.java
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",
"numberFormatException",
};
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
package com.singularityfx.passgen;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.List;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Compares the actual alphabet used for password generation, which is
* constructed by specifying the appropriate ASCII ranges (referred to below as
* "actual alphabet") to alphabet constructed from Strings (referred to below
* as "String alphabet").
* <p>
* testAlphabetWantedIn() and testAlphabetUnwantedOut() together ensures the
* equality between "actual alphabet" and "String alphabet".
* @author SingularityFX
*
*/
public class PassGenTest {
/**
* Password generator instance
*/
private PassGen pg = new PassGen();
/**
* "String alphabet"
* @see Class
*/
private static List<Character> alphabet = new ArrayList<Character>();
/**
* Constructs the "String alphabet"
* @see Class
*/
@BeforeClass
public static void setUp() {
String alphabetString = "abcdefghijklmnopqrstuvwxyz";
alphabetString += alphabetString.toUpperCase();
alphabetString += "0123456789";
char[] alphabetChars = alphabetString.toCharArray();
for (int i = 0; i < alphabetChars.length; i++) {
alphabet.add(alphabetChars[i]);
}
}
/**
* Ensures that all characters from "String alphabet" are inside
* "actual alphabet".
* @see Class
*/
@Test
public void testAlphabetWantedIn() {
for (Character c : alphabet) {
assertTrue(pg.getAlphabet().contains(c));
}
}
/**
* Ensures that all characters from "actual alphabet" are inside
* "String alphabet"
* @see Class
*/
@Test
public void testAlphabetUnwantedOut() {
for (Character c : pg.getAlphabet()) {
assertTrue(alphabet.contains(c));
}
}
}
Random
? \$\endgroup\$(int) '0'
, or better even,"0123456789"
\$\endgroup\$https
. \$\endgroup\$