July 2007: 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.

importcom.jgoodies.forms.builder.DefaultFormBuilder;

importcom.jgoodies.forms.factories.ButtonBarFactory;

importcom.jgoodies.forms.layout.FormLayout;

importjavax.swing.*;

importjava.awt.event.ActionEvent;

publicfinalclass Unvalidated {

privatefinalJFrame frame =newJFrame("Unvalidated");

privatefinalJTextField name =newJTextField(30);

privatefinalJTextField username =newJTextField(30);

privatefinalJTextField phoneNumber =newJTextField(30);

public Unvalidated(){

this.frame.add(createPanel());

this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

this.frame.pack();

}

privateJPanel 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(

newJButton(new OkAction()), newJButton(new CancelAction()));

builder.append(buttonBar, columnCount);

return builder.getPanel();

}

privatevoid show(){

this.frame.setVisible(true);

}

privatefinalclass OkAction extendsAbstractAction{

private OkAction(){

super("OK");

}

publicvoid actionPerformed(ActionEvent e){

frame.dispose();

}

}

privatefinalclass CancelAction extendsAbstractAction{

private CancelAction(){

super("Cancel");

}

publicvoid actionPerformed(ActionEvent e){

frame.dispose();

}

}

publicstaticvoid 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, 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.

ValidationResultValidationResult 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.ValidatableAn 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).

importcom.jgoodies.forms.builder.DefaultFormBuilder;

importcom.jgoodies.forms.factories.ButtonBarFactory;

importcom.jgoodies.forms.layout.FormLayout;

importcom.jgoodies.validation.ValidationResult;

importcom.jgoodies.validation.ValidationResultModel;

importcom.jgoodies.validation.util.DefaultValidationResultModel;

importcom.jgoodies.validation.util.ValidationUtils;

importcom.jgoodies.validation.view.ValidationResultViewFactory;

importjavax.swing.*;

importjava.awt.event.ActionEvent;

importjava.beans.PropertyChangeEvent;

importjava.beans.PropertyChangeListener;

importjava.util.regex.Matcher;

importjava.util.regex.Pattern;

publicfinalclass Validation {

privatefinalJFrame frame =newJFrame("Validation");

privatefinalJTextField name =newJTextField(30);

privatefinalJTextField username =newJTextField(30);

privatefinalJTextField phoneNumber =newJTextField(30);

privatefinal ValidationResultModel validationResultModel =

new DefaultValidationResultModel();

privatefinal 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();

}

privateJPanel 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(

newJButton(new OkAction()), newJButton(new CancelAction()));

builder.append(buttonBar, columnCount);

return builder.getPanel();

}

privatevoid 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.");

}elseif(!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;

}

privatefinalclass OkAction extendsAbstractAction{

private OkAction(){

super("OK");

}

publicvoid actionPerformed(ActionEvent e){

//don't close the frame on OK unless it validates

ValidationResult validationResult = validate();

validationResultModel.setResult(validationResult);

if(!validationResultModel.hasErrors()){

frame.dispose();

}

}

}

privatefinalclass CancelAction extendsAbstractAction{

private CancelAction(){

super("Cancel");

}

publicvoid actionPerformed(ActionEvent e){

frame.dispose();

}

}

//display informative dialogs for specific validation events

privatestaticfinalclass ValidationListener

implementsPropertyChangeListener{

publicvoid 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");

}

}

}

}

publicstaticvoid 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 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:

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 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:

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.

importcom.jgoodies.forms.builder.DefaultFormBuilder;

importcom.jgoodies.forms.factories.ButtonBarFactory;

importcom.jgoodies.forms.layout.FormLayout;

importcom.jgoodies.validation.ValidationResult;

importcom.jgoodies.validation.ValidationResultModel;

importcom.jgoodies.validation.util.DefaultValidationResultModel;

importcom.jgoodies.validation.util.ValidationUtils;

importcom.jgoodies.validation.view.ValidationComponentUtils;

importcom.jgoodies.validation.view.ValidationResultViewFactory;

importjavax.swing.*;

importjava.awt.*;

importjava.awt.event.ActionEvent;

importjava.beans.PropertyChangeEvent;

importjava.beans.PropertyChangeListener;

importjava.util.regex.Matcher;

importjava.util.regex.Pattern;

publicfinalclass InputHints {

privatefinalJFrame frame =newJFrame("InputHints");

privatefinalJLabel hintLabel =newJLabel();

privatefinalJTextField name =newJTextField(30);

privatefinalJTextField username =newJTextField(30);

privatefinalJTextField phoneNumber =newJTextField(30);

privatefinal ValidationResultModel validationResultModel =

new DefaultValidationResultModel();

privatefinal 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();

}

privateJPanel 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(

newJButton(new OkAction()), newJButton(new CancelAction()));

builder.append(buttonBar, columnCount);

return builder.getPanel();

}

//mark name as mandatory by changing the label's foreground color

privateJComponent buildLabelForegroundPanel(){

FormLayout layout =new FormLayout("50dlu, 2dlu, pref:grow");

DefaultFormBuilder builder =new DefaultFormBuilder(layout);

JLabel orderNoLabel =newJLabel("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

privateJComponent 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

privateJComponent 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();

}

privatevoid show(){

//same as in Validation.java

}

private ValidationResult validate(){

//same as in Validation.java

}

privatefinalclass OkAction extendsAbstractAction{

//same as in Validation.java

}

privatefinalclass CancelAction extendsAbstractAction{

//same as in Validation.java

}

//update the hint label's text based on which component has focus

privatefinalclass FocusChangeHandler

implementsPropertyChangeListener{

publicvoid propertyChange(PropertyChangeEvent evt){

String propertyName = evt.getPropertyName();

if("permanentFocusOwner".equals(propertyName)){

Component focusOwner = KeyboardFocusManager

.getCurrentKeyboardFocusManager().getFocusOwner();

if(focusOwner instanceofJTextField){

JTextField field =(JTextField) focusOwner;

String focusHint =(String) ValidationComponentUtils

.getInputHint(field);

hintLabel.setText(focusHint);

}else{

hintLabel.setText("");

}

}

}

}

publicstaticvoid 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 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.