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.
- import com.jgoodies.forms.builder.DefaultFormBuilder;
- import com.jgoodies.forms.factories.ButtonBarFactory;
- import com.jgoodies.forms.layout.FormLayout;
-
- import javax.swing.*;
- import java.awt.event.ActionEvent;
-
- public final class Unvalidated {
- private final JFrame frame = new JFrame("Unvalidated");
- private final JTextField name = new JTextField(30);
- private final JTextField username = new JTextField(30);
- private final JTextField phoneNumber = new JTextField(30);
-
- public Unvalidated() {
- this.frame.add(createPanel());
- this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
- this.frame.pack();
- }
-
- private JPanel createPanel() {
- FormLayout layout = new FormLayout("pref, 2dlu, pref:grow");
- DefaultFormBuilder builder = new DefaultFormBuilder(layout);
- int columnCount = builder.getColumnCount();
- builder.setDefaultDialogBorder();
-
- builder.append("Name", this.name);
- builder.append("Username", this.username);
- builder.append("Phone Number", this.phoneNumber);
-
- JPanel buttonBar = ButtonBarFactory.buildOKCancelBar(
- new JButton(new OkAction()), new JButton(new CancelAction()));
- builder.append(buttonBar, columnCount);
-
- return builder.getPanel();
- }
-
- private void show() {
- this.frame.setVisible(true);
- }
-
- private final class OkAction extends AbstractAction {
- private OkAction() {
- super("OK");
- }
-
- public void actionPerformed(ActionEvent e) {
- frame.dispose();
- }
- }
-
- private final class CancelAction extends AbstractAction {
- private CancelAction() {
- super("Cancel");
- }
-
- public void actionPerformed(ActionEvent e) {
- frame.dispose();
- }
- }
-
- public static void main(String[] args) {
- Unvalidated example = new Unvalidated();
- example.show();
- }
- }
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.OK
,Severity.WARNING
, andSeverity.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
.
ValidationResultValidationResult
encapsulates a list of ValidationMessage
s that are created by a validation. This class provides many convenience methods for adding messages, combining ValidationResult
s, 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 thevalidate(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 holdsValidationMessage
s). It provides bound, read-only properties for the result, severity, error and messages state.
Two default implementations of theValidationResultModel
interface are provided in the framework,DefaultValidationResultModel
andValidationResultModelContainer
, both of which extendAbstractValidationResultModel
.
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 DateFormatter
s and NumberFormatter
s, 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).
- import com.jgoodies.forms.builder.DefaultFormBuilder;
- import com.jgoodies.forms.factories.ButtonBarFactory;
- import com.jgoodies.forms.layout.FormLayout;
- import com.jgoodies.validation.ValidationResult;
- import com.jgoodies.validation.ValidationResultModel;
- import com.jgoodies.validation.util.DefaultValidationResultModel;
- import com.jgoodies.validation.util.ValidationUtils;
- import com.jgoodies.validation.view.ValidationResultViewFactory;
-
- import javax.swing.*;
- import java.awt.event.ActionEvent;
- import java.beans.PropertyChangeEvent;
- import java.beans.PropertyChangeListener;
- import java.util.regex.Matcher;
- import java.util.regex.Pattern;
-
- public final class Validation {
- private final JFrame frame = new JFrame("Validation");
- private final JTextField name = new JTextField(30);
- private final JTextField username = new JTextField(30);
- private final JTextField phoneNumber = new JTextField(30);
- private final ValidationResultModel validationResultModel =
- new DefaultValidationResultModel();
- private final Pattern phonePattern =
- Pattern.compile("\\b\\d{3}-\\d{3}-\\d{4}");
-
- public Validation() {
- this.validationResultModel.addPropertyChangeListener(
- new ValidationListener());
-
- this.frame.add(createPanel());
- this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
- this.frame.pack();
- }
-
- private JPanel createPanel() {
- FormLayout layout = new FormLayout("pref, 2dlu, pref:grow");
- DefaultFormBuilder builder = new DefaultFormBuilder(layout);
- int columnCount = builder.getColumnCount();
- builder.setDefaultDialogBorder();
-
- builder.append("Name", this.name);
- builder.append("Username", this.username);
- builder.append("Phone Number", this.phoneNumber);
-
- //add a component to show validation messages
- JComponent validationResultsComponent =
- ValidationResultViewFactory.createReportList(
- this.validationResultModel);
- builder.appendUnrelatedComponentsGapRow();
- builder.appendRow("fill:50dlu:grow");
- builder.nextLine(2);
- builder.append(validationResultsComponent, columnCount);
-
- JPanel buttonBar = ButtonBarFactory.buildOKCancelBar(
- new JButton(new OkAction()), new JButton(new CancelAction()));
- builder.append(buttonBar, columnCount);
-
- return builder.getPanel();
- }
-
- private void show() {
- this.frame.setVisible(true);
- }
- //validate each of the three input fields
- private ValidationResult validate() {
- ValidationResult validationResult = new ValidationResult();
-
- //validate the name field
- if (ValidationUtils.isEmpty(this.name.getText())) {
- validationResult.addError("The Name field can not be blank.");
- }
-
- //validate the username field
- if (ValidationUtils.isEmpty(this.username.getText())) {
- validationResult.addError("The Username field can not be blank.");
- } else if (!ValidationUtils.hasBoundedLength(
- this.username.getText(), 6, 12)) {
- validationResult.addError(
- "The Username field must be between 6 and 12 characters.");
- }
-
- //validate the phoneNumber field
- String phone = this.phoneNumber.getText();
- if (ValidationUtils.isEmpty(phone)) {
- validationResult.addError(
- "The Phone Number field can not be blank.");
- } else {
- Matcher matcher = this.phonePattern.matcher(phone);
- if (!matcher.matches()) {
- validationResult.addWarning(
- "The phone number must be a legal American number.");
- }
- }
-
- return validationResult;
- }
- private final class OkAction extends AbstractAction {
- private OkAction() {
- super("OK");
- }
-
- public void actionPerformed(ActionEvent e) {
- //don't close the frame on OK unless it validates
- ValidationResult validationResult = validate();
- validationResultModel.setResult(validationResult);
- if (!validationResultModel.hasErrors()) {
- frame.dispose();
- }
- }
- }
-
- private final class CancelAction extends AbstractAction {
- private CancelAction() {
- super("Cancel");
- }
-
- public void actionPerformed(ActionEvent e) {
- frame.dispose();
- }
- }
-
- //display informative dialogs for specific validation events
- private static final class ValidationListener
- implements PropertyChangeListener {
- public void propertyChange(PropertyChangeEvent evt) {
- String property = evt.getPropertyName();
- if (ValidationResultModel.PROPERTYNAME_RESULT.equals(property))
- {
- JOptionPane.showMessageDialog(null,
- "At least one validation result changed");
- } else
- if (ValidationResultModel.PROPERTYNAME_MESSAGES.equals(property))
- {
- if (Boolean.TRUE.equals(evt.getNewValue())) {
- JOptionPane.showMessageDialog(null,
- "Overall validation changed");
- }
- }
- }
- }
-
- public static void main(String[] args) {
- Validation example = new Validation();
- example.show();
- }
- }
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 ValidationMessage
s 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 ValidationMessage
s 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:
- The name must not be empty.
- The username not only must not be empty, but it also must be between 6 and 12 character long, inclusive. This validation uses a convenience method from the
ValidationUtils
class mentioned above. - The phone number must not be empty. Additionally, we validate that the phone number matches the pattern ###-###-####. However, since not every country in the world uses this format, this is just a warning.
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 ValidationMessage
s 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:
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:
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:
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.
- import com.jgoodies.forms.builder.DefaultFormBuilder;
- import com.jgoodies.forms.factories.ButtonBarFactory;
- import com.jgoodies.forms.layout.FormLayout;
- import com.jgoodies.validation.ValidationResult;
- import com.jgoodies.validation.ValidationResultModel;
- import com.jgoodies.validation.util.DefaultValidationResultModel;
- import com.jgoodies.validation.util.ValidationUtils;
- import com.jgoodies.validation.view.ValidationComponentUtils;
- import com.jgoodies.validation.view.ValidationResultViewFactory;
-
- import javax.swing.*;
- import java.awt.*;
- import java.awt.event.ActionEvent;
- import java.beans.PropertyChangeEvent;
- import java.beans.PropertyChangeListener;
- import java.util.regex.Matcher;
- import java.util.regex.Pattern;
-
- public final class InputHints {
-
- private final JFrame frame = new JFrame("InputHints");
- private final JLabel hintLabel = new JLabel();
- private final JTextField name = new JTextField(30);
- private final JTextField username = new JTextField(30);
- private final JTextField phoneNumber = new JTextField(30);
- private final ValidationResultModel validationResultModel =
- new DefaultValidationResultModel();
- private final Pattern phonePattern =
- Pattern.compile("\\b\\d{3}-\\d{3}-\\d{4}");
-
- public InputHints() {
- //create a hint for each of the three validated fields
- ValidationComponentUtils.setInputHint(name, "Enter a name.");
- ValidationComponentUtils.setInputHint(username,
- "Enter a username with 6-12 characters.");
- ValidationComponentUtils.setInputHint(phoneNumber,
- "Enter a phone number like 314-555-1212.");
- //update the hint based on which field has focus
- KeyboardFocusManager.getCurrentKeyboardFocusManager()
- .addPropertyChangeListener(new FocusChangeHandler());
-
- this.frame.add(createPanel());
- this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
- this.frame.pack();
- }
-
- private JPanel createPanel() {
- FormLayout layout = new FormLayout("pref, 2dlu, pref:grow");
- DefaultFormBuilder builder = new DefaultFormBuilder(layout);
- int columnCount = builder.getColumnCount();
- builder.setDefaultDialogBorder();
-
- //add the label that will show validation hints, with an icon
- hintLabel.setIcon(ValidationResultViewFactory.getInfoIcon());
- builder.append(this.hintLabel, columnCount);
-
- //add the three differently-decorated text fields
- builder.append(buildLabelForegroundPanel(), columnCount);
- builder.append(buildComponentBackgroundPanel(), columnCount);
- builder.append(buildComponentBorderPanel(), columnCount);
-
- JComponent validationResultsComponent =
- ValidationResultViewFactory.createReportList(
- this.validationResultModel);
- builder.appendUnrelatedComponentsGapRow();
- builder.appendRow("fill:50dlu:grow");
- builder.nextLine(2);
- builder.append(validationResultsComponent, columnCount);
-
- JPanel buttonBar = ButtonBarFactory.buildOKCancelBar(
- new JButton(new OkAction()), new JButton(new CancelAction()));
- builder.append(buttonBar, columnCount);
-
- return builder.getPanel();
- }
-
- //mark name as mandatory by changing the label's foreground color
- private JComponent buildLabelForegroundPanel() {
- FormLayout layout = new FormLayout("50dlu, 2dlu, pref:grow");
-
- DefaultFormBuilder builder = new DefaultFormBuilder(layout);
-
- JLabel orderNoLabel = new JLabel("Name");
- Color foreground = ValidationComponentUtils.getMandatoryForeground();
- orderNoLabel.setForeground(foreground);
- builder.append(orderNoLabel, this.name);
- return builder.getPanel();
- }
-
- //mark username as mandatory by changing the field's background color
- private JComponent buildComponentBackgroundPanel() {
- FormLayout layout = new FormLayout("50dlu, 2dlu, pref:grow");
-
- DefaultFormBuilder builder = new DefaultFormBuilder(layout);
-
- ValidationComponentUtils.setMandatory(this.username, true);
- builder.append("Username", this.username);
-
- ValidationComponentUtils.updateComponentTreeMandatoryBackground(
- builder.getPanel());
-
- return builder.getPanel();
- }
-
- //mark phoneNumber as mandatory by changing the field's border's color
- private JComponent buildComponentBorderPanel() {
- FormLayout layout = new FormLayout("50dlu, 2dlu, pref:grow");
-
- DefaultFormBuilder builder = new DefaultFormBuilder(layout);
-
- ValidationComponentUtils.setMandatory(this.phoneNumber, true);
- builder.append("Phone Number", this.phoneNumber);
-
- ValidationComponentUtils.updateComponentTreeMandatoryBorder(
- builder.getPanel());
-
- return builder.getPanel();
- }
-
- private void show() {
- //same as in Validation.java
- }
-
- private ValidationResult validate() {
- //same as in Validation.java
- }
-
- private final class OkAction extends AbstractAction {
- //same as in Validation.java
- }
-
- private final class CancelAction extends AbstractAction {
- //same as in Validation.java
- }
- //update the hint label's text based on which component has focus
- private final class FocusChangeHandler
- implements PropertyChangeListener {
- public void propertyChange(PropertyChangeEvent evt) {
- String propertyName = evt.getPropertyName();
- if ("permanentFocusOwner".equals(propertyName)) {
- Component focusOwner = KeyboardFocusManager
- .getCurrentKeyboardFocusManager().getFocusOwner();
-
- if (focusOwner instanceof JTextField) {
- JTextField field = (JTextField) focusOwner;
- String focusHint = (String) ValidationComponentUtils
- .getInputHint(field);
- hintLabel.setText(focusHint);
- } else {
- hintLabel.setText("");
- }
-
- }
- }
- }
-
- public static void main(String[] args) {
- InputHints example = new InputHints();
- example.show();
- }
- }
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:
Each of the three fields in required, and each uses a different visual indicator of the mandatory status.
Name
is marked as mandatory inbuildLabelForegroundPanel()
by simply changing the foreground color of its label toValidationComponentUtils.getMandatoryForeground()
.Username
is marked as mandatory inbuildComponentBackgroundPanel()
by changing the background color of the field to a specific color that the Validation framework uses for mandatory fields.Phone Number
is marked as mandatory inbuildComponentBorderPanel()
by changing the color of the field's border to a specific color that the Validation framework uses for mandatory fields.
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 Container
s 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
- [1] Validation Project home page
https://validation.dev.java.net/ - [2] JGoodies' Validation page
http://www.jgoodies.com/freeware/validationdemo/ - [3] JGoodies' WebStart Validation demo
http://www.jgoodies.com/download/demos/validation/validationdemo.jnlp - [4] Download page for JGoodies libraries, including Validation
http://www.jgoodies.com/downloads/libraries.html - [5] Desktop Java Live, a book with an entire chapter on the Validation framework as part of writing robust desktop applications
http://www.sourcebeat.com/books/desktopjavalive.html
Lance Finney thanks Michael Easter, Tom Wheeler, and Rob Smith for reviewing this article and providing useful suggestions.