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.EqualsAndHashCode;
19
20 import java.beans.Introspector;
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.Collection;
24 import java.util.Collections;
25 import java.util.List;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
28
29 import org.springframework.data.mapping.PropertyPath;
30 import org.springframework.util.Assert;
31
32 /**
33 * A single part of a method name that has to be transformed into a query part. The actual transformation is defined by
34 * a {@link Type} that is determined from inspecting the given part. The query part can then be looked up via
35 * {@link #getProperty()}.
36 *
37 * @author Oliver Gierke
38 * @author Martin Baumgartner
39 * @author Jens Schauder
40 */
41 @EqualsAndHashCode
42 public class Part {
43
44 private static final Pattern IGNORE_CASE = Pattern.compile("Ignor(ing|e)Case");
45
46 private final PropertyPath propertyPath;
47 private final Part.Type type;
48
49 private IgnoreCaseType ignoreCase = IgnoreCaseType.NEVER;
50
51 /**
52 * Creates a new {@link Part} from the given method name part, the {@link Class} the part originates from and the
53 * start parameter index.
54 *
55 * @param source must not be {@literal null}.
56 * @param clazz must not be {@literal null}.
57 */
58 public Part(String source, Class<?> clazz) {
59 this(source, clazz, false);
60 }
61
62 /**
63 * Creates a new {@link Part} from the given method name part, the {@link Class} the part originates from and the
64 * start parameter index.
65 *
66 * @param source must not be {@literal null}.
67 * @param clazz must not be {@literal null}.
68 * @param alwaysIgnoreCase
69 */
70 public Part(String source, Class<?> clazz, boolean alwaysIgnoreCase) {
71
72 Assert.hasText(source, "Part source must not be null or empty!");
73 Assert.notNull(clazz, "Type must not be null!");
74
75 String partToUse = detectAndSetIgnoreCase(source);
76
77 if (alwaysIgnoreCase && ignoreCase != IgnoreCaseType.ALWAYS) {
78 this.ignoreCase = IgnoreCaseType.WHEN_POSSIBLE;
79 }
80
81 this.type = Type.fromProperty(partToUse);
82 this.propertyPath = PropertyPath.from(type.extractProperty(partToUse), clazz);
83 }
84
85 private String detectAndSetIgnoreCase(String part) {
86
87 Matcher matcher = IGNORE_CASE.matcher(part);
88 String result = part;
89
90 if (matcher.find()) {
91 ignoreCase = IgnoreCaseType.ALWAYS;
92 result = part.substring(0, matcher.start()) + part.substring(matcher.end(), part.length());
93 }
94
95 return result;
96 }
97
98 boolean isParameterRequired() {
99 return getNumberOfArguments() > 0;
100 }
101
102 /**
103 * Returns how many method parameters are bound by this part.
104 *
105 * @return
106 */
107 public int getNumberOfArguments() {
108 return type.getNumberOfArguments();
109 }
110
111 /**
112 * @return the propertyPath
113 */
114 public PropertyPath getProperty() {
115 return propertyPath;
116 }
117
118 /**
119 * @return the type
120 */
121 public Part.Type getType() {
122 return type;
123 }
124
125 /**
126 * Returns whether the {@link PropertyPath} referenced should be matched ignoring case.
127 *
128 * @return
129 */
130 public IgnoreCaseType shouldIgnoreCase() {
131 return ignoreCase;
132 }
133
134 /*
135 * (non-Javadoc)
136 * @see java.lang.Object#toString()
137 */
138 @Override
139 public String toString() {
140 return String.format("%s %s %s", propertyPath.getSegment(), type, ignoreCase);
141 }
142
143 /**
144 * The type of a method name part. Used to create query parts in various ways.
145 *
146 * @author Oliver Gierke
147 * @author Thomas Darimont
148 * @author Michael Cramer
149 */
150 public static enum Type {
151
152 BETWEEN(2, "IsBetween", "Between"), IS_NOT_NULL(0, "IsNotNull", "NotNull"), IS_NULL(0, "IsNull", "Null"), LESS_THAN(
153 "IsLessThan", "LessThan"), LESS_THAN_EQUAL("IsLessThanEqual", "LessThanEqual"), GREATER_THAN("IsGreaterThan",
154 "GreaterThan"), GREATER_THAN_EQUAL("IsGreaterThanEqual", "GreaterThanEqual"), BEFORE("IsBefore",
155 "Before"), AFTER("IsAfter", "After"), NOT_LIKE("IsNotLike", "NotLike"), LIKE("IsLike",
156 "Like"), STARTING_WITH("IsStartingWith", "StartingWith", "StartsWith"), ENDING_WITH("IsEndingWith",
157 "EndingWith", "EndsWith"), IS_NOT_EMPTY(0, "IsNotEmpty", "NotEmpty"), IS_EMPTY(0, "IsEmpty",
158 "Empty"), NOT_CONTAINING("IsNotContaining", "NotContaining", "NotContains"), CONTAINING(
159 "IsContaining", "Containing", "Contains"), NOT_IN("IsNotIn", "NotIn"), IN("IsIn",
160 "In"), NEAR("IsNear", "Near"), WITHIN("IsWithin", "Within"), REGEX("MatchesRegex",
161 "Matches", "Regex"), EXISTS(0, "Exists"), TRUE(0, "IsTrue", "True"), FALSE(0,
162 "IsFalse", "False"), NEGATING_SIMPLE_PROPERTY("IsNot",
163 "Not"), SIMPLE_PROPERTY("Is", "Equals");
164
165 // Need to list them again explicitly as the order is important
166 // (esp. for IS_NULL, IS_NOT_NULL)
167 private static final List<Part.Type> ALL = Arrays.asList(IS_NOT_NULL, IS_NULL, BETWEEN, LESS_THAN, LESS_THAN_EQUAL,
168 GREATER_THAN, GREATER_THAN_EQUAL, BEFORE, AFTER, NOT_LIKE, LIKE, STARTING_WITH, ENDING_WITH, IS_NOT_EMPTY,
169 IS_EMPTY, NOT_CONTAINING, CONTAINING, NOT_IN, IN, NEAR, WITHIN, REGEX, EXISTS, TRUE, FALSE,
170 NEGATING_SIMPLE_PROPERTY, SIMPLE_PROPERTY);
171
172 public static final Collection<String> ALL_KEYWORDS;
173
174 static {
175 List<String> allKeywords = new ArrayList<>();
176 for (Type type : ALL) {
177 allKeywords.addAll(type.keywords);
178 }
179 ALL_KEYWORDS = Collections.unmodifiableList(allKeywords);
180 }
181
182 private final List<String> keywords;
183 private final int numberOfArguments;
184
185 /**
186 * Creates a new {@link Type} using the given keyword, number of arguments to be bound and operator. Keyword and
187 * operator can be {@literal null}.
188 *
189 * @param numberOfArguments
190 * @param keywords
191 */
192 private Type(int numberOfArguments, String... keywords) {
193
194 this.numberOfArguments = numberOfArguments;
195 this.keywords = Arrays.asList(keywords);
196 }
197
198 private Type(String... keywords) {
199 this(1, keywords);
200 }
201
202 /**
203 * Returns the {@link Type} of the {@link Part} for the given raw propertyPath. This will try to detect e.g.
204 * keywords contained in the raw propertyPath that trigger special query creation. Returns {@link #SIMPLE_PROPERTY}
205 * by default.
206 *
207 * @param rawProperty
208 * @return
209 */
210 public static Part.Type fromProperty(String rawProperty) {
211
212 for (Part.Type type : ALL) {
213 if (type.supports(rawProperty)) {
214 return type;
215 }
216 }
217
218 return SIMPLE_PROPERTY;
219 }
220
221 /**
222 * Returns all keywords supported by the current {@link Type}.
223 *
224 * @return
225 */
226 public Collection<String> getKeywords() {
227 return Collections.unmodifiableList(keywords);
228 }
229
230 /**
231 * Returns whether the the type supports the given raw property. Default implementation checks whether the property
232 * ends with the registered keyword. Does not support the keyword if the property is a valid field as is.
233 *
234 * @param property
235 * @return
236 */
237 protected boolean supports(String property) {
238
239 for (String keyword : keywords) {
240 if (property.endsWith(keyword)) {
241 return true;
242 }
243 }
244
245 return false;
246 }
247
248 /**
249 * Returns the number of arguments the propertyPath binds. By default this exactly one argument.
250 *
251 * @return
252 */
253 public int getNumberOfArguments() {
254 return numberOfArguments;
255 }
256
257 /**
258 * Callback method to extract the actual propertyPath to be bound from the given part. Strips the keyword from the
259 * part's end if available.
260 *
261 * @param part
262 * @return
263 */
264 public String extractProperty(String part) {
265
266 String candidate = Introspector.decapitalize(part);
267
268 for (String keyword : keywords) {
269 if (candidate.endsWith(keyword)) {
270 return candidate.substring(0, candidate.length() - keyword.length());
271 }
272 }
273
274 return candidate;
275 }
276
277 /*
278 * (non-Javadoc)
279 * @see java.lang.Enum#toString()
280 */
281 @Override
282 public String toString() {
283 return String.format("%s (%s): %s", name(), getNumberOfArguments(), getKeywords());
284 }
285 }
286
287 /**
288 * The various types of ignore case that are supported.
289 *
290 * @author Phillip Webb
291 */
292 public enum IgnoreCaseType {
293
294 /**
295 * Should not ignore the sentence case.
296 */
297 NEVER,
298
299 /**
300 * Should ignore the sentence case, throwing an exception if this is not possible.
301 */
302 ALWAYS,
303
304 /**
305 * Should ignore the sentence case when possible to do so, silently ignoring the option when not possible.
306 */
307 WHEN_POSSIBLE
308 }
309 }
310