BasicSpecifications.java
package expresspecs;
import java.util.Collection;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.collections4.CollectionUtils;
import org.jspecify.annotations.NonNull;
import org.springframework.data.jpa.domain.Specification;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.Path;
import lombok.experimental.UtilityClass;
/**
* Basic predicate factories for JPA Specifications, such as equality, null checks, and boolean logic.
*/
@UtilityClass
public class BasicSpecifications {
/**
* Returns {@code spec} unchanged, serving as a type anchor for chained specification expressions.
*
* <p>When building a specification by chaining factory methods, Java cannot infer the entity type
* {@code T} from a factory call alone (e.g. {@code isTrue("active")}) because there is no target
* type to constrain inference. Passing the entity class pins {@code T}, so subsequent {@code .and()}
* and {@code .or()} calls inherit the correct type and {@code var} works without explicit
* {@code Specification<MyEntity>} declarations:
*
* <pre>{@code
* var spec = where(MyEntity.class, isTrue("active"))
* .and(containsIgnoreCase("name", partialName));
* repository.findAll(spec);
* }</pre>
*
* @param <T> The entity type being queried.
* @param entityType The entity class, used only to pin the type parameter {@code T}.
* @param spec The first specification in the chain.
*/
public static <T> @NonNull Specification<T> where(@NonNull Class<T> entityType, @NonNull Specification<T> spec) {
return spec;
}
/**
* Returns an unrestricted specification that acts as a safe, backward-compatible replacement
* for returning {@code null} out of specification factories, which Spring Boot 4's {@code findAll()}
* no longer accepts.
* <p>
* This method exists for Spring Boot 3 support; Sprnig Boot 4 has it's own version of this
* (see {@link Specification#unrestricted()}.
*
* @param <T> The entity type being queried.
*/
public static <T> @NonNull Specification<T> unrestricted() {
return (root, query, cb) -> null;
}
/**
* Creates a specification that matches entities where the specified property is {@code true}.
*
* @param <T> The entity type being queried.
* @param propertyPath Dot-delimited property path to a boolean attribute.
* @see PropertyPath#from(String)
*/
public static <T> @NonNull Specification<T> isTrue(String propertyPath) {
return isTrue(PropertyPath.from(propertyPath));
}
/**
* Creates a specification that matches entities where the specified property is {@code true}.
*
* @param <T> The entity type being queried.
* @param propertyPath Resolved property path to a boolean attribute.
*/
public static <T> @NonNull Specification<T> isTrue(PropertyPath propertyPath) {
return (root, query, cb) -> {
Path<Boolean> path = propertyPath.asPath(root);
return cb.isTrue(path);
};
}
/**
* Creates a specification that matches entities where the specified property is {@code false}.
*
* @param <T> The entity type being queried.
* @param propertyPath Dot-delimited property path to a boolean attribute.
* @see PropertyPath#from(String)
*/
public static <T> @NonNull Specification<T> isFalse(String propertyPath) {
return isFalse(PropertyPath.from(propertyPath));
}
/**
* Creates a specification that matches entities where the specified property is {@code false}.
*
* @param <T> The entity type being queried.
* @param propertyPath Resolved property path to a boolean attribute.
*/
public static <T> @NonNull Specification<T> isFalse(PropertyPath propertyPath) {
return (root, query, cb) -> {
Path<Boolean> path = propertyPath.asPath(root);
return cb.isFalse(path);
};
}
/**
* Creates a specification that matches entities where the specified property is {@code null}.
*
* @param <T> The entity type being queried.
* @param propertyPath Dot-delimited property path to test for null.
* @see PropertyPath#from(String)
*/
public static <T> @NonNull Specification<T> isNull(String propertyPath) {
return isNull(PropertyPath.from(propertyPath));
}
/**
* Creates a specification that matches entities where the specified property is {@code null}.
*
* @param <T> The entity type being queried.
* @param propertyPath Resolved property path to test for null.
*/
public static <T> @NonNull Specification<T> isNull(PropertyPath propertyPath) {
return (root, query, cb) -> {
Path<?> path = propertyPath.asPath(root);
return cb.isNull(path);
};
}
/**
* Creates a specification that matches entities where the specified property is not {@code null}.
*
* @param <T> The entity type being queried.
* @param propertyPath Dot-delimited property path to test for non-null.
* @see PropertyPath#from(String)
*/
public static <T> @NonNull Specification<T> notNull(String propertyPath) {
return notNull(PropertyPath.from(propertyPath));
}
/**
* Creates a specification that matches entities where the specified property is not {@code null}.
*
* @param <T> The entity type being queried.
* @param propertyPath Resolved property path to test for non-null.
*/
public static <T> @NonNull Specification<T> notNull(PropertyPath propertyPath) {
return (root, query, cb) -> {
Path<?> path = propertyPath.asPath(root);
return cb.isNotNull(path);
};
}
/**
* Creates a specification that matches entities where the specified property equals {@code value},
* or an {@linkplain #unrestricted() unrestricted specification} if {@code value} is {@code null} or empty.
*
* <p>If the intent is to match entities where the property itself is {@code null}, use {@link #isNull} instead,
* as passing {@code null} here will produce an unrestricted specification rather than a {@code IS NULL} predicate.
*
* @param <T> The entity type being queried.
* @param <V> The property value type.
* @param propertyPath Dot-delimited property path to compare.
* @param value Value to compare against.
* @see PropertyPath#from(String)
*/
public static <T, V> @NonNull Specification<T> is(String propertyPath, V value) {
return is(PropertyPath.from(propertyPath), value);
}
/**
* Creates a specification that matches entities where the specified property equals {@code value},
* or an {@linkplain #unrestricted() unrestricted specification} if {@code value} is {@code null} or empty.
*
* <p>If the intent is to match entities where the property itself is {@code null}, use {@link #isNull} instead,
* as passing {@code null} here will produce an unrestricted specification rather than a {@code IS NULL} predicate.
*
* @param <T> The entity type being queried.
* @param <V> The property value type.
* @param propertyPath Resolved property path to compare.
* @param value Value to compare against.
*/
public static <T, V> @NonNull Specification<T> is(PropertyPath propertyPath, V value) {
if (ObjectUtils.isEmpty(value)) {
return unrestricted();
}
return (root, query, cb) -> {
Path<?> path = propertyPath.asPath(root);
return cb.equal(path, value);
};
}
/**
* Creates a specification that matches entities where the specified property does not equal {@code aValue},
* or an {@linkplain #unrestricted() unrestricted specification} if {@code aValue} is {@code null} or empty.
*
* <p>If the intent is to match entities where the property itself is not {@code null}, use {@link #notNull}
* instead, as passing {@code null} here will produce an unrestricted specification rather than a
* {@code IS NOT NULL} predicate.
*
* @param <T> The entity type being queried.
* @param <V> The property value type.
* @param propertyPath Dot-delimited property path to compare.
* @param aValue Value to compare against.
* @see PropertyPath#from(String)
*/
public static <T, V> @NonNull Specification<T> isNot(String propertyPath, V aValue) {
return isNot(PropertyPath.from(propertyPath), aValue);
}
/**
* Creates a specification that matches entities where the specified property does not equal {@code aValue},
* or an {@linkplain #unrestricted() unrestricted specification} if {@code aValue} is {@code null} or empty.
*
* <p>If the intent is to match entities where the property itself is not {@code null}, use {@link #notNull}
* instead, as passing {@code null} here will produce an unrestricted specification rather than a
* {@code IS NOT NULL} predicate.
*
* @param <T> The entity type being queried.
* @param <V> The property value type.
* @param propertyPath Resolved property path to compare.
* @param aValue Value to compare against.
*/
public static <T, V> @NonNull Specification<T> isNot(PropertyPath propertyPath, V aValue) {
if (ObjectUtils.isEmpty(aValue)) {
return unrestricted();
}
return (root, query, cb) -> {
Path<?> path = propertyPath.asPath(root);
return cb.notEqual(path, aValue);
};
}
/**
* Creates a specification that matches entities where two of their own properties are equal
* to each other (a column-to-column comparison, not a comparison against a fixed value).
*
* <p>Use this when the filter criterion is a relationship between two attributes on the same
* entity row; for example, matching orders whose {@code promisedDate} has not passed its
* {@code shippedDate}. For comparing a single property against a fixed value, use
* {@link #is(PropertyPath, Object)} instead.
*
* @param <T> The entity type being queried.
* @param path1 Path to the first property.
* @param path2 Path to the second property.
*/
public static <T> @NonNull Specification<T> areEqual(PropertyPath path1, PropertyPath path2) {
return (root, query, cb) -> cb.equal(path1.asPath(root), path2.asPath(root));
}
/**
* Creates a specification that matches entities where two of their own properties are equal
* to each other (a column-to-column comparison, not a comparison against a fixed value).
* Accepts dot-delimited path strings (e.g., {@code "address.city"}).
*
* <p>Use this when the filter criterion is a relationship between two attributes on the same
* entity row; for example, matching orders whose {@code promisedDate} has not passed its
* {@code shippedDate}. For comparing a single property against a fixed value, use
* {@link #is(PropertyPath, Object)} instead.
*
* @param <T> The entity type being queried.
* @param property1 Dot-delimited path to the first property.
* @param property2 Dot-delimited path to the second property.
*/
public static <T> @NonNull Specification<T> areEqual(String property1, String property2) {
return areEqual(PropertyPath.from(property1), PropertyPath.from(property2));
}
/**
* Creates a specification that matches entities where the specified property equals any value in
* {@code searchValues}, or an {@linkplain #unrestricted() unrestricted specification} if {@code searchValues}
* is {@code null} or empty.
*
* @param <T> The entity type being queried.
* @param <V> The property value type.
* @param propertyPath Dot-delimited property path to compare.
* @param searchValues Candidate values for an {@code IN (...)} predicate.
* @see PropertyPath#from(String)
*/
public static <T, V> @NonNull Specification<T> isAny(String propertyPath, Collection<V> searchValues) {
return isAny(PropertyPath.from(propertyPath), searchValues);
}
/**
* Creates a specification that matches entities where the specified property equals any value in
* {@code searchValues}, or an {@linkplain #unrestricted() unrestricted specification} if {@code searchValues}
* is {@code null} or empty.
*
* @param <T> The entity type being queried.
* @param <V> The property value type.
* @param propertyPath Resolved property path to compare.
* @param searchValues Candidate values for an {@code IN (...)} predicate.
*/
public static <T, V> @NonNull Specification<T> isAny(PropertyPath propertyPath, Collection<V> searchValues) {
if (CollectionUtils.isEmpty(searchValues)) {
return unrestricted();
}
return (root, query, cb) -> {
Path<V> path = propertyPath.asPath(root);
CriteriaBuilder.In<V> in = cb.in(path);
for (V v : searchValues) {
in.value(v);
}
return in;
};
}
/**
* Creates a specification that matches entities where the specified property does not equal any value in
* {@code searchValues}, or an {@linkplain #unrestricted() unrestricted specification} if {@code searchValues}
* is {@code null} or empty.
*
* @param <T> The entity type being queried.
* @param <V> The property value type.
* @param propertyPath Dot-delimited property path to compare.
* @param searchValues Candidate values for a {@code NOT IN (...)} predicate.
* @see PropertyPath#from(String)
*/
public static <T, V> @NonNull Specification<T> isNotAny(String propertyPath, Collection<V> searchValues) {
return isNotAny(PropertyPath.from(propertyPath), searchValues);
}
/**
* Creates a specification that matches entities where the specified property does not equal any value in
* {@code searchValues}, or an {@linkplain #unrestricted() unrestricted specification} if {@code searchValues}
* is {@code null} or empty.
*
* @param <T> The entity type being queried.
* @param <V> The property value type.
* @param propertyPath Resolved property path to compare.
* @param searchValues Candidate values for a {@code NOT IN (...)} predicate.
*/
public static <T, V> @NonNull Specification<T> isNotAny(PropertyPath propertyPath, Collection<V> searchValues) {
if (CollectionUtils.isEmpty(searchValues)) {
return unrestricted();
}
return (root, query, cb) -> {
Path<V> path = propertyPath.asPath(root);
CriteriaBuilder.In<V> in = cb.in(path);
for (V v : searchValues) {
in.value(v);
}
return cb.not(in);
};
}
}