Intro to JGoodies Validation

Intro to JGoodies Validation

By Lance Finney, OCI Senior Software Engineer

July 2007


Introduction

Allowing users to enter data is a vital part of almost every application. However, making sure that the data makes sense is a challenge in many different cases. Users might enter words in a field that requires only numbers, or they might create a password that is too small, or they might enter a phone number with the wrong number of digits.

To ensure the integrity of freely-entered data, Java introduced the InputVerifier in J2SE 1.3. Unfortunately, as others have noted, InputVerifier "is not very interesting. All it does is prevent the user from tabbing or mousing out of the component in question. That's pretty boring and also not very helpful to the user at helping them figure out why what they entered is invalid." A more flexible and more complete alternative is JGoodies Validation, created by Karsten Lentzsch, the creator of the previously-reviewed frameworks JGoodies Forms and JGoodies Binding.

Unlike InputValidator, the Validation framework allows validation at several points (at key change, at focus loss, etc.), presents several different ways to indicate an error condition (text fields, icons, color, etc.), and can give the user hints on what input is valid.

Simple Dialog Without Validation

For this article, let's create a basic dialog form that could use validation. Imagine this as a user signup form, where a user will enter a name, create a username, and enter a phone number. Later, we will require values in all three fields, require a specific length for the username, and show a warning if the phone number does not match the standard American format.

This layout uses FormLayout from JGoodies Forms.

Simple Dialogue Without Validation
  1. import com.jgoodies.forms.builder.DefaultFormBuilder;
  2. import com.jgoodies.forms.factories.ButtonBarFactory;
  3. import com.jgoodies.forms.layout.FormLayout;
  4.  
  5. import javax.swing.*;
  6. import java.awt.event.ActionEvent;
  7.  
  8. public final class Unvalidated {
  9. private final JFrame frame = new JFrame("Unvalidated");
  10. private final JTextField name = new JTextField(30);
  11. private final JTextField username = new JTextField(30);
  12. private final JTextField phoneNumber = new JTextField(30);
  13.  
  14. public Unvalidated() {
  15. this.frame.add(createPanel());
  16. this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  17. this.frame.pack();
  18. }
  19.  
  20. private JPanel createPanel() {
  21. FormLayout layout = new FormLayout("pref, 2dlu, pref:grow");
  22. DefaultFormBuilder builder = new DefaultFormBuilder(layout);
  23. int columnCount = builder.getColumnCount();
  24. builder.setDefaultDialogBorder();
  25.  
  26. builder.append("Name", this.name);
  27. builder.append("Username", this.username);
  28. builder.append("Phone Number", this.phoneNumber);
  29.  
  30. JPanel buttonBar = ButtonBarFactory.buildOKCancelBar(
  31. new JButton(new OkAction()), new JButton(new CancelAction()));
  32. builder.append(buttonBar, columnCount);
  33.  
  34. return builder.getPanel();
  35. }
  36.  
  37. private void show() {
  38. this.frame.setVisible(true);
  39. }
  40.  
  41. private final class OkAction extends AbstractAction {
  42. private OkAction() {
  43. super("OK");
  44. }
  45.  
  46. public void actionPerformed(ActionEvent e) {
  47. frame.dispose();
  48. }
  49. }
  50.  
  51. private final class CancelAction extends AbstractAction {
  52. private CancelAction() {
  53. super("Cancel");
  54. }
  55.  
  56. public void actionPerformed(ActionEvent e) {
  57. frame.dispose();
  58. }
  59. }
  60.  
  61. public static void main(String[] args) {
  62. Unvalidated example = new Unvalidated();
  63. example.show();
  64. }
  65. }

Core Classes

Now that we have a place to start, let's look at the core classes and interfaces that define the framework.

Severity
A typesafe enumeration that defines the three possible states of an individual validation, Severity.OKSeverity.WARNING, and Severity.ERROR.
 
ValidationMessage
An interface that defines the results of a validation. It requires the following three methods:
        Severity severity();
        String formattedText();
        Object key();

     The key() method allows a loosely-coupled association between message and view. This association is established by the message key that can be shared between messages, validators, views, and other parties. 
Two default implementations of the ValidationMessage interface are provided in the framework, SimpleValidationMessage and PropertyValidationMessage, both of which extend AbstractValidationMessage.

ValidationResult
ValidationResult encapsulates a list of ValidationMessages that are created by a validation. This class provides many convenience methods for adding messages, combining ValidationResults, retrieving message text, retrieving all messages of a certain Severity, retrieving the highest severity represented in the list, etc.

Validatable
An interface for objects that can self-validate. It requires the following method:

      ValidationResult validate();

Note: Before version 2.0 of the framework (released May 21, 2007), this interface was called Validator (or even ValidationCapable before version 1.2). Because of this change and a few other changes, version 2.0 is binary-incompatible with previous versions.

Validator
An interface for objects that can validate other objects. It requires the following method:

       ValidationResult validate(T validationTarget);
Note: Before version 2.0 of the framework, the interface with the same name served the purpose now served by the Validatable interface, and the signature of the validate(T validationTarget) method in this interface has changed. Because of this change and a few other changes, version 2.0 is binary-incompatible with previous versions. Also, version 2.0 uses Java 5 features, as can be seen in the parameterization of this interface.
 
ValidationResultModel
An interface to define a model that holds a ValidationResult (which in turn holds ValidationMessages). It provides bound, read-only properties for the result, severity, error and messages state.
Two default implementations of the ValidationResultModel interface are provided in the framework, DefaultValidationResultModel and ValidationResultModelContainer, both of which extend AbstractValidationResultModel.

In addition to the core classes, there are utility classes like ValidationUtils (very similar to StringUtils in the Jakarta Commons framework, but with more validation-specific static methods), some useful custom DateFormatters and NumberFormatters, and some adapters for some Swing objects like JTable and JList.

Note that these additional utility classes are the only parts of the framework that use Swing; there is no dependency on Swing in the core classes. That means that the core validation logic can be placed at a different level of the application than the GUI. It also means that the core of the Validation framework can be used for SWT applications or even for command-line applications.

Validation

Now that we have seen the core classes, let's use some of them to add validation to the form we used above (important additions are highlighted).

  1. import com.jgoodies.forms.builder.DefaultFormBuilder;
  2. import com.jgoodies.forms.factories.ButtonBarFactory;
  3. import com.jgoodies.forms.layout.FormLayout;
  4. import com.jgoodies.validation.ValidationResult;
  5. import com.jgoodies.validation.ValidationResultModel;
  6. import com.jgoodies.validation.util.DefaultValidationResultModel;
  7. import com.jgoodies.validation.util.ValidationUtils;
  8. import com.jgoodies.validation.view.ValidationResultViewFactory;
  9.  
  10. import javax.swing.*;
  11. import java.awt.event.ActionEvent;
  12. import java.beans.PropertyChangeEvent;
  13. import java.beans.PropertyChangeListener;
  14. import java.util.regex.Matcher;
  15. import java.util.regex.Pattern;
  16.  
  17. public final class Validation {
  18. private final JFrame frame = new JFrame("Validation");
  19. private final JTextField name = new JTextField(30);
  20. private final JTextField username = new JTextField(30);
  21. private final JTextField phoneNumber = new JTextField(30);
  22. private final ValidationResultModel validationResultModel =
  23. new DefaultValidationResultModel();
  24. private final Pattern phonePattern =
  25. Pattern.compile("\\b\\d{3}-\\d{3}-\\d{4}");
  26.  
  27. public Validation() {
  28. this.validationResultModel.addPropertyChangeListener(
  29. new ValidationListener());
  30.  
  31. this.frame.add(createPanel());
  32. this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  33. this.frame.pack();
  34. }
  35.  
  36. private JPanel createPanel() {
  37. FormLayout layout = new FormLayout("pref, 2dlu, pref:grow");
  38. DefaultFormBuilder builder = new DefaultFormBuilder(layout);
  39. int columnCount = builder.getColumnCount();
  40. builder.setDefaultDialogBorder();
  41.  
  42. builder.append("Name", this.name);
  43. builder.append("Username", this.username);
  44. builder.append("Phone Number", this.phoneNumber);
  45.  
  46. //add a component to show validation messages
  47. JComponent validationResultsComponent =
  48. ValidationResultViewFactory.createReportList(
  49. this.validationResultModel);
  50. builder.appendUnrelatedComponentsGapRow();
  51. builder.appendRow("fill:50dlu:grow");
  52. builder.nextLine(2);
  53. builder.append(validationResultsComponent, columnCount);
  54.  
  55. JPanel buttonBar = ButtonBarFactory.buildOKCancelBar(
  56. new JButton(new OkAction()), new JButton(new CancelAction()));
  57. builder.append(buttonBar, columnCount);
  58.  
  59. return builder.getPanel();
  60. }
  61.  
  62. private void show() {
  63. this.frame.setVisible(true);
  64. }
  1. //validate each of the three input fields
  2. private ValidationResult validate() {
  3. ValidationResult validationResult = new ValidationResult();
  4.  
  5. //validate the name field
  6. if (ValidationUtils.isEmpty(this.name.getText())) {
  7. validationResult.addError("The Name field can not be blank.");
  8. }
  9.  
  10. //validate the username field
  11. if (ValidationUtils.isEmpty(this.username.getText())) {
  12. validationResult.addError("The Username field can not be blank.");
  13. } else if (!ValidationUtils.hasBoundedLength(
  14. this.username.getText(), 6, 12)) {
  15. validationResult.addError(
  16. "The Username field must be between 6 and 12 characters.");
  17. }
  18.  
  19. //validate the phoneNumber field
  20. String phone = this.phoneNumber.getText();
  21. if (ValidationUtils.isEmpty(phone)) {
  22. validationResult.addError(
  23. "The Phone Number field can not be blank.");
  24. } else {
  25. Matcher matcher = this.phonePattern.matcher(phone);
  26. if (!matcher.matches()) {
  27. validationResult.addWarning(
  28. "The phone number must be a legal American number.");
  29. }
  30. }
  31.  
  32. return validationResult;
  33. }
  1. private final class OkAction extends AbstractAction {
  2. private OkAction() {
  3. super("OK");
  4. }
  5.  
  6. public void actionPerformed(ActionEvent e) {
  7. //don't close the frame on OK unless it validates
  8. ValidationResult validationResult = validate();
  9. validationResultModel.setResult(validationResult);
  10. if (!validationResultModel.hasErrors()) {
  11. frame.dispose();
  12. }
  13. }
  14. }
  15.  
  16. private final class CancelAction extends AbstractAction {
  17. private CancelAction() {
  18. super("Cancel");
  19. }
  20.  
  21. public void actionPerformed(ActionEvent e) {
  22. frame.dispose();
  23. }
  24. }
  25.  
  26. //display informative dialogs for specific validation events
  27. private static final class ValidationListener
  28. implements PropertyChangeListener {
  29. public void propertyChange(PropertyChangeEvent evt) {
  30. String property = evt.getPropertyName();
  31. if (ValidationResultModel.PROPERTYNAME_RESULT.equals(property))
  32. {
  33. JOptionPane.showMessageDialog(null,
  34. "At least one validation result changed");
  35. } else
  36. if (ValidationResultModel.PROPERTYNAME_MESSAGES.equals(property))
  37. {
  38. if (Boolean.TRUE.equals(evt.getNewValue())) {
  39. JOptionPane.showMessageDialog(null,
  40. "Overall validation changed");
  41. }
  42. }
  43. }
  44. }
  45.  
  46. public static void main(String[] args) {
  47. Validation example = new Validation();
  48. example.show();
  49. }
  50. }

Description of New Code

There's a lot of code here, so let's go from the top to the bottom looking at the new code.

We have declared two new instance variables. validationResultModel will hold onto and organize the ValidationMessages for us. phonePattern uses a regex to define the legal type of phone number we will accept (i.e. 314-555-1212); it will be used in later validation.

In the constructor, we have added ValidationListener as a listener to the validationResultModel to demonstrate some user notification. The effect of this listener will be described in more detail later.

Within createPanel() method, we have added a report list created by ValidationResultViewFactory. This report list is a custom JList that is blank if there are no known validation problems, but then it shows a listing of the ValidationMessages with an icon indicating the severity of each. The ValidationResultViewFactory that creates this JList for us also has methods that create convenient JTextArea and JTextPane components as well.

The new validate() method is the core of the validation. Here, we validate the following conditions:

Within the OkAction, we added a check that will dispose of the frame only if there are no validation errors. If we used validationResultModel.hasErrors() instead of validationResultModel.hasMessages(), then all warnings would have to resolved, too.

The new ValidationListener inner class was added to the validationResultModel in the constructor. The effect of this listener is that the user is notified when the state of validation (pass or fail) has changed (upon clicking the "OK" button), and when the list of ValidationMessages changes. So, the first time an invalid state is found, both "At least one validation result changed" and "Overall validation changed" will be displayed in popup dialogs. From then on, whenever one or more validations change (such as entering a value for the name field), the "At least one validation result changed" message will be displayed. In a real application, these popup dialogs would be annoying, but it is included here as an example.

Effects of the New Validation

On launch, this version looks slightly different because of the space reserved for the report list:

Effects of Validation

If we click the OK button now, the current state will be evaluated, and an error will be recorded for each of the three fields. After disposing of the two notification dialogs described above, we see the following new state:

Blank Validation

This shows very clearly the error messages in a list with icons indicating the severity.

In a first attempt to resolve the issues, we put a "z" in each field and click the OK button again. Once again, the current state is evaluated. The new value is legal for the name field, is illegal for the username field, and causes a warning in the phone number field. Because we have changed the validation state of one or more of the fields, we again have to close the "At least one validation result changed" dialog which comes from listening for changes to messages in the validationResultModel. However, the "Overall validation changed" dialog does not appear because the overall state (failure) has not changed. After disposing of the one dialog, this is the state:

Basic Validation 2

This again shows very clearly the validation messages in a list with icons indicating the severity (one is a warning and one is an error), and a message is given telling us how to resolve the problem.

The last step will be to change the username value to "validation," a legal value. Now, when we click on the OK button, the "Validation has been performed" dialog appears (because we have gone from an invalid state to a valid state), and the frame is disposed. Note that the form will close even though there is still a warning because of the invalid phone number; this happens because we told the OkAction to check for errors, not messages.

Adding Hints

In addition to enabling the validation itself, the Validation framework provides some nice hints and conveniences for the user. The following class is based on Validation, but replaces the ValidationListener with a FocusChangeHandler that updates a JLabel with a hint based on the field with focus. It also uses three different methods from the Validation Framework to provide a visual indication that a field is required (in a real application, only one of the three approaches would be used). Important additions since Validation are highlighted.

  1. import com.jgoodies.forms.builder.DefaultFormBuilder;
  2. import com.jgoodies.forms.factories.ButtonBarFactory;
  3. import com.jgoodies.forms.layout.FormLayout;
  4. import com.jgoodies.validation.ValidationResult;
  5. import com.jgoodies.validation.ValidationResultModel;
  6. import com.jgoodies.validation.util.DefaultValidationResultModel;
  7. import com.jgoodies.validation.util.ValidationUtils;
  8. import com.jgoodies.validation.view.ValidationComponentUtils;
  9. import com.jgoodies.validation.view.ValidationResultViewFactory;
  10.  
  11. import javax.swing.*;
  12. import java.awt.*;
  13. import java.awt.event.ActionEvent;
  14. import java.beans.PropertyChangeEvent;
  15. import java.beans.PropertyChangeListener;
  16. import java.util.regex.Matcher;
  17. import java.util.regex.Pattern;
  18.  
  19. public final class InputHints {
  20.  
  21. private final JFrame frame = new JFrame("InputHints");
  22. private final JLabel hintLabel = new JLabel();
  23. private final JTextField name = new JTextField(30);
  24. private final JTextField username = new JTextField(30);
  25. private final JTextField phoneNumber = new JTextField(30);
  26. private final ValidationResultModel validationResultModel =
  27. new DefaultValidationResultModel();
  28. private final Pattern phonePattern =
  29. Pattern.compile("\\b\\d{3}-\\d{3}-\\d{4}");
  30.  
  31. public InputHints() {
  32. //create a hint for each of the three validated fields
  33. ValidationComponentUtils.setInputHint(name, "Enter a name.");
  34. ValidationComponentUtils.setInputHint(username,
  35. "Enter a username with 6-12 characters.");
  36. ValidationComponentUtils.setInputHint(phoneNumber,
  37. "Enter a phone number like 314-555-1212.");
  38. //update the hint based on which field has focus
  39. KeyboardFocusManager.getCurrentKeyboardFocusManager()
  40. .addPropertyChangeListener(new FocusChangeHandler());
  41.  
  42. this.frame.add(createPanel());
  43. this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  44. this.frame.pack();
  45. }
  46.  
  47. private JPanel createPanel() {
  48. FormLayout layout = new FormLayout("pref, 2dlu, pref:grow");
  49. DefaultFormBuilder builder = new DefaultFormBuilder(layout);
  50. int columnCount = builder.getColumnCount();
  51. builder.setDefaultDialogBorder();
  52.  
  53. //add the label that will show validation hints, with an icon
  54. hintLabel.setIcon(ValidationResultViewFactory.getInfoIcon());
  55. builder.append(this.hintLabel, columnCount);
  56.  
  57. //add the three differently-decorated text fields
  58. builder.append(buildLabelForegroundPanel(), columnCount);
  59. builder.append(buildComponentBackgroundPanel(), columnCount);
  60. builder.append(buildComponentBorderPanel(), columnCount);
  61.  
  62. JComponent validationResultsComponent =
  63. ValidationResultViewFactory.createReportList(
  64. this.validationResultModel);
  65. builder.appendUnrelatedComponentsGapRow();
  66. builder.appendRow("fill:50dlu:grow");
  67. builder.nextLine(2);
  68. builder.append(validationResultsComponent, columnCount);
  69.  
  70. JPanel buttonBar = ButtonBarFactory.buildOKCancelBar(
  71. new JButton(new OkAction()), new JButton(new CancelAction()));
  72. builder.append(buttonBar, columnCount);
  73.  
  74. return builder.getPanel();
  75. }
  76.  
  1. //mark name as mandatory by changing the label's foreground color
  2. private JComponent buildLabelForegroundPanel() {
  3. FormLayout layout = new FormLayout("50dlu, 2dlu, pref:grow");
  4.  
  5. DefaultFormBuilder builder = new DefaultFormBuilder(layout);
  6.  
  7. JLabel orderNoLabel = new JLabel("Name");
  8. Color foreground = ValidationComponentUtils.getMandatoryForeground();
  9. orderNoLabel.setForeground(foreground);
  10. builder.append(orderNoLabel, this.name);
  11. return builder.getPanel();
  12. }
  13.  
  14. //mark username as mandatory by changing the field's background color
  15. private JComponent buildComponentBackgroundPanel() {
  16. FormLayout layout = new FormLayout("50dlu, 2dlu, pref:grow");
  17.  
  18. DefaultFormBuilder builder = new DefaultFormBuilder(layout);
  19.  
  20. ValidationComponentUtils.setMandatory(this.username, true);
  21. builder.append("Username", this.username);
  22.  
  23. ValidationComponentUtils.updateComponentTreeMandatoryBackground(
  24. builder.getPanel());
  25.  
  26. return builder.getPanel();
  27. }
  28.  
  29. //mark phoneNumber as mandatory by changing the field's border's color
  30. private JComponent buildComponentBorderPanel() {
  31. FormLayout layout = new FormLayout("50dlu, 2dlu, pref:grow");
  32.  
  33. DefaultFormBuilder builder = new DefaultFormBuilder(layout);
  34.  
  35. ValidationComponentUtils.setMandatory(this.phoneNumber, true);
  36. builder.append("Phone Number", this.phoneNumber);
  37.  
  38. ValidationComponentUtils.updateComponentTreeMandatoryBorder(
  39. builder.getPanel());
  40.  
  41. return builder.getPanel();
  42. }
  43.  
  44. private void show() {
  45. //same as in Validation.java
  46. }
  47.  
  48. private ValidationResult validate() {
  49. //same as in Validation.java
  50. }
  51.  
  52. private final class OkAction extends AbstractAction {
  53. //same as in Validation.java
  54. }
  55.  
  56. private final class CancelAction extends AbstractAction {
  57. //same as in Validation.java
  58. }
  1. //update the hint label's text based on which component has focus
  2. private final class FocusChangeHandler
  3. implements PropertyChangeListener {
  4. public void propertyChange(PropertyChangeEvent evt) {
  5. String propertyName = evt.getPropertyName();
  6. if ("permanentFocusOwner".equals(propertyName)) {
  7. Component focusOwner = KeyboardFocusManager
  8. .getCurrentKeyboardFocusManager().getFocusOwner();
  9.  
  10. if (focusOwner instanceof JTextField) {
  11. JTextField field = (JTextField) focusOwner;
  12. String focusHint = (String) ValidationComponentUtils
  13. .getInputHint(field);
  14. hintLabel.setText(focusHint);
  15. } else {
  16. hintLabel.setText("");
  17. }
  18.  
  19. }
  20. }
  21. }
  22.  
  23. public static void main(String[] args) {
  24. InputHints example = new InputHints();
  25. example.show();
  26. }
  27. }

Input hints are defined in the constructor for each of the three fields using the ValidationComponentUtils utility, and the FocusChangeHandler pulls the current focus hint from the ValidationComponentUtils as necessary when the focus changes.

This is what the program looks like when the Phone Number field has focus:

Input Hints

Each of the three fields in required, and each uses a different visual indicator of the mandatory status.

Interestingly, neither the Username field nor the Phone Number field is directly modified to set the mandatory color. Instead, each is first marked by a call to ValidationComponentUtils.setMandatory(JComponent comp, boolean mandatory). This call sets a per-instance value on the JComponent using the rarely-used putClientProperty(Object key, Object value) method. Later, when either the updateComponentTreeMandatoryBackground(Container) or updateComponentTreeMandatoryBorder(Container) method is called onValidationComponentUtils, the ValidationComponentUtils uses a visitor pattern to walk through the Swing component tree and decorate all the mandatory fields with the requested indicator. If we had more mandatory fields in the same Container, they would all be decorated by the same single method call.

In this example, the Username field and the Phone Number field required different Containers in order to demonstrate the different behavior decorated to the fields. Of course, in a real application, the same approach would be used for all mandatory decoration, so the fields would be on the same JPanel.

Summary

JGoodies Validation simplifies user input validation and notification for Swing applications. In this article, we have seen the power of the basic validation framework and the usability features of the framework that assist users with data requirements.

There are many other powerful features of the JGoodies framework, particularly when used in combination with the JGoodies Binding framework. To see even more power that the Validation framework gives you in the location, structure, timing, and presentation of validation and its results, look at JGoodies' excellent WebStart-powered Validation demo.

The code in this article was built using version 2.0.0 of JGoodies Validation and version 1.1.0 of JGoodies Forms, both available for free from JGoodies.

References

Lance Finney thanks Michael Easter, Tom Wheeler, and Rob Smith for reviewing this article and providing useful suggestions.