CollectionSpecifications.java

package expresspecs;

import static expresspecs.BasicSpecifications.unrestricted;

import java.util.Collection;

import org.jspecify.annotations.NonNull;
import org.springframework.data.jpa.domain.Specification;

import jakarta.persistence.criteria.Path;
import lombok.experimental.UtilityClass;

/**
 * Predicate factories for Collection-based JPA Specifications, such as membership and size checks.
 */
@UtilityClass
public class CollectionSpecifications {

	/**
	 * Creates a specification that matches entities where the specified collection property
	 * is not empty (has at least one element).
	 *
	 * @param <T>          The entity type being queried.
	 * @param propertyPath Dot-delimited property path to a collection attribute.
	 * @see PropertyPath#from(String)
	 */
	public static <T> @NonNull Specification<T> isNotEmpty(String propertyPath) {
		return isNotEmpty(PropertyPath.from(propertyPath));
	}

	/**
	 * Creates a specification that matches entities where the specified collection property
	 * is not empty (has at least one element).
	 *
	 * @param <T>          The entity type being queried.
	 * @param propertyPath Resolved property path to a collection attribute.
	 */
	public static <T> @NonNull Specification<T> isNotEmpty(PropertyPath propertyPath) {
		return (root, query, cb) -> {
			Path<Collection<?>> path = propertyPath.asPath(root);
			return cb.isNotEmpty(path);
		};
	}

	/**
	 * Creates a specification that matches entities where the specified collection property
	 * is empty (has no elements).
	 *
	 * @param <T>          The entity type being queried.
	 * @param propertyPath Dot-delimited property path to a collection attribute.
	 * @see PropertyPath#from(String)
	 */
	public static <T> @NonNull Specification<T> isEmpty(String propertyPath) {
		return isEmpty(PropertyPath.from(propertyPath));
	}

	/**
	 * Creates a specification that matches entities where the specified collection property
	 * is empty (has no elements).
	 *
	 * @param <T>          The entity type being queried.
	 * @param propertyPath Resolved property path to a collection attribute.
	 */
	public static <T> @NonNull Specification<T> isEmpty(PropertyPath propertyPath) {
		return (root, query, cb) -> {
			Path<Collection<?>> path = propertyPath.asPath(root);
			return cb.isEmpty(path);
		};
	}

	/**
	 * Creates a specification that matches entities where the specified collection property
	 * contains {@code value}, or an {@linkplain BasicSpecifications#unrestricted() unrestricted specification}
	 * if {@code value} is {@code null}.
	 *
	 * @param <T>          The entity type being queried.
	 * @param <V>          The element type in the collection.
	 * @param propertyPath Dot-delimited property path to a collection attribute.
	 * @param value        The value to check for membership.
	 * @see PropertyPath#from(String)
	 */
	public static <T, V> @NonNull Specification<T> containsMember(String propertyPath, V value) {
		return containsMember(PropertyPath.from(propertyPath), value);
	}

	/**
	 * Creates a specification that matches entities where the specified collection property
	 * contains {@code value}, or an {@linkplain BasicSpecifications#unrestricted() unrestricted specification}
	 * if {@code value} is {@code null}.
	 *
	 * @param <T>          The entity type being queried.
	 * @param <V>          The element type in the collection.
	 * @param propertyPath Resolved property path to a collection attribute.
	 * @param value        The value to check for membership.
	 */
	public static <T, V> @NonNull Specification<T> containsMember(PropertyPath propertyPath, V value) {
		if (value == null) {
			return unrestricted();
		}

		return (root, query, cb) -> {
			@SuppressWarnings("unchecked")
			Path<Collection<V>> path = (Path<Collection<V>>) (Path<?>) propertyPath.asPath(root);
			return cb.isMember(value, path);
		};
	}

	/**
	 * Creates a specification that matches entities where the specified collection property
	 * has at least {@code minSize} elements.
	 *
	 * @param <T>          The entity type being queried.
	 * @param propertyPath Dot-delimited property path to a collection attribute.
	 * @param minSize      The minimum number of elements required.
	 * @see PropertyPath#from(String)
	 */
	public static <T> @NonNull Specification<T> sizeAtLeast(String propertyPath, int minSize) {
		return sizeAtLeast(PropertyPath.from(propertyPath), minSize);
	}

	/**
	 * Creates a specification that matches entities where the specified collection property
	 * has at least {@code minSize} elements.
	 *
	 * @param <T>          The entity type being queried.
	 * @param propertyPath Resolved property path to a collection attribute.
	 * @param minSize      The minimum number of elements required.
	 */
	public static <T> @NonNull Specification<T> sizeAtLeast(PropertyPath propertyPath, int minSize) {
		return (root, query, cb) -> {
			Path<Collection<?>> path = propertyPath.asPath(root);
			return cb.greaterThanOrEqualTo(cb.size(path), minSize);
		};
	}

}