Module org.klojang.check
Klojang Check is a Java module dedicated to defensive programming. It lets you specify checks on program input, object state and method arguments in a concise and elegant manner. Here is an example:
this.numChairs = Check.that(numChairs).is(positive()).is(lte(), 4).is(even()).ok();
Documentation
See the User Guide for a detailed description of Defensive Programming using Klojang Check.
IntCheck and ObjectCheck
There are two types of check objects: IntCheck
,
for validating int
values, and
ObjectCheck<T>
, for validating values
of type T
. You cannot directly instantiate these classes. You obtain an
instance of them through the static factory methods on the
Check
class. In the example above, the
that()
static factory method returns an
IntCheck
instance.
Common Checks
Klojang Check provides a grab bag of common checks on arguments and other types of
values in the form of the CommonChecks
class. The lte()
, positive()
and even()
checks shown above
are static imports from this class. Here are some more examples:
Check.that(obj, "vehicle").is(instanceOf(), Car.class); Check.that(list).isNot(empty()); Check.that(word).is(keyIn(), dictionary); Check.that(dictionary).is(containsKey(), word); Check.that(file).is(writable());
These checks are associated with predefined error messages, so you don't need to
invent them yourself. The first of the above statements, for example, would cause
the following error message to be generated if the argument was an instance of
Bike
:
vehicle must be instance of Car (was Bike)
Predicates and Relations
The checks you pass to the is(...)
methods fall apart in two broad
categories: Predicate
and
IntPredicate
on the one hand, and
Relation
,
IntRelation
and
IntObjRelation
on the other. The
latter are not part of the JDK. They reside inside the Klojang Check module
itself. They can be thought of as a "BiPredicate" (which neither exists in the
JDK): a function that takes two arguments and returns a boolean. If the
two arguments have a certain relationship with each other, the relation is said
to exist and the function returns true
. Within the context of
Klojang Check, the first argument is always the value currently being validated
while the second argument is the value that it is to be validated against.
In the examples above
positive()
,
even()
,
empty()
and
writable()
are predicates;
lte()
,
instanceOf()
,
keyIn()
and
containsKey()
are relations.
For more information, see the package description of
org.klojang.check.relation
.
Handling Validation Errors
When a value fails a test, an error message needs to be generated and an exception needs to be thrown. You have three options here:
- Klojang Check generates both the exception and the exception message
- Klojang Check generates the exception and you provide the exception message
- You do both
Check.that(obj, "vehicle").is(instanceOf(), Car.class); Check.that(obj).is(instanceOf(), Car.class, "Bikes are not for rent here"); Check.that(obj).is(instanceOf(), Car.class, () -> new RentalException("Bikes are not for rent here"));
Custom Error Messages
If you prefer to emit a custom error message, you can do so by specifying a
message pattern and zero or more message arguments. The first message argument can
be referenced from within the message pattern as ${0}
, the second as
${1}
, etc. For example:
Check.that(word).is(keyIn(), dictionary, "Spelling error. Did you mean: \"${0}\"?", "Pterodactylus");
The following message arguments are automatically available within the message pattern:
${test}
The name of the check that was executed, like "lt" or "instanceOf" or "notNull".${arg}
The value being validated.${type}
The simple class name of the value.${tag}
The name of the parameter, field or variable being validated, or, possibly, something more descriptive. If you did not provide a name,${tag}
defaults to "argument".${obj}
The object of the relationship, in case the check took the form of aRelation
or one of its sister interfaces. For example, for theinstanceOf()
check,${obj}
would be the class that the argument must be an instance of (Car.class
in the example above). For checks expressed through aPredicate
orIntPredicate
, ${obj} will benull
.
Here is an example where you don't provide any message arguments yourself, yet still have a dynamically generated error message:
Check.that(word).is(keyIn(), dictionary, "Missing key \"${arg}\" in ${obj}");
Validating Argument Properties
Klojang Check lets you validate argument properties as part of validating the argument.
this.query = Check.that(query, "query") .notHas(Query::offset, "offset", negative()) .has(Query::limit, "limit", gte(), 10) .has(Query::limit, "limit", lt(), 100) .ok();
This would cause the following error message to be generated if the user specified a limit of 125:
query.limit must be < 100 (was 125)
The CommonProperties
class
provides some commonly used properties of well-known classes and interfaces, like
the size
property of a Collection
. As with the
CommonChecks
class, these properties are already associated with a
descriptive name of the property they expose. Thus, the error message to be
generated requires minimal input from you:
Check.notNull(emps, "employees").has(size(), gte(), 100);
This will cause the following error message to be generated if the size of the
emps
collection is, say, 42:
employees.size() must be >= 100 (was 42)
Custom Checks
You are not limited to using the checks from the CommonChecks
class.
You can also define your own checks in the form of lambdas or method references:
double angle = 45.0; Check.that(angle).is(a -> Math.sin(a) > 0, "sine of angle must be positive");
Be careful, however, when passing lambdas to the has
and notHas
methods. These methods are heavily overloaded. Therefore, "vanilla" lambdas
(without any type information) may cause the compiler to complain about an
Ambiguous method call:
// WON'T COMPILE! Ambiguous method call Check.that(temperature).has(i -> Math.abs(i), i -> i < 30);
You can disambiguate this for the compiler by specifying the type of the lambda parameter, or by casting the entire lambda or method reference:
// specify the type of lambda parameter: Check.that(temperature).has(i -> Math.abs(i), (int i) -> i < 30); // cast the lambda that extracts the property from the argument: Check.that(temperature).has((IntUnaryOperator) i -> Math.abs(i), i -> i < 30); // cast the lambda that tests the property: Check.that(temperature).has(i -> Math.abs(i), (IntPredicate) i -> i < 30);
Throwing a Different Type of Exception
By default, Klojang Check throws an IllegalArgumentException
if any
of the tests following Check.that(...)
fail. To customize this, use the
Check.on(...)
static factory methods instead. These allow you to
change the default exception. The default exception can itself be overridden
within the checks themselves. The following example changes the default exception
to SQLException
, but overrides it for the null check:
this.query = Check.on(SQLException::new, query, "query") .is(notNull(), () -> new NullPointerException()) .notHas(Query::offset, "offset", negative()) .has(Query::limit, "limit", gte(), 10) .has(Query::limit, "limit", lte(), 10000) .ok();
Here, too, Klojang Check provides some useful shortcuts through the
CommonExceptions
class:
Check.that(word) .is(notNull(), npe()) // throw a NullPointerException .is(keyIn(), dictionary, illegalState("no such word: \"" + word + "\"");
NB when you supply your own exception, you cannot use the ${...}
message arguments. You will have to construct the message yourself.
-
Packages
PackageDescriptionThe central package of this Java module.A small collection of classes and interfaces that do not themselves take part in the validation framework, but are treated as first-class citizens by it.A collection of functional interfaces that allow a checked exception to be thrown from their functional method.A collection of functional interfaces that together constitute one category of checks that can be executed using Klojang Check.