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

Usage

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 extends Validator<String> {
    @Override
    public boolean validate(Problems problems, String compName, String model) {
        boolean result = model.length() != 0;
        if (!result) {
            String message = NbBundle.getMessage(EmptyStringIllegalValidator.class,
                "MSG_MAY_NOT_BE_EMPTY", compName);
            problems.add (message);
        }
        return result;
    }
}
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 Validators class. This is an enum of validator factories each of which can produce validators for java.lang.Strings, javax.swing.text.Documents or javax.swing.ComboBoxModels. 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);
    ValidationGroup group = panel.getValidationGroup();

    //This is all we do to validate the URL:
    group.add(f, Validators.REQUIRE_NON_EMPTY_STRING,
            Validators.NO_WHITESPACE,
            Validators.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 Runs

The timing of validation is up to you. A variety of ValidationStrategies are provided so that you can run validation on focus loss, or when text input occurs. Custom validation of custom components is also possible.

Of course, if you are using a custom listener on a custom component, then validation runs when you receive an event and call ValidationListener.validate()

One Component, One Validator

More importantly, 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.

This can be as simple as

Validator<String> v = new MyValidator().or(new OtherValidator()).or(anotherValidator);

For the case of pre-built validators, imagine that we want a validator that determines if the user has entered a valid Java package name. We need to check that none of the parts are empty and that none of them are Java keywords.

We will need a validator which splits strings. We can wrap any other validator in one provided by Validators.splitString(). First let’s get a validator that will require strings not to be empty and which requires that each string be a legal Java identifier (i.e. something you can use as a variable name - not a keyword):

Validator<String> v = Validators.forString(true, Validators.REQUIRE_NON_EMPTY_STRING,
            Validators.REQUIRE_JAVA_IDENTIFIER);
        
The static method Validators.forString() lets us OR together any of the built-in validators that are provided by the Validators enum.

Now we just wrap it with a validator that will split strings according to a pattern and invoke the embedded validator for each entry:

Validator<String> forStrings = Validators.splitString("\\.", v);
        
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. The other key class is to implement the two methods in ValidationUI:

    void clearProblem();
    void setProblem (Problem problem);
        
Basically this should somehow display the problem to the user, and possibly disable some portion of the UI (such as 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. The package also comes with an example panel ValidationPanel which shows errors in a visually pleasing way and fires changes.

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.

How Validation Works

A ValidationGroup attaches a listener to your components. There are several ValidationStrategys that can be used, such as ON_FOCUS_LOSS or ON_CHANGE, depending on what you need.

When input happens, any validators attached to the component 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. As far as what gets displayed to the user, the most severe problem wins.

If input happens and there is no problem with the component receiving the input, or there is a problem with that component but it is not FATAL, then all other components in the ValidationGroup are also validated (in many UIs a change in one component can affect whether the state of another component is still valid). Again, the most severe Problem (if any) wins. In this way, the user is offered feedback on any problem with their most recent input, or the most severe problem in the ui if it is more severe than any problem with their most recent input.

If there are no problems, then the UI’s clearProblem() method is called to remove any visible indication of prior problems.

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 (Validator.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 ValidationListener. 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.validate(). Add your custom validator to a validation group by calling ValidationGroup.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 extends Validator<Color> {
    @Override
    public boolean validate(Problems problems, String compName, Color model) {
         float[] hsb = Color.RGBtoHSB(model.getRed(), model.getGreen(),
                 model.getBlue(), null);
         boolean result = true;
         if (hsb[2] < 0.25) {
             //Dark colors cause a fatal error
             problems.add("Color is too dark");
             result = false;
         }
         if (hsb[1] > 0.8) {
             //highly saturated colors get a warning
             problems.add("Color is very saturated", Severity.WARNING);
             result = false;
         }
         if (hsb[2] > 0.9) {
             //Very bright colors get an information message
             problems.add("Color is very bright", Severity.INFO);
             result = false;
         }
         return result;
    }
}
Then we create a listener class that extends ValidationListener:
        final ColorValidator colorValidator = new ColorValidator();
        class ColorListener extends ValidationListener implements ChangeListener {
            @Override
            protected boolean validate(Problems problems) {
                return colorValidator.validate(problems, null,
                        chooser.getColor());
            }
            public void stateChanged(ChangeEvent ce) {
                validate();
            }
        }
Next we attach it as a listener to the color chooser's selection model and add it to the panel’s ValidationGroup:
        ColorListener cl = new ColorListener();
        chooser.getSelectionModel().addChangeListener(cl);
        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.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
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 javax.swing.text.Document;
import org.netbeans.validation.api.Converter;
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.ValidationPanel;
import org.netbeans.validation.api.builtin.Validators;
import org.netbeans.validation.api.ui.ValidationListener;

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();
    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));
    inner.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
    pnl.setInnerComponent(inner);

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

    //So, we'll get a validator that works against a Document, which does
    //trim strings (that's the true argument), which will not like
    //empty strings or java keywords
    Validator<Document> d = Validators.forDocument(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);

    //First we'll get the same kind of validator as we did above (in
    //fact we could reuse it - validators are stateless):
    Validator<String> v = Validators.forString(true,
            Validators.REQUIRE_NON_EMPTY_STRING,
            Validators.REQUIRE_JAVA_IDENTIFIER);

    //Now we'll wrap it in a validator that will split the strings on the
    //character '.' and run our Validator v against each component
    Validator<String> forStrings = Validators.splitString("\\.", v);

    //Finally, we need a Validator<Document>, so we get a wrapper validator
    //that takes a document, converts it to a String and passes it to our
    //other validator
    Validator<Document> docValidator =
            Converter.find(String.class, Document.class).convert(forStrings);
    pnl.getValidationGroup().add(field, docValidator);

    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("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");
    //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());

    //Note there is an alternative to field.setName() if we are using that
    //for some other purpose
    field.putClientProperty(ValidationListener.CLIENT_PROP_NAME, "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");
    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();
    //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(chooser, BorderLayout.WEST);

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

    //Note if we could also implement Validator directly on this class;
    //however it's more reusable if we don't
    class ColorListener extends ValidationListener implements ChangeListener {

      @Override
      protected boolean validate(Problems problems) {
        return colorValidator.validate(problems, null,
                chooser.getColor());
      }

      public void stateChanged(ChangeEvent ce) {
        validate();
      }
    }
    ColorListener cl = new ColorListener();
    chooser.getSelectionModel().addChangeListener(cl);

    //Add our custom validation code to the validation group
    pnl.getValidationGroup().add(cl);

    //Now let's add some dialog buttons we want to control.  If there is
    //a fatal error, the OK button should be disabled
    final JButton okButton = new JButton("OK");
    okButton.addActionListener(new ActionListener() {

      public void actionPerformed(ActionEvent e) {
        System.exit(0);
      }
    });

    JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING));
    inner.add(buttonPanel);

    buttonPanel.add(okButton);

    //Add a cancel button that's always enabled
    JButton cancelButton = new JButton("Cancel");
    buttonPanel.add(cancelButton);
    cancelButton.addActionListener(new ActionListener() {

      public void actionPerformed(ActionEvent e) {
        System.exit(1);
      }
    });

    pnl.addChangeListener(new ChangeListener() {

      public void stateChanged(ChangeEvent e) {
        Problem p = pnl.getProblem();
        boolean enable = p == null ? true : p.severity() != Severity.FATAL;

        okButton.setEnabled(enable);
        jf.setDefaultCloseOperation(!enable ?
          WindowConstants.DO_NOTHING_ON_CLOSE :
          WindowConstants.EXIT_ON_CLOSE);
      }
    });

    jf.pack();
    jf.setVisible(true);
  }

  private static final class ColorValidator extends Validator<Color> {

    @Override
    public boolean 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);
      boolean result = true;
      if (hsb[2] < 0.25) {
        //Dark colors cause a fatal error
        problems.add("Color is too dark");
        result = false;
      }
      if (hsb[1] > 0.8) {
        //highly saturated colors get a warning
        problems.add("Color is very saturated", Severity.WARNING);
        result = false;
      }
      if (hsb[2] > 0.9) {
        //Very bright colors get an information message
        problems.add("Color is very bright", Severity.INFO);
        result = false;
      }
      return result;
    }
  }
}