1 /*
2 * Copyright 2008-2020 the original author or authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package org.springframework.data.repository.query.parser;
17
18 import lombok.Getter;
19
20 import java.util.Arrays;
21 import java.util.Iterator;
22 import java.util.List;
23 import java.util.Optional;
24 import java.util.regex.Matcher;
25 import java.util.regex.Pattern;
26 import java.util.stream.Collectors;
27
28 import org.springframework.data.domain.Sort;
29 import org.springframework.data.repository.query.parser.Part.Type;
30 import org.springframework.data.repository.query.parser.PartTree.OrPart;
31 import org.springframework.data.util.Streamable;
32 import org.springframework.lang.Nullable;
33 import org.springframework.util.Assert;
34 import org.springframework.util.StringUtils;
35
36 /**
37 * Class to parse a {@link String} into a tree or {@link OrPart}s consisting of simple {@link Part} instances in turn.
38 * Takes a domain class as well to validate that each of the {@link Part}s are referring to a property of the domain
39 * class. The {@link PartTree} can then be used to build queries based on its API instead of parsing the method name for
40 * each query execution.
41 *
42 * @author Oliver Gierke
43 * @author Thomas Darimont
44 * @author Christoph Strobl
45 * @author Mark Paluch
46 * @author Shaun Chyxion
47 */
48 public class PartTree implements Streamable<OrPart> {
49
50 /*
51 * We look for a pattern of: keyword followed by
52 *
53 * an upper-case letter that has a lower-case variant \p{Lu}
54 * OR
55 * any other letter NOT in the BASIC_LATIN Uni-code Block \\P{InBASIC_LATIN} (like Chinese, Korean, Japanese, etc.).
56 *
57 * @see <a href="https://www.regular-expressions.info/unicode.html">https://www.regular-expressions.info/unicode.html</a>
58 * @see <a href="https://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html#ubc">Pattern</a>
59 */
60 private static final String KEYWORD_TEMPLATE = "(%s)(?=(\\p{Lu}|\\P{InBASIC_LATIN}))";
61 private static final String QUERY_PATTERN = "find|read|get|query|search|stream";
62 private static final String COUNT_PATTERN = "count";
63 private static final String EXISTS_PATTERN = "exists";
64 private static final String DELETE_PATTERN = "delete|remove";
65 private static final Pattern PREFIX_TEMPLATE = Pattern.compile( //
66 "^(" + QUERY_PATTERN + "|" + COUNT_PATTERN + "|" + EXISTS_PATTERN + "|" + DELETE_PATTERN + ")((\\p{Lu}.*?))??By");
67
68 /**
69 * The subject, for example "findDistinctUserByNameOrderByAge" would have the subject "DistinctUser".
70 */
71 private final Subject subject;
72
73 /**
74 * The predicate, for example "findDistinctUserByNameOrderByAge" would have the predicate "NameOrderByAge".
75 */
76 private final Predicate predicate;
77
78 /**
79 * Creates a new {@link PartTree} by parsing the given {@link String}.
80 *
81 * @param source the {@link String} to parse
82 * @param domainClass the domain class to check individual parts against to ensure they refer to a property of the
83 * class
84 */
85 public PartTree(String source, Class<?> domainClass) {
86
87 Assert.notNull(source, "Source must not be null");
88 Assert.notNull(domainClass, "Domain class must not be null");
89
90 Matcher matcher = PREFIX_TEMPLATE.matcher(source);
91
92 if (!matcher.find()) {
93 this.subject = new Subject(Optional.empty());
94 this.predicate = new Predicate(source, domainClass);
95 } else {
96 this.subject = new Subject(Optional.of(matcher.group(0)));
97 this.predicate = new Predicate(source.substring(matcher.group().length()), domainClass);
98 }
99 }
100
101 /*
102 * (non-Javadoc)
103 * @see java.lang.Iterable#iterator()
104 */
105 public Iterator<OrPart> iterator() {
106 return predicate.iterator();
107 }
108
109 /**
110 * Returns the {@link Sort} specification parsed from the source.
111 *
112 * @return never {@literal null}.
113 */
114 public Sort getSort() {
115 return predicate.getOrderBySource().toSort();
116 }
117
118 /**
119 * Returns whether we indicate distinct lookup of entities.
120 *
121 * @return {@literal true} if distinct
122 */
123 public boolean isDistinct() {
124 return subject.isDistinct();
125 }
126
127 /**
128 * Returns whether a count projection shall be applied.
129 *
130 * @return
131 */
132 public boolean isCountProjection() {
133 return subject.isCountProjection();
134 }
135
136 /**
137 * Returns whether an exists projection shall be applied.
138 *
139 * @return
140 * @since 1.13
141 */
142 public boolean isExistsProjection() {
143 return subject.isExistsProjection();
144 }
145
146 /**
147 * return true if the created {@link PartTree} is meant to be used for delete operation.
148 *
149 * @return
150 * @since 1.8
151 */
152 public boolean isDelete() {
153 return subject.isDelete();
154 }
155
156 /**
157 * Return {@literal true} if the create {@link PartTree} is meant to be used for a query with limited maximal results.
158 *
159 * @return
160 * @since 1.9
161 */
162 public boolean isLimiting() {
163 return getMaxResults() != null;
164 }
165
166 /**
167 * Return the number of maximal results to return or {@literal null} if not restricted.
168 *
169 * @return {@literal null} if not restricted.
170 * @since 1.9
171 */
172 @Nullable
173 public Integer getMaxResults() {
174 return subject.getMaxResults().orElse(null);
175 }
176
177 /**
178 * Returns an {@link Iterable} of all parts contained in the {@link PartTree}.
179 *
180 * @return the iterable {@link Part}s
181 */
182 public Streamable<Part> getParts() {
183 return flatMap(OrPart::stream);
184 }
185
186 /**
187 * Returns all {@link Part}s of the {@link PartTree} of the given {@link Type}.
188 *
189 * @param type
190 * @return
191 */
192 public Streamable<Part> getParts(Type type) {
193 return getParts().filter(part -> part.getType().equals(type));
194 }
195
196 /**
197 * Returns whether the {@link PartTree} contains predicate {@link Part}s.
198 *
199 * @return
200 */
201 public boolean hasPredicate() {
202 return predicate.iterator().hasNext();
203 }
204
205 /*
206 * (non-Javadoc)
207 * @see java.lang.Object#toString()
208 */
209 @Override
210 public String toString() {
211
212 return String.format("%s %s", StringUtils.collectionToDelimitedString(predicate.nodes, " or "),
213 predicate.getOrderBySource().toString()).trim();
214 }
215
216 /**
217 * Splits the given text at the given keywords. Expects camel-case style to only match concrete keywords and not
218 * derivatives of it.
219 *
220 * @param text the text to split
221 * @param keyword the keyword to split around
222 * @return an array of split items
223 */
224 private static String[] split(String text, String keyword) {
225
226 Pattern pattern = Pattern.compile(String.format(KEYWORD_TEMPLATE, keyword));
227 return pattern.split(text);
228 }
229
230 /**
231 * A part of the parsed source that results from splitting up the resource around {@literal Or} keywords. Consists of
232 * {@link Part}s that have to be concatenated by {@literal And}.
233 */
234 public static class OrPart implements Streamable<Part> {
235
236 private final List<Part> children;
237
238 /**
239 * Creates a new {@link OrPart}.
240 *
241 * @param source the source to split up into {@literal And} parts in turn.
242 * @param domainClass the domain class to check the resulting {@link Part}s against.
243 * @param alwaysIgnoreCase if always ignoring case
244 */
245 OrPart(String source, Class<?> domainClass, boolean alwaysIgnoreCase) {
246
247 String[] split = split(source, "And");
248
249 this.children = Arrays.stream(split)//
250 .filter(StringUtils::hasText)//
251 .map(part -> new Part(part, domainClass, alwaysIgnoreCase))//
252 .collect(Collectors.toList());
253 }
254
255 public Iterator<Part> iterator() {
256 return children.iterator();
257 }
258
259 @Override
260 public String toString() {
261 return StringUtils.collectionToDelimitedString(children, " and ");
262 }
263 }
264
265 /**
266 * Represents the subject part of the query. E.g. {@code findDistinctUserByNameOrderByAge} would have the subject
267 * {@code DistinctUser}.
268 *
269 * @author Phil Webb
270 * @author Oliver Gierke
271 * @author Christoph Strobl
272 * @author Thomas Darimont
273 */
274 private static class Subject {
275
276 private static final String DISTINCT = "Distinct";
277 private static final Pattern COUNT_BY_TEMPLATE = Pattern.compile("^count(\\p{Lu}.*?)??By");
278 private static final Pattern EXISTS_BY_TEMPLATE = Pattern.compile("^(" + EXISTS_PATTERN + ")(\\p{Lu}.*?)??By");
279 private static final Pattern DELETE_BY_TEMPLATE = Pattern.compile("^(" + DELETE_PATTERN + ")(\\p{Lu}.*?)??By");
280 private static final String LIMITING_QUERY_PATTERN = "(First|Top)(\\d*)?";
281 private static final Pattern LIMITED_QUERY_TEMPLATE = Pattern
282 .compile("^(" + QUERY_PATTERN + ")(" + DISTINCT + ")?" + LIMITING_QUERY_PATTERN + "(\\p{Lu}.*?)??By");
283
284 private final boolean distinct;
285 private final boolean count;
286 private final boolean exists;
287 private final boolean delete;
288 private final Optional<Integer> maxResults;
289
290 public Subject(Optional<String> subject) {
291
292 this.distinct = subject.map(it -> it.contains(DISTINCT)).orElse(false);
293 this.count = matches(subject, COUNT_BY_TEMPLATE);
294 this.exists = matches(subject, EXISTS_BY_TEMPLATE);
295 this.delete = matches(subject, DELETE_BY_TEMPLATE);
296 this.maxResults = returnMaxResultsIfFirstKSubjectOrNull(subject);
297 }
298
299 /**
300 * @param subject
301 * @return
302 * @since 1.9
303 */
304 private Optional<Integer> returnMaxResultsIfFirstKSubjectOrNull(Optional<String> subject) {
305
306 return subject.map(it -> {
307
308 Matcher grp = LIMITED_QUERY_TEMPLATE.matcher(it);
309
310 if (!grp.find()) {
311 return null;
312 }
313
314 return StringUtils.hasText(grp.group(4)) ? Integer.valueOf(grp.group(4)) : 1;
315 });
316
317 }
318
319 /**
320 * Returns {@literal true} if {@link Subject} matches {@link #DELETE_BY_TEMPLATE}.
321 *
322 * @return
323 * @since 1.8
324 */
325 public boolean isDelete() {
326 return delete;
327 }
328
329 public boolean isCountProjection() {
330 return count;
331 }
332
333 /**
334 * Returns {@literal true} if {@link Subject} matches {@link #EXISTS_BY_TEMPLATE}.
335 *
336 * @return
337 * @since 1.13
338 */
339 public boolean isExistsProjection() {
340 return exists;
341 }
342
343 public boolean isDistinct() {
344 return distinct;
345 }
346
347 public Optional<Integer> getMaxResults() {
348 return maxResults;
349 }
350
351 private boolean matches(Optional<String> subject, Pattern pattern) {
352 return subject.map(it -> pattern.matcher(it).find()).orElse(false);
353 }
354 }
355
356 /**
357 * Represents the predicate part of the query.
358 *
359 * @author Oliver Gierke
360 * @author Phil Webb
361 */
362 private static class Predicate implements Streamable<OrPart> {
363
364 private static final Pattern ALL_IGNORE_CASE = Pattern.compile("AllIgnor(ing|e)Case");
365 private static final String ORDER_BY = "OrderBy";
366
367 private final List<OrPart> nodes;
368 private final @Getter OrderBySource orderBySource;
369 private boolean alwaysIgnoreCase;
370
371 public Predicate(String predicate, Class<?> domainClass) {
372
373 String[] parts = split(detectAndSetAllIgnoreCase(predicate), ORDER_BY);
374
375 if (parts.length > 2) {
376 throw new IllegalArgumentException("OrderBy must not be used more than once in a method name!");
377 }
378
379 this.nodes = Arrays.stream(split(parts[0], "Or")) //
380 .filter(StringUtils::hasText) //
381 .map(part -> new OrPart(part, domainClass, alwaysIgnoreCase)) //
382 .collect(Collectors.toList());
383
384 this.orderBySource = parts.length == 2 ? new OrderBySource(parts[1], Optional.of(domainClass))
385 : OrderBySource.EMPTY;
386 }
387
388 private String detectAndSetAllIgnoreCase(String predicate) {
389
390 Matcher matcher = ALL_IGNORE_CASE.matcher(predicate);
391
392 if (matcher.find()) {
393 alwaysIgnoreCase = true;
394 predicate = predicate.substring(0, matcher.start()) + predicate.substring(matcher.end(), predicate.length());
395 }
396
397 return predicate;
398 }
399
400 /*
401 * (non-Javadoc)
402 * @see java.lang.Iterable#iterator()
403 */
404 @Override
405 public Iterator<OrPart> iterator() {
406 return nodes.iterator();
407 }
408 }
409 }
410