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