|
13 | 13 | import life.qbic.logging.service.LoggerFactory; |
14 | 14 | import org.springframework.data.jpa.domain.Specification; |
15 | 15 |
|
| 16 | + |
16 | 17 | /** |
17 | | - * A factory for {@link Specification} based on commonly required parameters. |
| 18 | + * Utility class providing reusable helper methods for constructing Spring Data JPA |
| 19 | + * {@link org.springframework.data.jpa.domain.Specification} instances. |
| 20 | + * |
| 21 | + * <h2>Intention</h2> |
| 22 | + * <p> |
| 23 | + * This class centralizes common patterns for building query predicates based on entity |
| 24 | + * properties, property names, or expressions. It is designed as a collection of small, |
| 25 | + * reusable building blocks that simplify working with the JPA Criteria API and reduce |
| 26 | + * boilerplate code across the persistence layer. |
| 27 | + * </p> |
| 28 | + * |
| 29 | + * <h2>Relation to JPA Specifications</h2> |
| 30 | + * <p> |
| 31 | + * Spring Data JPA {@link org.springframework.data.jpa.domain.Specification} follows the |
| 32 | + * Composite pattern, allowing complex queries to be built by combining smaller predicates |
| 33 | + * using {@code and(...)} and {@code or(...)}. The methods in this class act as reusable |
| 34 | + * "leaf" specifications that can be composed into more complex query structures. |
| 35 | + * </p> |
| 36 | + * |
| 37 | + * <p> |
| 38 | + * This class is intentionally stateless and focuses solely on supporting the creation of |
| 39 | + * composable {@code Specification} instances that can be assembled where needed. |
| 40 | + * </p> |
18 | 41 | */ |
19 | | -public class SpecificationFactory { |
20 | 42 |
|
21 | | - private static final Logger log = LoggerFactory.logger(SpecificationFactory.class); |
| 43 | +public class JpaSpecifications { |
| 44 | + |
| 45 | + private static final Logger log = LoggerFactory.logger(JpaSpecifications.class); |
22 | 46 | /** |
23 | 47 | * A custom date time format not in any java.time ISO formats |
24 | 48 | */ |
25 | 49 | public static final String CUSTOM_DATE_TIME_PATTERN = "%Y-%m-%d %H:%i"; |
26 | 50 |
|
27 | 51 |
|
28 | 52 | /** |
29 | | - * Extracts an {@link Expression} of type {@link T }from another {@link Expression} of type {@link S} |
30 | | - * @param <S> The type of item used in the expression |
31 | | - * @param <T> the desired type of expression after extraction |
32 | | - * @param <X> the concrete type of source Expression |
| 53 | + * Interface for resolving a JPA {@link jakarta.persistence.criteria.Expression} |
| 54 | + * from a given source expression. |
| 55 | + * |
| 56 | + * <h2>Intention</h2> |
| 57 | + * <p> |
| 58 | + * An {@code ExpressionProvider} describes how to navigate from an existing JPA Criteria |
| 59 | + * expression (for example a {@code Root}, {@code From}, or another {@code Expression}) |
| 60 | + * to a target expression representing a specific attribute or derived value. |
| 61 | + * </p> |
| 62 | + * |
| 63 | + * <p> |
| 64 | + * It does not extract runtime values from entities. Instead, it defines how to build |
| 65 | + * a new {@link jakarta.persistence.criteria.Expression} that can later be used to |
| 66 | + * construct predicates in a {@link org.springframework.data.jpa.domain.Specification}. |
| 67 | + * </p> |
| 68 | + * |
| 69 | + * <h2>Usage context</h2> |
| 70 | + * <p> |
| 71 | + * This abstraction is especially useful when building reusable specification helpers. |
| 72 | + * Instead of referring to entity attributes via string-based paths, callers can provide |
| 73 | + * an {@code ExpressionProvider} that encapsulates how to access or derive the relevant |
| 74 | + * expression. This improves readability, reuse, and type-safety when constructing |
| 75 | + * dynamic queries with the JPA Criteria API. |
| 76 | + * </p> |
| 77 | + * |
| 78 | + * @param <S> the type represented by the source type (e.g. MyEntity) |
| 79 | + * @param <T> the type represented by the resolved expression (e.g. String) |
| 80 | + * @param <X> the source type from which the expression is resolved (e.g. Root<MyEntity>) |
33 | 81 | */ |
34 | | - public interface Extractor<S, T, X extends Expression<S>> extends Function<X, Expression<T>> { |
| 82 | + @FunctionalInterface |
| 83 | + public interface ExpressionProvider<S, T, X extends Expression<S>> extends |
| 84 | + Function<X, Expression<T>> { |
35 | 85 |
|
36 | 86 | @Override |
37 | 87 | default Expression<T> apply(X expression) { |
38 | 88 | return extractFrom(expression); |
39 | 89 | } |
40 | 90 |
|
41 | 91 | /** |
42 | | - * Extract an expression from the provided expression. |
| 92 | + * Resolves a target {@link jakarta.persistence.criteria.Expression} from the given source |
| 93 | + * expression. |
| 94 | + * |
| 95 | + * <p> |
| 96 | + * The source is typically a {@code Root<T>}, {@code From<?, ?>}, or another expression |
| 97 | + * that serves as the starting point for navigating to an attribute. Implementations |
| 98 | + * define how to derive the resulting expression, for example by: |
| 99 | + * </p> |
| 100 | + * <ul> |
| 101 | + * <li>Accessing an entity attribute</li> |
| 102 | + * <li>Navigating a join path</li> |
| 103 | + * <li>Returning a nested property expression</li> |
| 104 | + * <li>Applying a transformation or function</li> |
| 105 | + * </ul> |
| 106 | + * |
| 107 | + * <p> |
| 108 | + * The returned expression can then be used to build predicates such as equality, |
| 109 | + * LIKE, range, or null checks within a {@link org.springframework.data.jpa.domain.Specification}. |
| 110 | + * </p> |
43 | 111 | * |
44 | | - * @param expression the provided expression |
45 | | - * @return the extracted expression |
| 112 | + * @param expression the source expression from which the target expression should be derived |
| 113 | + * @return the resolved expression representing the desired attribute or value |
46 | 114 | */ |
47 | 115 | Expression<T> extractFrom(X expression); |
48 | 116 |
|
@@ -70,32 +138,32 @@ public static <T> Specification<T> propertyContains(String propertyName, String |
70 | 138 |
|
71 | 139 | /** |
72 | 140 | * Creates a {@link Specification} that passes when value of an {@link Expression} contains the search term. |
73 | | - * @param extractor an {@link Extractor} that provides an {@link Expression} of type {@link String} |
| 141 | + * @param expressionProvider an {@link ExpressionProvider} that provides an {@link Expression} of type {@link String} |
74 | 142 | * @param searchTerm the term to search for |
75 | 143 | * @return a configured Specification |
76 | 144 | * @param <T> the type of the {@link Specification} {@link Root} |
77 | 145 | */ |
78 | 146 | public static <T> Specification<T> contains( |
79 | | - Extractor<T, String, Root<T>> extractor, |
| 147 | + ExpressionProvider<T, String, Root<T>> expressionProvider, |
80 | 148 | String searchTerm) { |
81 | 149 | return (root, query, criteriaBuilder) -> |
82 | | - contains(criteriaBuilder, extractor.extractFrom(root), searchTerm); |
| 150 | + contains(criteriaBuilder, expressionProvider.extractFrom(root), searchTerm); |
83 | 151 | } |
84 | 152 |
|
85 | 153 | /** |
86 | 154 | * Creates a {@link Specification} that passes when value of an {@link Expression} equals the provided value. |
87 | | - * @param extractor an {@link Extractor} that provides an {@link Expression} of type {@link S} |
| 155 | + * @param expressionProvider an {@link ExpressionProvider} that provides an {@link Expression} of type {@link S} |
88 | 156 | * @param other the object to compare to |
89 | 157 | * @return a configured Specification |
90 | 158 | * @param <S> the type of item that is compared for equality |
91 | 159 | * @param <T> the type of the {@link Specification} {@link Root} |
92 | 160 | */ |
93 | 161 | public static <S, T> Specification<T> exactMatches( |
94 | | - Extractor<T, S, Root<T>> extractor, |
| 162 | + ExpressionProvider<T, S, Root<T>> expressionProvider, |
95 | 163 | S other |
96 | 164 | ) { |
97 | 165 | return (root, query, criteriaBuilder) -> |
98 | | - objectEquals(criteriaBuilder, extractor.extractFrom(root), other); |
| 166 | + objectEquals(criteriaBuilder, expressionProvider.extractFrom(root), other); |
99 | 167 | } |
100 | 168 |
|
101 | 169 | /** |
@@ -125,18 +193,19 @@ public static <T> Specification<T> formattedClientTimeContains(String instantPro |
125 | 193 |
|
126 | 194 | /** |
127 | 195 | * Creates a {@link Specification} that passes when an {@link Expression} resulting in a JSON contains the search term at the specified JSON path. |
128 | | - * @param extractor an {@link Extractor} that provides an {@link Expression} of type {@link S} |
| 196 | + * @param expressionProvider an {@link ExpressionProvider} that provides an {@link Expression} of type {@link S} |
129 | 197 | * @param jsonPath json path to be searched |
130 | 198 | * @param searchTerm the term to search for |
131 | 199 | * @return a configured Specification |
132 | 200 | * @param <S> the type of item that is compared for equality |
133 | 201 | * @param <T> the type of the {@link Specification} {@link Root} |
134 | 202 | * @see <a href="https://mariadb.com/docs/server/reference/sql-functions/special-functions/json-functions/jsonpath-expressions">JSONPath Expressions</a> |
135 | 203 | */ |
136 | | - public static <S, T> Specification<S> jsonContains(Extractor<S, T, Root<S>> extractor, |
| 204 | + public static <S, T> Specification<S> jsonContains( |
| 205 | + ExpressionProvider<S, T, Root<S>> expressionProvider, |
137 | 206 | String jsonPath, String searchTerm) { |
138 | 207 | return (root, query, criteriaBuilder) -> |
139 | | - jsonContains(criteriaBuilder, extractor.extractFrom(root), jsonPath, searchTerm); |
| 208 | + jsonContains(criteriaBuilder, expressionProvider.extractFrom(root), jsonPath, searchTerm); |
140 | 209 |
|
141 | 210 | } |
142 | 211 |
|
|
0 commit comments