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 trueif 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 trueif 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 nullif not restricted.
168      *
169      * @return {@literal nullif 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 trueif {@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 trueif {@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