Last active
August 24, 2024 10:51
-
-
Save josergdev/84da286f32c4d93a113b1e33bfe65da6 to your computer and use it in GitHub Desktop.
This class maps http query parameters to the JPA specification with common convention behavior
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package dev.joserg.jpa; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Map.Entry; | |
import jakarta.persistence.criteria.CriteriaBuilder; | |
import jakarta.persistence.criteria.CriteriaQuery; | |
import jakarta.persistence.criteria.JoinType; | |
import jakarta.persistence.criteria.Path; | |
import jakarta.persistence.criteria.Predicate; | |
import jakarta.persistence.criteria.Root; | |
import jakarta.persistence.metamodel.ListAttribute; | |
import jakarta.persistence.metamodel.SingularAttribute; | |
import org.springframework.data.jpa.domain.Specification; | |
public record SimpleHttpCriteria<E>( | |
Map<PathMaker<E, ?>, List<?>> filters, | |
Operator internalOperator, | |
Operator externalOperator) implements Specification<E> { | |
public SimpleHttpCriteria() { | |
this(new HashMap<>(), Operator.OR, Operator.AND); | |
} | |
@Override | |
public Predicate toPredicate(Root<E> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) { | |
return this.specification().toPredicate(root, query, criteriaBuilder); | |
} | |
public <J> SimpleHttpCriteria<E> filter(PathMaker<E, J> path, List<J> values) { | |
if (!values.isEmpty()) { | |
this.filters.put(path, values); | |
} | |
return this; | |
} | |
public SimpleHttpCriteria<E> internalOperator(Operator operator) { | |
return new SimpleHttpCriteria<>(this.filters, operator, this.externalOperator); | |
} | |
public SimpleHttpCriteria<E> externalOperator(Operator operator) { | |
return new SimpleHttpCriteria<>(this.filters, this.internalOperator, operator); | |
} | |
private Specification<E> specification() { | |
final var filtersSpecs = this.filters.entrySet().stream().map(this::byFilter).toList(); | |
return this.externalOperator.reduce(filtersSpecs); | |
} | |
private Specification<E> byFilter(Entry<PathMaker<E, ?>, List<?>> filter) { | |
final var filterSpecs = filter.getValue().stream().map(value -> this.byObject(filter.getKey(), value)).toList(); | |
return this.internalOperator.reduce(filterSpecs); | |
} | |
private Specification<E> byObject(PathMaker<E, ?> path, Object value) { | |
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(path.toPath(root), value); | |
} | |
@FunctionalInterface | |
public interface PathMaker<R, P> { | |
static <R, P> PathMaker<R, P> attr(final SingularAttribute<? super R, P> attribute) { | |
return root -> root.get(attribute); | |
} | |
static <R, J, P> PathMaker<R, P> join(final ListAttribute<? super R, J> list, | |
final SingularAttribute<? super J, P> attribute, | |
JoinType joinType) { | |
return root -> root.join(list, joinType).get(attribute); | |
} | |
static <R, J, P> PathMaker<R, P> joinLeft(final ListAttribute<? super R, J> list, | |
final SingularAttribute<? super J, P> attribute) { | |
return join(list, attribute, JoinType.LEFT); | |
} | |
Path<P> toPath(Root<R> root); | |
} | |
public enum Operator { | |
AND, OR; | |
public <E> Specification<E> reduce(Iterable<Specification<E>> specifications) { | |
return switch (this) { | |
case AND -> Specification.allOf(specifications); | |
case OR -> Specification.anyOf(specifications); | |
}; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage: