Simple Swing Validation Library

This is a library for validating the contents of Swing components, in order to show error messages to the user when they interact with those components. It provides a standard way for controlling a user interface where the user may enter invalid (or inadvisable data) and informing the user of what problems there are. It also provides built-in validators for many common needs.

For the most part that means it is a library for validating instances of java.lang.String in various ways, a facility for taking models from various kinds of components and turning them into Strings for evaluation, and a way to group things that need evaluating together so that the most important or most recent warning is the one the user sees.

Rationale

Why create another validation library - aren't there already a few out there? Yes there are, and if you are writing something from scratch, take a look at them (JGoodies Validation is an interesting one).

The point of creating this library was to make it easy to retrofit validation onto existing code easily, and in particular, to supply a lot of validators for common use cases, so that adding validation typically only means adding a few lines of code. Other solutions are great for coding from scratch; the goal of this library is that it can be applied quickly and solve most problems with very little code — without having to rewrite your UI. That's not much use if you have hundreds of existing UIs which could use validation support and don't have it.

A Validator

A Validator is quite simple— you implement one method, validate(). Here is a validator that registers a problem if a string is empty:

final class EmptyStringIllegalValidator implements Validator<String> {
    @Override
    public void validate(Problems problems, String compName, String model) {
        if (model.isEmpty()) {
            String message = NbBundle.getMessage(EmptyStringIllegalValidator.class,
                "MSG_MAY_NOT_BE_EMPTY", compName); //NOI18N
            problems.add (message);
        }
    }
}
Note: In these examples, localized strings are fetched using NetBeans APIs for these purposes, since this library is intended for use in NetBeans (and also other places). Stub versions of these classes, which provide these methods, are included with the project.

You'll notice that the validator has a generic type of String. But Swing components don't use Strings, they use Documents and other models! Not to worry. You just wrap a Validator<String> in a Validator<Document> which does the conversion. The library provides built-in converters for javax.swing.text.Document and javax.swing.ComboBoxModel. You can register a factory of your own and then simply call

Validator<MyModel> v = converter.find (String.class, MyModel.class);
whenever you need to use a String validator against a component that has a MyModel model. That way, you write your validation code against the thing that makes the most sense to work with; and your UI uses the class that makes the most sense for it to use.

Built-in Validators

A large complement of standard validators are available via the StringValidators enum. This is an enum of validator factories each of which can produce validators for java.lang.Strings. Producing validators that operate against other kinds of model objects is easy; just register a Converter which can take the object type you want, turn it into a String and pass it to the validator you want — or write a validator that directly calls some other type (this involves a little more work wiring the validator up to the UI since you will have to write your own listener).

Here are some of the built-in validators:

and hopefully more will be contributed over time.

Basic Usage

Here is a simple example of validating a URL:
  public static void main(String[] args) {
    //This is our actual UI
    JPanel inner = new JPanel();
    JLabel lbl = new JLabel("Enter a URL");
    JTextField f = new JTextField();
    f.setColumns(40);

    //Setting the component name is important - it is used in
    //error messages
    f.setName("URL");

    inner.add(lbl);
    inner.add(f);

    //Create a ValidationPanel - this is a panel that will show
    //any problem with the input at the bottom with an icon
    ValidationPanel panel = new ValidationPanel();
    panel.setInnerComponent(inner);
    SwingValidationGroup group = panel.getValidationGroup();

    //This is all we do to validate the URL:
    group.add(f, StringValidators.REQUIRE_NON_EMPTY_STRING,
            StringValidators.NO_WHITESPACE,
            StringValidators.URL_MUST_BE_VALID);

    //Convenience method to show a simple dialog
    if (panel.showOkCancelDialog("URL")) {
      System.out.println("User clicked OK.  URL is " + f.getText());
      System.exit(0);
    } else {
      System.err.println("User clicked cancel.");
      System.exit(1);
    }
  }

When Validation is Triggered

The timing of validation is up to you. A variety of ValidationStrategies such as ON_FOCUS_LOSS or ON_CHANGE_OR_ACTION are provided so that you can run validation on focus loss, or when text input occurs. When input happens (or focus loss etc), the validators attached to the affected component will run, and have a chance to add Problems to a list of problems passed to it. Problems each have a Severity, which can be INFO, WARNING or FATAL.

One Component, One Validator

Validators can be chained together. Each piece of validation logic is encapsulated in an individual validator, and chains of Validators together can be used in a group and applied to one or more components.

In other words, you almost never apply more than one Validator to a component — rather, you merge together multiple validators into one.

The code example above uses one of the SwingValidationGroup.add() methods that under the hood will merge the passed Validators into one and uses the result:

    group.add(f, StringValidators.REQUIRE_NON_EMPTY_STRING,
        Validators.NO_WHITESPACE,
        Validators.URL_MUST_BE_VALID);

Sometimes such merging needs done explicitly in client code. This can be as simple as

    Validator<String> v =
        ValidatorUtils.merge( new MyValidator(), new OtherValidator(), anotherValidator );

Validator chaining example

As an example (that also illustrates how the built in validators that are provided by the StringValidators enum may be used), imagine that we want a validator that determines if the user has entered a valid Java package name. What we could do is to first split the entered string on "." (the full-stop character) an then check that none of the parts is empty and also not a Java keyword.

To make sure that the splitting of the entered String is performed we'll use a a validator provided by StringValidators.splitString(). This Validator does not really validate anything, it just does the splitting. We can chain any other validator on to that one. We will then chain two other validators onto it. The first will require strings not to be empty and the second will require that each string be a legal Java identifier (i.e not a Java keyword):

    v = StringValidators.splitString("\\.", 
           Validators.REQUIRE_NON_EMPTY_STRING, Validators.REQUIRE_JAVA_IDENTIFIER);

Perhaps we can accept that the user has entered some whitespace before and/or after the string to be validated. In that case, we’ll create yet another Validator onto which we chain the one we currently have. This new one will perform a trim() on the String to be validated before passing it on to the chained one. This can be done as follows:

    v = StringValidators.trimString(v);

All this written more compactly:

    Validator<String> v =
        StringValidators.trimString(
          Validators.splitString("\\.",
             Validators.REQUIRE_NON_EMPTY_STRING, Validators.REQUIRE_JAVA_IDENTIFIER)
        );

We now have a validator which:

Wiring validators to a user interface

The org.netbeans.validation.api.ui package contains the classes for actually connecting validators to a user interface.

The key class here is the ValidationGroup class. A validation group is a group of components which belong to the same UI and are validated together. It will keep track of which problem that is the most severe and recent problem, the so called "lead problem", of the group. The other key class is to implement the method in ValidationUI:

    void showProblem (Problem problem);
Basically this should somehow display the lead problem to the user, perhaps as a message in a status bar, and possibly by disabling the OK button in a Dialog or the Next button in a Wizard, until the problem is fixed. For cases where the code has existing ways of doing these things, it is usually easy to write an adapter that calls those existing ways. If there are no problems, then null will be passed to the ValidationUI’s showProblem method to remove any visible indication of prior problems.

So to wire up your UI, you need an implementation of ValidationUI. Then you pass it to ValidationGroup.create(ValidationUI). Then you add Validators tied to various components to that ValidationGroup.

The package also comes with an example panel ValidationPanel which shows errors in a visually pleasing way and fires changes.

Component Names

The components added to a group generally need to have their name set to a localized, human-readable name — many of the error messages provided by stock validators need to include the name of the source component to provide a meaningful message. The name should be a noun that describes the purpose of the component.

If the name property is already being used for other purposes, you can also use

theComponent.putClientProperty (SwingValidationListener.CLIENT_PROP_NAME, theName);
If set, it overrides the value returned by getName(). This is useful because some frameworks (such as the Wizard project) make use of component names for their own purposes.

Validating custom components

To validate custom components, there is a little more plumbing necessary, but it is still quite simple. First, create a listener on your component that is a subclass of SwingValidationListener. Add it as a listener to the component in question. The superclass already contains the logic to run validation correctly - when an event you are interested in happens, simply call super.triggerValidation(). Add your custom validator to a validation group by calling SwingValidationGroup.add(myValidationListener) (it assumes that your validation listener knows what validators to run).

The example below includes an example of validating the color provided by a JColorChooser. The first step is to write a validator for colors:

  private static final class ColorValidator implements Validator<Color> {

    public void validate(Problems problems, String compName, Color model) {
      //Convert the color to Hue/Saturation/Brightness
      //scaled from 0F to 1.0F
      float[] hsb = Color.RGBtoHSB(model.getRed(), model.getGreen(),
              model.getBlue(), null);
      if (hsb[2] < 0.25) {
        //Dark colors cause a fatal error
        problems.add("Color is too dark");
      } else if (hsb[2] > 0.9) {
        //Very bright colors get an information message
        problems.add("Color is very bright", Severity.INFO);
      }
      if (hsb[1] > 0.8) {
        //highly saturated colors get a warning
        problems.add("Color is very saturated", Severity.WARNING);
      }
    }
  }

Then we create a listener class that extends SwingValidationListener:
    class ColorListener extends SwingValidationListener implements ChangeListener {
      ColorListener() {
        super(chooser, ccDecorator);
      }
      @Override
      protected Problem performValidation() {
        Problems ps = new Problems();
        colorValidator.validate(ps, null, chooser.getColor());
        Problem leadProblem = ps.getLeadProblem();
        super.getValidationUI().showProblem(leadProblem);
        return leadProblem;
      }

      public void stateChanged(ChangeEvent ce) {
        super.triggerValidation();
      }
    }
Next we attach it as a listener to the color chooser's selection model and add it to the panel’s SwingValidationGroup:
    ColorListener cl = new ColorListener();
    chooser.getSelectionModel().addChangeListener(cl);
    //Add our custom validation code to the validation group
    pnl.getValidationGroup().add(cl);
You will notice that our validator above only produces a fatal error for extremely dark colors; it produces a warning message for highly saturated colors, and an info message for very bright colors. When you run the demo, notice how these both are presented differently and also that if a warning or info message is present, and you modify one of the text fields to produce a fatal error, the fatal error supersedes the warning or info message as long as it remains uncorrected.

Example

Below is a simple example using ValidationPanel to make a dialog containing text fields with various restrictions, which shows feedback. If you have checked out the source code, you will find a copy of this example in the ValidationDemo/ subfolder.
package validationdemo;

import java.awt.BorderLayout;
import java.awt.Color;
import java.io.File;
import javax.swing.BoxLayout;
import javax.swing.JColorChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.UIManager;
import javax.swing.WindowConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.netbeans.validation.api.Problem;
import org.netbeans.validation.api.Problems;
import org.netbeans.validation.api.Severity;
import org.netbeans.validation.api.Validator;
import org.netbeans.validation.api.ui.swing.ValidationPanel;
import org.netbeans.validation.api.builtin.Validators;
import org.netbeans.validation.api.ui.swing.SwingComponentDecorationFactory;
import org.netbeans.validation.api.ui.swing.SwingValidationListener;
import org.netbeans.validation.api.ui.ValidationUI;

public class Main {

  public static void main(String[] args) throws Exception {
    //Set the system look and feel
    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());

    final JFrame jf = new JFrame("Validators Demo");
    jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

    //Here we create our Validation Panel.  It has a built-in
    //ValidationGroup we can use - we will just call
    //pnl.getValidationGroup() and add validators to it tied to
    //components
    final ValidationPanel pnl = new ValidationPanel();
    jf.setContentPane(pnl);

    //A panel to hold most of our components that we will be
    //validating
    JPanel inner = new JPanel();
    inner.setLayout(new BoxLayout(inner, BoxLayout.Y_AXIS));
    pnl.setInnerComponent(inner);
    JLabel lbl;
    JTextField field;

    //Okay, here's our first thing to validate
    lbl = new JLabel("Not a java keyword:");
    inner.add(lbl);
    field = new JTextField("listener");
    field.setName("Non Identifier");
    inner.add(field);

    //So, we'll get a validator, which does trim strings (that's the boolean
    // argument set to true), which will not accept empty strings or java keywords
    Validator<String> d = Validators.merge(true,
            Validators.REQUIRE_NON_EMPTY_STRING,
            Validators.REQUIRE_JAVA_IDENTIFIER);

    //Now we add it to the validation group
    pnl.getValidationGroup().add(field, d);

    //This one is similar to the example above, but it will split the string
    //into component parts divided by '.' characters first
    lbl = new JLabel("Legal java package name:");
    inner.add(lbl);
    field = new JTextField("com.foo.bar.baz");
    field.setName("package name");
    inner.add(field);
    Validator<String> ddd = Validators.merge(true,
            Validators.REQUIRE_NON_EMPTY_STRING, Validators.JAVA_PACKAGE_NAME,
            Validators.MAY_NOT_END_WITH_PERIOD);
    pnl.getValidationGroup().add (field, ddd);

    lbl = new JLabel("IP Address or Host Name");
    inner.add (lbl);
    field = new JTextField ("127.0.0.1");
    field.setName ("Address");
    inner.add (field);
    Validator<String> dd = Validators.merge(Validators.HOST_NAME_OR_IP_ADDRESS);
    pnl.getValidationGroup().add (field, dd);

    lbl = new JLabel("Must be a non-negative integer");
    inner.add(lbl);
    field = new JTextField("42");
    field.setName("the number");
    inner.add(field);

    //Note that we're very picky here - require non-negative number and
    //require valid number don't care that we want an Integer - we also
    //need to use require valid integer
    pnl.getValidationGroup().add(field,
            Validators.REQUIRE_NON_EMPTY_STRING,
            Validators.REQUIRE_VALID_NUMBER,
            Validators.REQUIRE_VALID_INTEGER,
            Validators.REQUIRE_NON_NEGATIVE_NUMBER);

    lbl = new JLabel("Email address");
    inner.add(lbl);
    field = new JTextField("Foo Bar <foo@bar.com>");
    field.setName("Email address");
    inner.add(field);

    //Note that we're very picky here - require non-negative number and
    //require valid number don't care that we want an Integer - we also
    //need to use require valid integer
    pnl.getValidationGroup().add(field,
            Validators.REQUIRE_NON_EMPTY_STRING,
            Validators.EMAIL_ADDRESS);

    lbl = new JLabel("Hexadecimal number ");
    inner.add(lbl);
    field = new JTextField("CAFEBABE");
    field.setName("hex number");
    inner.add(field);

    pnl.getValidationGroup().add(field,
            Validators.REQUIRE_NON_EMPTY_STRING,
            Validators.VALID_HEXADECIMAL_NUMBER);

    lbl = new JLabel("No spaces: ");
    field = new JTextField("ThisTextHasNoSpaces");
    field.setName("No spaces");
    pnl.getValidationGroup().add(field,
            Validators.REQUIRE_NON_EMPTY_STRING,
            Validators.NO_WHITESPACE);
    inner.add(lbl);
    inner.add(field);

    lbl = new JLabel("Enter a URL");
    field = new JTextField("http://netbeans.org/");
    field.setName("url");
    pnl.getValidationGroup().add(field, Validators.URL_MUST_BE_VALID);
    inner.add(lbl);
    inner.add(field);

    lbl = new JLabel("file that exists");
    //Find a random file so we can populate the field with a valid initial
    //value, if possible
    File userdir = new File(System.getProperty("user.dir"));
    File aFile = null;
    for (File f : userdir.listFiles()) {
      if (f.isFile()) {
        aFile = f;
        break;
      }
    }
    field = new JTextField(aFile == null ? "" : aFile.getAbsolutePath());

    //We could call field.setName("File").
    //Note there is an alternative to field.setName() if we are using that
    //for some other purpose:
    SwingValidationListener.setComponentName(field, "File");
    pnl.getValidationGroup().add(field,
            Validators.REQUIRE_NON_EMPTY_STRING,
            Validators.FILE_MUST_BE_FILE);
    inner.add(lbl);
    inner.add(field);

    lbl = new JLabel("Folder that exists");
    field = new JTextField(System.getProperty("user.dir"));
    field.setName("folder");
    pnl.getValidationGroup().add(field,
            Validators.REQUIRE_NON_EMPTY_STRING,
            Validators.FILE_MUST_BE_DIRECTORY);
    inner.add(lbl);
    inner.add(field);

    lbl = new JLabel("Valid file name");
    field = new JTextField("Validators.java");
    field.setName("File Name");

    //Here we're requiring a valid file name
    //(no file or path separator chars)
    pnl.getValidationGroup().add(field,
            Validators.REQUIRE_NON_EMPTY_STRING,
            Validators.REQUIRE_VALID_FILENAME);
    inner.add(lbl);
    inner.add(field);

    //Here we will do custom validation of a JColorChooser

    final JColorChooser chooser = new JColorChooser();
    //Use an intermediary panel to keep the layout from jumping when
    //the problem is shown/hidden
    final JPanel ccPanel = new JPanel();
    ccPanel.add (chooser);
    //Add it to the main panel because GridLayout will make it too small
    //ValidationPanel panel uses BorderLayout (and will throw an exception
    //if you try to change it)
    pnl.add(ccPanel, BorderLayout.EAST);

    //Set a default value that won't show an error
    chooser.setColor(new Color(191, 86, 86));

    //ColorValidator is defined below
    final ColorValidator colorValidator = new ColorValidator();
    final ValidationUI ccDecorator =
            SwingComponentDecorationFactory.get().decorationFor(chooser);

    //Note if we could also implement Validator directly on this class;
    //however it's more reusable if we don't
    class ColorListener extends SwingValidationListener implements ChangeListener {
      ColorListener() {
        super(chooser, ccDecorator);
      }
      @Override
      protected Problem performValidation() {
        Problems ps = new Problems();
        colorValidator.validate(ps, null, chooser.getColor());
        Problem p = ps.getLeadProblem();
        super.getValidationUI().showProblem( p );
        return p;
      }

      public void stateChanged(ChangeEvent ce) {
        super.triggerValidation();
      }
    }
    ColorListener cl = new ColorListener();
    chooser.getSelectionModel().addChangeListener(cl);
    //Add our custom validation code to the validation group
    pnl.getValidationGroup().add(cl);
    boolean okClicked = pnl.showOkCancelDialog("Validation Demo");
    System.out.println(okClicked ? "User clicked OK" : "User did not click OK");
    System.exit(0);
  }

  private static final class ColorValidator implements Validator<Color> {

    public void validate(Problems problems, String compName, Color model) {
      //Convert the color to Hue/Saturation/Brightness
      //scaled from 0F to 1.0F
      float[] hsb = Color.RGBtoHSB(model.getRed(), model.getGreen(),
              model.getBlue(), null);
      if (hsb[2] < 0.25) {
        //Dark colors cause a fatal error
        problems.add("Color is too dark");
      } else if (hsb[2] > 0.9) {
        //Very bright colors get an information message
        problems.add("Color is very bright", Severity.INFO);
      }
      if (hsb[1] > 0.8) {
        //highly saturated colors get a warning
        problems.add("Color is very saturated", Severity.WARNING);
      }
    }
  }
}