Skip to content

Commit 07b75a8

Browse files
committed
Introduce Sort unSafe checks.
QueryUtils checks if a given sort parameter is "safe" or not. Pulled this into the parsing solution. Also re-renabled a bunch of test cases, verifying much more functionality. The one lingering thing I don't quite see how to implement are Sort parameters the reference function aliases. For example: select avg(e.salary) as avg from Employee e // Sort.by("avg") In this scenario, it should NOT prefix "order by avg" with the "e" alias. Perhaps, the easiest thing is to put ALL aliases into a Set and then if a given Sort properity is IN that set, don't apply the alias?
1 parent 3bd71e8 commit 07b75a8

17 files changed

+347
-401
lines changed

spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ grammar Hql;
2020
/**
2121
* HQL per https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#query-language
2222
*
23-
* This is a mixture of Hibernate 6.1's BNF and missing bits of grammar. There are gaps and inconsistencies in the
23+
* This is a mixture of Hibernate's BNF and missing bits of grammar. There are gaps and inconsistencies in the
2424
* BNF itself, explained by other fragments of their spec. Additionally, alternate labels are used to provide easier
2525
* management of complex rules in the generated Visitor. Finally, there are labels applied to rule elements (op=('+'|'-')
2626
* to simplify the processing.

spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ grammar Jpql;
1919
/**
2020
* JPQL per https://jakarta.ee/specifications/persistence/3.1/jakarta-persistence-spec-3.1.html#bnf
2121
*
22-
* This is JPA 3.1 BNF for JPQL. There are gaps and inconsistencies in the BNF itself, explained by other fragments of the spec.
22+
* This is JPA BNF for JPQL. There are gaps and inconsistencies in the BNF itself, explained by other fragments of the spec.
2323
*
2424
* @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#bnf
2525
* @author Greg Turnquist

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryParser.java

+14-7
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import org.springframework.lang.Nullable;
2525

2626
/**
27-
* Implements the various parsing operations using the ANTLR-generated {@link HqlParser} and
27+
* Implements the parsing operations of a {@link QueryParser} using the ANTLR-generated {@link HqlParser} and
2828
* {@link HqlQueryTransformer}.
2929
*
3030
* @author Greg Turnquist
@@ -41,7 +41,7 @@ class HqlQueryParser extends QueryParser {
4141
}
4242

4343
/**
44-
* Convenience method to parse an HQL query using the ANTLR-generated {@link HqlParser}.
44+
* Convenience method to parse an HQL query. Will throw a {@link QueryParsingSyntaxError} if the query is invalid.
4545
*
4646
* @param query
4747
* @return a parsed query, ready for postprocessing
@@ -67,7 +67,7 @@ ParserRuleContext parse() {
6767
}
6868

6969
/**
70-
* Use the {@link HqlQueryTransformer} to transform the parsed query into a query with the {@link Sort} applied.
70+
* Use the {@link HqlQueryTransformer} to transform the original query into a query with the {@link Sort} applied.
7171
*
7272
* @param parsedQuery
7373
* @param sort can be {@literal null}
@@ -79,7 +79,7 @@ List<QueryParsingToken> doCreateQuery(ParserRuleContext parsedQuery, Sort sort)
7979
}
8080

8181
/**
82-
* Use the {@link HqlQueryTransformer} to transform the parsed query into a count query.
82+
* Use the {@link HqlQueryTransformer} to transform the original query into a count query.
8383
*
8484
* @param parsedQuery
8585
* @param countProjection
@@ -91,7 +91,7 @@ List<QueryParsingToken> doCreateCountQuery(ParserRuleContext parsedQuery, @Nulla
9191
}
9292

9393
/**
94-
* Using the parsed query, run it through the {@link HqlQueryTransformer} and look up its alias.
94+
* Run the parsed query through {@link HqlQueryTransformer} to find the primary FROM clause's alias.
9595
*
9696
* @param parsedQuery
9797
* @return can be {@literal null}
@@ -105,10 +105,10 @@ String findAlias(ParserRuleContext parsedQuery) {
105105
}
106106

107107
/**
108-
* Discern if the query has a new {@code com.example.Dto()} DTO constructor in the select clause.
108+
* Use {@link HqlQueryTransformer} to find the projection of the query.
109109
*
110110
* @param parsedQuery
111-
* @return Guaranteed to be {@literal true} or {@literal false}.
111+
* @return
112112
*/
113113
@Override
114114
List<QueryParsingToken> doFindProjection(ParserRuleContext parsedQuery) {
@@ -118,6 +118,13 @@ List<QueryParsingToken> doFindProjection(ParserRuleContext parsedQuery) {
118118
return transformVisitor.getProjection();
119119
}
120120

121+
/**
122+
* Use {@link HqlQueryTransformer} to detect if the query uses a {@code new com.example.Dto()} DTO constructor in the
123+
* primary select clause.
124+
*
125+
* @param parsedQuery
126+
* @return Guaranteed to be {@literal true} or {@literal false}.
127+
*/
121128
@Override
122129
boolean hasConstructor(ParserRuleContext parsedQuery) {
123130

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryTransformer.java

+12-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import org.springframework.lang.Nullable;
2626

2727
/**
28-
* An ANTLR visitor that transforms a parsed HQL query.
28+
* An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed HQL query.
2929
*
3030
* @author Greg Turnquist
3131
* @since 3.1
@@ -76,7 +76,7 @@ public boolean hasConstructorExpression() {
7676
}
7777

7878
/**
79-
* Is this a {@literal selectState} (main select statement) or a {@literal subquery}?
79+
* Is this select clause a {@literal subquery}?
8080
*
8181
* @return boolean
8282
*/
@@ -167,10 +167,19 @@ public List<QueryParsingToken> visitOrderedQuery(HqlParser.OrderedQueryContext c
167167

168168
this.sort.forEach(order -> {
169169

170+
QueryParser.checkSortExpression(order);
171+
170172
if (order.isIgnoreCase()) {
171173
tokens.add(new QueryParsingToken("lower(", false));
172174
}
173-
tokens.add(new QueryParsingToken(() -> this.alias + "." + order.getProperty(), true));
175+
tokens.add(new QueryParsingToken(() -> {
176+
177+
if (order.getProperty().contains("(")) {
178+
return order.getProperty();
179+
}
180+
181+
return this.alias + "." + order.getProperty();
182+
}, true));
174183
if (order.isIgnoreCase()) {
175184
NOSPACE(tokens);
176185
tokens.add(new QueryParsingToken(")", true));

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryParser.java

+8-7
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import org.springframework.lang.Nullable;
2525

2626
/**
27-
* Implements the various parsing operations using the ANTLR-generated {@link JpqlParser} and
27+
* Implements the parsing operations of a {@link QueryParser} using the ANTLR-generated {@link JpqlParser} and
2828
* {@link JpqlQueryTransformer}.
2929
*
3030
* @author Greg Turnquist
@@ -41,7 +41,7 @@ class JpqlQueryParser extends QueryParser {
4141
}
4242

4343
/**
44-
* Convenience method to parse a JPQL query using the ANTLR-generated {@link JpqlParser}.
44+
* Convenience method to parse a JPQL query. Will throw a {@link QueryParsingSyntaxError} if the query is invalid.
4545
*
4646
* @param query
4747
* @return a parsed query, ready for postprocessing
@@ -67,7 +67,7 @@ ParserRuleContext parse() {
6767
}
6868

6969
/**
70-
* Use the {@link JpqlQueryTransformer} to transform the parsed query into a query with the {@link Sort} applied.
70+
* Use the {@link JpqlQueryTransformer} to transform the original query into a query with the {@link Sort} applied.
7171
*
7272
* @param parsedQuery
7373
* @param sort can be {@literal null}
@@ -79,7 +79,7 @@ List<QueryParsingToken> doCreateQuery(ParserRuleContext parsedQuery, Sort sort)
7979
}
8080

8181
/**
82-
* Use the {@link JpqlQueryTransformer} to transform the parsed query into a count query.
82+
* Use the {@link JpqlQueryTransformer} to transform the original query into a count query.
8383
*
8484
* @param parsedQuery
8585
* @param countProjection
@@ -91,7 +91,7 @@ List<QueryParsingToken> doCreateCountQuery(ParserRuleContext parsedQuery, @Nulla
9191
}
9292

9393
/**
94-
* Using the parsed query, run it through the {@link JpqlQueryTransformer} and look up its alias.
94+
* Run the parsed query through {@link JpqlQueryTransformer} to find the primary FROM clause's alias.
9595
*
9696
* @param parsedQuery
9797
* @return can be {@literal null}
@@ -105,7 +105,7 @@ String findAlias(ParserRuleContext parsedQuery) {
105105
}
106106

107107
/**
108-
* Find the projection portion of the query.
108+
* Use {@link JpqlQueryTransformer} to find the projection of the query.
109109
*
110110
* @param parsedQuery
111111
* @return
@@ -119,7 +119,8 @@ List<QueryParsingToken> doFindProjection(ParserRuleContext parsedQuery) {
119119
}
120120

121121
/**
122-
* Discern if the query has a new {@code com.example.Dto()} DTO constructor in the select clause.
122+
* Use {@link JpqlQueryTransformer} to detect if the query uses a {@code new com.example.Dto()} DTO constructor in the
123+
* primary select clause.
123124
*
124125
* @param parsedQuery
125126
* @return Guaranteed to be {@literal true} or {@literal false}.

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformer.java

+17-9
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import org.springframework.lang.Nullable;
2525

2626
/**
27-
* An ANTLR visitor that transforms a parsed JPQL query.
27+
* An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed JPQL query.
2828
*
2929
* @author Greg Turnquist
3030
* @since 3.1
@@ -61,6 +61,7 @@ private JpqlQueryTransformer(@Nullable Sort sort, boolean countQuery, @Nullable
6161
this.countProjection = countProjection;
6262
}
6363

64+
@Nullable
6465
public String getAlias() {
6566
return this.alias;
6667
}
@@ -81,17 +82,15 @@ public List<QueryParsingToken> visitStart(JpqlParser.StartContext ctx) {
8182
@Override
8283
public List<QueryParsingToken> visitQl_statement(JpqlParser.Ql_statementContext ctx) {
8384

84-
List<QueryParsingToken> tokens = new ArrayList<>();
85-
8685
if (ctx.select_statement() != null) {
87-
tokens.addAll(visit(ctx.select_statement()));
86+
return visit(ctx.select_statement());
8887
} else if (ctx.update_statement() != null) {
89-
tokens.addAll(visit(ctx.update_statement()));
88+
return visit(ctx.update_statement());
9089
} else if (ctx.delete_statement() != null) {
91-
tokens.addAll(visit(ctx.delete_statement()));
90+
return visit(ctx.delete_statement());
91+
} else {
92+
return List.of();
9293
}
93-
94-
return tokens;
9594
}
9695

9796
@Override
@@ -134,10 +133,19 @@ public List<QueryParsingToken> visitSelect_statement(JpqlParser.Select_statement
134133

135134
this.sort.forEach(order -> {
136135

136+
QueryParser.checkSortExpression(order);
137+
137138
if (order.isIgnoreCase()) {
138139
tokens.add(new QueryParsingToken("lower(", false));
139140
}
140-
tokens.add(new QueryParsingToken(() -> this.alias + "." + order.getProperty(), true));
141+
tokens.add(new QueryParsingToken(() -> {
142+
143+
if (order.getProperty().contains("(")) {
144+
return order.getProperty();
145+
}
146+
147+
return this.alias + "." + order.getProperty();
148+
}, true));
141149
if (order.isIgnoreCase()) {
142150
NOSPACE(tokens);
143151
tokens.add(new QueryParsingToken(")", true));

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParser.java

+34-7
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@
1818
import static org.springframework.data.jpa.repository.query.QueryParsingToken.*;
1919

2020
import java.util.List;
21+
import java.util.regex.Pattern;
2122

2223
import org.antlr.v4.runtime.ParserRuleContext;
24+
import org.springframework.dao.InvalidDataAccessApiUsageException;
2325
import org.springframework.data.domain.Sort;
26+
import org.springframework.data.jpa.domain.JpaSort;
2427
import org.springframework.lang.Nullable;
2528
import org.springframework.util.Assert;
2629

@@ -56,8 +59,8 @@ String getQuery() {
5659
abstract ParserRuleContext parse();
5760

5861
/**
59-
* Create a string-based query using the original query with an @literal order by} added (or amended) based upon
60-
* {@link Sort}
62+
* Generate a query using the original query with an @literal order by} clause added (or amended) based upon the
63+
* provider {@link Sort} parameter.
6164
*
6265
* @param parsedQuery
6366
* @param sort can be {@literal null}
@@ -69,7 +72,7 @@ String createQuery(ParserRuleContext parsedQuery, Sort sort) {
6972
}
7073

7174
/**
72-
* Create a string-based count query using the original query.
75+
* Generate a count-based query using the original query.
7376
*
7477
* @param parsedQuery
7578
* @param countProjection
@@ -92,7 +95,8 @@ String projection(ParserRuleContext parsedQuery) {
9295
}
9396

9497
/**
95-
* Create a {@link QueryParsingToken}-based query with an {@literal order by} applied/amended based upon {@link Sort}.
98+
* Create a {@link QueryParsingToken}-based query with an {@literal order by} applied/amended based upon the
99+
* {@link Sort} parameter.
96100
*
97101
* @param parsedQuery
98102
* @param sort can be {@literal null}
@@ -108,25 +112,48 @@ String projection(ParserRuleContext parsedQuery) {
108112
abstract List<QueryParsingToken> doCreateCountQuery(ParserRuleContext parsedQuery, @Nullable String countProjection);
109113

110114
/**
111-
* Find the alias of the query's FROM clause
115+
* Find the alias of the query's primary FROM clause
112116
*
113117
* @return can be {@literal null}
114118
*/
115119
abstract String findAlias(ParserRuleContext parsedQuery);
116120

117121
/**
118-
* Find the projection of the query's selection clause.
122+
* Find the projection of the query's primary SELECT clause.
119123
*
120124
* @param parsedQuery
121125
*/
122126
abstract List<QueryParsingToken> doFindProjection(ParserRuleContext parsedQuery);
123127

124128
/**
125-
* Discern if the query has a new {@code com.example.Dto()} DTO constructor in the select clause.
129+
* Discern if the query has a {@code new com.example.Dto()} DTO constructor in the select clause.
126130
*
127131
* @param parsedQuery
128132
* @return Guaranteed to be {@literal true} or {@literal false}.
129133
*/
130134
abstract boolean hasConstructor(ParserRuleContext parsedQuery);
131135

136+
private static final Pattern PUNCTUATION_PATTERN = Pattern.compile(".*((?![._])[\\p{Punct}|\\s])");
137+
138+
private static final String UNSAFE_PROPERTY_REFERENCE = "Sort expression '%s' must only contain property references or "
139+
+ "aliases used in the select clause; If you really want to use something other than that for sorting, please use "
140+
+ "JpaSort.unsafe(…)";
141+
142+
/**
143+
* Check any given {@link JpaSort.JpaOrder#isUnsafe()} order for presence of at least one property offending the
144+
* {@link #PUNCTUATION_PATTERN} and throw an {@link Exception} indicating potential unsafe order by expression.
145+
*
146+
* @param order
147+
*/
148+
static void checkSortExpression(Sort.Order order) {
149+
150+
if (order instanceof JpaSort.JpaOrder && ((JpaSort.JpaOrder) order).isUnsafe()) {
151+
return;
152+
}
153+
154+
if (PUNCTUATION_PATTERN.matcher(order.getProperty()).find()) {
155+
throw new InvalidDataAccessApiUsageException(String.format(UNSAFE_PROPERTY_REFERENCE, order));
156+
}
157+
}
158+
132159
}

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParsingEnhancer.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
/**
2828
* Implementation of {@link QueryEnhancer} using a {@link QueryParser}.<br/>
2929
* <br/>
30-
* NOTE: The parser can find everything it needs for create sorted and count queries. Thus, looking up the alias or the
30+
* NOTE: The parser can find everything it needs to created sorted and count queries. Thus, looking up the alias or the
3131
* projection isn't needed for its primary function, and are simply implemented for test purposes.
3232
*
3333
* @author Greg Turnquist
@@ -122,6 +122,11 @@ public String createCountQueryFor() {
122122
return createCountQueryFor(null);
123123
}
124124

125+
/**
126+
* Create a count query from the original query, with potential custom projection.
127+
*
128+
* @param countProjection may be {@literal null}.
129+
*/
125130
@Override
126131
public String createCountQueryFor(@Nullable String countProjection) {
127132

@@ -193,6 +198,9 @@ public Set<String> getJoinAliases() {
193198
return Set.of();
194199
}
195200

201+
/**
202+
* Look up the {@link DeclaredQuery} from the {@link QueryParser}.
203+
*/
196204
@Override
197205
public DeclaredQuery getQuery() {
198206
return queryParser.getDeclaredQuery();

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParsingSyntaxError.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import org.antlr.v4.runtime.misc.ParseCancellationException;
1919

2020
/**
21-
* An exception to throw if a JPQL query is invalid.
21+
* An exception thrown if the JPQL query is invalid.
2222
*
2323
* @author Greg Turnquist
2424
* @since 3.1

0 commit comments

Comments
 (0)