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