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.
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
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 Document
s 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);
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.
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.String
s. 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:
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); } }
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
Problem
s
to a list of problems passed to it. Problem
s each have a
Severity
,
which can be INFO
, WARNING
or FATAL
.
Validators
can be chained together.
Each piece of validation logic is encapsulated in an individual
validator, and chains of Validator
s 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 );
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:
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);
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 Validator
s 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.
If the name
property is already being used for other
purposes, you can also use
theComponent.putClientProperty (SwingValidationListener.CLIENT_PROP_NAME, theName);
getName()
. This
is useful because some frameworks (such as the
Wizard project) make use of component names for their own purposes.
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); } } }
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(); } }
SwingValidationGroup
:
ColorListener cl = new ColorListener(); chooser.getSelectionModel().addChangeListener(cl); //Add our custom validation code to the validation group pnl.getValidationGroup().add(cl);
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); } } } }