1
16 package org.springframework.data.jpa.repository.query;
17
18 import static java.util.regex.Pattern.*;
19 import static javax.persistence.metamodel.Attribute.PersistentAttributeType.*;
20
21 import java.lang.annotation.Annotation;
22 import java.lang.reflect.AnnotatedElement;
23 import java.lang.reflect.Member;
24 import java.util.ArrayList;
25 import java.util.Collections;
26 import java.util.HashMap;
27 import java.util.HashSet;
28 import java.util.Iterator;
29 import java.util.List;
30 import java.util.Locale;
31 import java.util.Map;
32 import java.util.Set;
33 import java.util.regex.Matcher;
34 import java.util.regex.Pattern;
35 import java.util.stream.Collectors;
36
37 import javax.persistence.EntityManager;
38 import javax.persistence.ManyToOne;
39 import javax.persistence.OneToOne;
40 import javax.persistence.Parameter;
41 import javax.persistence.Query;
42 import javax.persistence.criteria.CriteriaBuilder;
43 import javax.persistence.criteria.Expression;
44 import javax.persistence.criteria.Fetch;
45 import javax.persistence.criteria.From;
46 import javax.persistence.criteria.Join;
47 import javax.persistence.criteria.JoinType;
48 import javax.persistence.criteria.Path;
49 import javax.persistence.metamodel.Attribute;
50 import javax.persistence.metamodel.Attribute.PersistentAttributeType;
51 import javax.persistence.metamodel.Bindable;
52 import javax.persistence.metamodel.ManagedType;
53 import javax.persistence.metamodel.PluralAttribute;
54
55 import org.springframework.core.annotation.AnnotationUtils;
56 import org.springframework.dao.InvalidDataAccessApiUsageException;
57 import org.springframework.data.domain.Sort;
58 import org.springframework.data.domain.Sort.Order;
59 import org.springframework.data.jpa.domain.JpaSort.JpaOrder;
60 import org.springframework.data.mapping.PropertyPath;
61 import org.springframework.data.util.Streamable;
62 import org.springframework.lang.Nullable;
63 import org.springframework.util.Assert;
64 import org.springframework.util.StringUtils;
65
66
85 public abstract class QueryUtils {
86
87 public static final String COUNT_QUERY_STRING = "select count(%s) from %s x";
88 public static final String DELETE_ALL_QUERY_STRING = "delete from %s x";
89
90
91
92
93
94
95 private static final String IDENTIFIER = "[._$[\\P{Z}&&\\P{Cc}&&\\P{Cf}&&\\P{Punct}]]+";
96 static final String COLON_NO_DOUBLE_COLON = "(?<![:\\\\]):";
97 static final String IDENTIFIER_GROUP = String.format("(%s)", IDENTIFIER);
98
99 private static final String COUNT_REPLACEMENT_TEMPLATE = "select count(%s) $5$6$7";
100 private static final String SIMPLE_COUNT_VALUE = "$2";
101 private static final String COMPLEX_COUNT_VALUE = "$3 $6";
102 private static final String COMPLEX_COUNT_LAST_VALUE = "$6";
103 private static final String ORDER_BY_PART = "(?iu)\\s+order\\s+by\\s+.*";
104
105 private static final Pattern ALIAS_MATCH;
106 private static final Pattern COUNT_MATCH;
107 private static final Pattern PROJECTION_CLAUSE = Pattern.compile("select\\s+(?:distinct\\s+)?(.+)\\s+from", Pattern.CASE_INSENSITIVE);
108
109 private static final Pattern NO_DIGITS = Pattern.compile("\\D+");
110
111 private static final String JOIN = "join\\s+(fetch\\s+)?" + IDENTIFIER + "\\s+(as\\s+)?" + IDENTIFIER_GROUP;
112 private static final Pattern JOIN_PATTERN = Pattern.compile(JOIN, Pattern.CASE_INSENSITIVE);
113
114 private static final String EQUALS_CONDITION_STRING = "%s.%s = :%s";
115 private static final Pattern ORDER_BY = Pattern.compile(".*order\\s+by\\s+.*", CASE_INSENSITIVE);
116
117 private static final Pattern NAMED_PARAMETER = Pattern
118 .compile(COLON_NO_DOUBLE_COLON + IDENTIFIER + "|#" + IDENTIFIER, CASE_INSENSITIVE);
119
120 private static final Pattern CONSTRUCTOR_EXPRESSION;
121
122 private static final Map<PersistentAttributeType, Class<? extends Annotation>> ASSOCIATION_TYPES;
123
124 private static final int QUERY_JOIN_ALIAS_GROUP_INDEX = 3;
125 private static final int VARIABLE_NAME_GROUP_INDEX = 4;
126 private static final int COMPLEX_COUNT_FIRST_INDEX = 3;
127
128 private static final Pattern PUNCTATION_PATTERN = Pattern.compile(".*((?![._])[\\p{Punct}|\\s])");
129 private static final Pattern FUNCTION_PATTERN;
130 private static final Pattern FIELD_ALIAS_PATTERN;
131
132 private static final String UNSAFE_PROPERTY_REFERENCE = "Sort expression '%s' must only contain property references or "
133 + "aliases used in the select clause. If you really want to use something other than that for sorting, please use "
134 + "JpaSort.unsafe(…)!";
135
136 static {
137
138 StringBuilder builder = new StringBuilder();
139 builder.append("(?<=from)");
140 builder.append("(?:\\s)+");
141 builder.append(IDENTIFIER_GROUP);
142 builder.append("(?:\\sas)*");
143 builder.append("(?:\\s)+");
144 builder.append("(?!(?:where|group\\s*by|order\\s*by))(\\w+)");
145
146 ALIAS_MATCH = compile(builder.toString(), CASE_INSENSITIVE);
147
148 builder = new StringBuilder();
149 builder.append("(select\\s+((distinct)?((?s).+?)?)\\s+)?(from\\s+");
150 builder.append(IDENTIFIER);
151 builder.append("(?:\\s+as)?\\s+)");
152 builder.append(IDENTIFIER_GROUP);
153 builder.append("(.*)");
154
155 COUNT_MATCH = compile(builder.toString(), CASE_INSENSITIVE);
156
157 Map<PersistentAttributeType, Class<? extends Annotation>> persistentAttributeTypes = new HashMap<>();
158 persistentAttributeTypes.put(ONE_TO_ONE, OneToOne.class);
159 persistentAttributeTypes.put(ONE_TO_MANY, null);
160 persistentAttributeTypes.put(MANY_TO_ONE, ManyToOne.class);
161 persistentAttributeTypes.put(MANY_TO_MANY, null);
162 persistentAttributeTypes.put(ELEMENT_COLLECTION, null);
163
164 ASSOCIATION_TYPES = Collections.unmodifiableMap(persistentAttributeTypes);
165
166 builder = new StringBuilder();
167 builder.append("select");
168 builder.append("\\s+");
169 builder.append("(.*\\s+)?");
170 builder.append("new");
171 builder.append("\\s+");
172 builder.append(IDENTIFIER);
173 builder.append("\\s*");
174 builder.append("\\(");
175 builder.append(".*");
176 builder.append("\\)");
177
178 CONSTRUCTOR_EXPRESSION = compile(builder.toString(), CASE_INSENSITIVE + DOTALL);
179
180 builder = new StringBuilder();
181
182 builder.append("\\w+\\s*\\([\\w\\.,\\s'=]+\\)");
183
184 builder.append("\\s+[as|AS]+\\s+(([\\w\\.]+))");
185
186 FUNCTION_PATTERN = compile(builder.toString());
187
188 builder = new StringBuilder();
189 builder.append("\\s+");
190 builder.append("[^\\s\\(\\)]+");
191 builder.append("\\s+[as|AS]+\\s+(([\\w\\.]+))");
192
193 FIELD_ALIAS_PATTERN = compile(builder.toString());
194
195 }
196
197
200 private QueryUtils() {
201
202 }
203
204
211 public static String getExistsQueryString(String entityName, String countQueryPlaceHolder,
212 Iterable<String> idAttributes) {
213
214 String whereClause = Streamable.of(idAttributes).stream()
215 .map(idAttribute -> String.format(EQUALS_CONDITION_STRING, "x", idAttribute, idAttribute))
216 .collect(Collectors.joining(" AND ", " WHERE ", ""));
217
218 return String.format(COUNT_QUERY_STRING, countQueryPlaceHolder, entityName) + whereClause;
219 }
220
221
228 public static String getQueryString(String template, String entityName) {
229
230 Assert.hasText(entityName, "Entity name must not be null or empty!");
231
232 return String.format(template, entityName);
233 }
234
235
242 public static String applySorting(String query, Sort sort) {
243 return applySorting(query, sort, detectAlias(query));
244 }
245
246
254 public static String applySorting(String query, Sort sort, @Nullable String alias) {
255
256 Assert.hasText(query, "Query must not be null or empty!");
257
258 if (sort.isUnsorted()) {
259 return query;
260 }
261
262 StringBuilder builder = new StringBuilder(query);
263
264 if (!ORDER_BY.matcher(query).matches()) {
265 builder.append(" order by ");
266 } else {
267 builder.append(", ");
268 }
269
270 Set<String> joinAliases = getOuterJoinAliases(query);
271 Set<String> selectionAliases = getFunctionAliases(query);
272 selectionAliases.addAll(getFieldAliases(query));
273
274 for (Order order : sort) {
275 builder.append(getOrderClause(joinAliases, selectionAliases, alias, order)).append(", ");
276 }
277
278 builder.delete(builder.length() - 2, builder.length());
279
280 return builder.toString();
281 }
282
283
292 private static String getOrderClause(Set<String> joinAliases, Set<String> selectionAlias, @Nullable String alias,
293 Order order) {
294
295 String property = order.getProperty();
296
297 checkSortExpression(order);
298
299 if (selectionAlias.contains(property)) {
300 return String.format("%s %s", property, toJpaDirection(order));
301 }
302
303 boolean qualifyReference = !property.contains("(");
304
305 for (String joinAlias : joinAliases) {
306 if (property.startsWith(joinAlias.concat("."))) {
307 qualifyReference = false;
308 break;
309 }
310 }
311
312 String reference = qualifyReference && StringUtils.hasText(alias) ? String.format("%s.%s", alias, property)
313 : property;
314 String wrapped = order.isIgnoreCase() ? String.format("lower(%s)", reference) : reference;
315
316 return String.format("%s %s", wrapped, toJpaDirection(order));
317 }
318
319
325 static Set<String> getOuterJoinAliases(String query) {
326
327 Set<String> result = new HashSet<>();
328 Matcher matcher = JOIN_PATTERN.matcher(query);
329
330 while (matcher.find()) {
331
332 String alias = matcher.group(QUERY_JOIN_ALIAS_GROUP_INDEX);
333 if (StringUtils.hasText(alias)) {
334 result.add(alias);
335 }
336 }
337
338 return result;
339 }
340
341
347 private static Set<String> getFieldAliases(String query) {
348 Set<String> result = new HashSet<>();
349 Matcher matcher = FIELD_ALIAS_PATTERN.matcher(query);
350
351 while (matcher.find()) {
352 String alias = matcher.group(1);
353
354 if (StringUtils.hasText(alias)) {
355 result.add(alias);
356 }
357 }
358 return result;
359 }
360
361
367 static Set<String> getFunctionAliases(String query) {
368
369 Set<String> result = new HashSet<>();
370 Matcher matcher = FUNCTION_PATTERN.matcher(query);
371
372 while (matcher.find()) {
373
374 String alias = matcher.group(1);
375
376 if (StringUtils.hasText(alias)) {
377 result.add(alias);
378 }
379 }
380
381 return result;
382 }
383
384 private static String toJpaDirection(Order order) {
385 return order.getDirection().name().toLowerCase(Locale.US);
386 }
387
388
395 @Nullable
396 @Deprecated
397 public static String detectAlias(String query) {
398
399 Matcher matcher = ALIAS_MATCH.matcher(query);
400
401 return matcher.find() ? matcher.group(2) : null;
402 }
403
404
414
415 public static <T> Query applyAndBind(String queryString, Iterable<T> entities, EntityManager entityManager) {
416
417 Assert.notNull(queryString, "Querystring must not be null!");
418 Assert.notNull(entities, "Iterable of entities must not be null!");
419 Assert.notNull(entityManager, "EntityManager must not be null!");
420
421 Iterator<T> iterator = entities.iterator();
422
423 if (!iterator.hasNext()) {
424 return entityManager.createQuery(queryString);
425 }
426
427 String alias = detectAlias(queryString);
428 StringBuilder builder = new StringBuilder(queryString);
429 builder.append(" where");
430
431 int i = 0;
432
433 while (iterator.hasNext()) {
434
435 iterator.next();
436
437 builder.append(String.format(" %s = ?%d", alias, ++i));
438
439 if (iterator.hasNext()) {
440 builder.append(" or");
441 }
442 }
443
444 Query query = entityManager.createQuery(builder.toString());
445
446 iterator = entities.iterator();
447 i = 0;
448
449 while (iterator.hasNext()) {
450 query.setParameter(++i, iterator.next());
451 }
452
453 return query;
454 }
455
456
463 @Deprecated
464 public static String createCountQueryFor(String originalQuery) {
465 return createCountQueryFor(originalQuery, null);
466 }
467
468
477 @Deprecated
478 public static String createCountQueryFor(String originalQuery, @Nullable String countProjection) {
479
480 Assert.hasText(originalQuery, "OriginalQuery must not be null or empty!");
481
482 Matcher matcher = COUNT_MATCH.matcher(originalQuery);
483 String countQuery;
484
485 if (countProjection == null) {
486
487 String variable = matcher.matches() ? matcher.group(VARIABLE_NAME_GROUP_INDEX) : null;
488 boolean useVariable = StringUtils.hasText(variable)
489 && !variable.startsWith(" new")
490 && !variable.startsWith("count(")
491 && !variable.contains(",");
492
493 String complexCountValue = matcher.matches() &&
494 StringUtils.hasText(matcher.group(COMPLEX_COUNT_FIRST_INDEX)) ?
495 COMPLEX_COUNT_VALUE : COMPLEX_COUNT_LAST_VALUE;
496
497 String replacement = useVariable ? SIMPLE_COUNT_VALUE : complexCountValue;
498 countQuery = matcher.replaceFirst(String.format(COUNT_REPLACEMENT_TEMPLATE, replacement));
499 } else {
500 countQuery = matcher.replaceFirst(String.format(COUNT_REPLACEMENT_TEMPLATE, countProjection));
501 }
502
503 return countQuery.replaceFirst(ORDER_BY_PART, "");
504 }
505
506
512 public static boolean hasNamedParameter(Query query) {
513
514 Assert.notNull(query, "Query must not be null!");
515
516 for (Parameter<?> parameter : query.getParameters()) {
517
518 String name = parameter.getName();
519
520
521 if (name != null && NO_DIGITS.matcher(name).find()) {
522 return true;
523 }
524 }
525
526 return false;
527 }
528
529
535 @Deprecated
536 static boolean hasNamedParameter(@Nullable String query) {
537 return StringUtils.hasText(query) && NAMED_PARAMETER.matcher(query).find();
538 }
539
540
548 public static List<javax.persistence.criteria.Order> toOrders(Sort sort, From<?, ?> from, CriteriaBuilder cb) {
549
550 if (sort.isUnsorted()) {
551 return Collections.emptyList();
552 }
553
554 Assert.notNull(from, "From must not be null!");
555 Assert.notNull(cb, "CriteriaBuilder must not be null!");
556
557 List<javax.persistence.criteria.Order> orders = new ArrayList<>();
558
559 for (org.springframework.data.domain.Sort.Order order : sort) {
560 orders.add(toJpaOrder(order, from, cb));
561 }
562
563 return orders;
564 }
565
566
573 public static boolean hasConstructorExpression(String query) {
574
575 Assert.hasText(query, "Query must not be null or empty!");
576
577 return CONSTRUCTOR_EXPRESSION.matcher(query).find();
578 }
579
580
587 public static String getProjection(String query) {
588
589 Assert.hasText(query, "Query must not be null or empty!");
590
591 Matcher matcher = PROJECTION_CLAUSE.matcher(query);
592 String projection = matcher.find() ? matcher.group(1) : "";
593 return projection.trim();
594 }
595
596
604 @SuppressWarnings("unchecked")
605 private static javax.persistence.criteria.Order toJpaOrder(Order order, From<?, ?> from, CriteriaBuilder cb) {
606
607 PropertyPath property = PropertyPath.from(order.getProperty(), from.getJavaType());
608 Expression<?> expression = toExpressionRecursively(from, property);
609
610 if (order.isIgnoreCase() && String.class.equals(expression.getJavaType())) {
611 Expression<String> lower = cb.lower((Expression<String>) expression);
612 return order.isAscending() ? cb.asc(lower) : cb.desc(lower);
613 } else {
614 return order.isAscending() ? cb.asc(expression) : cb.desc(expression);
615 }
616 }
617
618 static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath property) {
619 return toExpressionRecursively(from, property, false);
620 }
621
622 @SuppressWarnings("unchecked")
623 static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath property, boolean isForSelection) {
624
625 Bindable<?> propertyPathModel;
626 Bindable<?> model = from.getModel();
627 String segment = property.getSegment();
628
629 if (model instanceof ManagedType) {
630
631
635 propertyPathModel = (Bindable<?>) ((ManagedType<?>) model).getAttribute(segment);
636 } else {
637 propertyPathModel = from.get(segment).getModel();
638 }
639
640 if (requiresOuterJoin(propertyPathModel, model instanceof PluralAttribute, !property.hasNext(), isForSelection)
641 && !isAlreadyFetched(from, segment)) {
642 Join<?, ?> join = getOrCreateJoin(from, segment);
643 return (Expression<T>) (property.hasNext() ? toExpressionRecursively(join, property.next(), isForSelection)
644 : join);
645 } else {
646 Path<Object> path = from.get(segment);
647 return (Expression<T>) (property.hasNext() ? toExpressionRecursively(path, property.next()) : path);
648 }
649 }
650
651
661 private static boolean requiresOuterJoin(@Nullable Bindable<?> propertyPathModel, boolean isPluralAttribute,
662 boolean isLeafProperty, boolean isForSelection) {
663
664 if (propertyPathModel == null && isPluralAttribute) {
665 return true;
666 }
667
668 if (!(propertyPathModel instanceof Attribute)) {
669 return false;
670 }
671
672 Attribute<?, ?> attribute = (Attribute<?, ?>) propertyPathModel;
673
674 if (!ASSOCIATION_TYPES.containsKey(attribute.getPersistentAttributeType())) {
675 return false;
676 }
677
678
679
680
681 boolean isInverseOptionalOneToOne = PersistentAttributeType.ONE_TO_ONE == attribute.getPersistentAttributeType()
682 && StringUtils.hasText(getAnnotationProperty(attribute, "mappedBy", ""));
683
684
685
686
687 if (isLeafProperty && !isForSelection && !attribute.isCollection() && !isInverseOptionalOneToOne) {
688 return false;
689 }
690
691 return getAnnotationProperty(attribute, "optional", true);
692 }
693
694 private static <T> T getAnnotationProperty(Attribute<?, ?> attribute, String propertyName, T defaultValue) {
695
696 Class<? extends Annotation> associationAnnotation = ASSOCIATION_TYPES.get(attribute.getPersistentAttributeType());
697
698 if (associationAnnotation == null) {
699 return defaultValue;
700 }
701
702 Member member = attribute.getJavaMember();
703
704 if (!(member instanceof AnnotatedElement)) {
705 return defaultValue;
706 }
707
708 Annotation annotation = AnnotationUtils.getAnnotation((AnnotatedElement) member, associationAnnotation);
709 return annotation == null ? defaultValue : (T) AnnotationUtils.getValue(annotation, propertyName);
710 }
711
712 static Expression<Object> toExpressionRecursively(Path<Object> path, PropertyPath property) {
713
714 Path<Object> result = path.get(property.getSegment());
715 return property.hasNext() ? toExpressionRecursively(result, property.next()) : result;
716 }
717
718
725 private static Join<?, ?> getOrCreateJoin(From<?, ?> from, String attribute) {
726
727 for (Join<?, ?> join : from.getJoins()) {
728
729 boolean sameName = join.getAttribute().getName().equals(attribute);
730
731 if (sameName && join.getJoinType().equals(JoinType.LEFT)) {
732 return join;
733 }
734 }
735
736 return from.join(attribute, JoinType.LEFT);
737 }
738
739
746 private static boolean isAlreadyFetched(From<?, ?> from, String attribute) {
747
748 for (Fetch<?, ?> fetch : from.getFetches()) {
749
750 boolean sameName = fetch.getAttribute().getName().equals(attribute);
751
752 if (sameName && fetch.getJoinType().equals(JoinType.LEFT)) {
753 return true;
754 }
755 }
756
757 return false;
758 }
759
760
766 private static void checkSortExpression(Order order) {
767
768 if (order instanceof JpaOrder && ((JpaOrder) order).isUnsafe()) {
769 return;
770 }
771
772 if (PUNCTATION_PATTERN.matcher(order.getProperty()).find()) {
773 throw new InvalidDataAccessApiUsageException(String.format(UNSAFE_PROPERTY_REFERENCE, order));
774 }
775 }
776 }
777