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.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 /**
67  * Simple utility class to create JPA queries.
68  *
69  * @author Oliver Gierke
70  * @author Kevin Raymond
71  * @author Thomas Darimont
72  * @author Komi Innocent
73  * @author Christoph Strobl
74  * @author Mark Paluch
75  * @author Sébastien Péralta
76  * @author Jens Schauder
77  * @author Nils Borrmann
78  * @author Reda.Housni-Alaoui
79  * @author Florian Lüdiger
80  * @author Grégoire Druant
81  * @author Mohammad Hewedy
82  * @author Andriy Redko
83  * @author Peter Großmann
84  */

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     // Used Regex/Unicode categories (see https://www.unicode.org/reports/tr18/#General_Category_Property):
91     // Z Separator
92     // Cc Control
93     // Cf Format
94     // Punct Punctuation
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)"); // from as starting delimiter
140         builder.append("(?:\\s)+"); // at least one space separating
141         builder.append(IDENTIFIER_GROUP); // Entity name, can be qualified (any
142         builder.append("(?:\\sas)*"); // exclude possible "as" keyword
143         builder.append("(?:\\s)+"); // at least one space separating
144         builder.append("(?!(?:where|group\\s*by|order\\s*by))(\\w+)"); // the actual alias
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+"); // at least one space separating
169         builder.append("(.*\\s+)?"); // anything in between (e.g. distinct) at least one space separating
170         builder.append("new");
171         builder.append("\\s+"); // at least one space separating
172         builder.append(IDENTIFIER);
173         builder.append("\\s*"); // zero to unlimited space separating
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         // any function call including parameters within the brackets
182         builder.append("\\w+\\s*\\([\\w\\.,\\s'=]+\\)");
183         // the potential alias
184         builder.append("\\s+[as|AS]+\\s+(([\\w\\.]+))");
185
186         FUNCTION_PATTERN = compile(builder.toString());
187
188         builder = new StringBuilder();
189         builder.append("\\s+"); // at least one space
190         builder.append("[^\\s\\(\\)]+"); // No white char no bracket
191         builder.append("\\s+[as|AS]+\\s+(([\\w\\.]+))"); // the potential alias
192
193         FIELD_ALIAS_PATTERN = compile(builder.toString());
194
195     }
196
197     /**
198      * Private constructor to prevent instantiation.
199      */

200     private QueryUtils() {
201
202     }
203
204     /**
205      * Returns the query string to execute an exists query for the given id attributes.
206      *
207      * @param entityName the name of the entity to create the query for, must not be {@literal null}.
208      * @param countQueryPlaceHolder the placeholder for the count clause, must not be {@literal null}.
209      * @param idAttributes the id attributes for the entity, must not be {@literal null}.
210      */

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     /**
222      * Returns the query string for the given class name.
223      *
224      * @param template must not be {@literal null}.
225      * @param entityName must not be {@literal null}.
226      * @return the template with placeholders replaced by the {@literal entityName}. Guaranteed to be not {@literal null}.
227      */

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     /**
236      * Adds {@literal order by} clause to the JPQL query. Uses the first alias to bind the sorting property to.
237      *
238      * @param query the query string to which sorting is applied
239      * @param sort the sort specification to apply.
240      * @return the modified query string.
241      */

242     public static String applySorting(String query, Sort sort) {
243         return applySorting(query, sort, detectAlias(query));
244     }
245
246     /**
247      * Adds {@literal order by} clause to the JPQL query.
248      *
249      * @param query the query string to which sorting is applied. Must not be {@literal null} or empty.
250      * @param sort the sort specification to apply.
251      * @param alias the alias to be used in the order by clause. May be {@literal null} or empty.
252      * @return the modified query string.
253      */

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     /**
284      * Returns the order clause for the given {@link Order}. Will prefix the clause with the given alias if the referenced
285      * property refers to a join alias, i.e. starts with {@code $alias.}.
286      *
287      * @param joinAliases the join aliases of the original query. Must not be {@literal null}.
288      * @param alias the alias for the root entity. May be {@literal null}.
289      * @param order the order object to build the clause for. Must not be {@literal null}.
290      * @return a String containing a order clause. Guaranteed to be not {@literal null}.
291      */

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("("); // ( indicates a function
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     /**
320      * Returns the aliases used for {@code left (outer) join}s.
321      *
322      * @param query a query string to extract the aliases of joins from. Must not be {@literal null}.
323      * @return a {@literal Set} of aliases used in the query. Guaranteed to be not {@literal null}.
324      */

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     /**
342      * Returns the aliases used for fields in the query.
343      *
344      * @param query a {@literal String} containing a query. Must not be {@literal null}.
345      * @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}.
346      */

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     /**
362      * Returns the aliases used for aggregate functions like {@code SUM, COUNT, ...}.
363      *
364      * @param query a {@literal String} containing a query. Must not be {@literal null}.
365      * @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}.
366      */

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     /**
389      * Resolves the alias for the entity to be retrieved from the given JPA query.
390      *
391      * @param query must not be {@literal null}.
392      * @return Might return {@literal null}.
393      * @deprecated use {@link DeclaredQuery#getAlias()} instead.
394      */

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     /**
405      * Creates a where-clause referencing the given entities and appends it to the given query string. Binds the given
406      * entities to the query.
407      *
408      * @param <T> type of the entities.
409      * @param queryString must not be {@literal null}.
410      * @param entities must not be {@literal null}.
411      * @param entityManager must not be {@literal null}.
412      * @return Guaranteed to be not {@literal null}.
413      */

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     /**
457      * Creates a count projected query from the given original query.
458      *
459      * @param originalQuery must not be {@literal null} or empty.
460      * @return Guaranteed to be not {@literal null}.
461      * @deprecated use {@link DeclaredQuery#deriveCountQuery(String, String)} instead.
462      */

463     @Deprecated
464     public static String createCountQueryFor(String originalQuery) {
465         return createCountQueryFor(originalQuery, null);
466     }
467
468     /**
469      * Creates a count projected query from the given original query.
470      *
471      * @param originalQuery must not be {@literal null}.
472      * @param countProjection may be {@literal null}.
473      * @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}.
474      * @since 1.6
475      * @deprecated use {@link DeclaredQuery#deriveCountQuery(String, String)} instead.
476      */

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     /**
507      * Returns whether the given {@link Query} contains named parameters.
508      *
509      * @param query Must not be {@literal null}.
510      * @return whether the given {@link Query} contains named parameters.
511      */

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             // Hibernate 3 specific hack as it returns the index as String for the name.
521             if (name != null && NO_DIGITS.matcher(name).find()) {
522                 return true;
523             }
524         }
525
526         return false;
527     }
528
529     /**
530      * Returns whether the given query contains named parameters.
531      *
532      * @param query can be {@literal null} or empty.
533      * @return whether the given query contains named parameters.
534      */

535     @Deprecated
536     static boolean hasNamedParameter(@Nullable String query) {
537         return StringUtils.hasText(query) && NAMED_PARAMETER.matcher(query).find();
538     }
539
540     /**
541      * Turns the given {@link Sort} into {@link javax.persistence.criteria.Order}s.
542      *
543      * @param sort the {@link Sort} instance to be transformed into JPA {@link javax.persistence.criteria.Order}s.
544      * @param from must not be {@literal null}.
545      * @param cb must not be {@literal null}.
546      * @return a {@link List} of {@link javax.persistence.criteria.Order}s.
547      */

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     /**
567      * Returns whether the given JPQL query contains a constructor expression.
568      *
569      * @param query must not be {@literal null} or empty.
570      * @return whether the given JPQL query contains a constructor expression.
571      * @since 1.10
572      */

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     /**
581      * Returns the projection part of the query, i.e. everything between {@code select} and {@code from}.
582      *
583      * @param query must not be {@literal null} or empty.
584      * @return the projection part of the query.
585      * @since 1.10.2
586      */

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     /**
597      * Creates a criteria API {@link javax.persistence.criteria.Order} from the given {@link Order}.
598      *
599      * @param order the order to transform into a JPA {@link javax.persistence.criteria.Order}
600      * @param from the {@link From} the {@link Order} expression is based on
601      * @param cb the {@link CriteriaBuilder} to build the {@link javax.persistence.criteria.Order} with
602      * @return Guaranteed to be not {@literal null}.
603      */

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             /*
632              *  Required to keep support for EclipseLink 2.4.x. TODO: Remove once we drop that (probably Dijkstra M1)
633              *  See: https://bugs.eclipse.org/bugs/show_bug.cgi?id=413892
634              */

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     /**
652      * Returns whether the given {@code propertyPathModel} requires the creation of a join. This is the case if we find a
653      * optional association.
654      *
655      * @param propertyPathModel may be {@literal null}.
656      * @param isPluralAttribute is the attribute of Collection type?
657      * @param isLeafProperty is this the final property navigated by a {@link PropertyPath}?
658      * @param isForSelection is the property navigated for the selection part of the query?
659      * @return whether an outer join is to be used for integrating this attribute in a query.
660      */

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         // if this path is an optional one to one attribute navigated from the not owning side we also need an explicit
679         // outer join to avoid https://hibernate.atlassian.net/browse/HHH-12712 and
680         // https://github.com/eclipse-ee4j/jpa-api/issues/170
681         boolean isInverseOptionalOneToOne = PersistentAttributeType.ONE_TO_ONE == attribute.getPersistentAttributeType()
682                 && StringUtils.hasText(getAnnotationProperty(attribute, "mappedBy"""));
683
684         // if this path is part of the select list we need to generate an explicit outer join in order to prevent Hibernate
685         // to use an inner join instead.
686         // see https://hibernate.atlassian.net/browse/HHH-12999.
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     /**
719      * Returns an existing join for the given attribute if one already exists or creates a new one if not.
720      *
721      * @param from the {@link From} to get the current joins from.
722      * @param attribute the {@link Attribute} to look for in the current joins.
723      * @return will never be {@literal null}.
724      */

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     /**
740      * Return whether the given {@link From} contains a fetch declaration for the attribute with the given name.
741      *
742      * @param from the {@link From} to check for fetches.
743      * @param attribute the attribute name to check.
744      * @return
745      */

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     /**
761      * Check any given {@link JpaOrder#isUnsafe()} order for presence of at least one property offending the
762      * {@link #PUNCTATION_PATTERN} and throw an {@link Exception} indicating potential unsafe order by expression.
763      *
764      * @param order
765      */

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