1
16 package org.springframework.data.jpa.repository.query;
17
18 import static java.util.regex.Pattern.*;
19 import static org.springframework.util.ObjectUtils.*;
20
21 import java.lang.reflect.Array;
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.Collection;
25 import java.util.List;
26 import java.util.function.BiFunction;
27 import java.util.regex.Matcher;
28 import java.util.regex.Pattern;
29
30 import org.springframework.data.repository.query.SpelQueryContext;
31 import org.springframework.data.repository.query.SpelQueryContext.SpelExtractor;
32 import org.springframework.data.repository.query.parser.Part.Type;
33 import org.springframework.lang.Nullable;
34 import org.springframework.util.Assert;
35 import org.springframework.util.ObjectUtils;
36 import org.springframework.util.StringUtils;
37
38
50 class StringQuery implements DeclaredQuery {
51
52 private final String query;
53 private final List<ParameterBinding> bindings;
54 private final @Nullable String alias;
55 private final boolean hasConstructorExpression;
56 private final boolean containsPageableInSpel;
57 private final boolean usesJdbcStyleParameters;
58
59
64 @SuppressWarnings("deprecation")
65 StringQuery(String query) {
66
67 Assert.hasText(query, "Query must not be null or empty!");
68
69 this.bindings = new ArrayList<>();
70 this.containsPageableInSpel = query.contains("#pageable");
71
72 Metadata queryMeta = new Metadata();
73 this.query = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query,
74 this.bindings, queryMeta);
75
76 this.usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters;
77 this.alias = QueryUtils.detectAlias(query);
78 this.hasConstructorExpression = QueryUtils.hasConstructorExpression(query);
79 }
80
81
84 boolean hasParameterBindings() {
85 return !bindings.isEmpty();
86 }
87
88 String getProjection() {
89 return QueryUtils.getProjection(query);
90 }
91
92
96 @Override
97 public List<ParameterBinding> getParameterBindings() {
98 return bindings;
99 }
100
101
105 @Override
106 @SuppressWarnings("deprecation")
107 public DeclaredQuery deriveCountQuery(@Nullable String countQuery, @Nullable String countQueryProjection) {
108
109 return DeclaredQuery
110 .of(countQuery != null ? countQuery : QueryUtils.createCountQueryFor(query, countQueryProjection));
111 }
112
113
117 @Override
118 public boolean usesJdbcStyleParameters() {
119 return usesJdbcStyleParameters;
120 }
121
122
126 @Override
127 public String getQueryString() {
128 return query;
129 }
130
131
135 @Override
136 @Nullable
137 public String getAlias() {
138 return alias;
139 }
140
141
145 @Override
146 public boolean hasConstructorExpression() {
147 return hasConstructorExpression;
148 }
149
150
154 @Override
155 public boolean isDefaultProjection() {
156 return getProjection().equalsIgnoreCase(alias);
157 }
158
159
163 @Override
164 public boolean hasNamedParameter() {
165 return bindings.stream().anyMatch(b -> b.getName() != null);
166 }
167
168
172 @Override
173 public boolean usesPaging() {
174 return containsPageableInSpel;
175 }
176
177
182 enum ParameterBindingParser {
183
184 INSTANCE;
185
186 private static final String EXPRESSION_PARAMETER_PREFIX = "__$synthetic$__";
187 public static final String POSITIONAL_OR_INDEXED_PARAMETER = "\\?(\\d*+(?![#\\w]))";
188
189
190
191 private static final Pattern PARAMETER_BINDING_BY_INDEX = Pattern.compile(POSITIONAL_OR_INDEXED_PARAMETER);
192 private static final Pattern PARAMETER_BINDING_PATTERN;
193 private static final String MESSAGE = "Already found parameter binding with same index / parameter name but differing binding type! "
194 + "Already have: %s, found %s! If you bind a parameter multiple times make sure they use the same binding.";
195 private static final int INDEXED_PARAMETER_GROUP = 4;
196 private static final int NAMED_PARAMETER_GROUP = 6;
197 private static final int COMPARISION_TYPE_GROUP = 1;
198
199 static {
200
201 List<String> keywords = new ArrayList<>();
202
203 for (ParameterBindingType type : ParameterBindingType.values()) {
204 if (type.getKeyword() != null) {
205 keywords.add(type.getKeyword());
206 }
207 }
208
209 StringBuilder builder = new StringBuilder();
210 builder.append("(");
211 builder.append(StringUtils.collectionToDelimitedString(keywords, "|"));
212 builder.append(")?");
213 builder.append("(?: )?");
214 builder.append("\\(?");
215 builder.append("(");
216 builder.append("%?(" + POSITIONAL_OR_INDEXED_PARAMETER + ")%?");
217 builder.append("|");
218
219
220 builder.append("%?(" + QueryUtils.COLON_NO_DOUBLE_COLON + QueryUtils.IDENTIFIER_GROUP + ")%?");
221
222 builder.append(")");
223 builder.append("\\)?");
224
225 PARAMETER_BINDING_PATTERN = Pattern.compile(builder.toString(), CASE_INSENSITIVE);
226 }
227
228
232 private String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query,
233 List<ParameterBinding> bindings, Metadata queryMeta) {
234
235 int greatestParameterIndex = tryFindGreatestParameterIndexIn(query);
236 boolean parametersShouldBeAccessedByIndex = greatestParameterIndex != -1;
237
238
241 if (!parametersShouldBeAccessedByIndex && query.contains("?#{")) {
242 parametersShouldBeAccessedByIndex = true;
243 greatestParameterIndex = 0;
244 }
245
246 SpelExtractor spelExtractor = createSpelExtractor(query, parametersShouldBeAccessedByIndex,
247 greatestParameterIndex);
248
249 String resultingQuery = spelExtractor.getQueryString();
250 Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(resultingQuery);
251
252 int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0;
253
254 boolean usesJpaStyleParameters = false;
255 while (matcher.find()) {
256
257 if (spelExtractor.isQuoted(matcher.start())) {
258 continue;
259 }
260
261 String parameterIndexString = matcher.group(INDEXED_PARAMETER_GROUP);
262 String parameterName = parameterIndexString != null ? null : matcher.group(NAMED_PARAMETER_GROUP);
263 Integer parameterIndex = getParameterIndex(parameterIndexString);
264
265 String typeSource = matcher.group(COMPARISION_TYPE_GROUP);
266 String expression = spelExtractor.getParameter(parameterName == null ? parameterIndexString : parameterName);
267 String replacement = null;
268
269 Assert.isTrue(parameterIndexString != null || parameterName != null, () -> String.format("We need either a name or an index! Offending query string: %s", query));
270
271 expressionParameterIndex++;
272 if ("".equals(parameterIndexString)) {
273
274 queryMeta.usesJdbcStyleParameters = true;
275 parameterIndex = expressionParameterIndex;
276 } else {
277 usesJpaStyleParameters = true;
278 }
279
280 if (usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) {
281 throw new IllegalArgumentException("Mixing of ? parameters and other forms like ?1 is not supported!");
282 }
283
284 switch (ParameterBindingType.of(typeSource)) {
285
286 case LIKE:
287
288 Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2));
289 replacement = matcher.group(3);
290
291 if (parameterIndex != null) {
292 checkAndRegister(new LikeParameterBinding(parameterIndex, likeType, expression), bindings);
293 } else {
294 checkAndRegister(new LikeParameterBinding(parameterName, likeType, expression), bindings);
295
296 replacement = expression != null ? ":" + parameterName : matcher.group(5);
297 }
298
299 break;
300
301 case IN:
302
303 if (parameterIndex != null) {
304 checkAndRegister(new InParameterBinding(parameterIndex, expression), bindings);
305 } else {
306 checkAndRegister(new InParameterBinding(parameterName, expression), bindings);
307 }
308
309 break;
310
311 case AS_IS:
312 default:
313
314 bindings.add(parameterIndex != null ? new ParameterBinding(null, parameterIndex, expression)
315 : new ParameterBinding(parameterName, null, expression));
316 }
317
318 if (replacement != null) {
319 resultingQuery = replaceFirst(resultingQuery, matcher.group(2), replacement);
320 }
321
322 }
323
324 return resultingQuery;
325 }
326
327 private static SpelExtractor createSpelExtractor(String queryWithSpel, boolean parametersShouldBeAccessedByIndex,
328 int greatestParameterIndex) {
329
330
334 int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0;
335
336 BiFunction<Integer, String, String> indexToParameterName = parametersShouldBeAccessedByIndex
337 ? (index, expression) -> String.valueOf(index + expressionParameterIndex + 1)
338 : (index, expression) -> EXPRESSION_PARAMETER_PREFIX + (index + 1);
339
340 String fixedPrefix = parametersShouldBeAccessedByIndex ? "?" : ":";
341
342 BiFunction<String, String, String> parameterNameToReplacement = (prefix, name) -> fixedPrefix + name;
343
344 return SpelQueryContext.of(indexToParameterName, parameterNameToReplacement).parse(queryWithSpel);
345 }
346
347 private static String replaceFirst(String text, String substring, String replacement) {
348
349 int index = text.indexOf(substring);
350 if (index < 0) {
351 return text;
352 }
353
354 return text.substring(0, index) + replacement + text.substring(index + substring.length());
355 }
356
357 @Nullable
358 private static Integer getParameterIndex(@Nullable String parameterIndexString) {
359
360 if (parameterIndexString == null || parameterIndexString.isEmpty()) {
361 return null;
362 }
363 return Integer.valueOf(parameterIndexString);
364 }
365
366 private static int tryFindGreatestParameterIndexIn(String query) {
367
368 Matcher parameterIndexMatcher = PARAMETER_BINDING_BY_INDEX.matcher(query);
369
370 int greatestParameterIndex = -1;
371 while (parameterIndexMatcher.find()) {
372
373 String parameterIndexString = parameterIndexMatcher.group(1);
374 Integer parameterIndex = getParameterIndex(parameterIndexString);
375 if (parameterIndex != null) {
376 greatestParameterIndex = Math.max(greatestParameterIndex, parameterIndex);
377 }
378 }
379
380 return greatestParameterIndex;
381 }
382
383 private static void checkAndRegister(ParameterBinding binding, List<ParameterBinding> bindings) {
384
385 bindings.stream()
386 .filter(it -> it.hasName(binding.getName()) || it.hasPosition(binding.getPosition()))
387 .forEach(it -> Assert.isTrue(it.equals(binding), String.format(MESSAGE, it, binding)));
388
389 if (!bindings.contains(binding)) {
390 bindings.add(binding);
391 }
392 }
393
394
400 private enum ParameterBindingType {
401
402
403
404 LIKE("like "), IN("in "), AS_IS(null);
405
406 private final @Nullable String keyword;
407
408 ParameterBindingType(@Nullable String keyword) {
409 this.keyword = keyword;
410 }
411
412
418 @Nullable
419 public String getKeyword() {
420 return keyword;
421 }
422
423
427 static ParameterBindingType of(String typeSource) {
428
429 if (!StringUtils.hasText(typeSource)) {
430 return AS_IS;
431 }
432
433 for (ParameterBindingType type : values()) {
434 if (type.name().equalsIgnoreCase(typeSource.trim())) {
435 return type;
436 }
437 }
438
439 throw new IllegalArgumentException(String.format("Unsupported parameter binding type %s!", typeSource));
440 }
441 }
442 }
443
444
449 static class ParameterBinding {
450
451 private final @Nullable String name;
452 private final @Nullable String expression;
453 private final @Nullable Integer position;
454
455
460 ParameterBinding(Integer position) {
461 this(null, position, null);
462 }
463
464
472 ParameterBinding(@Nullable String name, @Nullable Integer position, @Nullable String expression) {
473
474 if (name == null) {
475 Assert.notNull(position, "Position must not be null!");
476 }
477
478 if (position == null) {
479 Assert.notNull(name, "Name must not be null!");
480 }
481
482 this.name = name;
483 this.position = position;
484 this.expression = expression;
485 }
486
487
491 boolean hasName(@Nullable String name) {
492 return this.position == null && this.name != null && this.name.equals(name);
493 }
494
495
499 boolean hasPosition(@Nullable Integer position) {
500 return position != null && this.name == null && position.equals(this.position);
501 }
502
503
506 @Nullable
507 public String getName() {
508 return name;
509 }
510
511
516 String getRequiredName() throws IllegalStateException {
517
518 String name = getName();
519
520 if (name != null) {
521 return name;
522 }
523
524 throw new IllegalStateException(String.format("Required name for %s not available!", this));
525 }
526
527
530 @Nullable
531 Integer getPosition() {
532 return position;
533 }
534
535
540 int getRequiredPosition() throws IllegalStateException {
541
542 Integer position = getPosition();
543
544 if (position != null) {
545 return position;
546 }
547
548 throw new IllegalStateException(String.format("Required position for %s not available!", this));
549 }
550
551
554 public boolean isExpression() {
555 return this.expression != null;
556 }
557
558
562 @Override
563 public int hashCode() {
564
565 int result = 17;
566
567 result += nullSafeHashCode(this.name);
568 result += nullSafeHashCode(this.position);
569 result += nullSafeHashCode(this.expression);
570
571 return result;
572 }
573
574
578 @Override
579 public boolean equals(Object obj) {
580
581 if (!(obj instanceof ParameterBinding)) {
582 return false;
583 }
584
585 ParameterBinding that = (ParameterBinding) obj;
586
587 return nullSafeEquals(this.name, that.name) && nullSafeEquals(this.position, that.position)
588 && nullSafeEquals(this.expression, that.expression);
589 }
590
591
595 @Override
596 public String toString() {
597 return String.format("ParameterBinding [name: %s, position: %d, expression: %s]", getName(), getPosition(),
598 getExpression());
599 }
600
601
604 @Nullable
605 public Object prepare(@Nullable Object valueToBind) {
606 return valueToBind;
607 }
608
609 @Nullable
610 public String getExpression() {
611 return expression;
612 }
613 }
614
615
621 static class InParameterBinding extends ParameterBinding {
622
623
626 InParameterBinding(String name, @Nullable String expression) {
627 super(name, null, expression);
628 }
629
630
633 InParameterBinding(int position, @Nullable String expression) {
634 super(null, position, expression);
635 }
636
637
641 @Override
642 public Object prepare(@Nullable Object value) {
643
644 if (!ObjectUtils.isArray(value)) {
645 return value;
646 }
647
648 int length = Array.getLength(value);
649 Collection<Object> result = new ArrayList<>(length);
650
651 for (int i = 0; i < length; i++) {
652 result.add(Array.get(value, i));
653 }
654
655 return result;
656 }
657 }
658
659
666 static class LikeParameterBinding extends ParameterBinding {
667
668 private static final List<Type> SUPPORTED_TYPES = Arrays.asList(Type.CONTAINING, Type.STARTING_WITH,
669 Type.ENDING_WITH, Type.LIKE);
670
671 private final Type type;
672
673
679 LikeParameterBinding(String name, Type type) {
680 this(name, type, null);
681 }
682
683
691 LikeParameterBinding(String name, Type type, @Nullable String expression) {
692
693 super(name, null, expression);
694
695 Assert.hasText(name, "Name must not be null or empty!");
696 Assert.notNull(type, "Type must not be null!");
697
698 Assert.isTrue(SUPPORTED_TYPES.contains(type),
699 String.format("Type must be one of %s!", StringUtils.collectionToCommaDelimitedString(SUPPORTED_TYPES)));
700
701 this.type = type;
702 }
703
704
710 LikeParameterBinding(int position, Type type) {
711 this(position, type, null);
712 }
713
714
721 LikeParameterBinding(int position, Type type, @Nullable String expression) {
722
723 super(null, position, expression);
724
725 Assert.isTrue(position > 0, "Position must be greater than zero!");
726 Assert.notNull(type, "Type must not be null!");
727
728 Assert.isTrue(SUPPORTED_TYPES.contains(type),
729 String.format("Type must be one of %s!", StringUtils.collectionToCommaDelimitedString(SUPPORTED_TYPES)));
730
731 this.type = type;
732 }
733
734
739 public Type getType() {
740 return type;
741 }
742
743
746 @Nullable
747 @Override
748 public Object prepare(@Nullable Object value) {
749
750 if (value == null) {
751 return null;
752 }
753
754 switch (type) {
755 case STARTING_WITH:
756 return String.format("%s%%", value.toString());
757 case ENDING_WITH:
758 return String.format("%%%s", value.toString());
759 case CONTAINING:
760 return String.format("%%%s%%", value.toString());
761 case LIKE:
762 default:
763 return value;
764 }
765 }
766
767
771 @Override
772 public boolean equals(Object obj) {
773
774 if (!(obj instanceof LikeParameterBinding)) {
775 return false;
776 }
777
778 LikeParameterBinding that = (LikeParameterBinding) obj;
779
780 return super.equals(obj) && this.type.equals(that.type);
781 }
782
783
787 @Override
788 public int hashCode() {
789
790 int result = super.hashCode();
791
792 result += nullSafeHashCode(this.type);
793
794 return result;
795 }
796
797
801 @Override
802 public String toString() {
803 return String.format("LikeBinding [name: %s, position: %d, type: %s]", getName(), getPosition(), type);
804 }
805
806
811 private static Type getLikeTypeFrom(String expression) {
812
813 Assert.hasText(expression, "Expression must not be null or empty!");
814
815 if (expression.matches("%.*%")) {
816 return Type.CONTAINING;
817 }
818
819 if (expression.startsWith("%")) {
820 return Type.ENDING_WITH;
821 }
822
823 if (expression.endsWith("%")) {
824 return Type.STARTING_WITH;
825 }
826
827 return Type.LIKE;
828 }
829 }
830
831 static class Metadata {
832 private boolean usesJdbcStyleParameters = false;
833 }
834 }
835