Over the past year I've quietly developed a library for checking method preconditions. The project is hosted at https://bitbucket.org/cowwoc/preconditions/
I would like to get your feedback on the design and gauge whether this kind of library is of interest to anyone.
Sample usage looks like this:
public class Dog
{
private final String name;
private final int friends;
/**
* Creates a new Dog.
*
* @param name the dog's name
* @param friends the number of friends the dog has
* @throws NullPointerException if name is null
* @throws IllegalArgumentException if name is empty; if friends is outside the range [0, 15]
*/
public Dog(String name, int friends)
{
Preconditions.requireThat(name, "name").isNotNull().isNotEmpty();
Preconditions.requireThat(friends, "friends").isIn(Range.closed(0, 15));
this.name = name;
this.friends = friends;
}
/**
* Causes the dog to bark.
*/
public void bark()
{
System.out.print("Bark! Bark!");
if (friends <= 2)
System.out.println("I am lonely. I only have " + friends + " friends!");
else
System.out.println("I am one happy dog. I have " + friends + " friends!");
}
}
Sample output looks like this:
java.lang.NullPointerException: name may not be null java.lang.IllegalArgumentException: friends (20) must be in the range [0, 15]
When reviewing the code, two classes might be of particular interest:
package org.bitbucket.cowwoc.preconditions;
import java.math.BigDecimal;
import java.net.URI;
import java.nio.file.Path;
import java.time.Year;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
/**
* Verifies preconditions of a parameter.
* <p>
* This class is not thread-safe.
* <p>
* @author Gili Tzabari
* @param <S> the type of preconditions that was instantiated
* @param <T> the type of the parameter
*/
public class Preconditions<S extends Preconditions<S, T>, T>
{
/**
* Creates new Preconditions.
* <p>
* @param <S> the type of preconditions that was instantiated
* @param <T> the type of the parameter
* @param parameter the value of the parameter
* @param name the name of the parameter
* @return Preconditions for the parameter
* @throws NullPointerException if name is null
* @throws IllegalArgumentException if name is empty
*/
public static <S extends Preconditions<S, T>, T> S requireThat(T parameter, String name)
throws NullPointerException, IllegalArgumentException
{
@SuppressWarnings("unchecked")
S self = (S) new Preconditions<>(parameter, name);
return self;
}
/**
* Creates new CollectionPreconditions.
* <p>
* @param <E> the type of element in the collection
* @param parameter the value of the parameter
* @param name the name of the parameter
* @return Preconditions for the parameter
* @throws NullPointerException if name is null
* @throws IllegalArgumentException if name is empty
*/
public static <E> CollectionPreconditions<E> requireThat(Collection<E> parameter, String name)
throws NullPointerException, IllegalArgumentException
{
return new CollectionPreconditions<>(parameter, name);
}
/**
* Creates new NumberPreconditions.
* <p>
* @param <S> the type of preconditions that was instantiated
* @param <T> the type of the number
* @param parameter the value of the parameter
* @param name the name of the parameter
* @return Preconditions for the parameter
* @throws NullPointerException if name is null
* @throws IllegalArgumentException if name is empty
*/
public static <S extends NumberPreconditions<S, T>, T extends Number & Comparable<? super T>>
NumberPreconditions<S, T> requireThat(T parameter, String name)
throws NullPointerException, IllegalArgumentException
{
return new NumberPreconditions<>(parameter, name);
}
/**
* Creates new BigDecimalPreconditions.
* <p>
* @param parameter the value of the parameter
* @param name the name of the parameter
* @return Preconditions for the parameter
* @throws NullPointerException if name is null
* @throws IllegalArgumentException if name is empty
*/
public static BigDecimalPreconditions requireThat(BigDecimal parameter, String name)
throws NullPointerException, IllegalArgumentException
{
return new BigDecimalPreconditions(parameter, name);
}
/**
* Creates new MapPreconditions.
* <p>
* @param <K> the type of key in the map
* @param <V> the type of value in the map
* @param parameter the value of the parameter
* @param name the name of the parameter
* @return Preconditions for the parameter
* @throws NullPointerException if name is null
* @throws IllegalArgumentException if name is empty
*/
public static <K, V> MapPreconditions<K, V> requireThat(Map<K, V> parameter, String name)
throws NullPointerException, IllegalArgumentException
{
return new MapPreconditions<>(parameter, name);
}
/**
* Creates new PathPreconditions.
* <p>
* @param parameter the value of the parameter
* @param name the name of the parameter
* @return Preconditions for the parameter
* @throws NullPointerException if name is null
* @throws IllegalArgumentException if name is empty
*/
public static PathPreconditions requireThat(Path parameter, String name)
throws NullPointerException, IllegalArgumentException
{
return new PathPreconditions(parameter, name);
}
/**
* Creates new StringPreconditions.
* <p>
* @param parameter the value of the parameter
* @param name the name of the parameter
* @return Preconditions for the parameter
* @throws NullPointerException if name is null
* @throws IllegalArgumentException if name is empty
*/
public static StringPreconditions requireThat(String parameter, String name)
throws NullPointerException, IllegalArgumentException
{
return new StringPreconditions(parameter, name);
}
/**
* Creates new UriPreconditions.
* <p>
* @param parameter the value of the parameter
* @param name the name of the parameter
* @return Preconditions for the parameter
* @throws NullPointerException if name is null
* @throws IllegalArgumentException if name is empty
*/
public static UriPreconditions requireThat(URI parameter, String name)
throws NullPointerException, IllegalArgumentException
{
return new UriPreconditions(parameter, name);
}
/**
* Creates new ClassPreconditions.
* <p>
* @param <T> the type of class
* @param parameter the value of the parameter
* @param name the name of the parameter
* @return Preconditions for the parameter
* @throws NullPointerException if name is null
* @throws IllegalArgumentException if name is empty
*/
public static <T> ClassPreconditions<T> requireThat(Class<T> parameter, String name)
throws NullPointerException, IllegalArgumentException
{
return new ClassPreconditions<>(parameter, name);
}
/**
* Creates new YearPreconditions.
* <p>
* @param parameter the value of the parameter
* @param name the name of the parameter
* @return Preconditions for the parameter
* @throws NullPointerException if name is null
* @throws IllegalArgumentException if name is empty
*/
public static YearPreconditions requireThat(Year parameter, String name)
throws NullPointerException, IllegalArgumentException
{
return new YearPreconditions(parameter, name);
}
/**
* Creates new OptionalPreconditions.
* <p>
* @param parameter the value of the parameter
* @param name the name of the parameter
* @return Preconditions for the parameter
* @throws NullPointerException if name is null
* @throws IllegalArgumentException if name is empty
*/
public static OptionalPreconditions requireThat(Optional<?> parameter, String name)
throws NullPointerException, IllegalArgumentException
{
return new OptionalPreconditions(parameter, name);
}
protected final S self;
protected T parameter;
protected final String name;
/**
* Creates new Preconditions.
* <p>
* @param parameter the value of the parameter
* @param name the name of the parameter
* @throws NullPointerException if name is null
* @throws IllegalArgumentException if name is empty
*/
protected Preconditions(T parameter, String name)
throws NullPointerException, IllegalArgumentException
{
if (name == null)
throw new NullPointerException("name may not be null");
if (name.trim().isEmpty())
throw new IllegalArgumentException("name may not be empty");
@SuppressWarnings(
{
"unchecked", "LocalVariableHidesMemberVariable"
})
S self = (S) this;
this.self = self;
this.parameter = parameter;
this.name = name;
}
/**
* Ensures that the parameter is not null.
* <p>
* @return this
* @throws NullPointerException if parameter is null
*/
public S isNotNull() throws NullPointerException
{
if (parameter == null)
throw new NullPointerException(name + " may not be null");
return self;
}
/**
* Ensures that the parameter is not null.
* <p>
* @return this
* @throws IllegalStateException if parameter is null
*/
public S stateIsNotNull() throws IllegalStateException
{
if (parameter == null)
throw new IllegalStateException(name + " may not be null");
return self;
}
/**
* Ensures that the parameter is equal to a value.
* <p>
* @param value the value to compare to
* @return this
* @throws IllegalArgumentException if parameter is not equal to value
*/
public S isEqualTo(T value) throws IllegalArgumentException
{
if (!Objects.equals(parameter, value))
throw new IllegalArgumentException(name + " must be equal to " + value + ". Was: " + parameter);
return self;
}
/**
* Ensures that the parameter is an instance of a class.
* <p>
* @param type the class to compare to
* @return this
* @throws NullPointerException if {@code parameter} or {@code type} are null
* @throws IllegalArgumentException if {@code parameter} is not an instance of {@code type}
*/
public S isInstanceOf(Class<?> type)
throws NullPointerException, IllegalArgumentException
{
if (parameter == null)
throw new NullPointerException("parameter may not be null");
if (type == null)
throw new NullPointerException("type may not be null");
if (!type.isInstance(parameter))
{
throw new IllegalArgumentException(name + " must be an instance of " + type + ". Was: " +
parameter.getClass());
}
return self;
}
}
package org.bitbucket.cowwoc.preconditions;
import com.google.common.collect.Range;
import java.math.BigDecimal;
/**
* Verifies preconditions of a {@link Number} parameter.
* <p>
* @param <S> the type of preconditions that was instantiated
* @param <T> the type of the parameter
* @author Gili Tzabari
*/
public class NumberPreconditions<S extends NumberPreconditions<S, T>, T extends Number & Comparable<? super T>>
extends Preconditions<S, T>
{
/**
* Creates new NumberPreconditions.
* <p>
* @param parameter the value of the parameter
* @param name the name of the parameter
* @throws NullPointerException if name is null
* @throws IllegalArgumentException if name is empty
*/
NumberPreconditions(T parameter, String name)
throws NullPointerException, IllegalArgumentException
{
super(parameter, name);
}
/**
* Ensures that the parameter is within range.
* <p>
* @param range the range
* @return this
* @throws NullPointerException if range is null
* @throws IllegalArgumentException if the parameter is not in range
*/
public S isIn(Range<T> range)
throws NullPointerException, IllegalArgumentException
{
Preconditions.requireThat(range, "range").isNotNull();
boolean inRange = range.contains(parameter);
if (inRange)
return self;
StringBuilder message = new StringBuilder(name + " (" + parameter + ") must be in the range ");
switch (range.lowerBoundType())
{
case OPEN:
message.append("(");
break;
case CLOSED:
message.append("[");
break;
default:
throw new AssertionError(range.lowerBoundType().name());
}
message.append(range.lowerEndpoint()).append(", ").append(range.upperEndpoint());
switch (range.lowerBoundType())
{
case OPEN:
message.append(")");
break;
case CLOSED:
message.append("]");
break;
default:
throw new AssertionError(range.lowerBoundType().name());
}
throw new IllegalArgumentException(message.toString());
}
/**
* Ensures that the parameter is negative.
* <p>
* @return this
* @throws IllegalArgumentException if the parameter is not negative
*/
public S isNegative() throws IllegalArgumentException
{
if (parameter.longValue() >= 0L)
throw new IllegalArgumentException(name + " must be negative");
return self;
}
/**
* Ensures that the parameter is not negative.
* <p>
* @return this
* @throws IllegalArgumentException if the parameter is negative
*/
public S isNotNegative() throws IllegalArgumentException
{
if (parameter.longValue() < 0L)
throw new IllegalArgumentException(name + " may not be negative");
return self;
}
/**
* Ensures that the parameter is zero.
* <p>
* @return this
* @throws IllegalArgumentException if the parameter is not zero
*/
public S isZero() throws IllegalArgumentException
{
if (parameter instanceof BigDecimal)
{
// Number.longValue() truncates the fractional portion, which we need to take into account
BigDecimal decimal = (BigDecimal) parameter;
if (decimal.signum() != 0)
throw new IllegalArgumentException(name + " must be zero");
return self;
}
if (parameter.longValue() != 0L)
throw new IllegalArgumentException(name + " must be zero");
return self;
}
/**
* Ensures that the parameter is not zero.
* <p>
* @return this
* @throws IllegalArgumentException if the parameter is zero
*/
public S isNotZero() throws IllegalArgumentException
{
if (parameter instanceof BigDecimal)
{
// Number.longValue() truncates the fractional portion, which we need to take into account
BigDecimal decimal = (BigDecimal) parameter;
if (decimal.signum() == 0)
throw new IllegalArgumentException(name + " may not be zero");
return self;
}
if (parameter.longValue() == 0L)
throw new IllegalArgumentException(name + " may not be zero");
return self;
}
/**
* Ensures that the parameter is positive.
* <p>
* @return this
* @throws IllegalArgumentException if the parameter is not positive
*/
public S isPositive() throws IllegalArgumentException
{
if (parameter.longValue() <= 0L)
throw new IllegalArgumentException(name + " must be positive");
return self;
}
/**
* Ensures that the parameter is not positive.
* <p>
* @return this
* @throws IllegalArgumentException if the parameter is positive
*/
public S isNotPositive() throws IllegalArgumentException
{
if (parameter.longValue() > 0L)
throw new IllegalArgumentException(name + " may not be positive");
return self;
}
/**
* Ensures that the parameter is less than the value of a variable.
* <p>
* @param value the value the parameter must be less than
* @param name the name of the variable
* @return this
* @throws IllegalArgumentException if the {@code parameter} is greater than or equal to
* {@code value}
*/
public S isLessThan(T value, String name) throws IllegalArgumentException
{
if (parameter.compareTo(value) >= 0)
{
throw new IllegalArgumentException(this.name + " (" + parameter + ") must be less than " +
name + " (" + value + ")");
}
return self;
}
/**
* Ensures that the parameter is less than a constant.
* <p>
* @param value the value the parameter must be less than
* @return this
* @throws IllegalArgumentException if the {@code parameter} is greater than or equal to
* {@code value}
*/
public S isLessThan(T value) throws IllegalArgumentException
{
if (parameter.compareTo(value) >= 0)
{
throw new IllegalArgumentException(this.name + " (" + parameter + ") must be less than " +
value);
}
return self;
}
/**
* Ensures that the parameter is less than or equal to a variable.
* <p>
* @param value the value the parameter must be less than or equal to
* @param name the name of the variable
* @return this
* @throws IllegalArgumentException if the {@code parameter} is greater than {@code value}
*/
public S isLessThanOrEqualTo(T value, String name)
throws IllegalArgumentException
{
if (parameter.compareTo(value) > 0)
{
throw new IllegalArgumentException(this.name + " (" + parameter + ") must be less than " +
"or equal to " + name + " (" + value + ")");
}
return self;
}
/**
* Ensures that the parameter is less than or equal to a constant.
* <p>
* @param value the value the parameter must be less than or equal to
* @return this
* @throws IllegalArgumentException if the {@code parameter} is greater than {@code value}
*/
public S isLessThanOrEqualTo(T value)
throws IllegalArgumentException
{
if (parameter.compareTo(value) > 0)
{
throw new IllegalArgumentException(this.name + " (" + parameter + ") must be less than " +
"or equal to " + value);
}
return self;
}
/**
* Ensures that the parameter is greater than a variable.
* <p>
* @param value the value the parameter must be greater than
* @param name the name of the variable
* @return this
* @throws IllegalArgumentException if the parameter is less than or equal to {@code value}
*/
public S isGreaterThan(T value, String name)
throws IllegalArgumentException
{
if (parameter.compareTo(value) <= 0)
{
throw new IllegalArgumentException(this.name + " (" + parameter + ") must be greater than " +
name + " (" + value + ")");
}
return self;
}
/**
* Ensures that the parameter is greater than a constant.
* <p>
* @param value the value the parameter must be greater than
* @return this
* @throws IllegalArgumentException if the parameter is less than or equal to {@code value}
*/
public S isGreaterThan(T value)
throws IllegalArgumentException
{
if (parameter.compareTo(value) <= 0)
{
throw new IllegalArgumentException(this.name + " (" + parameter + ") must be greater than " +
value);
}
return self;
}
/**
* Ensures that the parameter is greater than or bigger than a variable.
* <p>
* @param value the value the parameter must be greater than or equal to
* @param name the name of the variable
* @return this
* @throws IllegalArgumentException if the {@code parameter} is less than to {@code value}
*/
public S isGreaterThanOrEqualTo(T value, String name)
throws IllegalArgumentException
{
if (parameter.compareTo(value) < 0)
{
throw new IllegalArgumentException(this.name + " (" + parameter + ") must be greater than " +
"or equal to " + name + " (" + value + ")");
}
return self;
}
/**
* Ensures that the parameter is greater than or bigger than a constant.
* <p>
* @param value the value the parameter must be greater than or equal to
* @return this
* @throws IllegalArgumentException if the {@code parameter} is less than to {@code value}
*/
public S isGreaterThanOrEqualTo(T value)
throws IllegalArgumentException
{
if (parameter.compareTo(value) < 0)
{
throw new IllegalArgumentException(this.name + " (" + parameter + ") must be greater than " +
"or equal to " + value);
}
return self;
}
}
Notice that I use self-types in NumberPreconditions
so regardless of whether the user is handling an Integer
or BigDecimal
parameter the builder methods return the correct type.
That's it. Let me know what you think :)