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.
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(); } }
Now that we have a place to start, let's look at the core classes and interfaces that define the framework.
Severity.OK
,
Severity.WARNING
, and Severity.ERROR
.
Severity severity(); String formattedText(); Object key();
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.
ValidationMessage
interface are provided in the framework,
SimpleValidationMessage
and
PropertyValidationMessage
, both of which extend
AbstractValidationMessage
.
ValidationResult
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. ValidationResult validate();
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.
ValidationResult validate(T validationTarget);
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.
ValidationResult
(which in turn holds
ValidationMessage
s). It provides bound, read-only properties for
the result, severity, error and messages state. 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 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.
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 bold).
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(); } }
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:
ValidationUtils
class mentioned above.
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.
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.
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 bold.
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 in
buildLabelForegroundPanel()
by simply changing the foreground
color of its label to
ValidationComponentUtils.getMandatoryForeground()
.Username
is marked as mandatory in
buildComponentBackgroundPanel()
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 in
buildComponentBorderPanel()
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 on
ValidationComponentUtils
, 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
.
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.
Lance Finney thanks Michael Easter, Tom Wheeler, and Rob Smith for reviewing this article and providing useful suggestions.