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.
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
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.
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.String
s,
javax.swing.text.Document
s
or javax.swing.ComboBoxModel
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); 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); } }
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()
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.
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
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 Validator
s tied to various components to that
ValidationGroup
.
ValidationStrategy
s
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 Problem
s to a list
of problems passed to it. Problem
s 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.
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.
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.
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; } } }